932 lines
42 KiB
JavaScript
932 lines
42 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const express_1 = __importDefault(require("express"));
|
|
const multer_1 = __importDefault(require("multer"));
|
|
const DIContainer_1 = require("../../Application/Services/DIContainer");
|
|
const AuthMiddleware_1 = require("../../Application/Services/AuthMiddleware");
|
|
const ValidationMiddleware_1 = require("../../Application/Services/ValidationMiddleware");
|
|
const AdminBypassService_1 = require("../../Application/Services/AdminBypassService");
|
|
const Logger_1 = require("../../Application/Services/Logger");
|
|
const router = express_1.default.Router();
|
|
const container = DIContainer_1.DIContainer.getInstance();
|
|
// Configure multer for file uploads
|
|
const upload = (0, multer_1.default)({
|
|
storage: multer_1.default.memoryStorage(),
|
|
limits: {
|
|
fileSize: 10 * 1024 * 1024, // 10MB limit
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
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) {
|
|
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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
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.'
|
|
});
|
|
}
|
|
(0, Logger_1.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
|
|
}
|
|
};
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logError)('Error in admin get users endpoint', error, req, res);
|
|
return res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Get users by page (admin only) - RECOMMENDED
|
|
router.get('/users/page/:from/:to', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
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' });
|
|
}
|
|
(0, Logger_1.logRequest)('Admin get users by page endpoint accessed', req, res, { from, to, includeDeleted });
|
|
const result = includeDeleted
|
|
? await container.userRepository.findByPageIncludingDeleted(from, to)
|
|
: await container.userRepository.findByPage(from, to);
|
|
(0, Logger_1.logRequest)('Admin users page retrieved successfully', req, res, {
|
|
from,
|
|
to,
|
|
count: result.users.length,
|
|
total: result.totalCount,
|
|
includeDeleted
|
|
});
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get users by page endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Get user by ID including soft-deleted ones
|
|
router.get('/users/:userId', AuthMiddleware_1.adminRequired, ValidationMiddleware_1.ValidationMiddleware.validateUUIDFormat(['userId']), async (req, res) => {
|
|
try {
|
|
const targetUserId = req.params.userId;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logWarning)('User not found', { targetUserId, includeDeleted }, req, res);
|
|
return res.status(404).json({ error: 'User not found' });
|
|
}
|
|
(0, Logger_1.logRequest)('Admin user retrieved successfully', req, res, {
|
|
targetUserId,
|
|
username: user.username,
|
|
includeDeleted
|
|
});
|
|
res.json(user);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get user by id endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Search users including soft-deleted ones
|
|
router.get('/users/search/:searchTerm', AuthMiddleware_1.adminRequired, ValidationMiddleware_1.ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }), async (req, res) => {
|
|
try {
|
|
const { searchTerm } = req.params;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.logRequest)('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted });
|
|
const users = includeDeleted
|
|
? await container.userRepository.searchIncludingDeleted(searchTerm)
|
|
: await container.userRepository.search(searchTerm);
|
|
(0, Logger_1.logRequest)('Admin user search completed', req, res, {
|
|
searchTerm,
|
|
resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0),
|
|
includeDeleted
|
|
});
|
|
res.json(users);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin search users endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Update any user (admin only)
|
|
router.patch('/users/:userId', AuthMiddleware_1.adminRequired, ValidationMiddleware_1.ValidationMiddleware.validateUUIDFormat(['userId']), async (req, res) => {
|
|
try {
|
|
const targetUserId = req.params.userId;
|
|
const adminUserId = req.user.userId;
|
|
(0, Logger_1.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' });
|
|
}
|
|
(0, Logger_1.logRequest)('User updated by admin', req, res, {
|
|
adminUserId,
|
|
targetUserId,
|
|
username: result.username
|
|
});
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin update user endpoint error', 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' });
|
|
}
|
|
});
|
|
// Deactivate user (admin only)
|
|
router.post('/users/:userId/deactivate', AuthMiddleware_1.adminRequired, ValidationMiddleware_1.ValidationMiddleware.validateUUIDFormat(['userId']), async (req, res) => {
|
|
try {
|
|
const targetUserId = req.params.userId;
|
|
const adminUserId = req.user.userId;
|
|
(0, Logger_1.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' });
|
|
}
|
|
(0, Logger_1.logAuth)('User deactivated by admin', targetUserId, { adminUserId }, req, res);
|
|
res.json({ message: 'User deactivated successfully', user: result });
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Deactivate user endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Delete user (admin only)
|
|
router.delete('/users/:userId', AuthMiddleware_1.adminRequired, ValidationMiddleware_1.ValidationMiddleware.validateUUIDFormat(['userId']), async (req, res) => {
|
|
try {
|
|
const targetUserId = req.params.userId;
|
|
const adminUserId = req.user.userId;
|
|
(0, Logger_1.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' });
|
|
}
|
|
(0, Logger_1.logAuth)('User deleted by admin', targetUserId, { adminUserId }, req, res);
|
|
res.json({ message: 'User deleted successfully' });
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Delete user endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
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' });
|
|
}
|
|
(0, Logger_1.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.user.userId;
|
|
const result = await container.getDecksByPageQueryHandler.execute({
|
|
userId: adminUserId,
|
|
userOrgId: undefined,
|
|
isAdmin: true,
|
|
from,
|
|
to,
|
|
includeDeleted
|
|
});
|
|
(0, Logger_1.logRequest)('Admin decks page retrieved successfully', req, res, {
|
|
from,
|
|
to,
|
|
count: result.decks.length,
|
|
total: result.totalCount,
|
|
includeDeleted
|
|
});
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get decks by page endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Get deck by ID including soft-deleted ones
|
|
router.get('/decks/:id', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logWarning)('Deck not found', { deckId: id, includeDeleted }, req, res);
|
|
return res.status(404).json({ error: 'Deck not found' });
|
|
}
|
|
(0, Logger_1.logRequest)('Admin deck retrieved successfully', req, res, { deckId: id, includeDeleted });
|
|
res.json(deck);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get deck by id endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Search decks including soft-deleted ones
|
|
router.get('/decks/search/:searchTerm', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const { searchTerm } = req.params;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.logRequest)('Admin search decks endpoint accessed', req, res, { searchTerm, includeDeleted });
|
|
const decks = includeDeleted
|
|
? await container.deckRepository.searchIncludingDeleted(searchTerm)
|
|
: await container.deckRepository.search(searchTerm);
|
|
(0, Logger_1.logRequest)('Admin deck search completed', req, res, {
|
|
searchTerm,
|
|
resultCount: Array.isArray(decks) ? decks.length : (decks.totalCount || 0),
|
|
includeDeleted
|
|
});
|
|
res.json(decks);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin search decks endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Hard delete deck (admin only)
|
|
router.delete('/decks/:id/hard', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const deckId = req.params.id;
|
|
(0, Logger_1.logRequest)('Admin hard delete deck endpoint accessed', req, res, { deckId });
|
|
const result = await container.deleteDeckCommandHandler.execute({ id: deckId, soft: false });
|
|
(0, Logger_1.logRequest)('Admin deck hard delete successful', req, res, { deckId, success: result });
|
|
res.json({ success: result });
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin hard delete deck endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const adminUserId = req.user.userId;
|
|
(0, Logger_1.logRequest)('Admin create organization endpoint accessed', req, res, { name: req.body.name, adminUserId });
|
|
const result = await container.createOrganizationCommandHandler.execute(req.body);
|
|
AdminBypassService_1.AdminAuditService.logAdminAction('CREATE_ORGANIZATION', adminUserId, {
|
|
targetType: 'organization',
|
|
targetId: result.id,
|
|
operation: 'create',
|
|
changes: req.body
|
|
}, req, res);
|
|
(0, Logger_1.logRequest)('Admin organization created successfully', req, res, { organizationId: result.id, name: req.body.name, adminUserId });
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin create organization endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const organizationId = req.params.id;
|
|
const adminUserId = req.user.userId;
|
|
(0, Logger_1.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' });
|
|
}
|
|
AdminBypassService_1.AdminAuditService.logAdminAction('UPDATE_ORGANIZATION', adminUserId, {
|
|
targetType: 'organization',
|
|
targetId: organizationId,
|
|
operation: 'update',
|
|
changes: req.body,
|
|
sensitive: req.body.maxOrganizationalDecks !== undefined
|
|
}, req, res);
|
|
(0, Logger_1.logRequest)('Organization updated by admin', req, res, {
|
|
adminUserId,
|
|
organizationId,
|
|
organizationName: result.name
|
|
});
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin update organization endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
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' });
|
|
}
|
|
(0, Logger_1.logRequest)('Admin get organizations by page endpoint accessed', req, res, { from, to, includeDeleted });
|
|
const result = await container.getOrganizationsByPageQueryHandler.execute({
|
|
from,
|
|
to,
|
|
includeDeleted
|
|
});
|
|
(0, Logger_1.logRequest)('Admin organizations page retrieved successfully', req, res, {
|
|
from,
|
|
to,
|
|
count: result.organizations.length,
|
|
total: result.totalCount,
|
|
includeDeleted
|
|
});
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get organizations by page endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Get organization by ID including soft-deleted ones
|
|
router.get('/organizations/:id', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const organizationId = req.params.id;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logWarning)('Organization not found', { organizationId, includeDeleted }, req, res);
|
|
return res.status(404).json({ error: 'Organization not found' });
|
|
}
|
|
(0, Logger_1.logRequest)('Admin organization retrieved successfully', req, res, { organizationId, includeDeleted });
|
|
res.json(organization);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get organization by id endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Search organizations including soft-deleted ones
|
|
router.get('/organizations/search/:searchTerm', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const { searchTerm } = req.params;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.logRequest)('Admin search organizations endpoint accessed', req, res, { searchTerm, includeDeleted });
|
|
const organizations = includeDeleted
|
|
? await container.organizationRepository.searchIncludingDeleted(searchTerm)
|
|
: await container.organizationRepository.search(searchTerm);
|
|
(0, Logger_1.logRequest)('Admin organization search completed', req, res, {
|
|
searchTerm,
|
|
resultCount: Array.isArray(organizations) ? organizations.length : (organizations.totalCount || 0),
|
|
includeDeleted
|
|
});
|
|
res.json(organizations);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin search organizations endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Soft delete organization (admin only)
|
|
router.delete('/organizations/:id', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const organizationId = req.params.id;
|
|
(0, Logger_1.logRequest)('Admin soft delete organization endpoint accessed', req, res, { organizationId });
|
|
const result = await container.deleteOrganizationCommandHandler.execute({ id: organizationId, soft: true });
|
|
(0, Logger_1.logRequest)('Admin organization soft delete successful', req, res, { organizationId, success: result });
|
|
res.json({ success: result });
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin soft delete organization endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const organizationId = req.params.id;
|
|
(0, Logger_1.logRequest)('Admin hard delete organization endpoint accessed', req, res, { organizationId });
|
|
const result = await container.deleteOrganizationCommandHandler.execute({ id: organizationId, soft: false });
|
|
(0, Logger_1.logRequest)('Admin organization hard delete successful', req, res, { organizationId, success: result });
|
|
res.json({ success: result });
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin hard delete organization endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
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.'
|
|
});
|
|
}
|
|
(0, Logger_1.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
|
|
}
|
|
};
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logWarning)('Chat not found', { chatId: id, includeDeleted }, req, res);
|
|
return res.status(404).json({ error: 'Chat not found' });
|
|
}
|
|
(0, Logger_1.logRequest)('Admin chat retrieved successfully', req, res, { chatId: id, includeDeleted });
|
|
res.json(chat);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get chat by id endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
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' });
|
|
}
|
|
(0, Logger_1.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);
|
|
(0, Logger_1.logRequest)('Admin contacts page retrieved successfully', req, res, {
|
|
from,
|
|
to,
|
|
count: result.contacts.length,
|
|
total: result.totalCount,
|
|
includeDeleted
|
|
});
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get contacts by page endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Get contact by ID (admin only)
|
|
router.get('/contacts/:id', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const contactId = req.params.id;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logRequest)('Contact not found', req, res, { contactId, includeDeleted });
|
|
return res.status(404).json({ error: 'Contact not found' });
|
|
}
|
|
(0, Logger_1.logRequest)('Admin contact retrieved successfully', req, res, { contactId, includeDeleted });
|
|
res.json(result);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin get contact by ID endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Search contacts including soft-deleted ones (admin only)
|
|
router.get('/contacts/search/:searchTerm', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const { searchTerm } = req.params;
|
|
const includeDeleted = req.query.includeDeleted === 'true';
|
|
(0, Logger_1.logRequest)('Admin search contacts endpoint accessed', req, res, { searchTerm, includeDeleted });
|
|
const contacts = includeDeleted
|
|
? await container.contactRepository.searchIncludingDeleted(searchTerm)
|
|
: await container.contactRepository.search(searchTerm);
|
|
(0, Logger_1.logRequest)('Admin contact search completed', req, res, {
|
|
searchTerm,
|
|
resultCount: contacts.length,
|
|
includeDeleted
|
|
});
|
|
res.json(contacts);
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin search contacts endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Respond to contact (admin only)
|
|
router.put('/contacts/:id/respond', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const contactId = req.params.id;
|
|
const adminUserId = req.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'];
|
|
// Try to get language from custom headers (common frontend patterns)
|
|
const regionHeader = req.headers['x-region'];
|
|
const languageHeader = req.headers['x-language'];
|
|
const localeHeader = req.headers['x-locale'];
|
|
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();
|
|
}
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.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;
|
|
(0, Logger_1.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';
|
|
(0, Logger_1.logError)('Contact response email failed', emailErr, req, res);
|
|
}
|
|
}
|
|
AdminBypassService_1.AdminAuditService.logAdminAction('RESPOND_TO_CONTACT', adminUserId, {
|
|
targetType: 'contact',
|
|
targetId: contactId,
|
|
operation: 'update',
|
|
changes: { adminResponse, sendEmail, language: selectedLanguage },
|
|
metadata: { emailSent, emailError }
|
|
}, req, res);
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logError)('Admin respond to contact endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const contactId = req.params.id;
|
|
const adminUserId = req.user.userId;
|
|
const { language } = req.body;
|
|
(0, Logger_1.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
|
|
});
|
|
AdminBypassService_1.AdminAuditService.logAdminAction('RESEND_CONTACT_EMAIL', adminUserId, {
|
|
targetType: 'contact',
|
|
targetId: contactId,
|
|
operation: 'create',
|
|
metadata: { language: selectedLanguage, action: 'resend' }
|
|
}, req, res);
|
|
(0, Logger_1.logRequest)('Contact email resent successfully', req, res, {
|
|
contactId,
|
|
recipientEmail: contact.email,
|
|
language: selectedLanguage
|
|
});
|
|
res.json({
|
|
success: true,
|
|
message: 'Email resent successfully'
|
|
});
|
|
}
|
|
catch (emailErr) {
|
|
(0, Logger_1.logError)('Contact email resend failed', emailErr, req, res);
|
|
res.status(500).json({
|
|
error: 'Failed to resend email',
|
|
details: emailErr instanceof Error ? emailErr.message : 'Unknown error'
|
|
});
|
|
}
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin resend contact email endpoint error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
// Soft delete contact (admin only) - NEW ENDPOINT
|
|
router.delete('/contacts/:id', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const contactId = req.params.id;
|
|
const adminUserId = req.user.userId;
|
|
(0, Logger_1.logRequest)('Admin soft delete contact endpoint accessed', req, res, { contactId, adminUserId });
|
|
const result = await container.deleteContactCommandHandler.execute({
|
|
id: contactId,
|
|
hard: false
|
|
});
|
|
AdminBypassService_1.AdminAuditService.logAdminAction('SOFT_DELETE_CONTACT', adminUserId, {
|
|
targetType: 'contact',
|
|
targetId: contactId,
|
|
operation: 'update'
|
|
}, req, res);
|
|
(0, Logger_1.logAuth)('Contact soft deleted by admin', contactId, { adminUserId }, req, res);
|
|
res.json({ success: result });
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin soft delete contact endpoint error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const contactId = req.params.id;
|
|
const adminUserId = req.user.userId;
|
|
(0, Logger_1.logRequest)('Admin hard delete contact endpoint accessed', req, res, { contactId, adminUserId });
|
|
const result = await container.deleteContactCommandHandler.execute({
|
|
id: contactId,
|
|
hard: true
|
|
});
|
|
AdminBypassService_1.AdminAuditService.logAdminAction('HARD_DELETE_CONTACT', adminUserId, {
|
|
targetType: 'contact',
|
|
targetId: contactId,
|
|
operation: 'delete',
|
|
sensitive: true
|
|
}, req, res);
|
|
(0, Logger_1.logAuth)('Contact hard deleted by admin', contactId, { adminUserId }, req, res);
|
|
res.json({ success: result });
|
|
}
|
|
catch (error) {
|
|
(0, Logger_1.logError)('Admin hard delete contact endpoint error', 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', AuthMiddleware_1.adminRequired, upload.single('file'), async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No file uploaded' });
|
|
}
|
|
const userId = req.user.userId;
|
|
const fileContent = req.file.buffer.toString('utf-8');
|
|
(0, Logger_1.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);
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logError)('Admin deck import from JSON error', 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', AuthMiddleware_1.adminRequired, async (req, res) => {
|
|
try {
|
|
const { deckId } = req.params;
|
|
(0, Logger_1.logRequest)('Admin deck export as JSON endpoint accessed', req, res, { deckId });
|
|
const deck = await container.deckRepository.findById(deckId);
|
|
if (!deck) {
|
|
(0, Logger_1.logWarning)('Deck not found for export', { deckId }, req, res);
|
|
return res.status(404).json({ error: 'Deck not found' });
|
|
}
|
|
(0, Logger_1.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) {
|
|
(0, Logger_1.logError)('Admin deck export as JSON error', error, req, res);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
exports.default = router;
|
|
//# sourceMappingURL=adminRouter.js.map
|