diff --git a/backend/controllers/adminController.js b/backend/controllers/adminController.js index 7ac8974..0fe1a77 100644 --- a/backend/controllers/adminController.js +++ b/backend/controllers/adminController.js @@ -16,6 +16,7 @@ class AdminController { this.adminForceFalukantPregnancy = this.adminForceFalukantPregnancy.bind(this); this.adminClearFalukantPregnancy = this.adminClearFalukantPregnancy.bind(this); this.adminForceFalukantBirth = this.adminForceFalukantBirth.bind(this); + this.adminCleanupCharacterDeathArtifacts = this.adminCleanupCharacterDeathArtifacts.bind(this); this.adminGetPotentialFathersForCharacter = this.adminGetPotentialFathersForCharacter.bind(this); this.getFalukantUserBranches = this.getFalukantUserBranches.bind(this); this.updateFalukantStock = this.updateFalukantStock.bind(this); @@ -507,6 +508,21 @@ class AdminController { } } + async adminCleanupCharacterDeathArtifacts(req, res) { + try { + const { userid: userId } = req.headers; + const { characterId } = req.params; + const response = await AdminService.adminCleanupCharacterDeathArtifacts(userId, characterId); + res.status(200).json(response); + } catch (error) { + console.log(error); + const status = error.message === 'noaccess' + ? 403 + : (['invalidCharacter', 'notfound'].includes(error.message) ? 400 : 500); + res.status(status).json({ error: error.message }); + } + } + async getFalukantUserBranches(req, res) { try { const { userid: userId } = req.headers; diff --git a/backend/routers/adminRouter.js b/backend/routers/adminRouter.js index 80a5dd2..95df6d4 100644 --- a/backend/routers/adminRouter.js +++ b/backend/routers/adminRouter.js @@ -55,6 +55,7 @@ router.get('/falukant/character/:characterId/potential-fathers', authenticate, a router.post('/falukant/character/force-pregnancy', authenticate, adminController.adminForceFalukantPregnancy); router.post('/falukant/character/clear-pregnancy', authenticate, adminController.adminClearFalukantPregnancy); router.post('/falukant/character/force-birth', authenticate, adminController.adminForceFalukantBirth); +router.post('/falukant/character/:characterId/death-cleanup', authenticate, adminController.adminCleanupCharacterDeathArtifacts); router.get('/falukant/branches/:falukantUserId', authenticate, adminController.getFalukantUserBranches); router.put('/falukant/stock/:stockId', authenticate, adminController.updateFalukantStock); router.post('/falukant/stock', authenticate, adminController.addFalukantStock); diff --git a/backend/services/adminService.js b/backend/services/adminService.js index 129c7b6..ee3c25b 100644 --- a/backend/services/adminService.js +++ b/backend/services/adminService.js @@ -10,7 +10,7 @@ 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 { Op, QueryTypes, 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"; @@ -32,6 +32,10 @@ import ChildRelation from "../models/falukant/data/child_relation.js"; import Relationship from "../models/falukant/data/relationship.js"; import RelationshipType from "../models/falukant/type/relationship.js"; import RelationshipState from "../models/falukant/data/relationship_state.js"; +import Knowledge from '../models/falukant/data/product_knowledge.js'; +import DebtorsPrism from '../models/falukant/data/debtors_prism.js'; +import Candidate from '../models/falukant/data/candidate.js'; +import PoliticalOffice from '../models/falukant/data/political_office.js'; import { sequelize } from '../utils/sequelize.js'; import npcCreationJobService from './npcCreationJobService.js'; import VocabService from './vocabService.js'; @@ -1172,6 +1176,255 @@ class AdminService { return { success: true, childCharacterId, gender: childGender }; } + async adminCleanupCharacterDeathArtifacts(userId, characterId) { + if (!(await this.hasUserAccess(userId, 'falukantusers'))) { + throw new Error('noaccess'); + } + const parsedCharacterId = Number(characterId); + if (!Number.isFinite(parsedCharacterId) || parsedCharacterId <= 0) { + throw new Error('invalidCharacter'); + } + + const character = await FalukantCharacter.findByPk(parsedCharacterId, { + attributes: ['id', 'userId'] + }); + if (!character) { + throw new Error('notfound'); + } + + const summary = { + characterId: parsedCharacterId, + knowledgeDeleted: 0, + debtorsPrismDeleted: 0, + politicalOfficeArchived: 0, + politicalOfficeDeleted: 0, + electionCandidateDeleted: 0, + characterDeleted: false + }; + + await sequelize.transaction(async (t) => { + summary.knowledgeDeleted = await Knowledge.destroy({ + where: { characterId: parsedCharacterId }, + transaction: t + }); + summary.debtorsPrismDeleted = await DebtorsPrism.destroy({ + where: { characterId: parsedCharacterId }, + transaction: t + }); + summary.electionCandidateDeleted = await Candidate.destroy({ + where: { characterId: parsedCharacterId }, + transaction: t + }); + + const archivedRows = await sequelize.query( + ` + INSERT INTO falukant_log.political_office_history ( + character_id, + office_type_id, + start_date, + end_date, + created_at, + updated_at + ) + SELECT + po.character_id, + po.office_type_id, + COALESCE(po.created_at, NOW()), + NOW(), + NOW(), + NOW() + FROM falukant_data.political_office po + WHERE po.character_id = :characterId + RETURNING id + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.SELECT, + transaction: t + } + ); + summary.politicalOfficeArchived = archivedRows.length; + summary.politicalOfficeDeleted = await PoliticalOffice.destroy({ + where: { characterId: parsedCharacterId }, + transaction: t + }); + + await sequelize.query( + ` + DELETE FROM falukant_data.occupied_political_office + WHERE character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.political_benefit_last_tick + WHERE character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.falukant_character_trait + WHERE character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.church_application + WHERE character_id = :characterId + OR supervisor_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.church_office + WHERE character_id = :characterId + OR supervisor_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.political_appointment + WHERE appointer_character_id = :characterId + OR target_character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.marriage_proposals + WHERE requester_character_id = :characterId + OR proposed_character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.child_relation + WHERE father_character_id = :characterId + OR mother_character_id = :characterId + OR child_character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.relationship_state + WHERE relationship_id IN ( + SELECT id FROM falukant_data.relationship + WHERE character1_id = :characterId + OR character2_id = :characterId + ) + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.relationship + WHERE character1_id = :characterId + OR character2_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_data.region_tax_history + WHERE setter_character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_log.health_activity + WHERE character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + await sequelize.query( + ` + DELETE FROM falukant_log.political_office_history + WHERE character_id = :characterId + `, + { + replacements: { characterId: parsedCharacterId }, + type: QueryTypes.DELETE, + transaction: t + } + ); + + const deletedCharacters = await FalukantCharacter.destroy({ + where: { id: parsedCharacterId }, + transaction: t + }); + summary.characterDeleted = deletedCharacters > 0; + }); + + if (character.userId) { + const falukantUser = await FalukantUser.findByPk(character.userId, { + include: [{ model: User, as: 'user', attributes: ['hashedId'] }] + }); + const hashedId = falukantUser?.user?.hashedId; + if (hashedId) { + await notifyUser(hashedId, 'falukantUpdateStatus', {}); + await notifyUser(hashedId, 'familychanged', {}); + } + } + + return { success: true, ...summary }; + } + // --- User Administration --- async searchUsers(requestingHashedUserId, query) { if (!(await this.hasUserAccess(requestingHashedUserId, 'useradministration'))) { diff --git a/frontend/src/i18n/locales/de/admin.json b/frontend/src/i18n/locales/de/admin.json index f4b3a69..5d4c614 100644 --- a/frontend/src/i18n/locales/de/admin.json +++ b/frontend/src/i18n/locales/de/admin.json @@ -244,6 +244,14 @@ "force": "Geburt auslösen", "success": "Kind wurde angelegt (Taufe ausstehend).", "error": "Geburt konnte nicht ausgelöst werden." + }, + "deathCleanup": { + "title": "Tod/Erbe-Aufräumen (Admin)", + "hint": "Entfernt veraltete charaktergebundene Daten nach Tod/Erbe (Wissen, Schuldnerkartei, politische Ämter inkl. Archiv, Wahlkandidatur).", + "action": "Cleanup ausführen", + "confirm": "Diesen Cleanup für den aktuellen Charakter jetzt ausführen?", + "success": "Cleanup erfolgreich ausgeführt.", + "error": "Cleanup konnte nicht ausgeführt werden." } }, "map": { diff --git a/frontend/src/i18n/locales/en/admin.json b/frontend/src/i18n/locales/en/admin.json index df70708..c2d2da3 100644 --- a/frontend/src/i18n/locales/en/admin.json +++ b/frontend/src/i18n/locales/en/admin.json @@ -299,6 +299,14 @@ "force": "Trigger birth", "success": "Child created (baptism pending).", "error": "Could not trigger birth." + }, + "deathCleanup": { + "title": "Death/inheritance cleanup (admin)", + "hint": "Removes stale character-bound data after death/inheritance (knowledge, debtors prism, political offices incl. archive, election candidacy).", + "action": "Run cleanup", + "confirm": "Run this cleanup for the current character now?", + "success": "Cleanup completed successfully.", + "error": "Cleanup failed." } }, "createNPC": { diff --git a/frontend/src/views/admin/falukant/EditUserView.vue b/frontend/src/views/admin/falukant/EditUserView.vue index 436308a..3253566 100644 --- a/frontend/src/views/admin/falukant/EditUserView.vue +++ b/frontend/src/views/admin/falukant/EditUserView.vue @@ -128,6 +128,14 @@
+ +{{ $t('admin.falukant.edituser.deathCleanup.hint') }}
+