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:
@@ -29,6 +29,12 @@ class AdminController {
|
||||
this.getUser = this.getUser.bind(this);
|
||||
this.getUsers = this.getUsers.bind(this);
|
||||
this.updateUser = this.updateUser.bind(this);
|
||||
this.getAdultVerificationRequests = this.getAdultVerificationRequests.bind(this);
|
||||
this.setAdultVerificationStatus = this.setAdultVerificationStatus.bind(this);
|
||||
this.getAdultVerificationDocument = this.getAdultVerificationDocument.bind(this);
|
||||
this.getEroticModerationReports = this.getEroticModerationReports.bind(this);
|
||||
this.applyEroticModerationAction = this.applyEroticModerationAction.bind(this);
|
||||
this.getEroticModerationPreview = this.getEroticModerationPreview.bind(this);
|
||||
|
||||
// Rights
|
||||
this.listRightTypes = this.listRightTypes.bind(this);
|
||||
@@ -119,6 +125,97 @@ class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
async getAdultVerificationRequests(req, res) {
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
const { status = 'pending' } = req.query;
|
||||
const result = await AdminService.getAdultVerificationRequests(requester, status);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
const status = error.message === 'noaccess' ? 403 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async setAdultVerificationStatus(req, res) {
|
||||
const schema = Joi.object({
|
||||
status: Joi.string().valid('approved', 'rejected', 'pending').required()
|
||||
});
|
||||
const { error, value } = schema.validate(req.body || {});
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
const { id } = req.params;
|
||||
const result = await AdminService.setAdultVerificationStatus(requester, id, value.status);
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
const status = err.message === 'noaccess' ? 403 : (['notfound', 'notadult', 'wrongstatus', 'missingparamtype'].includes(err.message) ? 400 : 500);
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getAdultVerificationDocument(req, res) {
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
const { id } = req.params;
|
||||
const result = await AdminService.getAdultVerificationDocument(requester, id);
|
||||
res.setHeader('Content-Type', result.mimeType);
|
||||
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(result.originalName)}"`);
|
||||
res.sendFile(result.filePath);
|
||||
} catch (err) {
|
||||
const status = err.message === 'noaccess' ? 403 : (['notfound', 'norequest', 'nofile'].includes(err.message) ? 404 : 500);
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getEroticModerationReports(req, res) {
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
const { status = 'open' } = req.query;
|
||||
const result = await AdminService.getEroticModerationReports(requester, status);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
const status = error.message === 'noaccess' ? 403 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async applyEroticModerationAction(req, res) {
|
||||
const schema = Joi.object({
|
||||
action: Joi.string().valid('dismiss', 'hide_content', 'restore_content', 'delete_content', 'block_uploads', 'revoke_access').required(),
|
||||
note: Joi.string().allow('', null).max(2000).optional()
|
||||
});
|
||||
const { error, value } = schema.validate(req.body || {});
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
const { id } = req.params;
|
||||
const result = await AdminService.applyEroticModerationAction(requester, Number(id), value.action, value.note || null);
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
const status = err.message === 'noaccess' ? 403 : (['notfound', 'targetnotfound', 'wrongaction'].includes(err.message) ? 400 : 500);
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getEroticModerationPreview(req, res) {
|
||||
try {
|
||||
const { userid: requester } = req.headers;
|
||||
const { type, targetId } = req.params;
|
||||
const result = await AdminService.getEroticModerationPreview(requester, type, Number(targetId));
|
||||
res.setHeader('Content-Type', result.mimeType);
|
||||
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(result.originalName)}"`);
|
||||
res.sendFile(result.filePath);
|
||||
} catch (err) {
|
||||
const status = err.message === 'noaccess' ? 403 : (['notfound', 'nofile', 'wrongtype'].includes(err.message) ? 404 : 500);
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Rights ---
|
||||
async listRightTypes(req, res) {
|
||||
try {
|
||||
@@ -523,6 +620,7 @@ class AdminController {
|
||||
title: Joi.string().min(1).max(255).required(),
|
||||
roomTypeId: Joi.number().integer().required(),
|
||||
isPublic: Joi.boolean().required(),
|
||||
isAdultOnly: Joi.boolean().allow(null),
|
||||
genderRestrictionId: Joi.number().integer().allow(null),
|
||||
minAge: Joi.number().integer().min(0).allow(null),
|
||||
maxAge: Joi.number().integer().min(0).allow(null),
|
||||
@@ -534,7 +632,7 @@ class AdminController {
|
||||
if (error) {
|
||||
return res.status(400).json({ error: error.details[0].message });
|
||||
}
|
||||
const room = await AdminService.updateRoom(req.params.id, value);
|
||||
const room = await AdminService.updateRoom(userId, req.params.id, value);
|
||||
res.status(200).json(room);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -553,6 +651,7 @@ class AdminController {
|
||||
title: Joi.string().min(1).max(255).required(),
|
||||
roomTypeId: Joi.number().integer().required(),
|
||||
isPublic: Joi.boolean().required(),
|
||||
isAdultOnly: Joi.boolean().allow(null),
|
||||
genderRestrictionId: Joi.number().integer().allow(null),
|
||||
minAge: Joi.number().integer().min(0).allow(null),
|
||||
maxAge: Joi.number().integer().min(0).allow(null),
|
||||
@@ -579,7 +678,7 @@ class AdminController {
|
||||
if (!userId || !(await AdminService.hasUserAccess(userId, 'chatrooms'))) {
|
||||
return res.status(403).json({ error: 'Keine Berechtigung.' });
|
||||
}
|
||||
await AdminService.deleteRoom(req.params.id);
|
||||
await AdminService.deleteRoom(userId, req.params.id);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
@@ -172,7 +172,9 @@ class ChatController {
|
||||
async getRoomList(req, res) {
|
||||
// Öffentliche Räume für Chat-Frontend
|
||||
try {
|
||||
const rooms = await chatService.getRoomList();
|
||||
const { userid: hashedUserId } = req.headers;
|
||||
const adultOnly = String(req.query.adultOnly || '').toLowerCase() === 'true';
|
||||
const rooms = await chatService.getRoomList(hashedUserId, { adultOnly });
|
||||
res.status(200).json(rooms);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
@@ -258,6 +258,14 @@ const menuStructure = {
|
||||
visible: ["mainadmin", "useradministration"],
|
||||
path: "/admin/users"
|
||||
},
|
||||
adultverification: {
|
||||
visible: ["mainadmin", "useradministration"],
|
||||
path: "/admin/users/adult-verification"
|
||||
},
|
||||
eroticmoderation: {
|
||||
visible: ["mainadmin", "useradministration"],
|
||||
path: "/admin/users/erotic-moderation"
|
||||
},
|
||||
userstatistics: {
|
||||
visible: ["mainadmin"],
|
||||
path: "/admin/users/statistics"
|
||||
@@ -343,7 +351,14 @@ class NavigationController {
|
||||
return age;
|
||||
}
|
||||
|
||||
async filterMenu(menu, rights, age, userId) {
|
||||
normalizeAdultVerificationStatus(value) {
|
||||
if (['pending', 'approved', 'rejected'].includes(value)) {
|
||||
return value;
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
async filterMenu(menu, rights, age, userId, adultVerificationStatus = 'none') {
|
||||
const filteredMenu = {};
|
||||
try {
|
||||
const hasFalukantAccount = await this.hasFalukantAccount(userId);
|
||||
@@ -357,8 +372,17 @@ class NavigationController {
|
||||
|| (value.visible.includes('hasfalukantaccount') && hasFalukantAccount)) {
|
||||
const { visible, ...itemWithoutVisible } = value;
|
||||
filteredMenu[key] = { ...itemWithoutVisible };
|
||||
if (
|
||||
value.visible.includes("over18")
|
||||
&& age >= 18
|
||||
&& adultVerificationStatus !== 'approved'
|
||||
&& (value.path || value.action || value.view)
|
||||
) {
|
||||
filteredMenu[key].disabled = true;
|
||||
filteredMenu[key].disabledReasonKey = 'socialnetwork.erotic.lockedShort';
|
||||
}
|
||||
if (value.children) {
|
||||
filteredMenu[key].children = await this.filterMenu(value.children, rights, age, userId);
|
||||
filteredMenu[key].children = await this.filterMenu(value.children, rights, age, userId, adultVerificationStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,20 +409,29 @@ class NavigationController {
|
||||
required: false
|
||||
}]
|
||||
});
|
||||
const userBirthdateParams = await UserParam.findAll({
|
||||
const userParams = await UserParam.findAll({
|
||||
where: { userId: user.id },
|
||||
include: [
|
||||
{
|
||||
model: UserParamType,
|
||||
as: 'paramType',
|
||||
where: { description: 'birthdate' }
|
||||
where: { description: ['birthdate', 'adult_verification_status'] }
|
||||
}
|
||||
]
|
||||
});
|
||||
const birthDate = userBirthdateParams.length > 0 ? userBirthdateParams[0].value : (new Date()).toDateString();
|
||||
let birthDate = (new Date()).toDateString();
|
||||
let adultVerificationStatus = 'none';
|
||||
for (const param of userParams) {
|
||||
if (param.paramType?.description === 'birthdate' && param.value) {
|
||||
birthDate = param.value;
|
||||
}
|
||||
if (param.paramType?.description === 'adult_verification_status') {
|
||||
adultVerificationStatus = this.normalizeAdultVerificationStatus(param.value);
|
||||
}
|
||||
}
|
||||
const age = this.calculateAge(birthDate);
|
||||
const rights = userRights.map(ur => ur.rightType?.title).filter(Boolean);
|
||||
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id);
|
||||
const filteredMenu = await this.filterMenu(menuStructure, rights, age, user.id, adultVerificationStatus);
|
||||
|
||||
// Vokabeltrainer: Sprachen werden im Frontend dynamisch geladen (wie Forum)
|
||||
// Keine children mehr, da das Menü nur 2 Ebenen unterstützt
|
||||
|
||||
@@ -217,6 +217,25 @@ class SettingsController {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
async submitAdultVerificationRequest(req, res) {
|
||||
try {
|
||||
const hashedUserId = req.headers.userid;
|
||||
const note = req.body?.note || '';
|
||||
const file = req.file || null;
|
||||
const result = await settingsService.submitAdultVerificationRequest(hashedUserId, { note }, file);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('Error submitting adult verification request:', error);
|
||||
const status = [
|
||||
'User not found',
|
||||
'Adult verification can only be requested by adult users',
|
||||
'No verification document provided',
|
||||
'Unsupported verification document type'
|
||||
].includes(error.message) ? 400 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsController;
|
||||
|
||||
@@ -15,6 +15,16 @@ class SocialNetworkController {
|
||||
this.changeImage = this.changeImage.bind(this);
|
||||
this.getFoldersByUsername = this.getFoldersByUsername.bind(this);
|
||||
this.deleteFolder = this.deleteFolder.bind(this);
|
||||
this.getAdultFolders = this.getAdultFolders.bind(this);
|
||||
this.createAdultFolder = this.createAdultFolder.bind(this);
|
||||
this.getAdultFolderImageList = this.getAdultFolderImageList.bind(this);
|
||||
this.uploadAdultImage = this.uploadAdultImage.bind(this);
|
||||
this.getAdultImageByHash = this.getAdultImageByHash.bind(this);
|
||||
this.changeAdultImage = this.changeAdultImage.bind(this);
|
||||
this.listEroticVideos = this.listEroticVideos.bind(this);
|
||||
this.uploadEroticVideo = this.uploadEroticVideo.bind(this);
|
||||
this.getEroticVideoByHash = this.getEroticVideoByHash.bind(this);
|
||||
this.reportEroticContent = this.reportEroticContent.bind(this);
|
||||
this.createGuestbookEntry = this.createGuestbookEntry.bind(this);
|
||||
this.getGuestbookEntries = this.getGuestbookEntries.bind(this);
|
||||
this.deleteGuestbookEntry = this.deleteGuestbookEntry.bind(this);
|
||||
@@ -187,6 +197,138 @@ class SocialNetworkController {
|
||||
}
|
||||
}
|
||||
|
||||
async getAdultFolders(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const folders = await this.socialNetworkService.getAdultFolders(userId);
|
||||
res.status(200).json(folders);
|
||||
} catch (error) {
|
||||
console.error('Error in getAdultFolders:', error);
|
||||
res.status(error.status || 500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async createAdultFolder(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const folderData = req.body;
|
||||
const { folderId } = req.params;
|
||||
const folder = await this.socialNetworkService.createAdultFolder(userId, folderData, folderId);
|
||||
res.status(201).json(folder);
|
||||
} catch (error) {
|
||||
console.error('Error in createAdultFolder:', error);
|
||||
res.status(error.status || 500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getAdultFolderImageList(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const { folderId } = req.params;
|
||||
const images = await this.socialNetworkService.getAdultFolderImageList(userId, folderId);
|
||||
res.status(200).json(images);
|
||||
} catch (error) {
|
||||
console.error('Error in getAdultFolderImageList:', error);
|
||||
res.status(error.status || 500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async uploadAdultImage(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const file = req.file;
|
||||
const formData = req.body;
|
||||
const image = await this.socialNetworkService.uploadAdultImage(userId, file, formData);
|
||||
res.status(201).json(image);
|
||||
} catch (error) {
|
||||
console.error('Error in uploadAdultImage:', error);
|
||||
res.status(error.status || 500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getAdultImageByHash(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const { hash } = req.params;
|
||||
const filePath = await this.socialNetworkService.getAdultImageFilePath(userId, hash);
|
||||
res.sendFile(filePath, err => {
|
||||
if (err) {
|
||||
console.error('Error sending adult file:', err);
|
||||
res.status(500).json({ error: 'Error sending file' });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in getAdultImageByHash:', error);
|
||||
res.status(error.status || 403).json({ error: error.message || 'Access denied or image not found' });
|
||||
}
|
||||
}
|
||||
|
||||
async changeAdultImage(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const { imageId } = req.params;
|
||||
const { title, visibilities } = req.body;
|
||||
const folderId = await this.socialNetworkService.changeAdultImage(userId, imageId, title, visibilities);
|
||||
res.status(201).json(await this.socialNetworkService.getAdultFolderImageList(userId, folderId));
|
||||
} catch (error) {
|
||||
console.error('Error in changeAdultImage:', error);
|
||||
res.status(error.status || 403).json({ error: error.message || 'Access denied or image not found' });
|
||||
}
|
||||
}
|
||||
|
||||
async listEroticVideos(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const videos = await this.socialNetworkService.listEroticVideos(userId);
|
||||
res.status(200).json(videos);
|
||||
} catch (error) {
|
||||
console.error('Error in listEroticVideos:', error);
|
||||
res.status(error.status || 500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async uploadEroticVideo(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const file = req.file;
|
||||
const formData = req.body;
|
||||
const video = await this.socialNetworkService.uploadEroticVideo(userId, file, formData);
|
||||
res.status(201).json(video);
|
||||
} catch (error) {
|
||||
console.error('Error in uploadEroticVideo:', error);
|
||||
res.status(error.status || 500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async getEroticVideoByHash(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const { hash } = req.params;
|
||||
const { filePath, mimeType } = await this.socialNetworkService.getEroticVideoFilePath(userId, hash);
|
||||
res.type(mimeType);
|
||||
res.sendFile(filePath, err => {
|
||||
if (err) {
|
||||
console.error('Error sending adult video:', err);
|
||||
res.status(500).json({ error: 'Error sending file' });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in getEroticVideoByHash:', error);
|
||||
res.status(error.status || 403).json({ error: error.message || 'Access denied or video not found' });
|
||||
}
|
||||
}
|
||||
|
||||
async reportEroticContent(req, res) {
|
||||
try {
|
||||
const userId = req.headers.userid;
|
||||
const result = await this.socialNetworkService.createEroticContentReport(userId, req.body || {});
|
||||
res.status(201).json(result);
|
||||
} catch (error) {
|
||||
console.error('Error in reportEroticContent:', error);
|
||||
res.status(error.status || 400).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async createGuestbookEntry(req, res) {
|
||||
try {
|
||||
const { htmlContent, recipientName } = req.body;
|
||||
|
||||
Reference in New Issue
Block a user