- Enhanced deploy-backend.sh and update-backend.sh to create the adult verification directory under /opt/yourpart-data. - Updated permissions for the new directory to ensure proper access control. - Refactored file path handling in AdminService and SettingsService to utilize the new directory structure for adult verification files.
1728 lines
61 KiB
JavaScript
1728 lines
61 KiB
JavaScript
import RoomType from '../models/chat/room_type.js';
|
|
import ChatRight from '../models/chat/rights.js';
|
|
import UserRight from "../models/community/user_right.js";
|
|
import UserRightType from "../models/type/user_right.js";
|
|
import InterestType from "../models/type/interest.js";
|
|
import InterestTranslationType from "../models/type/interest_translation.js";
|
|
import User from "../models/community/user.js";
|
|
import UserParamValue from "../models/type/user_param_value.js";
|
|
import UserParamType from "../models/type/user_param.js";
|
|
import ContactMessage from "../models/service/contactmessage.js";
|
|
import ContactService from "./ContactService.js";
|
|
import { sendAnswerEmail } from './emailService.js';
|
|
import { Op, Sequelize } from 'sequelize';
|
|
import FalukantUser from "../models/falukant/data/user.js";
|
|
import FalukantCharacter from "../models/falukant/data/character.js";
|
|
import FalukantPredefineFirstname from "../models/falukant/predefine/firstname.js";
|
|
import FalukantPredefineLastname from "../models/falukant/predefine/lastname.js";
|
|
import Branch from "../models/falukant/data/branch.js";
|
|
import FalukantStock from "../models/falukant/data/stock.js";
|
|
import FalukantStockType from "../models/falukant/type/stock.js";
|
|
import RegionData from "../models/falukant/data/region.js";
|
|
import RegionType from "../models/falukant/type/region.js";
|
|
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';
|
|
import { getAdultVerificationBaseDir, getLegacyAdultVerificationBaseDir } from '../utils/storagePaths.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
class AdminService {
|
|
resolveAdultVerificationFile(requestData) {
|
|
if (!requestData || typeof requestData !== 'object') {
|
|
return null;
|
|
}
|
|
|
|
const candidates = [];
|
|
if (requestData.fileName) {
|
|
candidates.push(path.join(getAdultVerificationBaseDir(), requestData.fileName));
|
|
candidates.push(path.join(getLegacyAdultVerificationBaseDir(), requestData.fileName));
|
|
}
|
|
if (requestData.storedFileName && requestData.storedFileName !== requestData.fileName) {
|
|
candidates.push(path.join(getAdultVerificationBaseDir(), requestData.storedFileName));
|
|
candidates.push(path.join(getLegacyAdultVerificationBaseDir(), requestData.storedFileName));
|
|
}
|
|
if (requestData.filePath) {
|
|
candidates.push(path.isAbsolute(requestData.filePath)
|
|
? requestData.filePath
|
|
: path.join(__dirname, '..', requestData.filePath));
|
|
}
|
|
|
|
return candidates.find((candidate) => candidate && fs.existsSync(candidate)) || null;
|
|
}
|
|
|
|
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: [{
|
|
model: UserRightType,
|
|
as: 'rightType',
|
|
where: {
|
|
title: [section, 'mainadmin'],
|
|
}
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'user_with_rights',
|
|
where: {
|
|
hashedId: userId,
|
|
}
|
|
}
|
|
]
|
|
|
|
});
|
|
return userRights.length > 0;
|
|
}
|
|
|
|
async getOpenInterests(userId) {
|
|
if (!this.hasUserAccess(userId, 'interests')) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const openInterests = await InterestType.findAll({
|
|
where: {
|
|
allowed: false
|
|
},
|
|
include: {
|
|
model: InterestTranslationType,
|
|
as: 'interest_translations',
|
|
}
|
|
})
|
|
return openInterests;
|
|
}
|
|
|
|
async changeInterest(userId, interestId, active, adultOnly) {
|
|
if (!this.hasUserAccess(userId, 'interests')) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const interest = await InterestType.findOne({
|
|
where: {
|
|
id: interestId
|
|
}
|
|
});
|
|
if (interest) {
|
|
interest.allowed = active;
|
|
interest.adultOnly = adultOnly;
|
|
await interest.save();
|
|
}
|
|
}
|
|
|
|
async deleteInterest(userId, interestId) {
|
|
if (!this.hasUserAccess(userId, 'interests')) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const interest = await InterestType.findOne({
|
|
where: {
|
|
id: interestId
|
|
}
|
|
});
|
|
if (interest) {
|
|
await interest.destroy();
|
|
}
|
|
}
|
|
|
|
async changeTranslation(userId, interestId, translations) {
|
|
if (!this.hasUserAccess(userId, 'interests')) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const interest = await InterestType.findOne({
|
|
id: interestId
|
|
});
|
|
if (!interest) {
|
|
throw new Error('notexisting');
|
|
}
|
|
for (const languageId of Object.keys(translations)) {
|
|
const languageObject = await UserParamValue.findOne(
|
|
{
|
|
where: {
|
|
id: languageId
|
|
}
|
|
}
|
|
);
|
|
if (!languageObject) {
|
|
throw new Error('wronglanguage');
|
|
}
|
|
const translation = await InterestTranslationType.findOne(
|
|
{
|
|
where: {
|
|
interestsId: interestId,
|
|
language: languageObject.id
|
|
}
|
|
}
|
|
);
|
|
if (translation) {
|
|
translation.translation = translations[languageId];
|
|
translation.save();
|
|
} else {
|
|
await InterestTranslationType.create({
|
|
interestsId: interestId,
|
|
language: languageObject.id,
|
|
translation: translations[languageId]
|
|
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
async getOpenContacts(userId) {
|
|
if (!this.hasUserAccess(userId, 'contacts')) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const openContacts = await ContactMessage.findAll({
|
|
where: {
|
|
isFinished: false,
|
|
}
|
|
})
|
|
return openContacts;
|
|
}
|
|
|
|
async answerContact(contactId, answer) {
|
|
const contact = await ContactService.getContactById(contactId);
|
|
await ContactService.saveAnswer(contact, answer);
|
|
await sendAnswerEmail(contact.email, answer, contact.language || 'en');
|
|
}
|
|
|
|
async getFalukantUser(userId, userName, characterName) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
let users;
|
|
if (userName) {
|
|
users = await User.findAll({
|
|
where: {
|
|
username: {
|
|
[Op.like]: '%' + userName + '%'
|
|
}
|
|
},
|
|
include: [{
|
|
model: FalukantUser,
|
|
as: 'falukantData',
|
|
required: true,
|
|
include: [{
|
|
model: FalukantCharacter,
|
|
as: 'character',
|
|
required: true,
|
|
include: [{
|
|
model: FalukantPredefineFirstname,
|
|
as: 'definedFirstName',
|
|
required: true
|
|
}, {
|
|
model: FalukantPredefineLastname,
|
|
as: 'definedLastName',
|
|
required: true
|
|
}]
|
|
}]
|
|
}]
|
|
});
|
|
} else if (characterName) {
|
|
const [firstname, lastname] = characterName.split(' ');
|
|
users = await User.findAll({
|
|
include: [{
|
|
model: FalukantUser,
|
|
as: 'falukantData',
|
|
required: true,
|
|
include: [{
|
|
model: FalukantCharacter,
|
|
as: 'character',
|
|
required: true,
|
|
include: [{
|
|
model: FalukantPredefineFirstname,
|
|
as: 'definedFirstName',
|
|
required: true,
|
|
where: {
|
|
name: firstname
|
|
}
|
|
}, {
|
|
model: FalukantPredefineLastname,
|
|
as: 'definedLastName',
|
|
required: true,
|
|
where: {
|
|
name: lastname
|
|
}
|
|
}]
|
|
}]
|
|
}]
|
|
});
|
|
} else {
|
|
throw new Error('no search parameter');
|
|
}
|
|
return users.map(user => {
|
|
return {
|
|
id: user.hashedId,
|
|
username: user.username,
|
|
falukantUser: user.falukantData
|
|
}
|
|
});
|
|
}
|
|
|
|
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', 'adult_verification_request'] }
|
|
}
|
|
]
|
|
}
|
|
],
|
|
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;
|
|
}
|
|
const resolvedDocumentPath = this.resolveAdultVerificationFile(verificationRequest);
|
|
return {
|
|
id: user.hashedId,
|
|
username: user.username,
|
|
active: !!user.active,
|
|
age,
|
|
adultVerificationStatus: verificationStatus,
|
|
adultVerificationRequest: verificationRequest,
|
|
adultVerificationDocumentAvailable: !!resolvedDocumentPath
|
|
};
|
|
}).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 = this.resolveAdultVerificationFile(requestData);
|
|
if (!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');
|
|
}
|
|
const user = await User.findOne({
|
|
where: {
|
|
hashedId: hashedId
|
|
},
|
|
attributes: ['hashedId', 'username'],
|
|
include: [{
|
|
model: FalukantUser,
|
|
as: 'falukantData',
|
|
required: true,
|
|
attributes: ['money', 'certificate', 'id'],
|
|
include: [{
|
|
model: FalukantCharacter,
|
|
as: 'character',
|
|
attributes: ['birthdate', 'health', 'title_of_nobility'],
|
|
include: [{
|
|
model: FalukantPredefineFirstname,
|
|
as: 'definedFirstName',
|
|
}, {
|
|
model: FalukantPredefineLastname,
|
|
as: 'definedLastName',
|
|
}]
|
|
}]
|
|
}]
|
|
});
|
|
return user;
|
|
}
|
|
|
|
async getFalukantUserBranches(userId, falukantUserId) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
try {
|
|
// Zuerst die Branches laden
|
|
const branches = await Branch.findAll({
|
|
where: {
|
|
falukantUserId: falukantUserId
|
|
}
|
|
});
|
|
|
|
// Dann für jede Branch die zusätzlichen Daten laden
|
|
const branchesWithData = await Promise.all(branches.map(async (branch) => {
|
|
const region = await RegionData.findByPk(branch.regionId);
|
|
const branchType = await BranchType.findByPk(branch.branchTypeId);
|
|
const stocks = await FalukantStock.findAll({
|
|
where: { branchId: branch.id },
|
|
include: [{
|
|
model: FalukantStockType,
|
|
as: 'stockType',
|
|
attributes: ['labelTr']
|
|
}]
|
|
});
|
|
|
|
return {
|
|
...branch.toJSON(),
|
|
region: region ? { name: region.name } : null,
|
|
branchType: branchType ? { labelTr: branchType.labelTr } : null,
|
|
stocks: stocks
|
|
};
|
|
}));
|
|
|
|
return branchesWithData;
|
|
} catch (error) {
|
|
console.error('Error in getFalukantUserBranches:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getFalukantRegions(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
const regions = await RegionData.findAll({
|
|
attributes: ['id', 'name', 'map'],
|
|
include: [
|
|
{
|
|
model: RegionType,
|
|
as: 'regionType',
|
|
where: { labelTr: 'city' },
|
|
attributes: ['labelTr'],
|
|
},
|
|
],
|
|
order: [['name', 'ASC']],
|
|
});
|
|
|
|
return regions;
|
|
}
|
|
|
|
async getTitlesOfNobility(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const titles = await TitleOfNobility.findAll({
|
|
order: [['id', 'ASC']],
|
|
attributes: ['id', 'labelTr', 'level']
|
|
});
|
|
return titles;
|
|
}
|
|
|
|
async updateFalukantRegionMap(userId, regionId, map) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
const region = await RegionData.findByPk(regionId);
|
|
if (!region) {
|
|
throw new Error('regionNotFound');
|
|
}
|
|
|
|
region.map = map || {};
|
|
await region.save();
|
|
|
|
return region;
|
|
}
|
|
|
|
async getRegionDistances(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
const distances = await RegionDistance.findAll();
|
|
return distances;
|
|
}
|
|
|
|
async upsertRegionDistance(userId, { sourceRegionId, targetRegionId, transportMode, distance }) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
if (!sourceRegionId || !targetRegionId || !transportMode) {
|
|
throw new Error('missingParameters');
|
|
}
|
|
|
|
const src = await RegionData.findByPk(sourceRegionId);
|
|
const tgt = await RegionData.findByPk(targetRegionId);
|
|
if (!src || !tgt) {
|
|
throw new Error('regionNotFound');
|
|
}
|
|
|
|
const mode = String(transportMode);
|
|
const dist = Number(distance);
|
|
if (!Number.isFinite(dist) || dist <= 0) {
|
|
throw new Error('invalidDistance');
|
|
}
|
|
|
|
const [record] = await RegionDistance.findOrCreate({
|
|
where: {
|
|
sourceRegionId: src.id,
|
|
targetRegionId: tgt.id,
|
|
transportMode: mode,
|
|
},
|
|
defaults: {
|
|
distance: dist,
|
|
},
|
|
});
|
|
|
|
if (record.distance !== dist) {
|
|
record.distance = dist;
|
|
await record.save();
|
|
}
|
|
|
|
return record;
|
|
}
|
|
|
|
async deleteRegionDistance(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const record = await RegionDistance.findByPk(id);
|
|
if (!record) {
|
|
throw new Error('notfound');
|
|
}
|
|
await record.destroy();
|
|
return { success: true };
|
|
}
|
|
|
|
async updateFalukantStock(userId, stockId, quantity) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
const stock = await FalukantStock.findByPk(stockId);
|
|
if (!stock) {
|
|
throw new Error('Stock not found');
|
|
}
|
|
|
|
stock.quantity = quantity;
|
|
await stock.save();
|
|
|
|
return stock;
|
|
}
|
|
|
|
async addFalukantStock(userId, branchId, stockTypeId, quantity) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
// Prüfe ob Branch existiert
|
|
const branch = await Branch.findByPk(branchId);
|
|
if (!branch) {
|
|
throw new Error('Branch not found');
|
|
}
|
|
|
|
// Prüfe ob StockType existiert
|
|
const stockType = await FalukantStockType.findByPk(stockTypeId);
|
|
if (!stockType) {
|
|
throw new Error('Stock type not found');
|
|
}
|
|
|
|
// Prüfe ob bereits ein Stock dieses Typs für diesen Branch existiert
|
|
const existingStock = await FalukantStock.findOne({
|
|
where: {
|
|
branchId: branchId,
|
|
stockTypeId: stockTypeId
|
|
}
|
|
});
|
|
|
|
if (existingStock) {
|
|
throw new Error('Stock of this type already exists for this branch');
|
|
}
|
|
|
|
// Erstelle neuen Stock
|
|
const newStock = await FalukantStock.create({
|
|
branchId: branchId,
|
|
stockTypeId: stockTypeId,
|
|
quantity: quantity
|
|
});
|
|
|
|
// Lade den neuen Stock mit allen Beziehungen
|
|
const stockWithData = await FalukantStock.findByPk(newStock.id, {
|
|
include: [{ model: FalukantStockType, as: 'stockType', attributes: ['labelTr'] }]
|
|
});
|
|
|
|
return stockWithData;
|
|
}
|
|
|
|
async getFalukantStockTypes(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
const stockTypes = await FalukantStockType.findAll({
|
|
attributes: ['id', 'labelTr']
|
|
});
|
|
|
|
return stockTypes;
|
|
}
|
|
|
|
async changeFalukantUser(userId, falukantUserId, falukantData) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const falukantUser = await FalukantUser.findOne({
|
|
where: {
|
|
id: falukantUserId
|
|
}
|
|
});
|
|
if (!falukantUser) {
|
|
throw new Error('notfound');
|
|
}
|
|
const character = await FalukantCharacter.findOne({
|
|
where: {
|
|
userId: falukantUserId
|
|
}
|
|
});
|
|
if (!character) {
|
|
throw new Error('notfound');
|
|
}
|
|
if (Object.keys(falukantData).indexOf('age') >= 0) {
|
|
const birthDate = (new Date()) - (falukantData.age * 24 * 3600000);
|
|
await character.update({
|
|
birthdate: birthDate
|
|
});
|
|
}
|
|
if (Object.keys(falukantData).indexOf('money') >= 0) {
|
|
await falukantUser.update({
|
|
money: falukantData.money
|
|
});
|
|
}
|
|
if (Object.keys(falukantData).indexOf('title_of_nobility') >= 0) {
|
|
await character.update({
|
|
titleOfNobility: falukantData.title_of_nobility
|
|
});
|
|
}
|
|
await falukantUser.save();
|
|
await character.save();
|
|
}
|
|
|
|
// --- User Administration ---
|
|
async searchUsers(requestingHashedUserId, query) {
|
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
if (!query || query.trim().length === 0) return [];
|
|
|
|
const users = await User.findAll({
|
|
where: {
|
|
[Op.or]: [
|
|
{ username: { [Op.iLike]: `%${query}%` } },
|
|
// email is encrypted, can't search directly reliably; skip email search
|
|
]
|
|
},
|
|
attributes: ['id', 'hashedId', 'username', 'active', 'registrationDate']
|
|
});
|
|
return users.map(u => ({ id: u.hashedId, username: u.username, active: u.active, registrationDate: u.registrationDate }));
|
|
}
|
|
|
|
async getUserByHashedId(requestingHashedUserId, targetHashedId) {
|
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const user = await User.findOne({
|
|
where: { hashedId: targetHashedId },
|
|
attributes: ['id', 'hashedId', 'username', 'active', 'registrationDate']
|
|
});
|
|
if (!user) throw new Error('notfound');
|
|
return { id: user.hashedId, username: user.username, active: user.active, registrationDate: user.registrationDate };
|
|
}
|
|
|
|
async getUsersByHashedIds(requestingHashedUserId, targetHashedIds) {
|
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
if (!Array.isArray(targetHashedIds) || targetHashedIds.length === 0) {
|
|
return [];
|
|
}
|
|
const users = await User.findAll({
|
|
where: { hashedId: { [Op.in]: targetHashedIds } },
|
|
attributes: ['id', 'hashedId', 'username', 'active', 'registrationDate']
|
|
});
|
|
// Erstelle ein Map für schnellen Zugriff
|
|
const userMap = {};
|
|
users.forEach(user => {
|
|
userMap[user.hashedId] = {
|
|
id: user.hashedId,
|
|
username: user.username,
|
|
active: user.active,
|
|
registrationDate: user.registrationDate
|
|
};
|
|
});
|
|
return userMap;
|
|
}
|
|
|
|
async updateUser(requestingHashedUserId, targetHashedId, data) {
|
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const user = await User.findOne({ where: { hashedId: targetHashedId } });
|
|
if (!user) throw new Error('notfound');
|
|
|
|
const updates = {};
|
|
if (typeof data.username === 'string' && data.username.trim().length > 0) {
|
|
updates.username = data.username.trim();
|
|
}
|
|
if (typeof data.active === 'boolean') {
|
|
updates.active = data.active;
|
|
}
|
|
if (Object.keys(updates).length === 0) return { id: user.hashedId, username: user.username, active: user.active };
|
|
await user.update(updates);
|
|
return { id: user.hashedId, username: user.username, active: user.active };
|
|
}
|
|
|
|
// --- User Rights Administration ---
|
|
async listUserRightTypes(requestingHashedUserId) {
|
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'rights'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const types = await UserRightType.findAll({ attributes: ['id', 'title'] });
|
|
// map to tr keys if needed; keep title as key used elsewhere
|
|
return types.map(t => ({ id: t.id, title: t.title }));
|
|
}
|
|
|
|
async listUserRightsForUser(requestingHashedUserId, targetHashedId) {
|
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'rights'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const user = await User.findOne({ where: { hashedId: targetHashedId }, attributes: ['id', 'hashedId', 'username'] });
|
|
if (!user) throw new Error('notfound');
|
|
const rights = await UserRight.findAll({
|
|
where: { userId: user.id },
|
|
include: [{ model: UserRightType, as: 'rightType' }]
|
|
});
|
|
return rights.map(r => ({ rightTypeId: r.rightTypeId, title: r.rightType?.title }));
|
|
}
|
|
|
|
async addUserRight(requestingHashedUserId, targetHashedId, rightTypeId) {
|
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'rights'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const user = await User.findOne({ where: { hashedId: targetHashedId } });
|
|
if (!user) throw new Error('notfound');
|
|
const type = await UserRightType.findByPk(rightTypeId);
|
|
if (!type) throw new Error('wrongtype');
|
|
const existing = await UserRight.findOne({ where: { userId: user.id, rightTypeId } });
|
|
if (existing) return existing; // idempotent
|
|
const created = await UserRight.create({ userId: user.id, rightTypeId });
|
|
return created;
|
|
}
|
|
|
|
async removeUserRight(requestingHashedUserId, targetHashedId, rightTypeId) {
|
|
if (!(await this.hasUserAccess(requestingHashedUserId, 'rights'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const user = await User.findOne({ where: { hashedId: targetHashedId } });
|
|
if (!user) throw new Error('notfound');
|
|
await UserRight.destroy({ where: { userId: user.id, rightTypeId } });
|
|
return true;
|
|
}
|
|
|
|
// --- Chat Room Admin ---
|
|
async getRoomTypes(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
return await RoomType.findAll();
|
|
}
|
|
|
|
async getGenderRestrictions(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
// Find the UserParamType for gender restriction (e.g. description = 'gender')
|
|
const genderType = await UserParamType.findOne({ where: { description: 'gender' } });
|
|
if (!genderType) return [];
|
|
return await UserParamValue.findAll({ where: { userParamTypeId: genderType.id } });
|
|
}
|
|
|
|
async getUserRights(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
return await ChatRight.findAll();
|
|
}
|
|
|
|
async getRooms(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
// Only return necessary fields to the frontend
|
|
return await Room.findAll({
|
|
attributes: [
|
|
'id',
|
|
'title',
|
|
'roomTypeId',
|
|
'isPublic',
|
|
'isAdultOnly',
|
|
'genderRestrictionId',
|
|
'minAge',
|
|
'maxAge',
|
|
'friendsOfOwnerOnly',
|
|
'requiredUserRightId',
|
|
'password' // only if needed for editing, otherwise remove
|
|
],
|
|
include: [
|
|
{ model: RoomType, as: 'roomType' },
|
|
{
|
|
model: UserParamValue,
|
|
as: 'genderRestriction',
|
|
required: false // Wichtig: required: false, damit auch Räume ohne genderRestriction geladen werden
|
|
},
|
|
]
|
|
});
|
|
}
|
|
|
|
async updateRoom(userId, id, data) {
|
|
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const room = await Room.findByPk(id);
|
|
if (!room) throw new Error('Room not found');
|
|
await room.update(data);
|
|
return room;
|
|
}
|
|
|
|
async createRoom(userId, data) {
|
|
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
return await Room.create(data);
|
|
}
|
|
|
|
async deleteRoom(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'chatrooms'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
return await Room.destroy({ where: { id } });
|
|
}
|
|
|
|
// --- Match3 Admin Methods ---
|
|
async getMatch3Campaigns(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
|
return await Match3Campaign.findAll({
|
|
include: [{
|
|
model: (await import('../models/match3/level.js')).default,
|
|
as: 'levels',
|
|
include: [{
|
|
model: (await import('../models/match3/objective.js')).default,
|
|
as: 'objectives',
|
|
required: false
|
|
}],
|
|
required: false
|
|
}]
|
|
});
|
|
}
|
|
|
|
async getMatch3Campaign(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
|
return await Match3Campaign.findByPk(id, {
|
|
include: [{
|
|
model: (await import('../models/match3/level.js')).default,
|
|
as: 'levels',
|
|
include: [{
|
|
model: (await import('../models/match3/objective.js')).default,
|
|
as: 'objectives',
|
|
required: false
|
|
}],
|
|
required: false
|
|
}]
|
|
});
|
|
}
|
|
|
|
async createMatch3Campaign(userId, data) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
|
return await Match3Campaign.create(data);
|
|
}
|
|
|
|
async updateMatch3Campaign(userId, id, data) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
|
const campaign = await Match3Campaign.findByPk(id);
|
|
if (!campaign) throw new Error('Campaign not found');
|
|
await campaign.update(data);
|
|
return campaign;
|
|
}
|
|
|
|
async deleteMatch3Campaign(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
|
return await Match3Campaign.destroy({ where: { id } });
|
|
}
|
|
|
|
async getMatch3Levels(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Level = (await import('../models/match3/level.js')).default;
|
|
return await Match3Level.findAll({
|
|
include: [
|
|
{
|
|
model: (await import('../models/match3/campaign.js')).default,
|
|
as: 'campaign',
|
|
required: false
|
|
},
|
|
{
|
|
model: (await import('../models/match3/objective.js')).default,
|
|
as: 'objectives',
|
|
required: false
|
|
}
|
|
],
|
|
order: [['order', 'ASC']]
|
|
});
|
|
}
|
|
|
|
async getMatch3Level(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Level = (await import('../models/match3/level.js')).default;
|
|
return await Match3Level.findByPk(id, {
|
|
include: [
|
|
{
|
|
model: (await import('../models/match3/campaign.js')).default,
|
|
as: 'campaign',
|
|
required: false
|
|
},
|
|
{
|
|
model: (await import('../models/match3/objective.js')).default,
|
|
as: 'objectives',
|
|
required: false
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
async createMatch3Level(userId, data) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Level = (await import('../models/match3/level.js')).default;
|
|
|
|
// Wenn keine campaignId gesetzt ist, setze eine Standard-Campaign-ID
|
|
if (!data.campaignId) {
|
|
// Versuche eine Standard-Campaign zu finden oder erstelle eine
|
|
const Match3Campaign = (await import('../models/match3/campaign.js')).default;
|
|
let defaultCampaign = await Match3Campaign.findOne({ where: { isActive: true } });
|
|
|
|
if (!defaultCampaign) {
|
|
// Erstelle eine Standard-Campaign falls keine existiert
|
|
defaultCampaign = await Match3Campaign.create({
|
|
name: 'Standard Campaign',
|
|
description: 'Standard Campaign für Match3 Levels',
|
|
isActive: true,
|
|
order: 1
|
|
});
|
|
}
|
|
|
|
data.campaignId = defaultCampaign.id;
|
|
}
|
|
|
|
// Validiere, dass campaignId gesetzt ist
|
|
if (!data.campaignId) {
|
|
throw new Error('CampaignId ist erforderlich');
|
|
}
|
|
|
|
return await Match3Level.create(data);
|
|
}
|
|
|
|
async updateMatch3Level(userId, id, data) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Level = (await import('../models/match3/level.js')).default;
|
|
const level = await Match3Level.findByPk(id);
|
|
if (!level) throw new Error('Level not found');
|
|
await level.update(data);
|
|
return level;
|
|
}
|
|
|
|
async deleteMatch3Level(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Level = (await import('../models/match3/level.js')).default;
|
|
return await Match3Level.destroy({ where: { id } });
|
|
}
|
|
|
|
// Match3 Objectives
|
|
async getMatch3Objectives(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
|
return await Match3Objective.findAll({
|
|
include: [{
|
|
model: (await import('../models/match3/level.js')).default,
|
|
as: 'level',
|
|
required: false
|
|
}],
|
|
order: [['order', 'ASC']]
|
|
});
|
|
}
|
|
|
|
async getMatch3Objective(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
|
return await Match3Objective.findByPk(id, {
|
|
include: [{
|
|
model: (await import('../models/match3/level.js')).default,
|
|
as: 'level',
|
|
required: false
|
|
}]
|
|
});
|
|
}
|
|
|
|
async createMatch3Objective(userId, data) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
|
|
|
// Validiere, dass levelId gesetzt ist
|
|
if (!data.levelId) {
|
|
throw new Error('LevelId ist erforderlich');
|
|
}
|
|
|
|
// Validiere, dass target eine ganze Zahl ist
|
|
if (data.target && !Number.isInteger(Number(data.target))) {
|
|
throw new Error('Target muss eine ganze Zahl sein');
|
|
}
|
|
|
|
return await Match3Objective.create(data);
|
|
}
|
|
|
|
async updateMatch3Objective(userId, id, data) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
|
const objective = await Match3Objective.findByPk(id);
|
|
if (!objective) throw new Error('Objective not found');
|
|
|
|
// Validiere, dass target eine ganze Zahl ist
|
|
if (data.target && !Number.isInteger(Number(data.target))) {
|
|
throw new Error('Target muss eine ganze Zahl sein');
|
|
}
|
|
|
|
await objective.update(data);
|
|
return objective;
|
|
}
|
|
|
|
async deleteMatch3Objective(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3Objective = (await import('../models/match3/objective.js')).default;
|
|
return await Match3Objective.destroy({ where: { id } });
|
|
}
|
|
|
|
async getMatch3TileTypes(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3TileType = (await import('../models/match3/tileType.js')).default;
|
|
return await Match3TileType.findAll({
|
|
order: [['name', 'ASC']]
|
|
});
|
|
}
|
|
|
|
async createMatch3TileType(userId, data) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3TileType = (await import('../models/match3/tileType.js')).default;
|
|
return await Match3TileType.create(data);
|
|
}
|
|
|
|
async updateMatch3TileType(userId, id, data) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3TileType = (await import('../models/match3/tileType.js')).default;
|
|
const tileType = await Match3TileType.findByPk(id);
|
|
if (!tileType) throw new Error('Tile type not found');
|
|
await tileType.update(data);
|
|
return tileType;
|
|
}
|
|
|
|
async deleteMatch3TileType(userId, id) {
|
|
if (!(await this.hasUserAccess(userId, 'match3'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
const Match3TileType = (await import('../models/match3/tileType.js')).default;
|
|
return await Match3TileType.destroy({ where: { id } });
|
|
}
|
|
|
|
async getUserStatistics(userId) {
|
|
if (!(await this.hasUserAccess(userId, 'mainadmin'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
// Gesamtanzahl angemeldeter Benutzer
|
|
const totalUsers = await User.count({
|
|
where: { active: true }
|
|
});
|
|
|
|
// Geschlechterverteilung - ohne raw: true um Entschlüsselung zu ermöglichen
|
|
const genderStats = await UserParam.findAll({
|
|
include: [{
|
|
model: UserParamType,
|
|
as: 'paramType',
|
|
where: { description: 'gender' }
|
|
}]
|
|
});
|
|
|
|
const genderDistribution = {};
|
|
for (const stat of genderStats) {
|
|
const genderId = stat.value; // Dies ist die ID des Geschlechts
|
|
if (genderId) {
|
|
const genderValue = await UserParamValue.findOne({
|
|
where: { id: genderId }
|
|
});
|
|
if (genderValue) {
|
|
const gender = genderValue.value; // z.B. 'male', 'female'
|
|
genderDistribution[gender] = (genderDistribution[gender] || 0) + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Altersverteilung basierend auf Geburtsdatum - ohne raw: true um Entschlüsselung zu ermöglichen
|
|
const birthdateStats = await UserParam.findAll({
|
|
include: [{
|
|
model: UserParamType,
|
|
as: 'paramType',
|
|
where: { description: 'birthdate' }
|
|
}]
|
|
});
|
|
|
|
const ageGroups = {
|
|
'unter 12': 0,
|
|
'12-14': 0,
|
|
'14-16': 0,
|
|
'16-18': 0,
|
|
'18-21': 0,
|
|
'21-25': 0,
|
|
'25-30': 0,
|
|
'30-40': 0,
|
|
'40-50': 0,
|
|
'50-60': 0,
|
|
'über 60': 0
|
|
};
|
|
|
|
const now = new Date();
|
|
for (const stat of birthdateStats) {
|
|
try {
|
|
const birthdate = new Date(stat.value);
|
|
if (isNaN(birthdate.getTime())) continue;
|
|
|
|
const age = now.getFullYear() - birthdate.getFullYear();
|
|
const monthDiff = now.getMonth() - birthdate.getMonth();
|
|
|
|
let actualAge = age;
|
|
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthdate.getDate())) {
|
|
actualAge--;
|
|
}
|
|
|
|
if (actualAge < 12) {
|
|
ageGroups['unter 12']++;
|
|
} else if (actualAge >= 12 && actualAge < 14) {
|
|
ageGroups['12-14']++;
|
|
} else if (actualAge >= 14 && actualAge < 16) {
|
|
ageGroups['14-16']++;
|
|
} else if (actualAge >= 16 && actualAge < 18) {
|
|
ageGroups['16-18']++;
|
|
} else if (actualAge >= 18 && actualAge < 21) {
|
|
ageGroups['18-21']++;
|
|
} else if (actualAge >= 21 && actualAge < 25) {
|
|
ageGroups['21-25']++;
|
|
} else if (actualAge >= 25 && actualAge < 30) {
|
|
ageGroups['25-30']++;
|
|
} else if (actualAge >= 30 && actualAge < 40) {
|
|
ageGroups['30-40']++;
|
|
} else if (actualAge >= 40 && actualAge < 50) {
|
|
ageGroups['40-50']++;
|
|
} else if (actualAge >= 50 && actualAge < 60) {
|
|
ageGroups['50-60']++;
|
|
} else {
|
|
ageGroups['über 60']++;
|
|
}
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten des Geburtsdatums:', error);
|
|
}
|
|
}
|
|
|
|
return {
|
|
totalUsers,
|
|
genderDistribution,
|
|
ageGroups
|
|
};
|
|
}
|
|
|
|
async createNPCs(userId, options) {
|
|
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
|
throw new Error('noaccess');
|
|
}
|
|
|
|
const {
|
|
regionIds, // Array von Region-IDs oder null für alle Städte
|
|
minAge, // Mindestalter in Jahren
|
|
maxAge, // Maximalalter in Jahren
|
|
minTitleId, // Minimale Title-ID
|
|
maxTitleId, // Maximale Title-ID
|
|
count // Anzahl der zu erstellenden NPCs
|
|
} = options;
|
|
|
|
// Berechne zuerst die Gesamtanzahl, um den Job richtig zu initialisieren
|
|
// WICHTIG: Nur Städte (city) verwenden, keine anderen Region-Typen
|
|
let targetRegions = [];
|
|
if (regionIds && regionIds.length > 0) {
|
|
targetRegions = await RegionData.findAll({
|
|
where: {
|
|
id: { [Op.in]: regionIds }
|
|
},
|
|
include: [{
|
|
model: RegionType,
|
|
as: 'regionType',
|
|
where: { labelTr: 'city' },
|
|
required: true // INNER JOIN - nur Regionen mit city-Type
|
|
}]
|
|
});
|
|
} else {
|
|
targetRegions = await RegionData.findAll({
|
|
include: [{
|
|
model: RegionType,
|
|
as: 'regionType',
|
|
where: { labelTr: 'city' },
|
|
required: true // INNER JOIN - nur Regionen mit city-Type
|
|
}]
|
|
});
|
|
}
|
|
|
|
// Zusätzliche Sicherheit: Filtere explizit nach city-Type
|
|
targetRegions = targetRegions.filter(region => {
|
|
return region.regionType && region.regionType.labelTr === 'city';
|
|
});
|
|
|
|
console.log(`[createNPCs] Found ${targetRegions.length} cities (filtered)`);
|
|
if (targetRegions.length > 0) {
|
|
console.log(`[createNPCs] City names: ${targetRegions.map(r => r.name).join(', ')}`);
|
|
}
|
|
|
|
if (targetRegions.length === 0) {
|
|
throw new Error('No cities found');
|
|
}
|
|
|
|
const titles = await TitleOfNobility.findAll({
|
|
where: {
|
|
id: {
|
|
[Op.between]: [minTitleId, maxTitleId]
|
|
}
|
|
},
|
|
order: [['id', 'ASC']]
|
|
});
|
|
|
|
if (titles.length === 0) {
|
|
throw new Error('No titles found in specified range');
|
|
}
|
|
|
|
const totalNPCs = targetRegions.length * titles.length * count;
|
|
|
|
// Erstelle Job-ID
|
|
const jobId = uuidv4();
|
|
npcCreationJobService.createJob(userId, jobId);
|
|
npcCreationJobService.updateProgress(jobId, 0, totalNPCs);
|
|
npcCreationJobService.setStatus(jobId, 'running');
|
|
|
|
// Starte asynchronen Prozess
|
|
this._createNPCsAsync(jobId, userId, {
|
|
regionIds,
|
|
minAge,
|
|
maxAge,
|
|
minTitleId,
|
|
maxTitleId,
|
|
count,
|
|
targetRegions,
|
|
titles
|
|
}).catch(error => {
|
|
console.error('Error in _createNPCsAsync:', error);
|
|
const errorMessage = error?.message || error?.toString() || 'Unknown error occurred';
|
|
npcCreationJobService.setError(jobId, errorMessage);
|
|
});
|
|
|
|
return { jobId };
|
|
}
|
|
|
|
async _createNPCsAsync(jobId, userId, options) {
|
|
try {
|
|
const {
|
|
regionIds,
|
|
minAge,
|
|
maxAge,
|
|
minTitleId,
|
|
maxTitleId,
|
|
count,
|
|
targetRegions,
|
|
titles
|
|
} = options;
|
|
|
|
const genders = ['male', 'female'];
|
|
const createdNPCs = [];
|
|
const totalNPCs = targetRegions.length * titles.length * count;
|
|
let currentNPC = 0;
|
|
|
|
console.log(`[NPC Creation Job ${jobId}] Starting creation of ${totalNPCs} NPCs`);
|
|
|
|
// Erstelle NPCs in einer Transaktion
|
|
// Für jede Stadt-Titel-Kombination wird die angegebene Anzahl erstellt
|
|
await sequelize.transaction(async (t) => {
|
|
for (const region of targetRegions) {
|
|
for (const title of titles) {
|
|
// Erstelle 'count' NPCs für diese Stadt-Titel-Kombination
|
|
for (let i = 0; i < count; i++) {
|
|
// Zufälliges Geschlecht
|
|
const gender = genders[Math.floor(Math.random() * genders.length)];
|
|
|
|
// Zufälliger Vorname
|
|
const firstName = await FalukantPredefineFirstname.findAll({
|
|
where: { gender },
|
|
order: sequelize.fn('RANDOM'),
|
|
limit: 1,
|
|
transaction: t
|
|
});
|
|
if (firstName.length === 0) {
|
|
throw new Error(`No first names found for gender: ${gender}`);
|
|
}
|
|
const fnObj = firstName[0];
|
|
|
|
// Zufälliger Nachname
|
|
const lastName = await FalukantPredefineLastname.findAll({
|
|
order: sequelize.fn('RANDOM'),
|
|
limit: 1,
|
|
transaction: t
|
|
});
|
|
if (lastName.length === 0) {
|
|
throw new Error('No last names found');
|
|
}
|
|
const lnObj = lastName[0];
|
|
|
|
// Zufälliges Alter (in Jahren, wird in Tage umgerechnet)
|
|
const randomAge = Math.floor(Math.random() * (maxAge - minAge + 1)) + minAge;
|
|
const birthdate = new Date();
|
|
birthdate.setDate(birthdate.getDate() - randomAge); // 5 Tage = 5 Jahre alt
|
|
|
|
// Erstelle den NPC-Charakter (ohne userId = NPC)
|
|
const npc = await FalukantCharacter.create({
|
|
userId: null, // Wichtig: null = NPC
|
|
regionId: region.id,
|
|
firstName: fnObj.id,
|
|
lastName: lnObj.id,
|
|
gender: gender,
|
|
birthdate: birthdate,
|
|
titleOfNobility: title.id,
|
|
health: 100,
|
|
moodId: 1
|
|
}, { transaction: t });
|
|
|
|
createdNPCs.push({
|
|
id: npc.id,
|
|
firstName: fnObj.name,
|
|
lastName: lnObj.name,
|
|
gender: gender,
|
|
age: randomAge,
|
|
region: region.name,
|
|
title: title.labelTr
|
|
});
|
|
|
|
// Update Progress
|
|
currentNPC++;
|
|
npcCreationJobService.updateProgress(jobId, currentNPC, totalNPCs);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log(`[NPC Creation Job ${jobId}] Completed: ${createdNPCs.length} NPCs created`);
|
|
|
|
// Job abschließen
|
|
npcCreationJobService.setResult(jobId, {
|
|
success: true,
|
|
count: createdNPCs.length,
|
|
countPerCombination: count,
|
|
totalCombinations: targetRegions.length * titles.length,
|
|
npcs: createdNPCs
|
|
});
|
|
} catch (error) {
|
|
console.error(`[NPC Creation Job ${jobId}] Error:`, error);
|
|
throw error; // Re-throw für den catch-Block in createNPCs
|
|
}
|
|
}
|
|
|
|
getNPCsCreationStatus(userId, jobId) {
|
|
const job = npcCreationJobService.getJob(jobId);
|
|
if (!job) {
|
|
throw new Error('Job not found');
|
|
}
|
|
if (job.userId !== userId) {
|
|
throw new Error('Access denied');
|
|
}
|
|
return job;
|
|
}
|
|
}
|
|
|
|
export default new AdminService();
|