Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import helmet from 'helmet';
|
||||
import { AppDataSource } from '../Infrastructure/ormconfig';
|
||||
import userRouter from './routers/userRouter';
|
||||
import organizationRouter from './routers/organizationRouter';
|
||||
import deckRouter from './routers/deckRouter';
|
||||
import chatRouter from './routers/chatRouter';
|
||||
import contactRouter from './routers/contactRouter';
|
||||
import adminRouter from './routers/adminRouter';
|
||||
import deckImportExportRouter from './routers/deckImportExportRouter';
|
||||
import gameRouter from './routers/gameRouter';
|
||||
import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
|
||||
import { WebSocketService } from '../Application/Services/WebSocketService';
|
||||
import { setupSwagger } from './swagger/swaggerUiSetup';
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
const loggingService = LoggingService.getInstance();
|
||||
|
||||
logStartup('SerpentRace Backend starting up', {
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
port: PORT,
|
||||
nodeVersion: process.version,
|
||||
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
|
||||
});
|
||||
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: isDevelopment ? false : undefined
|
||||
}));
|
||||
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(loggingService.requestLoggingMiddleware());
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const origin = req.headers.origin;
|
||||
const allowedOrigins = ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:8080'];
|
||||
|
||||
if (!origin || allowedOrigins.includes(origin)) {
|
||||
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
||||
}
|
||||
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Cookie');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200).end();
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
if (isDevelopment) {
|
||||
app.use((req, res, next) => {
|
||||
logRequest(`${req.method} ${req.path}`, req, res);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Setup Swagger documentation
|
||||
setupSwagger(app);
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
service: 'SerpentRace Backend API',
|
||||
status: 'running',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
swagger: '/api-docs',
|
||||
users: '/api/users',
|
||||
organizations: '/api/organizations',
|
||||
decks: '/api/decks',
|
||||
chats: '/api/chats',
|
||||
contacts: '/api/contacts',
|
||||
admin: '/api/admin',
|
||||
deckImportExport: '/api/deck-import-export',
|
||||
health: '/health'
|
||||
},
|
||||
websocket: {
|
||||
enabled: true,
|
||||
events: [
|
||||
'chat:join', 'chat:leave', 'message:send',
|
||||
'group:create', 'chat:direct', 'game:chat:create',
|
||||
'chat:history'
|
||||
]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/health', async (req, res) => {
|
||||
try {
|
||||
const isDbConnected = AppDataSource.isInitialized;
|
||||
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'SerpentRace Backend API',
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
database: {
|
||||
connected: isDbConnected,
|
||||
type: AppDataSource.options.type
|
||||
},
|
||||
websocket: {
|
||||
enabled: true
|
||||
},
|
||||
uptime: process.uptime()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
status: 'unhealthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Service health check failed'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// API Routes
|
||||
app.use('/api/users', userRouter);
|
||||
app.use('/api/organizations', organizationRouter);
|
||||
app.use('/api/decks', deckRouter);
|
||||
app.use('/api/chats', chatRouter);
|
||||
app.use('/api/contacts', contactRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/api/deck-import-export', deckImportExportRouter);
|
||||
app.use('/api/games', gameRouter);
|
||||
|
||||
// Global error handler (must be after routes)
|
||||
app.use(loggingService.errorLoggingMiddleware());
|
||||
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logError('Global error handler caught unhandled error', error, req, res);
|
||||
|
||||
// Don't expose internal error details in production
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
timestamp: new Date().toISOString(),
|
||||
...(isDevelopment && { details: error.message, stack: error.stack })
|
||||
});
|
||||
});
|
||||
|
||||
// Handle 404 routes
|
||||
app.use((req: express.Request, res: express.Response) => {
|
||||
res.status(404).json({
|
||||
error: 'Route not found',
|
||||
path: req.originalUrl,
|
||||
method: req.method,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize WebSocket service after database connection
|
||||
let webSocketService: WebSocketService;
|
||||
|
||||
// Initialize database connection
|
||||
AppDataSource.initialize()
|
||||
.then(() => {
|
||||
const dbOptions = AppDataSource.options as any;
|
||||
logConnection('Database connection established', 'postgresql', 'success', {
|
||||
type: dbOptions.type,
|
||||
host: dbOptions.host,
|
||||
database: dbOptions.database
|
||||
});
|
||||
|
||||
// Initialize WebSocket service after database is connected
|
||||
webSocketService = new WebSocketService(httpServer);
|
||||
logStartup('WebSocket service initialized', {
|
||||
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
const dbOptions = AppDataSource.options as any;
|
||||
logConnection('Database connection failed', 'postgresql', 'failure', {
|
||||
error: error.message,
|
||||
type: dbOptions.type,
|
||||
host: dbOptions.host,
|
||||
database: dbOptions.database
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Start server with WebSocket support
|
||||
const server = httpServer.listen(PORT, () => {
|
||||
logStartup('Server started successfully', {
|
||||
port: PORT,
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
timestamp: new Date().toISOString(),
|
||||
endpoints: {
|
||||
health: `/health`,
|
||||
swagger: `/api-docs`,
|
||||
users: `/api/users`,
|
||||
organizations: `/api/organizations`,
|
||||
decks: `/api/decks`,
|
||||
chats: `/api/chats`
|
||||
},
|
||||
websocket: {
|
||||
enabled: true,
|
||||
chatInactivityTimeout: `${process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'} minutes`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const gracefulShutdown = async (signal: string) => {
|
||||
logStartup(`Received ${signal}. Shutting down gracefully...`);
|
||||
|
||||
server.close(() => {
|
||||
logStartup('HTTP server closed');
|
||||
|
||||
if (AppDataSource.isInitialized) {
|
||||
AppDataSource.destroy()
|
||||
.then(() => {
|
||||
logConnection('Database connection closed', 'postgresql', 'success');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
logError('Error during database shutdown', error);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
// Handle uncaught exceptions
|
||||
process.on('uncaughtException', (error) => {
|
||||
logError('Uncaught Exception - Server will shut down', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logError('Unhandled Rejection - Server will shut down', new Error(String(reason)), undefined, undefined);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Export WebSocket service for game integration
|
||||
export { webSocketService };
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { JWTService } from '../../Application/Services/JWTService';
|
||||
import { UserState } from '../../Domain/User/UserAggregate';
|
||||
import { logAuth, logWarning } from '../../Application/Services/Logger';
|
||||
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
authLevel: 0 | 1;
|
||||
userStatus: UserState;
|
||||
orgId: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional authentication middleware - extracts JWT data if present but doesn't require authentication
|
||||
* Used for endpoints that work for both authenticated and anonymous users
|
||||
*/
|
||||
export const optionalAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
||||
const jwtService = new JWTService();
|
||||
|
||||
try {
|
||||
// Try to extract token from Authorization header or cookies
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader?.startsWith('Bearer ')
|
||||
? authHeader.substring(7)
|
||||
: req.cookies?.auth_token;
|
||||
|
||||
if (token) {
|
||||
// If token exists, try to verify it
|
||||
const payload = jwtService.verify(req);
|
||||
|
||||
if (payload) {
|
||||
req.user = {
|
||||
userId: payload.userId,
|
||||
authLevel: payload.authLevel,
|
||||
userStatus: payload.userStatus,
|
||||
orgId: payload.orgId || null
|
||||
};
|
||||
|
||||
logAuth('Optional auth - user authenticated', payload.userId, {
|
||||
authLevel: payload.authLevel,
|
||||
userStatus: payload.userStatus,
|
||||
orgId: payload.orgId
|
||||
});
|
||||
} else {
|
||||
logWarning('Optional auth - invalid token provided', {
|
||||
hasToken: true,
|
||||
tokenLength: token.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Continue regardless of authentication status
|
||||
next();
|
||||
|
||||
} catch (error) {
|
||||
// Log the error but continue without authentication
|
||||
logWarning('Optional auth - error processing token', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
hasAuthHeader: !!req.headers.authorization,
|
||||
hasCookie: !!req.cookies?.auth_token
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,287 @@
|
||||
import express from 'express';
|
||||
import { authRequired } from '../../Application/Services/AuthMiddleware';
|
||||
import { container } from '../../Application/Services/DIContainer';
|
||||
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
|
||||
import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware';
|
||||
import { logAuth, logError, logRequest, logWarning } from '../../Application/Services/Logger';
|
||||
|
||||
const chatRouter = express.Router();
|
||||
|
||||
// Get user's chats
|
||||
chatRouter.get('/user-chats', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const includeArchived = req.query.includeArchived === 'true';
|
||||
|
||||
logRequest('Get user chats endpoint accessed', req, res, { userId, includeArchived });
|
||||
|
||||
const chats = await container.getUserChatsQueryHandler.execute({
|
||||
userId,
|
||||
includeArchived
|
||||
});
|
||||
|
||||
logRequest('User chats retrieved successfully', req, res, {
|
||||
userId,
|
||||
chatCount: chats.length
|
||||
});
|
||||
|
||||
res.json(chats);
|
||||
} catch (error) {
|
||||
logError('Get user chats endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Get chat history
|
||||
chatRouter.get('/history/:chatId',
|
||||
authRequired,
|
||||
ValidationMiddleware.validateUUIDFormat(['chatId']),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const chatId = req.params.chatId;
|
||||
|
||||
logRequest('Get chat history endpoint accessed', req, res, { userId, chatId });
|
||||
|
||||
const history = await container.getChatHistoryQueryHandler.execute({
|
||||
chatId,
|
||||
userId
|
||||
});
|
||||
|
||||
if (!history) {
|
||||
logWarning('Chat history not found or unauthorized', { userId, chatId }, req, res);
|
||||
return ErrorResponseService.sendNotFound(res, 'Chat not found or unauthorized');
|
||||
}
|
||||
|
||||
logRequest('Chat history retrieved successfully', req, res, {
|
||||
userId,
|
||||
chatId,
|
||||
messageCount: history.messages.length,
|
||||
isArchived: history.isArchived
|
||||
});
|
||||
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
logError('Get chat history endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Create new chat (direct/group)
|
||||
chatRouter.post('/create',
|
||||
authRequired,
|
||||
ValidationMiddleware.combine([
|
||||
ValidationMiddleware.validateRequiredFields(['type', 'userIds']),
|
||||
ValidationMiddleware.validateAllowedValues({ type: ['direct', 'group'] }),
|
||||
ValidationMiddleware.validateNonEmptyArrays(['userIds'])
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const { type, name, userIds } = req.body;
|
||||
|
||||
logRequest('Create chat endpoint accessed', req, res, {
|
||||
userId,
|
||||
type,
|
||||
targetUserCount: userIds?.length || 0
|
||||
});
|
||||
|
||||
if (type === 'group' && !name?.trim()) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Group name is required');
|
||||
}
|
||||
|
||||
const chat = await container.createChatCommandHandler.execute({
|
||||
type,
|
||||
name: name?.trim(),
|
||||
createdBy: userId,
|
||||
userIds
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Failed to create chat');
|
||||
}
|
||||
|
||||
logRequest('Chat created successfully', req, res, {
|
||||
userId,
|
||||
chatId: chat.id,
|
||||
chatType: chat.type
|
||||
});
|
||||
|
||||
res.json({
|
||||
id: chat.id,
|
||||
type: chat.type,
|
||||
name: chat.name,
|
||||
users: chat.users,
|
||||
messages: chat.messages
|
||||
});
|
||||
} catch (error) {
|
||||
logError('Create chat endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Premium subscription required')) {
|
||||
return ErrorResponseService.sendForbidden(res, 'Premium subscription required to create groups');
|
||||
}
|
||||
if (error.message.includes('not found')) {
|
||||
return ErrorResponseService.sendNotFound(res, 'One or more users not found');
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Send message (REST endpoint - mainly for testing, real messaging is via WebSocket)
|
||||
chatRouter.post('/message',
|
||||
authRequired,
|
||||
ValidationMiddleware.combine([
|
||||
ValidationMiddleware.validateRequiredFields(['chatId', 'message']),
|
||||
ValidationMiddleware.validateUUIDFormat(['chatId']),
|
||||
ValidationMiddleware.validateStringLength({ message: { min: 1, max: 2000 } })
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const { chatId, message } = req.body;
|
||||
|
||||
logRequest('Send message endpoint accessed', req, res, {
|
||||
userId,
|
||||
chatId,
|
||||
messageLength: message?.length || 0
|
||||
});
|
||||
|
||||
const sentMessage = await container.sendMessageCommandHandler.execute({
|
||||
chatId,
|
||||
userId,
|
||||
message
|
||||
});
|
||||
|
||||
if (!sentMessage) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Failed to send message');
|
||||
}
|
||||
|
||||
logRequest('Message sent successfully', req, res, {
|
||||
userId,
|
||||
chatId,
|
||||
messageId: sentMessage.id
|
||||
});
|
||||
|
||||
res.json(sentMessage);
|
||||
} catch (error) {
|
||||
logError('Send message endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Chat not found')) {
|
||||
return ErrorResponseService.sendNotFound(res, 'Chat not found');
|
||||
}
|
||||
if (error.message.includes('not a member')) {
|
||||
return ErrorResponseService.sendForbidden(res, 'Not authorized to send messages to this chat');
|
||||
}
|
||||
if (error.message.includes('non-empty string')) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Message must be a non-empty string');
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Archive chat manually
|
||||
chatRouter.post('/archive/:chatId',
|
||||
authRequired,
|
||||
ValidationMiddleware.validateUUIDFormat(['chatId']),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const chatId = req.params.chatId;
|
||||
|
||||
logRequest('Archive chat endpoint accessed', req, res, { userId, chatId });
|
||||
|
||||
// Check if user has access to this chat
|
||||
const chat = await container.chatRepository.findById(chatId);
|
||||
if (!chat) {
|
||||
return ErrorResponseService.sendNotFound(res, 'Chat not found');
|
||||
}
|
||||
|
||||
if (!chat.users.includes(userId)) {
|
||||
return ErrorResponseService.sendForbidden(res, 'Not authorized to archive this chat');
|
||||
}
|
||||
|
||||
const success = await container.archiveChatCommandHandler.execute({ chatId });
|
||||
|
||||
if (!success) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Failed to archive chat');
|
||||
}
|
||||
|
||||
logRequest('Chat archived successfully', req, res, { userId, chatId });
|
||||
res.json({ success: true, message: 'Chat archived successfully' });
|
||||
|
||||
} catch (error) {
|
||||
logError('Archive chat endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore chat from archive
|
||||
chatRouter.post('/restore/:chatId',
|
||||
authRequired,
|
||||
ValidationMiddleware.validateUUIDFormat(['chatId']),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const chatId = req.params.chatId;
|
||||
|
||||
logRequest('Restore chat endpoint accessed', req, res, { userId, chatId });
|
||||
|
||||
// Check if user has access to this archived chat
|
||||
const archive = await container.chatArchiveRepository.findByChatId(chatId);
|
||||
const userArchive = archive.find((a: any) => a.participants.includes(userId));
|
||||
|
||||
if (!userArchive) {
|
||||
return ErrorResponseService.sendNotFound(res, 'Archived chat not found or unauthorized');
|
||||
}
|
||||
|
||||
const success = await container.restoreChatCommandHandler.execute({ chatId });
|
||||
|
||||
if (!success) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Failed to restore chat (game chats cannot be restored)');
|
||||
}
|
||||
|
||||
logRequest('Chat restored successfully', req, res, { userId, chatId });
|
||||
res.json({ success: true, message: 'Chat restored successfully' });
|
||||
|
||||
} catch (error) {
|
||||
logError('Restore chat endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Get archived chats for a game
|
||||
chatRouter.get('/archived/game/:gameId',
|
||||
authRequired,
|
||||
ValidationMiddleware.validateUUIDFormat(['gameId']),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const gameId = req.params.gameId;
|
||||
|
||||
logRequest('Get archived game chats endpoint accessed', req, res, { userId, gameId });
|
||||
|
||||
const archivedChats = await container.getArchivedChatsQueryHandler.execute({
|
||||
userId,
|
||||
gameId
|
||||
});
|
||||
|
||||
logRequest('Archived game chats retrieved successfully', req, res, {
|
||||
userId,
|
||||
gameId,
|
||||
chatCount: archivedChats.length
|
||||
});
|
||||
|
||||
res.json(archivedChats);
|
||||
} catch (error) {
|
||||
logError('Get archived game chats endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
export default chatRouter;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Router } from 'express';
|
||||
import { container } from '../../Application/Services/DIContainer';
|
||||
import { logRequest, logError } from '../../Application/Services/Logger';
|
||||
import { ContactType } from '../../Domain/Contact/ContactAggregate';
|
||||
|
||||
const contactRouter = Router();
|
||||
|
||||
// Public endpoint - anyone can create a contact
|
||||
contactRouter.post('/', async (req, res) => {
|
||||
try {
|
||||
// Get user ID if authenticated (optional)
|
||||
const userId = (req as any).user?.userId || null;
|
||||
|
||||
const { name, email, type, txt } = req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !email || type === undefined || !txt) {
|
||||
return res.status(400).json({
|
||||
error: 'Missing required fields: name, email, type, and txt are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (!Object.values(ContactType).includes(Number(type))) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid contact type. Must be one of: 0 (Bug), 1 (Problem), 2 (Question), 3 (Sales), 4 (Other)'
|
||||
});
|
||||
}
|
||||
|
||||
logRequest('Create contact endpoint accessed', req, res, { name, email, type, userId });
|
||||
|
||||
const result = await container.createContactCommandHandler.execute({
|
||||
name,
|
||||
email,
|
||||
userid: userId,
|
||||
type: Number(type),
|
||||
txt
|
||||
});
|
||||
|
||||
logRequest('Contact created successfully', req, res, { contactId: result.id, name, email, type });
|
||||
res.status(201).json(result);
|
||||
} catch (error) {
|
||||
logError('Create contact endpoint error', error as Error, req, res);
|
||||
|
||||
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' });
|
||||
}
|
||||
});
|
||||
|
||||
export default contactRouter;
|
||||
@@ -0,0 +1,124 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import multer from 'multer';
|
||||
import { DIContainer } from '../../Application/Services/DIContainer';
|
||||
import { authRequired } from '../../Application/Services/AuthMiddleware';
|
||||
import { logRequest, logError, logWarning } 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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Export deck to .spr file (encrypted) - users can only export their own decks
|
||||
router.get('/export/:deckId', authRequired, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { deckId } = req.params;
|
||||
const userId = (req as any).user.userId;
|
||||
|
||||
logRequest('Export deck endpoint accessed', req, res, { deckId, userId });
|
||||
|
||||
// Check if user owns the deck
|
||||
const deck = await container.deckRepository.findById(deckId);
|
||||
if (!deck) {
|
||||
logWarning('Deck not found for export', { deckId, userId }, req, res);
|
||||
return res.status(404).json({ error: 'Deck not found' });
|
||||
}
|
||||
|
||||
// Users can only export their own decks
|
||||
if (deck.userid !== userId) {
|
||||
logWarning('Access denied - user attempted to export deck they do not own', {
|
||||
deckId,
|
||||
userId,
|
||||
deckOwnerId: deck.userid
|
||||
}, req, res);
|
||||
return res.status(403).json({ error: 'Access denied - you can only export your own decks' });
|
||||
}
|
||||
|
||||
const sprData = await container.deckImportExportService.exportDeckToSpr(deckId, userId);
|
||||
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${deck.name || 'deck'}.spr"`);
|
||||
|
||||
logRequest('Deck exported successfully', req, res, {
|
||||
deckId,
|
||||
userId,
|
||||
deckName: deck.name,
|
||||
fileSize: sprData.length
|
||||
});
|
||||
|
||||
res.send(sprData);
|
||||
} catch (error) {
|
||||
logError('Export deck endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Import deck from .spr file (encrypted) - imported deck will be owned by the importing user
|
||||
router.post('/import', authRequired, upload.single('file'), async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
|
||||
logRequest('Import deck endpoint accessed', req, res, {
|
||||
userId,
|
||||
hasFile: !!req.file,
|
||||
fileName: req.file?.originalname,
|
||||
fileSize: req.file?.size
|
||||
});
|
||||
|
||||
if (!req.file) {
|
||||
logWarning('No file uploaded for deck import', { userId }, req, res);
|
||||
return res.status(400).json({ error: 'No file uploaded' });
|
||||
}
|
||||
|
||||
const fileBuffer = req.file!.buffer;
|
||||
|
||||
// Import the deck and assign ownership to the current user
|
||||
const result = await container.deckImportExportService.importDeckFromSpr(fileBuffer, userId);
|
||||
|
||||
logRequest('Deck imported successfully', req, res, {
|
||||
userId,
|
||||
deckId: result.id,
|
||||
deckName: result.name || 'Unknown',
|
||||
fileName: req.file.originalname,
|
||||
fileSize: req.file.size
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Deck imported successfully and added to your collection',
|
||||
deckId: result.id
|
||||
});
|
||||
} catch (error) {
|
||||
logError('Import deck endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error && error.message.includes('Invalid')) {
|
||||
return res.status(400).json({ error: 'Invalid file format or corrupted data' });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Router } from 'express';
|
||||
import { authRequired } from '../../Application/Services/AuthMiddleware';
|
||||
import { container } from '../../Application/Services/DIContainer';
|
||||
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
|
||||
import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware';
|
||||
import { GeneralSearchService } from '../../Application/Search/Generalsearch';
|
||||
import { logRequest, logError, logWarning } from '../../Application/Services/Logger';
|
||||
|
||||
const deckRouter = Router();
|
||||
|
||||
// Create search service that isn't in the container yet
|
||||
const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository);
|
||||
|
||||
// Authenticated routes - Get decks with pagination (RECOMMENDED)
|
||||
deckRouter.get('/page/:from/:to', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const userOrgId = (req as any).user.orgId;
|
||||
const isAdmin = (req as any).user.authLevel === 1;
|
||||
const from = parseInt(req.params.from);
|
||||
const to = parseInt(req.params.to);
|
||||
|
||||
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('Get decks by page endpoint accessed', req, res, {
|
||||
userId,
|
||||
userOrgId,
|
||||
isAdmin,
|
||||
from,
|
||||
to
|
||||
});
|
||||
|
||||
// Use paginated query handler for memory efficiency
|
||||
const result = await container.getDecksByPageQueryHandler.execute({
|
||||
userId,
|
||||
userOrgId,
|
||||
isAdmin,
|
||||
from,
|
||||
to
|
||||
});
|
||||
|
||||
logRequest('Get decks page completed successfully', req, res, {
|
||||
userId,
|
||||
from,
|
||||
to,
|
||||
returnedCount: result.decks.length,
|
||||
totalCount: result.totalCount
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Get decks by page endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
deckRouter.post('/', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
logRequest('Create deck endpoint accessed', req, res, { name: req.body.name, userId });
|
||||
req.body.userid = userId; // Set userId in request body
|
||||
const result = await container.createDeckCommandHandler.execute(req.body);
|
||||
|
||||
logRequest('Deck created successfully', req, res, { deckId: result.id, name: req.body.name, userId });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Create deck 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: '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' });
|
||||
}
|
||||
});
|
||||
|
||||
deckRouter.get('/search', authRequired, async (req, res) => {
|
||||
try {
|
||||
const { query, limit, offset } = req.query;
|
||||
logRequest('Search decks endpoint accessed', req, res, { query, limit, offset });
|
||||
|
||||
if (!query || typeof query !== 'string') {
|
||||
logWarning('Deck search attempted without query', { query, hasQuery: !!query }, req, res);
|
||||
return res.status(400).json({ error: 'Search query is required' });
|
||||
}
|
||||
|
||||
const searchQuery = {
|
||||
query: query.trim(),
|
||||
limit: limit ? parseInt(limit as string) : 20,
|
||||
offset: offset ? parseInt(offset as string) : 0
|
||||
};
|
||||
|
||||
// Validate pagination parameters
|
||||
if (searchQuery.limit < 1 || searchQuery.limit > 100) {
|
||||
logWarning('Invalid deck search limit parameter', { limit: searchQuery.limit }, req, res);
|
||||
return res.status(400).json({ error: 'Limit must be between 1 and 100' });
|
||||
}
|
||||
|
||||
if (searchQuery.offset < 0) {
|
||||
logWarning('Invalid deck search offset parameter', { offset: searchQuery.offset }, req, res);
|
||||
return res.status(400).json({ error: 'Offset must be non-negative' });
|
||||
}
|
||||
|
||||
const result = await searchService.searchFromUrl(req.originalUrl, searchQuery);
|
||||
|
||||
logRequest('Deck search completed successfully', req, res, {
|
||||
query: searchQuery.query,
|
||||
resultCount: Array.isArray(result) ? result.length : 0
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Search decks endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
deckRouter.get('/:id', authRequired, async (req, res) => {
|
||||
try {
|
||||
const deckId = req.params.id;
|
||||
logRequest('Get deck by id endpoint accessed', req, res, { deckId });
|
||||
|
||||
const result = await container.getDeckByIdQueryHandler.execute({ id: deckId });
|
||||
|
||||
if (!result) {
|
||||
logWarning('Deck not found', { deckId }, req, res);
|
||||
return res.status(404).json({ error: 'Deck not found' });
|
||||
}
|
||||
|
||||
logRequest('Deck retrieved successfully', req, res, { deckId });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Get deck by id endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
deckRouter.patch('/:id', authRequired, async (req, res) => {
|
||||
try {
|
||||
const deckId = req.params.id;
|
||||
const userId = (req as any).user.userId;
|
||||
logRequest('Update deck endpoint accessed', req, res, { deckId, userId, updateFields: Object.keys(req.body) });
|
||||
|
||||
const result = await container.updateDeckCommandHandler.execute({ id: deckId, ...req.body });
|
||||
|
||||
logRequest('Deck updated successfully', req, res, { deckId, userId });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('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 });
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes('admin')) {
|
||||
return res.status(403).json({ error: 'Forbidden: ' + error.message });
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
deckRouter.delete('/:id', authRequired, async (req, res) => {
|
||||
try {
|
||||
const deckId = req.params.id;
|
||||
const userId = (req as any).user.userId;
|
||||
logRequest('Soft delete deck endpoint accessed', req, res, { deckId, userId });
|
||||
|
||||
const result = await container.deleteDeckCommandHandler.execute({ id: deckId, soft: true });
|
||||
|
||||
logRequest('Deck soft delete successful', req, res, { deckId, userId, success: result });
|
||||
res.json({ success: result });
|
||||
} catch (error) {
|
||||
logError('Soft 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' });
|
||||
}
|
||||
});
|
||||
|
||||
export default deckRouter;
|
||||
@@ -0,0 +1,308 @@
|
||||
import { Router } from 'express';
|
||||
import { authRequired } from '../../Application/Services/AuthMiddleware';
|
||||
import { optionalAuth } from '../middleware/optionalAuth';
|
||||
import { container } from '../../Application/Services/DIContainer';
|
||||
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
|
||||
import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware';
|
||||
import { logRequest, logError, logWarning } from '../../Application/Services/Logger';
|
||||
import { LoginType } from '../../Domain/Game/GameAggregate';
|
||||
|
||||
const gameRouter = Router();
|
||||
|
||||
gameRouter.post('/start', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const orgId = (req as any).user.orgId;
|
||||
const { deckids, maxplayers, logintype } = req.body;
|
||||
|
||||
logRequest('Start game endpoint accessed', req, res, {
|
||||
userId,
|
||||
orgId,
|
||||
deckCount: deckids?.length,
|
||||
maxplayers,
|
||||
logintype
|
||||
});
|
||||
|
||||
// Validate required fields
|
||||
if (!deckids || !Array.isArray(deckids) || deckids.length === 0) {
|
||||
return res.status(400).json({ error: 'deckids is required and must be a non-empty array' });
|
||||
}
|
||||
|
||||
if (!maxplayers || typeof maxplayers !== 'number') {
|
||||
return res.status(400).json({ error: 'maxplayers is required and must be a number' });
|
||||
}
|
||||
|
||||
if (logintype === undefined || typeof logintype !== 'number') {
|
||||
return res.status(400).json({ error: 'logintype is required and must be a number (0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION)' });
|
||||
}
|
||||
|
||||
// Start the game using the GameService
|
||||
const game = await container.gameService.startGame(
|
||||
deckids,
|
||||
maxplayers,
|
||||
logintype as LoginType,
|
||||
userId,
|
||||
orgId
|
||||
);
|
||||
|
||||
logRequest('Game started successfully', req, res, {
|
||||
userId,
|
||||
gameId: game.id,
|
||||
gameCode: game.gamecode,
|
||||
deckCount: game.gamedecks.length,
|
||||
totalCards: game.gamedecks.reduce((sum, deck) => sum + deck.cards.length, 0)
|
||||
});
|
||||
|
||||
res.json(game);
|
||||
} catch (error) {
|
||||
logError('Start game endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('validation') ||
|
||||
error.message.includes('must be') ||
|
||||
error.message.includes('required') ||
|
||||
error.message.includes('Invalid')) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
gameRouter.post('/join', optionalAuth, async (req, res) => {
|
||||
try {
|
||||
const user = (req as any).user;
|
||||
const { gameCode, playerName } = req.body;
|
||||
|
||||
logRequest('Join game endpoint accessed', req, res, {
|
||||
gameCode,
|
||||
playerName,
|
||||
hasAuth: !!user,
|
||||
userId: user?.userId,
|
||||
orgId: user?.orgId
|
||||
});
|
||||
|
||||
// Validate required fields
|
||||
if (!gameCode || typeof gameCode !== 'string') {
|
||||
return res.status(400).json({ error: 'gameCode is required and must be a string' });
|
||||
}
|
||||
|
||||
if (gameCode.length !== 6) {
|
||||
return res.status(400).json({ error: 'gameCode must be exactly 6 characters long' });
|
||||
}
|
||||
|
||||
// First, we need to find the game to determine its type
|
||||
const gameRepository = container.gameRepository;
|
||||
const gameToJoin = await gameRepository.findByGameCode(gameCode);
|
||||
|
||||
if (!gameToJoin) {
|
||||
return res.status(404).json({ error: 'Game not found' });
|
||||
}
|
||||
|
||||
// Determine join requirements based on game login type
|
||||
let actualPlayerId: string | undefined;
|
||||
let actualPlayerName: string | undefined;
|
||||
let actualOrgId: string | null = null;
|
||||
|
||||
switch (gameToJoin.logintype) {
|
||||
case LoginType.PUBLIC:
|
||||
// Public games: playerName required, authentication optional
|
||||
// If user is logged in and no playerName provided, use their username
|
||||
if (!playerName || typeof playerName !== 'string' || !playerName.trim()) {
|
||||
if (user && user.userId) {
|
||||
// User is logged in, fetch their username to use as playerName
|
||||
try {
|
||||
const userDetails = await container.getUserByIdQueryHandler.execute({ id: user.userId });
|
||||
if (userDetails && userDetails.username) {
|
||||
actualPlayerName = userDetails.username;
|
||||
logRequest('Using logged-in user\'s username as playerName', req, res, {
|
||||
userId: user.userId,
|
||||
username: userDetails.username
|
||||
});
|
||||
} else {
|
||||
return res.status(400).json({
|
||||
error: 'playerName is required for public games'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Failed to fetch user details for playerName', error as Error, req, res);
|
||||
return res.status(400).json({
|
||||
error: 'playerName is required for public games'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// User is not logged in, playerName is required
|
||||
return res.status(400).json({
|
||||
error: 'playerName is required for public games'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// playerName was provided, use it
|
||||
actualPlayerName = playerName.trim();
|
||||
}
|
||||
actualPlayerId = user?.userId; // Use authenticated user ID if available, otherwise undefined
|
||||
break;
|
||||
|
||||
case LoginType.PRIVATE:
|
||||
// Private games: authentication required
|
||||
if (!user || !user.userId) {
|
||||
return res.status(401).json({
|
||||
error: 'Authentication required to join private games'
|
||||
});
|
||||
}
|
||||
actualPlayerId = user.userId;
|
||||
actualPlayerName = playerName;
|
||||
break;
|
||||
|
||||
case LoginType.ORGANIZATION:
|
||||
// Organization games: authentication + organization membership required
|
||||
if (!user || !user.userId) {
|
||||
return res.status(401).json({
|
||||
error: 'Authentication required to join organization games'
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.orgId) {
|
||||
return res.status(403).json({
|
||||
error: 'Organization membership required to join organization games'
|
||||
});
|
||||
}
|
||||
|
||||
if (gameToJoin.orgid && user.orgId !== gameToJoin.orgid) {
|
||||
return res.status(403).json({
|
||||
error: 'You must be a member of the same organization to join this game'
|
||||
});
|
||||
}
|
||||
|
||||
actualPlayerId = user.userId;
|
||||
actualPlayerName = playerName;
|
||||
actualOrgId = user.orgId;
|
||||
break;
|
||||
|
||||
default:
|
||||
return res.status(400).json({ error: 'Invalid game type' });
|
||||
}
|
||||
|
||||
// Join the game using the GameService with determined parameters
|
||||
const game = await container.gameService.joinGame(
|
||||
gameCode,
|
||||
actualPlayerId,
|
||||
actualPlayerName,
|
||||
actualOrgId,
|
||||
gameToJoin.logintype
|
||||
);
|
||||
|
||||
logRequest('Player joined game successfully', req, res, {
|
||||
userId: actualPlayerId || 'anonymous',
|
||||
gameId: game.id,
|
||||
gameCode: game.gamecode,
|
||||
gameType: LoginType[gameToJoin.logintype],
|
||||
playerCount: game.players.length,
|
||||
maxPlayers: game.maxplayers,
|
||||
playerName: actualPlayerName
|
||||
});
|
||||
|
||||
res.json(game);
|
||||
} catch (error) {
|
||||
logError('Join game endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('Authentication required')) {
|
||||
return res.status(401).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('Organization') || error.message.includes('organization')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('full') ||
|
||||
error.message.includes('already in') ||
|
||||
error.message.includes('not accepting')) {
|
||||
return res.status(409).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('validation') ||
|
||||
error.message.includes('must be') ||
|
||||
error.message.includes('required') ||
|
||||
error.message.includes('Invalid')) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
gameRouter.post('/:gameId/start', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const { gameId } = req.params;
|
||||
|
||||
logRequest('Start gameplay endpoint accessed', req, res, {
|
||||
userId,
|
||||
gameId
|
||||
});
|
||||
|
||||
// Validate required fields
|
||||
if (!gameId || typeof gameId !== 'string') {
|
||||
return res.status(400).json({ error: 'gameId is required and must be a string' });
|
||||
}
|
||||
|
||||
// Start the gameplay using the GameService
|
||||
const result = await container.gameService.startGamePlay(gameId, userId);
|
||||
|
||||
logRequest('Game gameplay started successfully', req, res, {
|
||||
userId,
|
||||
gameId,
|
||||
playerCount: result.game.players.length
|
||||
});
|
||||
|
||||
res.json({
|
||||
message: 'Game started successfully',
|
||||
gameId: gameId,
|
||||
playerCount: result.game.players.length,
|
||||
game: result.game,
|
||||
boardData: result.boardData
|
||||
});
|
||||
} catch (error) {
|
||||
logError('Start gameplay endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('Only') || error.message.includes('master')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('already started') ||
|
||||
error.message.includes('not ready') ||
|
||||
error.message.includes('minimum players') ||
|
||||
error.message.includes('not in waiting state') ||
|
||||
error.message.includes('cannot be started')) {
|
||||
return res.status(409).json({ error: error.message });
|
||||
}
|
||||
if (error.message.includes('validation') ||
|
||||
error.message.includes('must be') ||
|
||||
error.message.includes('required') ||
|
||||
error.message.includes('Invalid')) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
// Board generation specific errors
|
||||
if (error.message.includes('Board generation') ||
|
||||
error.message.includes('board not found') ||
|
||||
error.message.includes('BoardGenerationService') ||
|
||||
error.message.includes('Failed to wait for board generation') ||
|
||||
error.message.includes('board generation timeout')) {
|
||||
return res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
export default gameRouter;
|
||||
@@ -0,0 +1,204 @@
|
||||
import { Router } from 'express';
|
||||
import { authRequired } from '../../Application/Services/AuthMiddleware';
|
||||
import { container } from '../../Application/Services/DIContainer';
|
||||
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
|
||||
import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware';
|
||||
import { GeneralSearchService } from '../../Application/Search/Generalsearch';
|
||||
import { logRequest, logError, logWarning, logAuth } from '../../Application/Services/Logger';
|
||||
|
||||
const organizationRouter = Router();
|
||||
|
||||
// Create search service that isn't in the container yet
|
||||
const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository);
|
||||
|
||||
// Auth routes - Get organizations with pagination (RECOMMENDED)
|
||||
organizationRouter.get('/page/:from/:to', authRequired, async (req, res) => {
|
||||
try {
|
||||
const from = parseInt(req.params.from);
|
||||
const to = parseInt(req.params.to);
|
||||
|
||||
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('Get organizations by page endpoint accessed', req, res, { from, to });
|
||||
|
||||
const result = await container.getOrganizationsByPageQueryHandler.execute({ from, to });
|
||||
|
||||
logRequest('Organizations page retrieved successfully', req, res, {
|
||||
from,
|
||||
to,
|
||||
count: result.organizations.length,
|
||||
totalCount: result.totalCount
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Get organizations by page endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
organizationRouter.get('/search', authRequired, async (req, res) => {
|
||||
try {
|
||||
const { query, limit, offset } = req.query;
|
||||
logRequest('Search organizations endpoint accessed', req, res, { query, limit, offset });
|
||||
|
||||
if (!query || typeof query !== 'string') {
|
||||
logWarning('Organization search attempted without query', { query, hasQuery: !!query }, req, res);
|
||||
return res.status(400).json({ error: 'Search query is required' });
|
||||
}
|
||||
|
||||
const searchQuery = {
|
||||
query: query.trim(),
|
||||
limit: limit ? parseInt(limit as string) : 20,
|
||||
offset: offset ? parseInt(offset as string) : 0
|
||||
};
|
||||
|
||||
// Validate pagination parameters
|
||||
if (searchQuery.limit < 1 || searchQuery.limit > 100) {
|
||||
logWarning('Invalid organization search limit parameter', { limit: searchQuery.limit }, req, res);
|
||||
return res.status(400).json({ error: 'Limit must be between 1 and 100' });
|
||||
}
|
||||
|
||||
if (searchQuery.offset < 0) {
|
||||
logWarning('Invalid organization search offset parameter', { offset: searchQuery.offset }, req, res);
|
||||
return res.status(400).json({ error: 'Offset must be non-negative' });
|
||||
}
|
||||
|
||||
const result = await searchService.searchFromUrl(req.originalUrl, searchQuery);
|
||||
|
||||
logRequest('Organization search completed successfully', req, res, {
|
||||
query: searchQuery.query,
|
||||
resultCount: Array.isArray(result) ? result.length : 0
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Search organizations endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get organization login URL
|
||||
organizationRouter.get('/:orgId/login-url', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const { orgId } = req.params;
|
||||
|
||||
logRequest('Get organization login URL endpoint accessed', req, res, {
|
||||
userId,
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
const result = await container.getOrganizationLoginUrlQueryHandler.execute({
|
||||
organizationId: orgId
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
logWarning('Organization login URL not found', {
|
||||
organizationId: orgId,
|
||||
userId
|
||||
}, req, res);
|
||||
return ErrorResponseService.sendNotFound(res, 'Organization login URL not found');
|
||||
}
|
||||
|
||||
logRequest('Organization login URL retrieved successfully', req, res, {
|
||||
organizationId: orgId,
|
||||
organizationName: result.organizationName,
|
||||
hasUrl: !!result.loginUrl,
|
||||
userId
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Get organization login URL endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Process third-party authentication callback
|
||||
organizationRouter.post('/auth-callback', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const { organizationId, status, authToken } = req.body;
|
||||
|
||||
logRequest('Organization auth callback endpoint accessed', req, res, {
|
||||
userId,
|
||||
organizationId,
|
||||
status,
|
||||
hasAuthToken: !!authToken
|
||||
});
|
||||
|
||||
// Validate required fields
|
||||
if (!organizationId || !status) {
|
||||
logWarning('Missing required fields for organization auth callback', {
|
||||
organizationId: !!organizationId,
|
||||
status: !!status,
|
||||
userId
|
||||
}, req, res);
|
||||
return ErrorResponseService.sendBadRequest(res, 'organizationId and status are required');
|
||||
}
|
||||
|
||||
if (status !== 'ok' && status !== 'not_ok') {
|
||||
logWarning('Invalid status value for organization auth callback', {
|
||||
status,
|
||||
userId,
|
||||
organizationId
|
||||
}, req, res);
|
||||
return ErrorResponseService.sendBadRequest(res, 'status must be either "ok" or "not_ok"');
|
||||
}
|
||||
|
||||
const result = await container.processOrgAuthCallbackCommandHandler.execute({
|
||||
organizationId,
|
||||
userId,
|
||||
status,
|
||||
authToken
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
if (result.message.includes('not found')) {
|
||||
logWarning('Organization auth callback failed - entity not found', {
|
||||
userId,
|
||||
organizationId,
|
||||
message: result.message
|
||||
}, req, res);
|
||||
return ErrorResponseService.sendNotFound(res, result.message);
|
||||
}
|
||||
if (result.message.includes('does not belong')) {
|
||||
logWarning('Organization auth callback failed - authorization error', {
|
||||
userId,
|
||||
organizationId,
|
||||
message: result.message
|
||||
}, req, res);
|
||||
return ErrorResponseService.sendForbidden(res, result.message);
|
||||
}
|
||||
if (result.message.includes('authentication failed')) {
|
||||
logAuth('Organization authentication failed via callback', userId, {
|
||||
organizationId,
|
||||
status
|
||||
}, req, res);
|
||||
return ErrorResponseService.sendUnauthorized(res, result.message);
|
||||
}
|
||||
|
||||
logError('Organization auth callback internal error', new Error(result.message), req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
|
||||
logAuth('Organization auth callback processed successfully', userId, {
|
||||
organizationId,
|
||||
status,
|
||||
updatedFields: result.updatedFields
|
||||
}, req, res);
|
||||
|
||||
res.json({
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
updatedFields: result.updatedFields
|
||||
});
|
||||
} catch (error) {
|
||||
logError('Organization auth callback endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
export default organizationRouter;
|
||||
@@ -0,0 +1,66 @@
|
||||
import e, { Router } from 'express';
|
||||
import { container, DIContainer } from '../../Application/Services/DIContainer';
|
||||
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
|
||||
import { logRequest, logError, logAuth, logWarning, logOther } from '../../Application/Services/Logger';
|
||||
import { GenerateBoardCommand } from '../../Application/Game/commands/GenerateBoardCommand';
|
||||
|
||||
const router = Router();
|
||||
|
||||
//function to test the search service
|
||||
async function triggerAsyncBoardGeneration(gameId: string): Promise<boolean> {
|
||||
try {
|
||||
// Calculate default field counts based on game configuration
|
||||
// For now, use reasonable defaults - this should be configurable by host in the future
|
||||
const maxSpecialFieldsPercentage = parseInt(process.env.MAX_SPECIAL_FIELDS_PERCENTAGE || '67');
|
||||
const maxSpecialFields = Math.floor((100 * maxSpecialFieldsPercentage) / 100);
|
||||
|
||||
// Default distribution: 60% positive, 25% negative, 15% luck
|
||||
const positiveFieldCount = Math.floor(maxSpecialFields * 0.6);
|
||||
const negativeFieldCount = Math.floor(maxSpecialFields * 0.25);
|
||||
const luckFieldCount = Math.floor(maxSpecialFields * 0.15);
|
||||
|
||||
const command: GenerateBoardCommand = {
|
||||
gameId,
|
||||
positiveFieldCount,
|
||||
negativeFieldCount,
|
||||
luckFieldCount
|
||||
};
|
||||
|
||||
logOther(`Triggering async board generation for game ${gameId}`, {
|
||||
positiveFieldCount,
|
||||
negativeFieldCount,
|
||||
luckFieldCount,
|
||||
totalSpecialFields: positiveFieldCount + negativeFieldCount + luckFieldCount
|
||||
});
|
||||
|
||||
// Execute board generation in background
|
||||
await DIContainer.getInstance().generateBoardCommandHandler.execute(command);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
logError(`Async board generation failed for game ${gameId}`, error as Error);
|
||||
// Don't propagate error - board generation failure shouldn't affect game creation
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Game board generation endpoint
|
||||
router.post('/gameBoardGeneration', async (req, res) => {
|
||||
try {
|
||||
logRequest('Game board generation endpoint accessed', req, res);
|
||||
|
||||
const result = await triggerAsyncBoardGeneration("######-#####-#####-######");
|
||||
|
||||
if (result) {
|
||||
logOther('Game board generation triggered successfully', result);
|
||||
return res.json({ message: 'Game board generation triggered successfully' });
|
||||
} else {
|
||||
throw new Error('Game board generation failed to trigger');
|
||||
}
|
||||
} catch (error : any) {
|
||||
logError('Error in game board generation endpoint', error);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
export default router;
|
||||
@@ -0,0 +1,313 @@
|
||||
import { Router } from 'express';
|
||||
import { authRequired } from '../../Application/Services/AuthMiddleware';
|
||||
import { container } from '../../Application/Services/DIContainer';
|
||||
import { ErrorResponseService } from '../../Application/Services/ErrorResponseService';
|
||||
import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware';
|
||||
import { GeneralSearchService } from '../../Application/Search/Generalsearch';
|
||||
import { logRequest, logError, logAuth, logWarning } from '../../Application/Services/Logger';
|
||||
|
||||
const userRouter = Router();
|
||||
|
||||
// Create search service that isn't in the container yet
|
||||
const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository);
|
||||
|
||||
// Login endpoint
|
||||
userRouter.post('/login',
|
||||
ValidationMiddleware.combine([
|
||||
ValidationMiddleware.validateRequiredFields(['username', 'password']),
|
||||
ValidationMiddleware.validateStringLength({
|
||||
username: { min: 3, max: 50 },
|
||||
password: { min: 6, max: 100 }
|
||||
})
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
logRequest('Login endpoint accessed', req, res, { username: req.body.username });
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
const result = await container.loginCommandHandler.execute({ username, password }, res);
|
||||
|
||||
if (result) {
|
||||
logAuth('User login successful', result.user.id, { username: result.user.username }, req, res);
|
||||
res.json(result);
|
||||
} else {
|
||||
throw new Error(`Login failed: ${result}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError('Login endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Invalid username')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password');
|
||||
}
|
||||
if (error.message.includes('Invalid password')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password');
|
||||
}
|
||||
if (error.message.includes('not verified')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address');
|
||||
}
|
||||
if (error.message.includes('restriction')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address');
|
||||
}
|
||||
if (error.message.includes('deactivated')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Account has been deactivated');
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Create user endpoint
|
||||
userRouter.post('/create',
|
||||
ValidationMiddleware.combine([
|
||||
ValidationMiddleware.validateRequiredFields(['username', 'email', 'password']),
|
||||
ValidationMiddleware.validateEmailFormat(['email']),
|
||||
ValidationMiddleware.validateStringLength({
|
||||
username: { min: 3, max: 50 },
|
||||
password: { min: 6, max: 100 }
|
||||
})
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
logRequest('Create user endpoint accessed', req, res, {
|
||||
username: req.body.username,
|
||||
email: req.body.email
|
||||
});
|
||||
|
||||
const result = await container.createUserCommandHandler.execute(req.body);
|
||||
|
||||
logRequest('User created successfully', req, res, {
|
||||
userId: result.id,
|
||||
username: result.username
|
||||
});
|
||||
|
||||
res.status(201).json(result);
|
||||
|
||||
} catch (error) {
|
||||
// Don't log here since CreateUserCommandHandler already logs system errors
|
||||
// Only log validation/user input errors at router level
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('already exists')) {
|
||||
return ErrorResponseService.sendConflict(res, error.message);
|
||||
}
|
||||
if (error.message.includes('validation')) {
|
||||
return ErrorResponseService.sendBadRequest(res, error.message);
|
||||
}
|
||||
// Log unexpected errors that weren't handled by the command handler
|
||||
if (!error.message.includes('Failed to create user')) {
|
||||
logError('Unexpected create user endpoint error', error as Error, req, res);
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Get user profile (current user)
|
||||
userRouter.get('/profile', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
|
||||
logRequest('Get user profile endpoint accessed', req, res, { userId });
|
||||
|
||||
const result = await container.getUserByIdQueryHandler.execute({ id: userId });
|
||||
|
||||
if (!result) {
|
||||
logWarning('User profile not found', { userId }, req, res);
|
||||
return ErrorResponseService.sendNotFound(res, 'User not found');
|
||||
}
|
||||
|
||||
logRequest('User profile retrieved successfully', req, res, {
|
||||
userId,
|
||||
username: result.username
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
|
||||
} catch (error) {
|
||||
logError('Get user profile endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Update user profile (current user)
|
||||
userRouter.patch('/profile', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
|
||||
logRequest('Update user profile endpoint accessed', req, res, {
|
||||
userId,
|
||||
fieldsToUpdate: Object.keys(req.body)
|
||||
});
|
||||
|
||||
const result = await container.updateUserCommandHandler.execute({ id: userId, ...req.body });
|
||||
|
||||
if (!result) {
|
||||
return ErrorResponseService.sendNotFound(res, 'User not found');
|
||||
}
|
||||
|
||||
logRequest('User profile updated successfully', req, res, {
|
||||
userId,
|
||||
username: result.username
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
|
||||
} catch (error) {
|
||||
logError('Update user profile endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('already exists')) {
|
||||
return ErrorResponseService.sendConflict(res, error.message);
|
||||
}
|
||||
if (error.message.includes('validation')) {
|
||||
return ErrorResponseService.sendBadRequest(res, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
//Soft delete user (current user)
|
||||
userRouter.delete('/profile', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
const result = await container.deleteUserCommandHandler.execute({ id: userId, soft: true });
|
||||
logRequest('User soft deleted successfully', req, res, { userId });
|
||||
res.json({ success: result });
|
||||
} catch (error) {
|
||||
logError('Soft delete user endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
//logout user (current user)
|
||||
userRouter.post('/logout', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
await container.logoutCommandHandler.execute(userId, res, req);
|
||||
logRequest('User logged out successfully', req, res, { userId });
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError('Logout user endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Email verification endpoint
|
||||
userRouter.get('/verify-email/:token', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.params;
|
||||
|
||||
logRequest('Email verification endpoint accessed', req, res, {
|
||||
tokenPrefix: token.substring(0, 8) + '...'
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Verification token is required');
|
||||
}
|
||||
|
||||
const result = await container.verifyEmailCommandHandler.execute({ token });
|
||||
|
||||
if (result) {
|
||||
logAuth('Email verification successful', undefined, { tokenPrefix: token.substring(0, 8) + '...' }, req, res);
|
||||
res.json({ success: true, message: 'Email verified successfully' });
|
||||
} else {
|
||||
throw new Error('Email verification failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError('Email verification endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Invalid') || error.message.includes('expired')) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Invalid or expired verification token');
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Forgot password request endpoint
|
||||
userRouter.post('/forgot-password',
|
||||
ValidationMiddleware.combine([
|
||||
ValidationMiddleware.validateRequiredFields(['email']),
|
||||
ValidationMiddleware.validateEmailFormat(['email'])
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
logRequest('Forgot password endpoint accessed', req, res, { email });
|
||||
|
||||
const result = await container.requestPasswordResetCommandHandler.execute({ email });
|
||||
|
||||
if (result) {
|
||||
logAuth('Password reset request successful', undefined, { email }, req, res);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'If an account with this email exists, a password reset link has been sent'
|
||||
});
|
||||
} else {
|
||||
throw new Error('Password reset request failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError('Forgot password endpoint error', error as Error, req, res);
|
||||
|
||||
// Always return success for security (don't reveal if email exists)
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'If an account with this email exists, a password reset link has been sent'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Reset password endpoint
|
||||
userRouter.post('/reset-password',
|
||||
ValidationMiddleware.combine([
|
||||
ValidationMiddleware.validateRequiredFields(['token', 'newPassword']),
|
||||
ValidationMiddleware.validateStringLength({
|
||||
newPassword: { min: 6, max: 100 }
|
||||
})
|
||||
]),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { token, newPassword } = req.body;
|
||||
|
||||
logRequest('Reset password endpoint accessed', req, res, {
|
||||
tokenPrefix: token.substring(0, 8) + '...'
|
||||
});
|
||||
|
||||
const result = await container.resetPasswordCommandHandler.execute({ token, newPassword });
|
||||
|
||||
if (result) {
|
||||
logAuth('Password reset successful', undefined, { tokenPrefix: token.substring(0, 8) + '...' }, req, res);
|
||||
res.json({ success: true, message: 'Password reset successfully' });
|
||||
} else {
|
||||
throw new Error('Password reset failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logError('Reset password endpoint error', error as Error, req, res);
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Invalid') || error.message.includes('expired')) {
|
||||
return ErrorResponseService.sendBadRequest(res, 'Invalid or expired reset token');
|
||||
}
|
||||
if (error.message.includes('Password validation')) {
|
||||
return ErrorResponseService.sendBadRequest(res, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
export default userRouter;
|
||||
@@ -0,0 +1,101 @@
|
||||
import swaggerJSDoc from 'swagger-jsdoc';
|
||||
import path from 'path';
|
||||
|
||||
export const swaggerOptions = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: 'SerpentRace API',
|
||||
version: '1.0.0',
|
||||
description: 'Comprehensive API documentation for SerpentRace Backend',
|
||||
contact: {
|
||||
name: 'SerpentRace Development Team',
|
||||
email: 'dev@serpentrace.com'
|
||||
},
|
||||
license: {
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3001',
|
||||
description: 'Local development server'
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:3000',
|
||||
description: 'Local development server (alt)'
|
||||
},
|
||||
{
|
||||
url: 'https://api.serpentrace.com',
|
||||
description: 'Production server'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'Enter JWT token obtained from /api/users/login'
|
||||
}
|
||||
}
|
||||
},
|
||||
security: [{ bearerAuth: [] }],
|
||||
tags: [
|
||||
{
|
||||
name: 'Users',
|
||||
description: 'User authentication and profile management'
|
||||
},
|
||||
{
|
||||
name: 'Organizations',
|
||||
description: 'Organization management and authentication'
|
||||
},
|
||||
{
|
||||
name: 'Decks',
|
||||
description: 'Deck creation, management, and gameplay'
|
||||
},
|
||||
{
|
||||
name: 'Chats',
|
||||
description: 'Real-time chat and messaging system'
|
||||
},
|
||||
{
|
||||
name: 'Contacts',
|
||||
description: 'Contact form and support requests'
|
||||
},
|
||||
{
|
||||
name: 'Deck Import/Export',
|
||||
description: 'Import and export deck functionality'
|
||||
},
|
||||
{
|
||||
name: 'Games',
|
||||
description: 'Game management and gameplay'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Users',
|
||||
description: 'Admin user management operations'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Decks',
|
||||
description: 'Admin deck management operations'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Organizations',
|
||||
description: 'Admin organization management operations'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Chats',
|
||||
description: 'Admin chat management operations'
|
||||
},
|
||||
{
|
||||
name: 'Admin - Contacts',
|
||||
description: 'Admin contact management operations'
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: [
|
||||
'./src/Api/swagger/swaggerDefinitionsFixed.ts'
|
||||
],
|
||||
};
|
||||
|
||||
export const swaggerSpec = swaggerJSDoc(swaggerOptions);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
import express from 'express';
|
||||
import swaggerUi from 'swagger-ui-express';
|
||||
import { swaggerSpec } from './swaggerConfig';
|
||||
|
||||
export function setupSwagger(app: express.Application) {
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
}
|
||||
Reference in New Issue
Block a user