Some checks failed
Deploy to production / deploy (push) Failing after 2m6s
- Added new admin functionalities to force pregnancy, clear pregnancy, and trigger birth for characters. - Introduced corresponding routes and controller methods in adminRouter and adminController. - Enhanced the FalukantCharacter model to include pregnancy-related fields. - Created database migration for adding pregnancy columns to the character table. - Updated frontend views and internationalization files to support new pregnancy and birth management features. - Improved user feedback and error handling for these new actions.
1876 lines
67 KiB
JavaScript
1876 lines
67 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 ChildRelation from "../models/falukant/data/child_relation.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';
|
||
import { notifyUser } from '../utils/socket.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 }
|
||
});
|
||
|
||
const previousStatus = existing?.value || 'none';
|
||
|
||
if (existing) {
|
||
await existing.update({ value: status });
|
||
} else {
|
||
await UserParam.create({
|
||
userId: user.id,
|
||
paramTypeId: paramType.id,
|
||
value: status
|
||
});
|
||
}
|
||
|
||
await notifyUser(targetHashedId, 'reloadmenu', {});
|
||
await notifyUser(targetHashedId, 'adultVerificationChanged', {
|
||
status,
|
||
previousStatus
|
||
});
|
||
|
||
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',
|
||
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();
|
||
}
|
||
|
||
/**
|
||
* Admin: Charakter als schwanger markieren (erwarteter Geburtstermin).
|
||
* @param {number} fatherCharacterId - optional; Vater-Charakter-ID
|
||
* @param {number} dueInDays - Tage bis zur „Geburt“ (Default 21)
|
||
*/
|
||
async adminForceFalukantPregnancy(userId, characterId, { fatherCharacterId = null, dueInDays = 21 } = {}) {
|
||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||
throw new Error('noaccess');
|
||
}
|
||
const mother = await FalukantCharacter.findByPk(characterId);
|
||
if (!mother) throw new Error('notfound');
|
||
if (fatherCharacterId != null) {
|
||
const father = await FalukantCharacter.findByPk(Number(fatherCharacterId));
|
||
if (!father) throw new Error('fatherNotFound');
|
||
}
|
||
const days = Math.max(1, Math.min(365, Number(dueInDays) || 21));
|
||
const due = new Date();
|
||
due.setDate(due.getDate() + days);
|
||
await mother.update({
|
||
pregnancyDueAt: due,
|
||
pregnancyFatherCharacterId: fatherCharacterId != null ? Number(fatherCharacterId) : null,
|
||
});
|
||
const fu = mother.userId ? await FalukantUser.findByPk(mother.userId) : null;
|
||
if (fu) {
|
||
const u = await User.findByPk(fu.userId);
|
||
if (u?.hashedId) await notifyUser(u.hashedId, 'familychanged', {});
|
||
}
|
||
return {
|
||
success: true,
|
||
pregnancyDueAt: due.toISOString(),
|
||
pregnancyFatherCharacterId: fatherCharacterId != null ? Number(fatherCharacterId) : null,
|
||
};
|
||
}
|
||
|
||
async adminClearFalukantPregnancy(userId, characterId) {
|
||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||
throw new Error('noaccess');
|
||
}
|
||
const mother = await FalukantCharacter.findByPk(characterId);
|
||
if (!mother) throw new Error('notfound');
|
||
await mother.update({
|
||
pregnancyDueAt: null,
|
||
pregnancyFatherCharacterId: null,
|
||
});
|
||
const fu = mother.userId ? await FalukantUser.findByPk(mother.userId) : null;
|
||
if (fu) {
|
||
const u = await User.findByPk(fu.userId);
|
||
if (u?.hashedId) await notifyUser(u.hashedId, 'familychanged', {});
|
||
}
|
||
return { success: true };
|
||
}
|
||
|
||
/**
|
||
* Admin: Geburt auslösen – Kind-Charakter + child_relation; setzt Schwangerschaft zurück.
|
||
*/
|
||
async adminForceFalukantBirth(userId, motherCharacterId, {
|
||
fatherCharacterId,
|
||
birthContext = 'marriage',
|
||
legitimacy = 'legitimate',
|
||
gender = null,
|
||
} = {}) {
|
||
if (!(await this.hasUserAccess(userId, 'falukantusers'))) {
|
||
throw new Error('noaccess');
|
||
}
|
||
if (fatherCharacterId == null || fatherCharacterId === '') {
|
||
throw new Error('fatherRequired');
|
||
}
|
||
const mother = await FalukantCharacter.findByPk(motherCharacterId);
|
||
if (!mother) throw new Error('notfound');
|
||
const father = await FalukantCharacter.findByPk(Number(fatherCharacterId));
|
||
if (!father) throw new Error('fatherNotFound');
|
||
if (Number(fatherCharacterId) === Number(motherCharacterId)) {
|
||
throw new Error('invalidParents');
|
||
}
|
||
const ctx = ['marriage', 'lover'].includes(birthContext) ? birthContext : 'marriage';
|
||
const leg = ['legitimate', 'acknowledged_bastard', 'hidden_bastard'].includes(legitimacy)
|
||
? legitimacy
|
||
: 'legitimate';
|
||
const childGender = gender === 'male' || gender === 'female'
|
||
? gender
|
||
: (Math.random() < 0.5 ? 'male' : 'female');
|
||
|
||
const nobility = await TitleOfNobility.findOne({ where: { labelTr: 'noncivil' } });
|
||
if (!nobility) throw new Error('titleNotFound');
|
||
|
||
const fnObj = await FalukantPredefineFirstname.findOne({
|
||
where: { gender: childGender },
|
||
order: Sequelize.fn('RANDOM'),
|
||
});
|
||
if (!fnObj) throw new Error('firstNameNotFound');
|
||
|
||
let childCharacterId;
|
||
await sequelize.transaction(async (t) => {
|
||
const baby = await FalukantCharacter.create({
|
||
userId: null,
|
||
regionId: mother.regionId,
|
||
firstName: fnObj.id,
|
||
lastName: mother.lastName,
|
||
gender: childGender,
|
||
birthdate: new Date(),
|
||
titleOfNobility: nobility.id,
|
||
health: 100,
|
||
moodId: 1,
|
||
}, { transaction: t });
|
||
childCharacterId = baby.id;
|
||
|
||
await ChildRelation.create({
|
||
fatherCharacterId: father.id,
|
||
motherCharacterId: mother.id,
|
||
childCharacterId: baby.id,
|
||
nameSet: false,
|
||
isHeir: false,
|
||
legitimacy: leg,
|
||
birthContext: ctx,
|
||
publicKnown: ctx === 'marriage',
|
||
}, { transaction: t });
|
||
|
||
await mother.update({
|
||
pregnancyDueAt: null,
|
||
pregnancyFatherCharacterId: null,
|
||
}, { transaction: t });
|
||
});
|
||
|
||
const notifyParent = async (char) => {
|
||
if (!char?.userId) return;
|
||
const fu = await FalukantUser.findByPk(char.userId);
|
||
if (!fu) return;
|
||
const u = await User.findByPk(fu.userId);
|
||
if (u?.hashedId) {
|
||
await notifyUser(u.hashedId, 'familychanged', {});
|
||
await notifyUser(u.hashedId, 'falukantUpdateStatus', {});
|
||
}
|
||
};
|
||
await notifyParent(mother);
|
||
await notifyParent(father);
|
||
|
||
return { success: true, childCharacterId, gender: childGender };
|
||
}
|
||
|
||
// --- 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();
|