Files
SerpentRace/SerpentRace_Backend/src/Api/routers/adminRouter.ts
T
2025-10-27 20:35:07 +01:00

1141 lines
44 KiB
TypeScript

import express, { Request, Response } from 'express';
import multer from 'multer';
import { DIContainer } from '../../Application/Services/DIContainer';
import { adminRequired } from '../../Application/Services/AuthMiddleware';
import { UserState } from '../../Domain/User/UserAggregate';
import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware';
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
import { AdminAuditService } from '../../Application/Services/AdminBypassService';
import { webSocketService } from '../index';
import { logRequest, logError, logWarning, logAuth } from '../../Application/Services/Logger';
// Extend Express Request interface for file uploads
declare global {
namespace Express {
interface Request {
file?: Express.Multer.File;
}
}
}
const router = express.Router();
const container = DIContainer.getInstance();
// Configure multer for file uploads
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
},
fileFilter: (req: any, file: any, cb: any) => {
if (file.mimetype === 'application/json' || file.originalname.endsWith('.spr')) {
cb(null, true);
} else {
cb(new Error('Only JSON and .spr files are allowed'));
}
}
});
// Helper function to extract language from Accept-Language header
function extractLanguageFromAcceptHeader(acceptLanguage: string): string | null {
if (!acceptLanguage) return null;
const languages = acceptLanguage.split(',');
if (languages.length > 0) {
const primaryLanguage = languages[0].split(';')[0].trim().substring(0, 2);
return primaryLanguage;
}
return null;
}
// =============================================================================
// USER MANAGEMENT ROUTES
// =============================================================================
// Get users with pagination (RECOMMENDED)
router.get('/users/page/:from/:to', adminRequired, async (req: Request, res: Response) => {
try {
const from = parseInt(req.params.from);
const to = parseInt(req.params.to);
const includeDeleted = req.query.includeDeleted === 'true';
if (isNaN(from) || isNaN(to) || from < 0 || to < from) {
return res.status(400).json({
error: 'Invalid pagination parameters. From and to must be valid numbers with from <= to.'
});
}
const limit = to - from + 1;
if (limit > 100) {
return res.status(400).json({
error: 'Page size too large. Maximum 100 records per request.'
});
}
logRequest('Admin paginated users endpoint accessed', req, res, { from, to, includeDeleted });
const result = await container.getUsersByPageQueryHandler.execute({
from,
to,
includeDeleted
});
const response = {
users: result.users,
pagination: {
from,
to,
returned: result.users.length,
totalCount: result.totalCount,
includeDeleted
}
};
logRequest('Admin users retrieved successfully', req, res, {
returnedUsers: result.users.length,
totalCount: result.totalCount,
from,
to,
includeDeleted
});
return res.status(200).json(response);
} catch (error: any) {
logError('Error in admin get users endpoint', error, req, res);
return res.status(500).json({ error: 'Internal server error' });
}
});
// Get user by ID including soft-deleted ones
router.get('/users/:userId',
adminRequired,
ValidationMiddleware.validateUUIDFormat(['userId']),
async (req: Request, res: Response) => {
try {
const targetUserId = req.params.userId;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin get user by id endpoint accessed', req, res, { targetUserId, includeDeleted });
const user = includeDeleted
? await container.userRepository.findByIdIncludingDeleted(targetUserId)
: await container.userRepository.findById(targetUserId);
if (!user) {
logWarning('User not found', { targetUserId, includeDeleted }, req, res);
return res.status(404).json({ error: 'User not found' });
}
logRequest('Admin user retrieved successfully', req, res, {
targetUserId,
username: user.username,
includeDeleted
});
res.json(user);
} catch (error) {
logError('Admin get user by id endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Search users including soft-deleted ones
router.get('/users/search/:searchTerm',
adminRequired,
ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }),
async (req: Request, res: Response) => {
try {
const { searchTerm } = req.params;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted });
const users = includeDeleted
? await container.userRepository.searchIncludingDeleted(searchTerm)
: await container.userRepository.search(searchTerm);
logRequest('Admin user search completed', req, res, {
searchTerm,
resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0),
includeDeleted
});
res.json(users);
} catch (error) {
logError('Admin search users endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Update any user (admin only)
router.patch('/users/:userId',
adminRequired,
ValidationMiddleware.validateUUIDFormat(['userId']),
async (req: Request, res: Response) => {
try {
const targetUserId = req.params.userId;
const adminUserId = (req as any).user.userId;
logRequest('Admin update user endpoint accessed', req, res, {
adminUserId,
targetUserId,
fieldsToUpdate: Object.keys(req.body)
});
const result = await container.updateUserCommandHandler.execute({ id: targetUserId, ...req.body });
if (!result) {
return res.status(404).json({ error: 'User not found' });
}
logRequest('User updated by admin', req, res, {
adminUserId,
targetUserId,
username: result.username
});
res.json(result);
} catch (error) {
logError('Admin update user endpoint error', error as Error, req, res);
if (error instanceof Error) {
if (error.message.includes('already exists')) {
return res.status(409).json({ error: error.message });
}
if (error.message.includes('validation')) {
return res.status(400).json({ error: error.message });
}
}
res.status(500).json({ error: 'Internal server error' });
}
});
// Activate user (admin only)
router.post('/users/:userId/activate',
adminRequired,
ValidationMiddleware.validateUUIDFormat(['userId']),
async (req: Request, res: Response) => {
try {
const targetUserId = req.params.userId;
const adminUserId = (req as any).user.userId;
logRequest('Admin activate user endpoint accessed', req, res, { adminUserId, targetUserId });
const result = await container.activateUserCommandHandler.execute({ id: targetUserId });
if (!result) {
return res.status(404).json({ error: 'User not found' });
}
logAuth('User activated by admin', targetUserId, { adminUserId }, req, res);
res.json({ message: 'User activated successfully', user: result });
} catch (error) {
logError('Admin activate user endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Deactivate user (admin only)
router.post('/users/:userId/deactivate',
adminRequired,
ValidationMiddleware.validateUUIDFormat(['userId']),
async (req: Request, res: Response) => {
try {
const targetUserId = req.params.userId;
const adminUserId = (req as any).user.userId;
logRequest('Deactivate user endpoint accessed', req, res, { adminUserId, targetUserId });
const result = await container.deactivateUserCommandHandler.execute({ id: targetUserId });
if (!result) {
return res.status(404).json({ error: 'User not found' });
}
logAuth('User deactivated by admin', targetUserId, { adminUserId }, req, res);
res.json({ message: 'User deactivated successfully', user: result });
} catch (error) {
logError('Deactivate user endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Delete user (admin only)
router.delete('/users/:userId',
adminRequired,
ValidationMiddleware.validateUUIDFormat(['userId']),
async (req: Request, res: Response) => {
try {
const targetUserId = req.params.userId;
const adminUserId = (req as any).user.userId;
logRequest('Delete user endpoint accessed', req, res, { adminUserId, targetUserId });
const result = await container.deleteUserCommandHandler.execute({ id: targetUserId });
if (!result) {
return res.status(404).json({ error: 'User not found' });
}
logAuth('User deleted by admin', targetUserId, { adminUserId }, req, res);
res.json({ message: 'User deleted successfully' });
} catch (error) {
logError('Delete user endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// =============================================================================
// DECK MANAGEMENT ROUTES
// =============================================================================
// Get decks by page (admin only) - RECOMMENDED
router.get('/decks/page/:from/:to', adminRequired, async (req: Request, res: Response) => {
try {
const from = parseInt(req.params.from);
const to = parseInt(req.params.to);
const includeDeleted = req.query.includeDeleted === 'true';
if (isNaN(from) || isNaN(to) || from < 0 || to < from) {
return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' });
}
logRequest('Admin get decks by page endpoint accessed', req, res, { from, to, includeDeleted });
// For admin, we need to pass admin context to get unrestricted decks
const adminUserId = (req as any).user.userId;
const result = await container.getDecksByPageQueryHandler.execute({
userId: adminUserId,
userOrgId: undefined,
isAdmin: true,
from,
to,
includeDeleted
});
logRequest('Admin decks page retrieved successfully', req, res, {
from,
to,
count: result.decks.length,
total: result.totalCount,
includeDeleted
});
res.json(result);
} catch (error) {
logError('Admin get decks by page endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get deck by ID including soft-deleted ones
router.get('/decks/:id', adminRequired, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin get deck by id endpoint accessed', req, res, { deckId: id, includeDeleted });
const deck = includeDeleted
? await container.deckRepository.findByIdIncludingDeleted(id)
: await container.deckRepository.findById(id);
if (!deck) {
logWarning('Deck not found', { deckId: id, includeDeleted }, req, res);
return res.status(404).json({ error: 'Deck not found' });
}
logRequest('Admin deck retrieved successfully', req, res, { deckId: id, includeDeleted });
res.json(deck);
} catch (error) {
logError('Admin get deck by id endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Search decks including soft-deleted ones
router.get('/decks/search/:searchTerm', adminRequired, async (req: Request, res: Response) => {
try {
const { searchTerm } = req.params;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin search decks endpoint accessed', req, res, { searchTerm, includeDeleted });
const decks = includeDeleted
? await container.deckRepository.searchIncludingDeleted(searchTerm)
: await container.deckRepository.search(searchTerm);
logRequest('Admin deck search completed', req, res, {
searchTerm,
resultCount: Array.isArray(decks) ? decks.length : (decks.totalCount || 0),
includeDeleted
});
res.json(decks);
} catch (error) {
logError('Admin search decks endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
//modify deck (admin only)
router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) => {
try {
const deckId = req.params.id;
const adminUserId = (req as any).user.userId;
logRequest('Admin update deck endpoint accessed', req, res, { deckId, adminUserId, updateFields: Object.keys(req.body) });
const result = await container.updateDeckCommandHandler.execute({ id: deckId, userstate: 1 , ...req.body});
logRequest('Deck updated successfully by admin', req, res, { deckId, adminUserId });
res.json(result);
} catch (error) {
logError('Admin update deck endpoint error', error as Error, req, res);
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ error: `Deck not found` });
}
if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) {
return res.status(409).json({ error: 'Deck with this name already exists' });
}
if (error instanceof Error && error.message.includes('validation')) {
return res.status(400).json({ error: 'Invalid input data', details: error.message });
}
res.status(500).json({ error: 'Internal server error' });
}
});
// Hard delete deck (admin only)
router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => {
try {
const adminUserId = (req as any).user.userId;
const deckId = req.params.id;
logRequest('Admin hard delete deck endpoint accessed', req, res, { deckId });
const result = await container.deleteDeckCommandHandler.execute({ userid: adminUserId, authLevel: 1, id: deckId, soft: false });
logRequest('Admin deck hard delete successful', req, res, { deckId, success: result });
res.json({ success: result });
} catch (error) {
logError('Admin hard delete deck endpoint error', error as Error, req, res);
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ error: 'Deck not found' });
}
res.status(500).json({ error: 'Internal server error' });
}
});
// =============================================================================
// ORGANIZATION MANAGEMENT ROUTES
// =============================================================================
// Create organization (admin only)
router.post('/organizations', adminRequired, async (req: Request, res: Response) => {
try {
const adminUserId = (req as any).user.userId;
logRequest('Admin create organization endpoint accessed', req, res, { name: req.body.name, adminUserId });
const result = await container.createOrganizationCommandHandler.execute(req.body);
AdminAuditService.logAdminAction('CREATE_ORGANIZATION', adminUserId, {
targetType: 'organization',
targetId: result.id,
operation: 'create',
changes: req.body
}, req, res);
logRequest('Admin organization created successfully', req, res, { organizationId: result.id, name: req.body.name, adminUserId });
res.json(result);
} catch (error) {
logError('Admin create organization endpoint error', error as Error, req, res);
if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) {
return res.status(409).json({ error: 'Organization with this name already exists' });
}
if (error instanceof Error && error.message.includes('validation')) {
return res.status(400).json({ error: 'Invalid input data', details: error.message });
}
res.status(500).json({ error: 'Internal server error' });
}
});
// Update organization (admin only) - NEW ENDPOINT
router.patch('/organizations/:id', adminRequired, async (req: Request, res: Response) => {
try {
const organizationId = req.params.id;
const adminUserId = (req as any).user.userId;
logRequest('Admin update organization endpoint accessed', req, res, {
adminUserId,
organizationId,
fieldsToUpdate: Object.keys(req.body)
});
const result = await container.updateOrganizationCommandHandler.execute({
id: organizationId,
...req.body
});
if (!result) {
return res.status(404).json({ error: 'Organization not found' });
}
AdminAuditService.logAdminAction('UPDATE_ORGANIZATION', adminUserId, {
targetType: 'organization',
targetId: organizationId,
operation: 'update',
changes: req.body,
sensitive: req.body.maxOrganizationalDecks !== undefined
}, req, res);
logRequest('Organization updated by admin', req, res, {
adminUserId,
organizationId,
organizationName: result.name
});
res.json(result);
} catch (error) {
logError('Admin update organization endpoint error', error as Error, req, res);
if (error instanceof Error) {
if (error.message.includes('already exists')) {
return res.status(409).json({ error: error.message });
}
if (error.message.includes('validation')) {
return res.status(400).json({ error: error.message });
}
}
res.status(500).json({ error: 'Internal server error' });
}
});
// Get organizations by page (admin only) - RECOMMENDED
router.get('/organizations/page/:from/:to', adminRequired, async (req: Request, res: Response) => {
try {
const from = parseInt(req.params.from);
const to = parseInt(req.params.to);
const includeDeleted = req.query.includeDeleted === 'true';
if (isNaN(from) || isNaN(to) || from < 0 || to < from) {
return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' });
}
logRequest('Admin get organizations by page endpoint accessed', req, res, { from, to, includeDeleted });
const result = await container.getOrganizationsByPageQueryHandler.execute({
from,
to,
includeDeleted
});
logRequest('Admin organizations page retrieved successfully', req, res, {
from,
to,
count: result.organizations.length,
total: result.totalCount,
includeDeleted
});
res.json(result);
} catch (error) {
logError('Admin get organizations by page endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get organization by ID including soft-deleted ones
router.get('/organizations/:id', adminRequired, async (req: Request, res: Response) => {
try {
const organizationId = req.params.id;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin get organization by id endpoint accessed', req, res, { organizationId, includeDeleted });
const organization = includeDeleted
? await container.organizationRepository.findByIdIncludingDeleted(organizationId)
: await container.organizationRepository.findById(organizationId);
if (!organization) {
logWarning('Organization not found', { organizationId, includeDeleted }, req, res);
return res.status(404).json({ error: 'Organization not found' });
}
logRequest('Admin organization retrieved successfully', req, res, { organizationId, includeDeleted });
res.json(organization);
} catch (error) {
logError('Admin get organization by id endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Search organizations including soft-deleted ones
router.get('/organizations/search/:searchTerm', adminRequired, async (req: Request, res: Response) => {
try {
const { searchTerm } = req.params;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin search organizations endpoint accessed', req, res, { searchTerm, includeDeleted });
const organizations = includeDeleted
? await container.organizationRepository.searchIncludingDeleted(searchTerm)
: await container.organizationRepository.search(searchTerm);
logRequest('Admin organization search completed', req, res, {
searchTerm,
resultCount: Array.isArray(organizations) ? organizations.length : (organizations.totalCount || 0),
includeDeleted
});
res.json(organizations);
} catch (error) {
logError('Admin search organizations endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Soft delete organization (admin only)
router.delete('/organizations/:id', adminRequired, async (req: Request, res: Response) => {
try {
const organizationId = req.params.id;
logRequest('Admin soft delete organization endpoint accessed', req, res, { organizationId });
const result = await container.deleteOrganizationCommandHandler.execute({ id: organizationId, soft: true });
logRequest('Admin organization soft delete successful', req, res, { organizationId, success: result });
res.json({ success: result });
} catch (error) {
logError('Admin soft delete organization endpoint error', error as Error, req, res);
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ error: 'Organization not found' });
}
res.status(500).json({ error: 'Internal server error' });
}
});
// Hard delete organization (admin only)
router.delete('/organizations/:id/hard', adminRequired, async (req: Request, res: Response) => {
try {
const organizationId = req.params.id;
logRequest('Admin hard delete organization endpoint accessed', req, res, { organizationId });
const result = await container.deleteOrganizationCommandHandler.execute({ id: organizationId, soft: false });
logRequest('Admin organization hard delete successful', req, res, { organizationId, success: result });
res.json({ success: result });
} catch (error) {
logError('Admin hard delete organization endpoint error', error as Error, req, res);
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ error: 'Organization not found' });
}
res.status(500).json({ error: 'Internal server error' });
}
});
// =============================================================================
// CHAT MANAGEMENT ROUTES
// =============================================================================
// Get chats with pagination (RECOMMENDED)
router.get('/chats/page/:from/:to', adminRequired, async (req: Request, res: Response) => {
try {
const from = parseInt(req.params.from);
const to = parseInt(req.params.to);
const includeDeleted = req.query.includeDeleted === 'true';
if (isNaN(from) || isNaN(to) || from < 0 || to < from) {
return res.status(400).json({
error: 'Invalid pagination parameters. From and to must be valid numbers with from <= to.'
});
}
const limit = to - from + 1;
if (limit > 100) {
return res.status(400).json({
error: 'Page size too large. Maximum 100 records per request.'
});
}
logRequest('Admin paginated chats endpoint accessed', req, res, { from, to, includeDeleted });
const result = await container.getChatsByPageQueryHandler.execute({
from,
to,
includeDeleted
});
const response = {
chats: result.chats,
pagination: {
from,
to,
returned: result.chats.length,
totalCount: result.totalCount,
includeDeleted
}
};
logRequest('Admin chats retrieved successfully', req, res, {
returnedChats: result.chats.length,
totalCount: result.totalCount,
from,
to,
includeDeleted
});
return res.status(200).json(response);
} catch (error: any) {
logError('Error in admin get chats endpoint', error, req, res);
return res.status(500).json({ error: 'Internal server error' });
}
});
// Get chat by ID including soft-deleted ones
router.get('/chats/:id', adminRequired, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin get chat by id endpoint accessed', req, res, { chatId: id, includeDeleted });
const chat = includeDeleted
? await container.chatRepository.findByIdIncludingDeleted(id)
: await container.chatRepository.findById(id);
if (!chat) {
logWarning('Chat not found', { chatId: id, includeDeleted }, req, res);
return res.status(404).json({ error: 'Chat not found' });
}
logRequest('Admin chat retrieved successfully', req, res, { chatId: id, includeDeleted });
res.json(chat);
} catch (error) {
logError('Admin get chat by id endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// =============================================================================
// CONTACT MANAGEMENT ROUTES
// =============================================================================
// Get contacts by page (admin only) - RECOMMENDED (already exists, enhanced)
router.get('/contacts/page/:from/:to', adminRequired, async (req: Request, res: Response) => {
try {
const from = parseInt(req.params.from);
const to = parseInt(req.params.to);
const includeDeleted = req.query.includeDeleted === 'true';
if (isNaN(from) || isNaN(to) || from < 0 || to < from) {
return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' });
}
logRequest('Admin get contacts by page endpoint accessed', req, res, { from, to, includeDeleted });
const result = includeDeleted
? await container.contactRepository.findByPageIncludingDeleted(from, to)
: await container.contactRepository.findByPage(from, to);
logRequest('Admin contacts page retrieved successfully', req, res, {
from,
to,
count: result.contacts.length,
total: result.totalCount,
includeDeleted
});
res.json(result);
} catch (error) {
logError('Admin get contacts by page endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Get contact by ID (admin only)
router.get('/contacts/:id', adminRequired, async (req: Request, res: Response) => {
try {
const contactId = req.params.id;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin get contact by ID endpoint accessed', req, res, { contactId, includeDeleted });
const result = includeDeleted
? await container.contactRepository.findByIdIncludingDeleted(contactId)
: await container.getContactByIdQueryHandler.execute({ id: contactId });
if (!result) {
logRequest('Contact not found', req, res, { contactId, includeDeleted });
return res.status(404).json({ error: 'Contact not found' });
}
logRequest('Admin contact retrieved successfully', req, res, { contactId, includeDeleted });
res.json(result);
} catch (error) {
logError('Admin get contact by ID endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Search contacts including soft-deleted ones (admin only)
router.get('/contacts/search/:searchTerm', adminRequired, async (req: Request, res: Response) => {
try {
const { searchTerm } = req.params;
const includeDeleted = req.query.includeDeleted === 'true';
logRequest('Admin search contacts endpoint accessed', req, res, { searchTerm, includeDeleted });
const contacts = includeDeleted
? await container.contactRepository.searchIncludingDeleted(searchTerm)
: await container.contactRepository.search(searchTerm);
logRequest('Admin contact search completed', req, res, {
searchTerm,
resultCount: contacts.length,
includeDeleted
});
res.json(contacts);
} catch (error) {
logError('Admin search contacts endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Respond to contact (admin only)
router.put('/contacts/:id/respond', adminRequired, async (req: Request, res: Response) => {
try {
const contactId = req.params.id;
const adminUserId = (req as any).user.userId;
const { adminResponse, sendEmail, language } = req.body;
if (!adminResponse) {
return res.status(400).json({ error: 'Admin response is required' });
}
// Determine language from body, headers, or default to English
let selectedLanguage = language;
if (!selectedLanguage) {
// Try to get language from Accept-Language header
const acceptLanguage = req.headers['accept-language'] as string;
// Try to get language from custom headers (common frontend patterns)
const regionHeader = req.headers['x-region'] as string;
const languageHeader = req.headers['x-language'] as string;
const localeHeader = req.headers['x-locale'] as string;
selectedLanguage = languageHeader ||
localeHeader ||
regionHeader ||
extractLanguageFromAcceptHeader(acceptLanguage) ||
'en';
}
// Validate and normalize language parameter
if (!['en', 'hu', 'de'].includes(selectedLanguage.toLowerCase())) {
selectedLanguage = 'en'; // Fallback to English for unsupported languages
} else {
selectedLanguage = selectedLanguage.toLowerCase();
}
logRequest('Admin respond to contact endpoint accessed', req, res, {
contactId,
adminUserId,
sendEmail,
language: selectedLanguage,
headerLanguage: req.headers['accept-language'] || req.headers['x-language'] || 'none'
});
// Update contact with response
const result = await container.updateContactCommandHandler.execute({
id: contactId,
adminResponse,
respondedBy: adminUserId
});
if (!result) {
logWarning('Contact not found for response', { contactId }, req, res);
return res.status(404).json({ error: 'Contact not found' });
}
// Send email if requested
let emailSent = false;
let emailError = null;
if (sendEmail === true && adminResponse) {
try {
await container.contactEmailService.sendResponse({
to: result.email,
message: adminResponse,
contactId: contactId,
adminUserId: adminUserId,
contactName: result.name,
contactType: result.type,
originalMessage: result.txt,
language: selectedLanguage
});
emailSent = true;
logRequest('Contact response email sent successfully', req, res, {
contactId,
recipientEmail: result.email,
language: selectedLanguage
});
} catch (emailErr) {
emailError = emailErr instanceof Error ? emailErr.message : 'Email sending failed';
logError('Contact response email failed', emailErr as Error, req, res);
}
}
AdminAuditService.logAdminAction('RESPOND_TO_CONTACT', adminUserId, {
targetType: 'contact',
targetId: contactId,
operation: 'update',
changes: { adminResponse, sendEmail, language: selectedLanguage },
metadata: { emailSent, emailError }
}, req, res);
logRequest('Admin contact response saved successfully', req, res, {
contactId,
sendEmail,
emailSent,
language: selectedLanguage
});
res.json({
success: true,
message: 'Response saved successfully',
contact: result,
emailSent,
emailError: emailSent ? null : emailError
});
} catch (error) {
logError('Admin respond to contact endpoint error', error as Error, req, res);
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ error: 'Contact not found' });
}
if (error instanceof Error && error.message.includes('validation')) {
return res.status(400).json({ error: 'Invalid input data', details: error.message });
}
res.status(500).json({ error: 'Internal server error' });
}
});
// Resend contact email (admin only) - NEW ENDPOINT
router.post('/contacts/:id/resend-email', adminRequired, async (req: Request, res: Response) => {
try {
const contactId = req.params.id;
const adminUserId = (req as any).user.userId;
const { language } = req.body;
logRequest('Admin resend contact email endpoint accessed', req, res, {
contactId,
adminUserId,
language
});
// Get contact details
const contact = await container.getContactByIdQueryHandler.execute({ id: contactId });
if (!contact) {
return res.status(404).json({ error: 'Contact not found' });
}
if (!contact.adminResponse) {
return res.status(400).json({ error: 'No admin response found to resend' });
}
const selectedLanguage = language || 'en';
try {
await container.contactEmailService.sendResponse({
to: contact.email,
message: contact.adminResponse,
contactId: contactId,
adminUserId: adminUserId,
contactName: contact.name,
contactType: contact.type,
originalMessage: contact.txt,
language: selectedLanguage
});
AdminAuditService.logAdminAction('RESEND_CONTACT_EMAIL', adminUserId, {
targetType: 'contact',
targetId: contactId,
operation: 'create',
metadata: { language: selectedLanguage, action: 'resend' }
}, req, res);
logRequest('Contact email resent successfully', req, res, {
contactId,
recipientEmail: contact.email,
language: selectedLanguage
});
res.json({
success: true,
message: 'Email resent successfully'
});
} catch (emailErr) {
logError('Contact email resend failed', emailErr as Error, req, res);
res.status(500).json({
error: 'Failed to resend email',
details: emailErr instanceof Error ? emailErr.message : 'Unknown error'
});
}
} catch (error) {
logError('Admin resend contact email endpoint error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
// Soft delete contact (admin only) - NEW ENDPOINT
router.delete('/contacts/:id', adminRequired, async (req: Request, res: Response) => {
try {
const contactId = req.params.id;
const adminUserId = (req as any).user.userId;
logRequest('Admin soft delete contact endpoint accessed', req, res, { contactId, adminUserId });
const result = await container.deleteContactCommandHandler.execute({
id: contactId,
hard: false
});
AdminAuditService.logAdminAction('SOFT_DELETE_CONTACT', adminUserId, {
targetType: 'contact',
targetId: contactId,
operation: 'update'
}, req, res);
logAuth('Contact soft deleted by admin', contactId, { adminUserId }, req, res);
res.json({ success: result });
} catch (error) {
logError('Admin soft delete contact endpoint error', error as Error, req, res);
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ error: 'Contact not found' });
}
res.status(500).json({ error: 'Internal server error' });
}
});
// Hard delete contact (admin only) - NEW ENDPOINT
router.delete('/contacts/:id/hard', adminRequired, async (req: Request, res: Response) => {
try {
const contactId = req.params.id;
const adminUserId = (req as any).user.userId;
logRequest('Admin hard delete contact endpoint accessed', req, res, { contactId, adminUserId });
const result = await container.deleteContactCommandHandler.execute({
id: contactId,
hard: true
});
AdminAuditService.logAdminAction('HARD_DELETE_CONTACT', adminUserId, {
targetType: 'contact',
targetId: contactId,
operation: 'delete',
sensitive: true
}, req, res);
logAuth('Contact hard deleted by admin', contactId, { adminUserId }, req, res);
res.json({ success: result });
} catch (error) {
logError('Admin hard delete contact endpoint error', error as Error, req, res);
if (error instanceof Error && error.message.includes('not found')) {
return res.status(404).json({ error: 'Contact not found' });
}
res.status(500).json({ error: 'Internal server error' });
}
});
// =============================================================================
// DECK IMPORT/EXPORT ROUTES (ADMIN)
// =============================================================================
// Import deck from JSON file (unencrypted, admin only)
router.post('/decks/import', adminRequired, upload.single('file'), async (req: Request, res: Response) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const userId = (req as any).user.userId;
const fileContent = req.file!.buffer.toString('utf-8');
logRequest('Admin deck import from JSON endpoint accessed', req, res, { fileName: req.file.originalname });
let jsonData;
try {
jsonData = JSON.parse(fileContent);
} catch (parseError) {
return res.status(400).json({ error: 'Invalid JSON format' });
}
// For admin import, we need to specify both target user and admin user
// Let's assume the deck will be owned by the admin user doing the import
const result = await container.deckImportExportService.adminImportFromJson(jsonData, userId, userId);
logRequest('Admin deck import successful', req, res, { deckId: result.id, fileName: req.file.originalname });
res.json({
success: true,
message: 'Deck imported successfully',
deckId: result.id
});
} catch (error) {
logError('Admin deck import from JSON error', error as Error, req, res);
if (error instanceof Error && error.message.includes('Invalid')) {
res.status(400).json({ error: 'Invalid deck data structure' });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
// Export deck as JSON (unencrypted, admin only)
router.get('/decks/:deckId/export', adminRequired, async (req: Request, res: Response) => {
try {
const { deckId } = req.params;
logRequest('Admin deck export as JSON endpoint accessed', req, res, { deckId });
const deck = await container.deckRepository.findById(deckId);
if (!deck) {
logWarning('Deck not found for export', { deckId }, req, res);
return res.status(404).json({ error: 'Deck not found' });
}
logRequest('Admin deck export successful', req, res, { deckId, deckName: deck.name });
// Return deck as JSON for admin export
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', `attachment; filename="${deck.name || 'deck'}.json"`);
res.json(deck);
} catch (error) {
logError('Admin deck export as JSON error', error as Error, req, res);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;