Files
yourpart3/backend/services/adminService.js
Torsten Schulz (local) c52d4b60f9
Some checks failed
Deploy to production / deploy (push) Failing after 2m6s
feat(admin): implement pregnancy and birth management features
- 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.
2026-03-30 13:44:43 +02:00

1876 lines
67 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();