Add adult verification and erotic moderation features: Implement new routes and controller methods for managing adult verification requests, status updates, and document retrieval. Introduce erotic moderation actions and reports, enhancing administrative capabilities. Update chat and navigation controllers to support adult content filtering and access control. Enhance user parameter handling for adult verification status and requests, improving overall user experience and compliance.
This commit is contained in:
@@ -24,12 +24,44 @@ import BranchType from "../models/falukant/type/branch.js";
|
||||
import RegionDistance from "../models/falukant/data/region_distance.js";
|
||||
import Room from '../models/chat/room.js';
|
||||
import UserParam from '../models/community/user_param.js';
|
||||
import Image from '../models/community/image.js';
|
||||
import EroticVideo from '../models/community/erotic_video.js';
|
||||
import EroticContentReport from '../models/community/erotic_content_report.js';
|
||||
import TitleOfNobility from "../models/falukant/type/title_of_nobility.js";
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import npcCreationJobService from './npcCreationJobService.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
class AdminService {
|
||||
removeEroticStorageFile(type, hash) {
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
const storageFolder = type === 'image' ? 'erotic' : 'erotic-video';
|
||||
const filePath = path.join(__dirname, '..', 'images', storageFolder, hash);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
calculateAgeFromBirthdate(birthdate) {
|
||||
if (!birthdate) return null;
|
||||
const today = new Date();
|
||||
const birthDateObj = new Date(birthdate);
|
||||
let age = today.getFullYear() - birthDateObj.getFullYear();
|
||||
const monthDiff = today.getMonth() - birthDateObj.getMonth();
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDateObj.getDate())) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
async hasUserAccess(userId, section) {
|
||||
const userRights = await UserRight.findAll({
|
||||
include: [{
|
||||
@@ -232,6 +264,359 @@ class AdminService {
|
||||
});
|
||||
}
|
||||
|
||||
async getAdultVerificationRequests(userId, status = 'pending') {
|
||||
if (!(await this.hasUserAccess(userId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
|
||||
const users = await User.findAll({
|
||||
attributes: ['id', 'hashedId', 'username', 'active'],
|
||||
include: [
|
||||
{
|
||||
model: UserParam,
|
||||
as: 'user_params',
|
||||
required: false,
|
||||
include: [
|
||||
{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: ['birthdate', 'adult_verification_status'] }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
order: [['username', 'ASC']]
|
||||
});
|
||||
|
||||
const rows = users.map((user) => {
|
||||
const birthdateParam = user.user_params.find((param) => param.paramType?.description === 'birthdate');
|
||||
const statusParam = user.user_params.find((param) => param.paramType?.description === 'adult_verification_status');
|
||||
const requestParam = user.user_params.find((param) => param.paramType?.description === 'adult_verification_request');
|
||||
const age = this.calculateAgeFromBirthdate(birthdateParam?.value);
|
||||
const verificationStatus = ['pending', 'approved', 'rejected'].includes(statusParam?.value)
|
||||
? statusParam.value
|
||||
: 'none';
|
||||
let verificationRequest = null;
|
||||
try {
|
||||
verificationRequest = requestParam?.value ? JSON.parse(requestParam.value) : null;
|
||||
} catch {
|
||||
verificationRequest = null;
|
||||
}
|
||||
return {
|
||||
id: user.hashedId,
|
||||
username: user.username,
|
||||
active: !!user.active,
|
||||
age,
|
||||
adultVerificationStatus: verificationStatus,
|
||||
adultVerificationRequest: verificationRequest
|
||||
};
|
||||
}).filter((row) => row.age !== null && row.age >= 18);
|
||||
|
||||
if (status === 'all') {
|
||||
return rows;
|
||||
}
|
||||
|
||||
return rows.filter((row) => row.adultVerificationStatus === status);
|
||||
}
|
||||
|
||||
async setAdultVerificationStatus(requesterId, targetHashedId, status) {
|
||||
if (!(await this.hasUserAccess(requesterId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
if (!['approved', 'rejected', 'pending'].includes(status)) {
|
||||
throw new Error('wrongstatus');
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { hashedId: targetHashedId },
|
||||
attributes: ['id']
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error('notfound');
|
||||
}
|
||||
|
||||
const birthdateParam = await UserParam.findOne({
|
||||
where: { userId: user.id },
|
||||
include: [{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: 'birthdate' }
|
||||
}]
|
||||
});
|
||||
const age = this.calculateAgeFromBirthdate(birthdateParam?.value);
|
||||
if (age === null || age < 18) {
|
||||
throw new Error('notadult');
|
||||
}
|
||||
|
||||
const paramType = await UserParamType.findOne({
|
||||
where: { description: 'adult_verification_status' }
|
||||
});
|
||||
if (!paramType) {
|
||||
throw new Error('missingparamtype');
|
||||
}
|
||||
|
||||
const existing = await UserParam.findOne({
|
||||
where: { userId: user.id, paramTypeId: paramType.id }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await existing.update({ value: status });
|
||||
} else {
|
||||
await UserParam.create({
|
||||
userId: user.id,
|
||||
paramTypeId: paramType.id,
|
||||
value: status
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getAdultVerificationDocument(requesterId, targetHashedId) {
|
||||
if (!(await this.hasUserAccess(requesterId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
|
||||
const user = await User.findOne({
|
||||
where: { hashedId: targetHashedId },
|
||||
attributes: ['id', 'username']
|
||||
});
|
||||
if (!user) {
|
||||
throw new Error('notfound');
|
||||
}
|
||||
|
||||
const requestParam = await UserParam.findOne({
|
||||
where: { userId: user.id },
|
||||
include: [{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: 'adult_verification_request' }
|
||||
}]
|
||||
});
|
||||
if (!requestParam?.value) {
|
||||
throw new Error('norequest');
|
||||
}
|
||||
|
||||
let requestData;
|
||||
try {
|
||||
requestData = JSON.parse(requestParam.value);
|
||||
} catch {
|
||||
throw new Error('norequest');
|
||||
}
|
||||
|
||||
const filePath = path.join(__dirname, '..', 'images', 'adult-verification', requestData.fileName);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error('nofile');
|
||||
}
|
||||
|
||||
return {
|
||||
filePath,
|
||||
mimeType: requestData.mimeType || 'application/octet-stream',
|
||||
originalName: requestData.originalName || `${user.username}-verification`
|
||||
};
|
||||
}
|
||||
|
||||
async ensureUserParam(userId, description, value) {
|
||||
const paramType = await UserParamType.findOne({
|
||||
where: { description }
|
||||
});
|
||||
if (!paramType) {
|
||||
throw new Error('missingparamtype');
|
||||
}
|
||||
const existing = await UserParam.findOne({
|
||||
where: { userId, paramTypeId: paramType.id }
|
||||
});
|
||||
if (existing) {
|
||||
await existing.update({ value });
|
||||
return existing;
|
||||
}
|
||||
return await UserParam.create({
|
||||
userId,
|
||||
paramTypeId: paramType.id,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
async getEroticModerationReports(userId, status = 'open') {
|
||||
if (!(await this.hasUserAccess(userId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
|
||||
const where = status === 'all' ? {} : { status };
|
||||
const reports = await EroticContentReport.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: 'reporter',
|
||||
attributes: ['id', 'hashedId', 'username']
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: 'moderator',
|
||||
attributes: ['id', 'hashedId', 'username'],
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
|
||||
const rows = [];
|
||||
for (const report of reports) {
|
||||
let target = null;
|
||||
if (report.targetType === 'image') {
|
||||
target = await Image.findByPk(report.targetId, {
|
||||
attributes: ['id', 'title', 'hash', 'userId', 'isModeratedHidden']
|
||||
});
|
||||
} else if (report.targetType === 'video') {
|
||||
target = await EroticVideo.findByPk(report.targetId, {
|
||||
attributes: ['id', 'title', 'hash', 'userId', 'isModeratedHidden']
|
||||
});
|
||||
}
|
||||
const owner = target ? await User.findByPk(target.userId, {
|
||||
attributes: ['hashedId', 'username']
|
||||
}) : null;
|
||||
rows.push({
|
||||
id: report.id,
|
||||
targetType: report.targetType,
|
||||
targetId: report.targetId,
|
||||
reason: report.reason,
|
||||
note: report.note,
|
||||
status: report.status,
|
||||
actionTaken: report.actionTaken,
|
||||
handledAt: report.handledAt,
|
||||
createdAt: report.createdAt,
|
||||
reporter: report.reporter,
|
||||
moderator: report.moderator,
|
||||
target: target ? {
|
||||
id: target.id,
|
||||
title: target.title,
|
||||
hash: target.hash,
|
||||
isModeratedHidden: !!target.isModeratedHidden
|
||||
} : null,
|
||||
owner
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async applyEroticModerationAction(requesterId, reportId, action, note = null) {
|
||||
if (!(await this.hasUserAccess(requesterId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
if (!['dismiss', 'hide_content', 'restore_content', 'delete_content', 'block_uploads', 'revoke_access'].includes(action)) {
|
||||
throw new Error('wrongaction');
|
||||
}
|
||||
|
||||
const moderator = await User.findOne({
|
||||
where: { hashedId: requesterId },
|
||||
attributes: ['id']
|
||||
});
|
||||
const report = await EroticContentReport.findByPk(reportId);
|
||||
if (!report) {
|
||||
throw new Error('notfound');
|
||||
}
|
||||
|
||||
let target = null;
|
||||
if (report.targetType === 'image') {
|
||||
target = await Image.findByPk(report.targetId);
|
||||
} else if (report.targetType === 'video') {
|
||||
target = await EroticVideo.findByPk(report.targetId);
|
||||
}
|
||||
|
||||
if (action === 'dismiss') {
|
||||
await report.update({
|
||||
status: 'dismissed',
|
||||
actionTaken: 'dismiss',
|
||||
handledBy: moderator?.id || null,
|
||||
handledAt: new Date(),
|
||||
note: note ?? report.note
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
throw new Error('targetnotfound');
|
||||
}
|
||||
|
||||
if (action === 'hide_content') {
|
||||
await target.update({ isModeratedHidden: true });
|
||||
} else if (action === 'restore_content') {
|
||||
await target.update({ isModeratedHidden: false });
|
||||
} else if (action === 'delete_content') {
|
||||
this.removeEroticStorageFile(report.targetType, target.hash);
|
||||
await target.destroy();
|
||||
} else if (action === 'block_uploads') {
|
||||
await this.ensureUserParam(target.userId, 'adult_upload_blocked', 'true');
|
||||
} else if (action === 'revoke_access') {
|
||||
await this.ensureUserParam(target.userId, 'adult_verification_status', 'rejected');
|
||||
}
|
||||
|
||||
await report.update({
|
||||
status: 'actioned',
|
||||
actionTaken: action,
|
||||
handledBy: moderator?.id || null,
|
||||
handledAt: new Date(),
|
||||
note: note ?? report.note
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getEroticModerationPreview(requesterId, targetType, targetId) {
|
||||
if (!(await this.hasUserAccess(requesterId, 'useradministration'))) {
|
||||
throw new Error('noaccess');
|
||||
}
|
||||
|
||||
if (targetType === 'image') {
|
||||
const target = await Image.findByPk(targetId, {
|
||||
attributes: ['hash', 'originalFileName']
|
||||
});
|
||||
if (!target) {
|
||||
throw new Error('notfound');
|
||||
}
|
||||
const filePath = path.join(__dirname, '..', 'images', 'erotic', target.hash);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error('nofile');
|
||||
}
|
||||
return {
|
||||
filePath,
|
||||
mimeType: this.getMimeTypeFromName(target.originalFileName) || 'image/jpeg',
|
||||
originalName: target.originalFileName || target.hash
|
||||
};
|
||||
}
|
||||
|
||||
if (targetType === 'video') {
|
||||
const target = await EroticVideo.findByPk(targetId, {
|
||||
attributes: ['hash', 'mimeType', 'originalFileName']
|
||||
});
|
||||
if (!target) {
|
||||
throw new Error('notfound');
|
||||
}
|
||||
const filePath = path.join(__dirname, '..', 'images', 'erotic-video', target.hash);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
throw new Error('nofile');
|
||||
}
|
||||
return {
|
||||
filePath,
|
||||
mimeType: target.mimeType || this.getMimeTypeFromName(target.originalFileName) || 'application/octet-stream',
|
||||
originalName: target.originalFileName || target.hash
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('wrongtype');
|
||||
}
|
||||
|
||||
getMimeTypeFromName(fileName) {
|
||||
const lower = String(fileName || '').toLowerCase();
|
||||
if (lower.endsWith('.png')) return 'image/png';
|
||||
if (lower.endsWith('.webp')) return 'image/webp';
|
||||
if (lower.endsWith('.gif')) return 'image/gif';
|
||||
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg';
|
||||
return null;
|
||||
}
|
||||
|
||||
async getFalukantUserById(userId, hashedId) {
|
||||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||||
throw new Error('noaccess');
|
||||
@@ -682,6 +1067,7 @@ class AdminService {
|
||||
'title',
|
||||
'roomTypeId',
|
||||
'isPublic',
|
||||
'isAdultOnly',
|
||||
'genderRestrictionId',
|
||||
'minAge',
|
||||
'maxAge',
|
||||
@@ -1312,4 +1698,4 @@ class AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
export default new AdminService();
|
||||
export default new AdminService();
|
||||
|
||||
@@ -2,6 +2,8 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import amqp from 'amqplib/callback_api.js';
|
||||
import User from '../models/community/user.js';
|
||||
import Room from '../models/chat/room.js';
|
||||
import UserParam from '../models/community/user_param.js';
|
||||
import UserParamType from '../models/type/user_param.js';
|
||||
|
||||
const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost';
|
||||
const QUEUE = 'oneToOne_messages';
|
||||
@@ -166,17 +168,69 @@ class ChatService {
|
||||
(chat.user1Id === user2HashId && chat.user2Id === user1HashId)
|
||||
);
|
||||
}
|
||||
|
||||
calculateAge(birthdate) {
|
||||
const birthDate = new Date(birthdate);
|
||||
const ageDifMs = Date.now() - birthDate.getTime();
|
||||
const ageDate = new Date(ageDifMs);
|
||||
return Math.abs(ageDate.getUTCFullYear() - 1970);
|
||||
}
|
||||
|
||||
normalizeAdultVerificationStatus(value) {
|
||||
if (!value) return 'none';
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
return ['pending', 'approved', 'rejected'].includes(normalized) ? normalized : 'none';
|
||||
}
|
||||
|
||||
async getAdultAccessState(hashedUserId) {
|
||||
if (!hashedUserId) {
|
||||
return { isAdult: false, adultVerificationStatus: 'none', adultAccessEnabled: false };
|
||||
}
|
||||
|
||||
const user = await User.findOne({ where: { hashedId: hashedUserId }, attributes: ['id'] });
|
||||
if (!user) {
|
||||
throw new Error('user_not_found');
|
||||
}
|
||||
|
||||
const params = await UserParam.findAll({
|
||||
where: { userId: user.id },
|
||||
include: [{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: ['birthdate', 'adult_verification_status'] }
|
||||
}]
|
||||
});
|
||||
|
||||
const birthdateParam = params.find(param => param.paramType?.description === 'birthdate');
|
||||
const statusParam = params.find(param => param.paramType?.description === 'adult_verification_status');
|
||||
const age = birthdateParam ? this.calculateAge(birthdateParam.value) : 0;
|
||||
const adultVerificationStatus = this.normalizeAdultVerificationStatus(statusParam?.value);
|
||||
return {
|
||||
isAdult: age >= 18,
|
||||
adultVerificationStatus,
|
||||
adultAccessEnabled: age >= 18 && adultVerificationStatus === 'approved'
|
||||
};
|
||||
}
|
||||
|
||||
async getRoomList() {
|
||||
async getRoomList(hashedUserId, { adultOnly = false } = {}) {
|
||||
// Nur öffentliche Räume, keine sensiblen Felder
|
||||
const { default: Room } = await import('../models/chat/room.js');
|
||||
const { default: RoomType } = await import('../models/chat/room_type.js');
|
||||
const where = { isPublic: true, isAdultOnly: Boolean(adultOnly) };
|
||||
|
||||
if (adultOnly) {
|
||||
const adultAccess = await this.getAdultAccessState(hashedUserId);
|
||||
if (!adultAccess.adultAccessEnabled) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return Room.findAll({
|
||||
attributes: [
|
||||
'id', 'title', 'roomTypeId', 'isPublic', 'genderRestrictionId',
|
||||
'id', 'title', 'roomTypeId', 'isPublic', 'isAdultOnly', 'genderRestrictionId',
|
||||
'minAge', 'maxAge', 'friendsOfOwnerOnly', 'requiredUserRightId'
|
||||
],
|
||||
where: { isPublic: true },
|
||||
where,
|
||||
include: [
|
||||
{ model: RoomType, as: 'roomType' }
|
||||
]
|
||||
@@ -215,7 +269,7 @@ class ChatService {
|
||||
|
||||
return Room.findAll({
|
||||
where: { ownerId: user.id },
|
||||
attributes: ['id', 'title', 'isPublic', 'roomTypeId', 'ownerId'],
|
||||
attributes: ['id', 'title', 'isPublic', 'isAdultOnly', 'roomTypeId', 'ownerId'],
|
||||
order: [['title', 'ASC']]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,6 +76,13 @@ import ProductWeatherEffect from '../models/falukant/type/product_weather_effect
|
||||
import WeatherType from '../models/falukant/type/weather.js';
|
||||
import ReputationActionType from '../models/falukant/type/reputation_action.js';
|
||||
import ReputationActionLog from '../models/falukant/log/reputation_action.js';
|
||||
import {
|
||||
productionPieceCost,
|
||||
productionCostTotal,
|
||||
effectiveWorthPercent,
|
||||
KNOWLEDGE_PRICE_FLOOR,
|
||||
calcRegionalSellPriceSync,
|
||||
} from '../utils/falukant/falukantProductEconomy.js';
|
||||
|
||||
function calcAge(birthdate) {
|
||||
const b = new Date(birthdate); b.setHours(0, 0);
|
||||
@@ -97,54 +104,6 @@ async function getBranchOrFail(userId, branchId) {
|
||||
return branch;
|
||||
}
|
||||
|
||||
const PRODUCTION_COST_BASE = 6.0;
|
||||
const PRODUCTION_COST_PER_PRODUCT_CATEGORY = 1.0;
|
||||
const PRODUCTION_HEADROOM_DISCOUNT_PER_STEP = 0.035;
|
||||
const PRODUCTION_HEADROOM_DISCOUNT_CAP = 0.14;
|
||||
|
||||
function productionPieceCost(certificate, category) {
|
||||
const c = Math.max(1, Number(category) || 1);
|
||||
const cert = Math.max(1, Number(certificate) || 1);
|
||||
const raw = PRODUCTION_COST_BASE + (c * PRODUCTION_COST_PER_PRODUCT_CATEGORY);
|
||||
const headroom = Math.max(0, cert - c);
|
||||
const discount = Math.min(
|
||||
headroom * PRODUCTION_HEADROOM_DISCOUNT_PER_STEP,
|
||||
PRODUCTION_HEADROOM_DISCOUNT_CAP
|
||||
);
|
||||
return raw * (1 - discount);
|
||||
}
|
||||
|
||||
function productionCostTotal(quantity, category, certificate) {
|
||||
const q = Math.min(100, Math.max(1, Number(quantity) || 1));
|
||||
return q * productionPieceCost(certificate, category);
|
||||
}
|
||||
|
||||
/** Regionaler Nachfragewert: sehr niedrige Werte aus der DB sonst unspielbar; Decke nach oben bei 100. */
|
||||
function effectiveWorthPercent(worthPercent) {
|
||||
const w = Number(worthPercent);
|
||||
if (Number.isNaN(w)) return 75;
|
||||
return Math.min(100, Math.max(75, w));
|
||||
}
|
||||
|
||||
/** Untergrenze für den Wissens-Multiplikator auf den Basispreis (0 = minAnteil, 100 = voller Basispreis). */
|
||||
const KNOWLEDGE_PRICE_FLOOR = 0.7;
|
||||
|
||||
function calcSellPrice(product, knowledgeFactor = 0) {
|
||||
const max = product.sellCost;
|
||||
const min = max * KNOWLEDGE_PRICE_FLOOR;
|
||||
return min + (max - min) * (knowledgeFactor / 100);
|
||||
}
|
||||
|
||||
/** Synchrone Preisberechnung, wenn worthPercent bereits bekannt ist (kein DB-Zugriff). */
|
||||
function calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent) {
|
||||
if (product.sellCost === null || product.sellCost === undefined) return null;
|
||||
const w = effectiveWorthPercent(worthPercent);
|
||||
const basePrice = product.sellCost * (w / 100);
|
||||
const min = basePrice * KNOWLEDGE_PRICE_FLOOR;
|
||||
const max = basePrice;
|
||||
return min + (max - min) * (knowledgeFactor / 100);
|
||||
}
|
||||
|
||||
const POLITICAL_OFFICE_RANKS = {
|
||||
assessor: 1,
|
||||
councillor: 1,
|
||||
|
||||
@@ -12,6 +12,13 @@ import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
||||
import UserParamVisibility from '../models/community/user_param_visibility.js';
|
||||
import { encrypt } from '../utils/encryption.js';
|
||||
import { sequelize } from '../utils/sequelize.js';
|
||||
import fsPromises from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/** Wie UserParam.value-Setter: bei Verschlüsselungsfehler leeren String speichern, nicht crashen. */
|
||||
function encryptUserParamValue(plain) {
|
||||
@@ -24,6 +31,90 @@ function encryptUserParamValue(plain) {
|
||||
}
|
||||
|
||||
class SettingsService extends BaseService{
|
||||
parseAdultVerificationRequest(value) {
|
||||
if (!value) return null;
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
normalizeAdultVerificationStatus(value) {
|
||||
if (['pending', 'approved', 'rejected'].includes(value)) {
|
||||
return value;
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
async getAdultAccessStateByUserId(userId) {
|
||||
const userParams = await this.getUserParams(userId, ['birthdate', 'adult_verification_status', 'adult_verification_request']);
|
||||
let birthdate = null;
|
||||
let adultVerificationStatus = 'none';
|
||||
let adultVerificationRequest = null;
|
||||
for (const param of userParams) {
|
||||
if (param.paramType.description === 'birthdate') {
|
||||
birthdate = param.value;
|
||||
}
|
||||
if (param.paramType.description === 'adult_verification_status') {
|
||||
adultVerificationStatus = this.normalizeAdultVerificationStatus(param.value);
|
||||
}
|
||||
if (param.paramType.description === 'adult_verification_request') {
|
||||
adultVerificationRequest = this.parseAdultVerificationRequest(param.value);
|
||||
}
|
||||
}
|
||||
const age = birthdate ? this.calculateAge(birthdate) : null;
|
||||
const isAdult = age !== null && age >= 18;
|
||||
return {
|
||||
age,
|
||||
isAdult,
|
||||
adultVerificationStatus: isAdult ? adultVerificationStatus : 'none',
|
||||
adultVerificationRequest: isAdult ? adultVerificationRequest : null,
|
||||
adultAccessEnabled: isAdult && adultVerificationStatus === 'approved'
|
||||
};
|
||||
}
|
||||
|
||||
buildAdultVerificationFilePath(fileName) {
|
||||
return path.join(__dirname, '..', 'images', 'adult-verification', fileName);
|
||||
}
|
||||
|
||||
async saveAdultVerificationDocument(file) {
|
||||
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
|
||||
if (!file || !file.buffer) {
|
||||
throw new Error('No verification document provided');
|
||||
}
|
||||
if (!allowedMimeTypes.includes(file.mimetype)) {
|
||||
throw new Error('Unsupported verification document type');
|
||||
}
|
||||
|
||||
const ext = path.extname(file.originalname || '').toLowerCase();
|
||||
const safeExt = ext && ext.length <= 8 ? ext : (file.mimetype === 'application/pdf' ? '.pdf' : '.bin');
|
||||
const fileName = `${uuidv4()}${safeExt}`;
|
||||
const filePath = this.buildAdultVerificationFilePath(fileName);
|
||||
await fsPromises.mkdir(path.dirname(filePath), { recursive: true });
|
||||
await fsPromises.writeFile(filePath, file.buffer);
|
||||
return { fileName, filePath };
|
||||
}
|
||||
|
||||
async upsertUserParam(userId, description, value) {
|
||||
const paramType = await UserParamType.findOne({ where: { description } });
|
||||
if (!paramType) {
|
||||
throw new Error(`Missing user param type: ${description}`);
|
||||
}
|
||||
const existingParam = await UserParam.findOne({
|
||||
where: { userId, paramTypeId: paramType.id }
|
||||
});
|
||||
if (existingParam) {
|
||||
await existingParam.update({ value });
|
||||
return existingParam;
|
||||
}
|
||||
return UserParam.create({
|
||||
userId,
|
||||
paramTypeId: paramType.id,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
async getUserParams(userId, paramDescriptions) {
|
||||
return await UserParam.findAll({
|
||||
where: { userId },
|
||||
@@ -299,10 +390,13 @@ class SettingsService extends BaseService{
|
||||
email = null;
|
||||
}
|
||||
|
||||
const adultAccess = await this.getAdultAccessStateByUserId(user.id);
|
||||
|
||||
return {
|
||||
username: user.username,
|
||||
email: email,
|
||||
showinsearch: user.searchable
|
||||
showinsearch: user.searchable,
|
||||
...adultAccess
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting account settings:', error);
|
||||
@@ -317,6 +411,8 @@ class SettingsService extends BaseService{
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
const adultAccess = await this.getAdultAccessStateByUserId(user.id);
|
||||
|
||||
// Update username if provided
|
||||
if (settings.username !== undefined) {
|
||||
await user.update({ username: settings.username });
|
||||
@@ -332,6 +428,17 @@ class SettingsService extends BaseService{
|
||||
await user.update({ searchable: settings.showinsearch });
|
||||
}
|
||||
|
||||
if (settings.requestAdultVerification) {
|
||||
if (!adultAccess.isAdult) {
|
||||
throw new Error('Adult verification can only be requested by adult users');
|
||||
}
|
||||
|
||||
const normalizedValue = adultAccess.adultVerificationStatus === 'approved'
|
||||
? 'approved'
|
||||
: 'pending';
|
||||
await this.upsertUserParam(user.id, 'adult_verification_status', normalizedValue);
|
||||
}
|
||||
|
||||
// Update password if provided and not empty
|
||||
if (settings.newpassword && settings.newpassword.trim() !== '') {
|
||||
if (!settings.oldpassword || settings.oldpassword.trim() === '') {
|
||||
@@ -357,6 +464,34 @@ class SettingsService extends BaseService{
|
||||
}
|
||||
}
|
||||
|
||||
async submitAdultVerificationRequest(hashedUserId, { note }, file) {
|
||||
const user = await this.getUserByHashedId(hashedUserId);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const adultAccess = await this.getAdultAccessStateByUserId(user.id);
|
||||
if (!adultAccess.isAdult) {
|
||||
throw new Error('Adult verification can only be requested by adult users');
|
||||
}
|
||||
if (!file) {
|
||||
throw new Error('No verification document provided');
|
||||
}
|
||||
|
||||
const savedFile = await this.saveAdultVerificationDocument(file);
|
||||
const requestPayload = {
|
||||
fileName: savedFile.fileName,
|
||||
originalName: file.originalname,
|
||||
mimeType: file.mimetype,
|
||||
note: note || '',
|
||||
submittedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
await this.upsertUserParam(user.id, 'adult_verification_request', JSON.stringify(requestPayload));
|
||||
await this.upsertUserParam(user.id, 'adult_verification_status', adultAccess.adultVerificationStatus === 'approved' ? 'approved' : 'pending');
|
||||
|
||||
return requestPayload;
|
||||
}
|
||||
|
||||
async getVisibilities() {
|
||||
return UserParamVisibilityType.findAll();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import UserParamVisibility from '../models/community/user_param_visibility.js';
|
||||
import UserParamVisibilityType from '../models/type/user_param_visibility.js';
|
||||
import Folder from '../models/community/folder.js';
|
||||
import Image from '../models/community/image.js';
|
||||
import EroticVideo from '../models/community/erotic_video.js';
|
||||
import EroticContentReport from '../models/community/erotic_content_report.js';
|
||||
import ImageVisibilityType from '../models/type/image_visibility.js';
|
||||
import FolderImageVisibility from '../models/community/folder_image_visibility.js';
|
||||
import ImageImageVisibility from '../models/community/image_image_visibility.js';
|
||||
@@ -30,6 +32,150 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
class SocialNetworkService extends BaseService {
|
||||
normalizeAdultVerificationStatus(value) {
|
||||
if (!value) return 'none';
|
||||
const normalized = String(value).trim().toLowerCase();
|
||||
return ['pending', 'approved', 'rejected'].includes(normalized) ? normalized : 'none';
|
||||
}
|
||||
|
||||
async getAdultAccessState(userId) {
|
||||
const params = await this.getUserParams(userId, ['birthdate', 'adult_verification_status', 'adult_upload_blocked']);
|
||||
const birthdateParam = params.find(param => param.paramType.description === 'birthdate');
|
||||
const statusParam = params.find(param => param.paramType.description === 'adult_verification_status');
|
||||
const uploadBlockedParam = params.find(param => param.paramType.description === 'adult_upload_blocked');
|
||||
const age = birthdateParam ? this.calculateAge(birthdateParam.value) : 0;
|
||||
const adultVerificationStatus = this.normalizeAdultVerificationStatus(statusParam?.value);
|
||||
return {
|
||||
age,
|
||||
isAdult: age >= 18,
|
||||
adultVerificationStatus,
|
||||
adultAccessEnabled: age >= 18 && adultVerificationStatus === 'approved',
|
||||
adultUploadBlocked: String(uploadBlockedParam?.value).toLowerCase() === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
async requireAdultAreaAccessByHash(hashedId) {
|
||||
const userId = await this.checkUserAccess(hashedId);
|
||||
const adultAccess = await this.getAdultAccessState(userId);
|
||||
if (!adultAccess.adultAccessEnabled) {
|
||||
const error = new Error('Adult area access denied');
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
async ensureAdultUploadsAllowed(userId) {
|
||||
const adultAccess = await this.getAdultAccessState(userId);
|
||||
if (adultAccess.adultUploadBlocked) {
|
||||
const error = new Error('Adult uploads are blocked');
|
||||
error.status = 403;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async resolveEroticTarget(targetType, targetId) {
|
||||
if (targetType === 'image') {
|
||||
const image = await Image.findOne({
|
||||
where: {
|
||||
id: targetId,
|
||||
isAdultContent: true
|
||||
}
|
||||
});
|
||||
if (!image) {
|
||||
throw new Error('Target not found');
|
||||
}
|
||||
return { targetType, target: image, ownerId: image.userId };
|
||||
}
|
||||
if (targetType === 'video') {
|
||||
const video = await EroticVideo.findByPk(targetId);
|
||||
if (!video) {
|
||||
throw new Error('Target not found');
|
||||
}
|
||||
return { targetType, target: video, ownerId: video.userId };
|
||||
}
|
||||
throw new Error('Unsupported target type');
|
||||
}
|
||||
|
||||
async ensureRootFolder(userId) {
|
||||
let rootFolder = await Folder.findOne({
|
||||
where: { parentId: null, userId, isAdultArea: false },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}]
|
||||
});
|
||||
if (rootFolder) {
|
||||
return rootFolder;
|
||||
}
|
||||
|
||||
const user = await User.findOne({ where: { id: userId } });
|
||||
const visibility = await ImageVisibilityType.findOne({ where: { description: 'everyone' } });
|
||||
rootFolder = await Folder.create({
|
||||
name: user.username,
|
||||
parentId: null,
|
||||
userId,
|
||||
isAdultArea: false
|
||||
});
|
||||
if (visibility) {
|
||||
await FolderImageVisibility.create({
|
||||
folderId: rootFolder.id,
|
||||
visibilityTypeId: visibility.id
|
||||
});
|
||||
}
|
||||
return await Folder.findOne({
|
||||
where: { id: rootFolder.id },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async ensureAdultRootFolder(userId) {
|
||||
const rootFolder = await this.ensureRootFolder(userId);
|
||||
let adultRoot = await Folder.findOne({
|
||||
where: {
|
||||
parentId: rootFolder.id,
|
||||
userId,
|
||||
isAdultArea: true,
|
||||
name: 'Erotik'
|
||||
},
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}]
|
||||
});
|
||||
if (adultRoot) {
|
||||
return adultRoot;
|
||||
}
|
||||
|
||||
const adultsVisibility = await ImageVisibilityType.findOne({ where: { description: 'adults' } });
|
||||
adultRoot = await Folder.create({
|
||||
name: 'Erotik',
|
||||
parentId: rootFolder.id,
|
||||
userId,
|
||||
isAdultArea: true
|
||||
});
|
||||
if (adultsVisibility) {
|
||||
await FolderImageVisibility.create({
|
||||
folderId: adultRoot.id,
|
||||
visibilityTypeId: adultsVisibility.id
|
||||
});
|
||||
}
|
||||
return await Folder.findOne({
|
||||
where: { id: adultRoot.id },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async searchUsers({ hashedUserId, username, ageFrom, ageTo, genders }) {
|
||||
const whereClause = this.buildSearchWhereClause(username);
|
||||
const user = await this.loadUserByHash(hashedUserId);
|
||||
@@ -49,15 +195,19 @@ class SocialNetworkService extends BaseService {
|
||||
return this.constructUserProfile(user, requestingUserId);
|
||||
}
|
||||
|
||||
async createFolder(hashedUserId, data, folderId) {
|
||||
async createFolder(hashedUserId, data, folderId, options = {}) {
|
||||
await this.checkUserAccess(hashedUserId);
|
||||
const user = await this.loadUserByHash(hashedUserId);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const isAdultArea = Boolean(options.isAdultArea);
|
||||
if (isAdultArea) {
|
||||
await this.requireAdultAreaAccessByHash(hashedUserId);
|
||||
}
|
||||
console.log('given data', data, folderId);
|
||||
const parentFolder = data.parentId ? await Folder.findOne({
|
||||
where: { id: data.parentId, userId: user.id }
|
||||
where: { id: data.parentId, userId: user.id, isAdultArea }
|
||||
}) : null;
|
||||
if (data.parentId && !parentFolder) {
|
||||
throw new Error('Parent folder not found');
|
||||
@@ -68,11 +218,12 @@ class SocialNetworkService extends BaseService {
|
||||
newFolder = await Folder.create({
|
||||
parentId: parentFolder.id || null,
|
||||
userId: user.id,
|
||||
name: data.name
|
||||
name: data.name,
|
||||
isAdultArea
|
||||
});
|
||||
} else {
|
||||
newFolder = await Folder.findOne({
|
||||
where: { id: folderId, userId: user.id }
|
||||
where: { id: folderId, userId: user.id, isAdultArea }
|
||||
});
|
||||
if (!newFolder) {
|
||||
throw new Error('Folder not found or user does not own the folder');
|
||||
@@ -94,38 +245,8 @@ class SocialNetworkService extends BaseService {
|
||||
|
||||
async getFolders(hashedId) {
|
||||
const userId = await this.checkUserAccess(hashedId);
|
||||
let rootFolder = await Folder.findOne({
|
||||
where: { parentId: null, userId },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}]
|
||||
});
|
||||
if (!rootFolder) {
|
||||
const user = await User.findOne({ where: { id: userId } });
|
||||
const visibility = await ImageVisibilityType.findOne({
|
||||
where: { description: 'everyone' }
|
||||
});
|
||||
rootFolder = await Folder.create({
|
||||
name: user.username,
|
||||
parentId: null,
|
||||
userId
|
||||
});
|
||||
await FolderImageVisibility.create({
|
||||
folderId: rootFolder.id,
|
||||
visibilityTypeId: visibility.id
|
||||
});
|
||||
rootFolder = await Folder.findOne({
|
||||
where: { id: rootFolder.id },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
attributes: ['id'],
|
||||
}]
|
||||
});
|
||||
}
|
||||
const children = await this.getSubFolders(rootFolder.id, userId);
|
||||
const rootFolder = await this.ensureRootFolder(userId);
|
||||
const children = await this.getSubFolders(rootFolder.id, userId, false);
|
||||
rootFolder = rootFolder.get();
|
||||
rootFolder.visibilityTypeIds = rootFolder.image_visibility_types.map(v => v.id);
|
||||
delete rootFolder.image_visibility_types;
|
||||
@@ -133,9 +254,9 @@ class SocialNetworkService extends BaseService {
|
||||
return rootFolder;
|
||||
}
|
||||
|
||||
async getSubFolders(parentId, userId) {
|
||||
async getSubFolders(parentId, userId, isAdultArea = false) {
|
||||
const folders = await Folder.findAll({
|
||||
where: { parentId, userId },
|
||||
where: { parentId, userId, isAdultArea },
|
||||
include: [{
|
||||
model: ImageVisibilityType,
|
||||
through: { model: FolderImageVisibility },
|
||||
@@ -146,7 +267,7 @@ class SocialNetworkService extends BaseService {
|
||||
]
|
||||
});
|
||||
for (const folder of folders) {
|
||||
const children = await this.getSubFolders(folder.id, userId);
|
||||
const children = await this.getSubFolders(folder.id, userId, isAdultArea);
|
||||
const visibilityTypeIds = folder.image_visibility_types.map(v => v.id);
|
||||
folder.setDataValue('visibilityTypeIds', visibilityTypeIds);
|
||||
folder.setDataValue('children', children);
|
||||
@@ -160,13 +281,15 @@ class SocialNetworkService extends BaseService {
|
||||
const folder = await Folder.findOne({
|
||||
where: {
|
||||
id: folderId,
|
||||
userId
|
||||
userId,
|
||||
isAdultArea: false
|
||||
}
|
||||
});
|
||||
if (!folder) throw new Error('Folder not found');
|
||||
return await Image.findAll({
|
||||
where: {
|
||||
folderId: folder.id
|
||||
folderId: folder.id,
|
||||
isAdultContent: false
|
||||
},
|
||||
order: [
|
||||
['title', 'asc']
|
||||
@@ -176,13 +299,13 @@ class SocialNetworkService extends BaseService {
|
||||
|
||||
async uploadImage(hashedId, file, formData) {
|
||||
const userId = await this.getUserId(hashedId);
|
||||
const processedImageName = await this.processAndUploadUserImage(file);
|
||||
const newImage = await this.createImageRecord(formData, userId, file, processedImageName);
|
||||
const processedImageName = await this.processAndUploadUserImage(file, 'user');
|
||||
const newImage = await this.createImageRecord(formData, userId, file, processedImageName, { isAdultContent: false });
|
||||
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
||||
return newImage;
|
||||
}
|
||||
|
||||
async processAndUploadUserImage(file) {
|
||||
async processAndUploadUserImage(file, storageType = 'user') {
|
||||
try {
|
||||
const img = sharp(file.buffer);
|
||||
const metadata = await img.metadata();
|
||||
@@ -199,7 +322,7 @@ class SocialNetworkService extends BaseService {
|
||||
withoutEnlargement: true
|
||||
});
|
||||
const newFileName = this.generateUniqueFileName(file.originalname);
|
||||
const filePath = this.buildFilePath(newFileName, 'user');
|
||||
const filePath = this.buildFilePath(newFileName, storageType);
|
||||
await resizedImg.toFile(filePath);
|
||||
return newFileName;
|
||||
} catch (error) {
|
||||
@@ -231,7 +354,7 @@ class SocialNetworkService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async createImageRecord(formData, userId, file, fileName) {
|
||||
async createImageRecord(formData, userId, file, fileName, options = {}) {
|
||||
try {
|
||||
return await Image.create({
|
||||
title: formData.title,
|
||||
@@ -240,6 +363,7 @@ class SocialNetworkService extends BaseService {
|
||||
hash: fileName,
|
||||
folderId: formData.folderId,
|
||||
userId: userId,
|
||||
isAdultContent: Boolean(options.isAdultContent),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create image record: ${error.message}`);
|
||||
@@ -271,6 +395,7 @@ class SocialNetworkService extends BaseService {
|
||||
async getImage(imageId) {
|
||||
const image = await Image.findByPk(imageId);
|
||||
if (!image) throw new Error('Image not found');
|
||||
if (image.isAdultContent) throw new Error('Access denied');
|
||||
await this.checkUserAccess(image.userId);
|
||||
return image;
|
||||
}
|
||||
@@ -455,6 +580,9 @@ class SocialNetworkService extends BaseService {
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
if (image.isAdultContent) {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
const userId = await this.checkUserAccess(hashedUserId);
|
||||
const hasAccess = await this.checkUserImageAccess(userId, image.id);
|
||||
if (!hasAccess) {
|
||||
@@ -467,6 +595,178 @@ class SocialNetworkService extends BaseService {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
async getAdultFolders(hashedId) {
|
||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||
const rootFolder = await this.ensureAdultRootFolder(userId);
|
||||
const children = await this.getSubFolders(rootFolder.id, userId, true);
|
||||
const data = rootFolder.get();
|
||||
data.visibilityTypeIds = data.image_visibility_types.map(v => v.id);
|
||||
delete data.image_visibility_types;
|
||||
data.children = children;
|
||||
return data;
|
||||
}
|
||||
|
||||
async getAdultFolderImageList(hashedId, folderId) {
|
||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||
const folder = await Folder.findOne({
|
||||
where: { id: folderId, userId, isAdultArea: true }
|
||||
});
|
||||
if (!folder) {
|
||||
throw new Error('Folder not found');
|
||||
}
|
||||
return await Image.findAll({
|
||||
where: {
|
||||
folderId: folder.id,
|
||||
isAdultContent: true,
|
||||
userId
|
||||
},
|
||||
order: [['title', 'asc']]
|
||||
});
|
||||
}
|
||||
|
||||
async createAdultFolder(hashedId, data, folderId) {
|
||||
await this.requireAdultAreaAccessByHash(hashedId);
|
||||
if (!data.parentId) {
|
||||
const userId = await this.checkUserAccess(hashedId);
|
||||
const adultRoot = await this.ensureAdultRootFolder(userId);
|
||||
data.parentId = adultRoot.id;
|
||||
}
|
||||
return this.createFolder(hashedId, data, folderId, { isAdultArea: true });
|
||||
}
|
||||
|
||||
async uploadAdultImage(hashedId, file, formData) {
|
||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||
await this.ensureAdultUploadsAllowed(userId);
|
||||
const folder = await Folder.findOne({
|
||||
where: {
|
||||
id: formData.folderId,
|
||||
userId,
|
||||
isAdultArea: true
|
||||
}
|
||||
});
|
||||
if (!folder) {
|
||||
throw new Error('Folder not found');
|
||||
}
|
||||
const processedImageName = await this.processAndUploadUserImage(file, 'erotic');
|
||||
const newImage = await this.createImageRecord(formData, userId, file, processedImageName, { isAdultContent: true });
|
||||
await this.saveImageVisibilities(newImage.id, formData.visibility);
|
||||
return newImage;
|
||||
}
|
||||
|
||||
async getAdultImageFilePath(hashedId, hash) {
|
||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||
const image = await Image.findOne({
|
||||
where: {
|
||||
hash,
|
||||
userId,
|
||||
isAdultContent: true
|
||||
}
|
||||
});
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
if (image.isModeratedHidden) {
|
||||
throw new Error('Image hidden by moderation');
|
||||
}
|
||||
const imagePath = this.buildFilePath(image.hash, 'erotic');
|
||||
if (!fs.existsSync(imagePath)) {
|
||||
throw new Error(`File "${imagePath}" not found`);
|
||||
}
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
async listEroticVideos(hashedId) {
|
||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||
return await EroticVideo.findAll({
|
||||
where: { userId },
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
}
|
||||
|
||||
async uploadEroticVideo(hashedId, file, formData) {
|
||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||
await this.ensureAdultUploadsAllowed(userId);
|
||||
if (!file) {
|
||||
throw new Error('Video file is required');
|
||||
}
|
||||
const allowedMimeTypes = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime'];
|
||||
if (!allowedMimeTypes.includes(file.mimetype)) {
|
||||
throw new Error('Unsupported video format');
|
||||
}
|
||||
|
||||
const fileName = this.generateUniqueFileName(file.originalname);
|
||||
const filePath = this.buildFilePath(fileName, 'erotic-video');
|
||||
await this.saveFile(file.buffer, filePath);
|
||||
|
||||
return await EroticVideo.create({
|
||||
title: formData.title || file.originalname,
|
||||
description: formData.description || null,
|
||||
originalFileName: file.originalname,
|
||||
hash: fileName,
|
||||
mimeType: file.mimetype,
|
||||
userId
|
||||
});
|
||||
}
|
||||
|
||||
async getEroticVideoFilePath(hashedId, hash) {
|
||||
const userId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||
const video = await EroticVideo.findOne({
|
||||
where: { hash, userId }
|
||||
});
|
||||
if (!video) {
|
||||
throw new Error('Video not found');
|
||||
}
|
||||
if (video.isModeratedHidden) {
|
||||
throw new Error('Video hidden by moderation');
|
||||
}
|
||||
const videoPath = this.buildFilePath(video.hash, 'erotic-video');
|
||||
if (!fs.existsSync(videoPath)) {
|
||||
throw new Error(`File "${videoPath}" not found`);
|
||||
}
|
||||
return { filePath: videoPath, mimeType: video.mimeType };
|
||||
}
|
||||
|
||||
async createEroticContentReport(hashedId, payload) {
|
||||
const reporterId = await this.requireAdultAreaAccessByHash(hashedId);
|
||||
const targetType = String(payload.targetType || '').trim().toLowerCase();
|
||||
const targetId = Number(payload.targetId);
|
||||
const reason = String(payload.reason || '').trim().toLowerCase();
|
||||
const note = payload.note ? String(payload.note).trim() : null;
|
||||
|
||||
if (!['image', 'video'].includes(targetType) || !Number.isInteger(targetId) || targetId <= 0) {
|
||||
throw new Error('Invalid report target');
|
||||
}
|
||||
if (!['suspected_minor', 'non_consensual', 'violence', 'harassment', 'spam', 'other'].includes(reason)) {
|
||||
throw new Error('Invalid report reason');
|
||||
}
|
||||
|
||||
const { ownerId } = await this.resolveEroticTarget(targetType, targetId);
|
||||
if (ownerId === reporterId) {
|
||||
throw new Error('Own content cannot be reported');
|
||||
}
|
||||
|
||||
const existingOpen = await EroticContentReport.findOne({
|
||||
where: {
|
||||
reporterId,
|
||||
targetType,
|
||||
targetId,
|
||||
status: 'open'
|
||||
}
|
||||
});
|
||||
if (existingOpen) {
|
||||
return existingOpen;
|
||||
}
|
||||
|
||||
return await EroticContentReport.create({
|
||||
reporterId,
|
||||
targetType,
|
||||
targetId,
|
||||
reason,
|
||||
note,
|
||||
status: 'open'
|
||||
});
|
||||
}
|
||||
|
||||
// Public variant used by blog: allow access if the image's folder is visible to 'everyone'.
|
||||
async getImageFilePathPublicByHash(hash) {
|
||||
const image = await Image.findOne({ where: { hash } });
|
||||
@@ -510,7 +810,7 @@ class SocialNetworkService extends BaseService {
|
||||
async changeImage(hashedUserId, imageId, title, visibilities) {
|
||||
const userId = await this.checkUserAccess(hashedUserId);
|
||||
await this.checkUserImageAccess(userId, imageId);
|
||||
const image = await Image.findOne({ where: { id: imageId } });
|
||||
const image = await Image.findOne({ where: { id: imageId, isAdultContent: false } });
|
||||
if (!image) {
|
||||
throw new Error('image not found')
|
||||
}
|
||||
@@ -522,13 +822,33 @@ class SocialNetworkService extends BaseService {
|
||||
return image.folderId;
|
||||
}
|
||||
|
||||
async changeAdultImage(hashedUserId, imageId, title, visibilities) {
|
||||
const userId = await this.requireAdultAreaAccessByHash(hashedUserId);
|
||||
const image = await Image.findOne({
|
||||
where: {
|
||||
id: imageId,
|
||||
userId,
|
||||
isAdultContent: true
|
||||
}
|
||||
});
|
||||
if (!image) {
|
||||
throw new Error('image not found');
|
||||
}
|
||||
await image.update({ title });
|
||||
await ImageImageVisibility.destroy({ where: { imageId } });
|
||||
for (const visibility of visibilities) {
|
||||
await ImageImageVisibility.create({ imageId, visibilityTypeId: visibility.id });
|
||||
}
|
||||
return image.folderId;
|
||||
}
|
||||
|
||||
async getFoldersByUsername(username, hashedUserId) {
|
||||
const user = await this.loadUserByName(username);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
const requestingUserId = await this.checkUserAccess(hashedUserId);
|
||||
let rootFolder = await Folder.findOne({ where: { parentId: null, userId: user.id } });
|
||||
let rootFolder = await Folder.findOne({ where: { parentId: null, userId: user.id, isAdultArea: false } });
|
||||
if (!rootFolder) {
|
||||
return null;
|
||||
}
|
||||
@@ -542,9 +862,9 @@ class SocialNetworkService extends BaseService {
|
||||
const folderIdString = String(folderId);
|
||||
const requestingUserIdString = String(requestingUserId);
|
||||
const requestingUser = await User.findOne({ where: { id: requestingUserIdString } });
|
||||
const isAdult = this.isUserAdult(requestingUser.id);
|
||||
const isAdult = await this.isUserAdult(requestingUser.id);
|
||||
const accessibleFolders = await Folder.findAll({
|
||||
where: { parentId: folderIdString },
|
||||
where: { parentId: folderIdString, isAdultArea: false },
|
||||
include: [
|
||||
{
|
||||
model: ImageVisibilityType,
|
||||
|
||||
Reference in New Issue
Block a user