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,141 @@
|
||||
import { GetChatHistoryQuery, GetArchivedChatsQuery } from './ChatQueries';
|
||||
import { IChatRepository } from '../../../Domain/IRepository/IChatRepository';
|
||||
import { IChatArchiveRepository } from '../../../Domain/IRepository/IChatArchiveRepository';
|
||||
import { Message } from '../../../Domain/Chat/ChatAggregate';
|
||||
import { logAuth, logError, logWarning } from '../../Services/Logger';
|
||||
|
||||
interface ChatHistoryResult {
|
||||
chatId: string;
|
||||
messages: Message[];
|
||||
isArchived: boolean;
|
||||
chatInfo: {
|
||||
type: string;
|
||||
name: string | null;
|
||||
gameId: string | null;
|
||||
users: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export class GetChatHistoryQueryHandler {
|
||||
constructor(
|
||||
private chatRepository: IChatRepository,
|
||||
private chatArchiveRepository: IChatArchiveRepository
|
||||
) {}
|
||||
|
||||
async execute(query: GetChatHistoryQuery): Promise<ChatHistoryResult | null> {
|
||||
try {
|
||||
// First try to find active chat
|
||||
const chat = await this.chatRepository.findById(query.chatId);
|
||||
|
||||
if (chat) {
|
||||
// Check authorization
|
||||
if (!chat.users.includes(query.userId)) {
|
||||
logWarning('Unauthorized chat history access attempt', {
|
||||
chatId: query.chatId,
|
||||
userId: query.userId
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
logAuth('Chat history retrieved', query.userId, {
|
||||
chatId: query.chatId,
|
||||
messageCount: chat.messages.length,
|
||||
isArchived: false
|
||||
});
|
||||
|
||||
return {
|
||||
chatId: query.chatId,
|
||||
messages: chat.messages,
|
||||
isArchived: false,
|
||||
chatInfo: {
|
||||
type: chat.type,
|
||||
name: chat.name,
|
||||
gameId: chat.gameId,
|
||||
users: chat.users
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Try to find in archives
|
||||
const archives = await this.chatArchiveRepository.findByChatId(query.chatId);
|
||||
const userArchive = archives.find(archive =>
|
||||
archive.participants.includes(query.userId)
|
||||
);
|
||||
|
||||
if (userArchive) {
|
||||
logAuth('Archived chat history retrieved', query.userId, {
|
||||
chatId: query.chatId,
|
||||
messageCount: userArchive.archivedMessages.length,
|
||||
isArchived: true
|
||||
});
|
||||
|
||||
return {
|
||||
chatId: query.chatId,
|
||||
messages: userArchive.archivedMessages,
|
||||
isArchived: true,
|
||||
chatInfo: {
|
||||
type: userArchive.chatType,
|
||||
name: userArchive.chatName,
|
||||
gameId: userArchive.gameId,
|
||||
users: userArchive.participants
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
logWarning('Chat history not found', {
|
||||
chatId: query.chatId,
|
||||
userId: query.userId
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
} catch (error) {
|
||||
logError('GetChatHistoryQueryHandler error', error as Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class GetArchivedChatsQueryHandler {
|
||||
constructor(private chatArchiveRepository: IChatArchiveRepository) {}
|
||||
|
||||
async execute(query: GetArchivedChatsQuery): Promise<ChatHistoryResult[]> {
|
||||
try {
|
||||
let archives: any[] = [];
|
||||
|
||||
if (query.gameId) {
|
||||
// Get archived game chats
|
||||
archives = await this.chatArchiveRepository.findByGameId(query.gameId);
|
||||
} else {
|
||||
// Get all archived chats for user (would need different query)
|
||||
// For now, return empty - this would need a new repository method
|
||||
archives = [];
|
||||
}
|
||||
|
||||
const result = archives
|
||||
.filter(archive => archive.participants.includes(query.userId))
|
||||
.map(archive => ({
|
||||
chatId: archive.chatId,
|
||||
messages: archive.archivedMessages,
|
||||
isArchived: true,
|
||||
chatInfo: {
|
||||
type: archive.chatType,
|
||||
name: archive.chatName,
|
||||
gameId: archive.gameId,
|
||||
users: archive.participants
|
||||
}
|
||||
}));
|
||||
|
||||
logAuth('Archived chats retrieved', query.userId, {
|
||||
count: result.length,
|
||||
gameId: query.gameId
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
logError('GetArchivedChatsQueryHandler error', error as Error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface GetUserChatsQuery {
|
||||
userId: string;
|
||||
includeArchived?: boolean;
|
||||
}
|
||||
|
||||
export interface GetChatHistoryQuery {
|
||||
chatId: string;
|
||||
userId: string; // For authorization
|
||||
}
|
||||
|
||||
export interface GetArchivedChatsQuery {
|
||||
userId: string;
|
||||
gameId?: string;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface GetChatsByPageQuery {
|
||||
from: number;
|
||||
to: number;
|
||||
includeDeleted?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { IChatRepository } from '../../../Domain/IRepository/IChatRepository';
|
||||
import { GetChatsByPageQuery } from './GetChatsByPageQuery';
|
||||
import { ShortChatDto } from '../../DTOs/ChatDto';
|
||||
import { ChatMapper } from '../../DTOs/Mappers/ChatMapper';
|
||||
import { logRequest, logError } from '../../Services/Logger';
|
||||
|
||||
export class GetChatsByPageQueryHandler {
|
||||
constructor(private readonly chatRepo: IChatRepository) {}
|
||||
|
||||
async execute(query: GetChatsByPageQuery): Promise<{ chats: ShortChatDto[], totalCount: number }> {
|
||||
try {
|
||||
// Validate pagination parameters
|
||||
if (query.from < 0 || query.to < query.from) {
|
||||
throw new Error('Invalid pagination parameters');
|
||||
}
|
||||
|
||||
const limit = query.to - query.from + 1;
|
||||
if (limit > 100) {
|
||||
throw new Error('Page size too large. Maximum 100 records per request');
|
||||
}
|
||||
|
||||
logRequest('Get chats by page query started', undefined, undefined, {
|
||||
from: query.from,
|
||||
to: query.to,
|
||||
includeDeleted: query.includeDeleted || false
|
||||
});
|
||||
|
||||
const result = query.includeDeleted
|
||||
? await this.chatRepo.findByPageIncludingDeleted(query.from, query.to)
|
||||
: await this.chatRepo.findByPage(query.from, query.to);
|
||||
|
||||
logRequest('Get chats by page query completed', undefined, undefined, {
|
||||
from: query.from,
|
||||
to: query.to,
|
||||
returned: result.chats.length,
|
||||
totalCount: result.totalCount,
|
||||
includeDeleted: query.includeDeleted || false
|
||||
});
|
||||
|
||||
return {
|
||||
chats: ChatMapper.toShortDtoList(result.chats),
|
||||
totalCount: result.totalCount
|
||||
};
|
||||
} catch (error) {
|
||||
logError('GetChatsByPageQueryHandler error', error instanceof Error ? error : new Error(String(error)));
|
||||
|
||||
// Re-throw validation errors as-is
|
||||
if (error instanceof Error && (error.message.includes('Invalid pagination') || error.message.includes('Page size'))) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error('Failed to retrieve chats page');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { GetUserChatsQuery } from './ChatQueries';
|
||||
import { IChatRepository } from '../../../Domain/IRepository/IChatRepository';
|
||||
import { IChatArchiveRepository } from '../../../Domain/IRepository/IChatArchiveRepository';
|
||||
import { ChatAggregate } from '../../../Domain/Chat/ChatAggregate';
|
||||
import { ChatArchiveAggregate } from '../../../Domain/Chat/ChatArchiveAggregate';
|
||||
import { logAuth, logError } from '../../Services/Logger';
|
||||
|
||||
interface ChatWithMetadata {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string | null;
|
||||
gameId: string | null;
|
||||
users: string[];
|
||||
lastActivity: Date | null;
|
||||
isArchived: boolean;
|
||||
messageCount: number;
|
||||
unreadCount?: number;
|
||||
}
|
||||
|
||||
export class GetUserChatsQueryHandler {
|
||||
constructor(
|
||||
private chatRepository: IChatRepository,
|
||||
private chatArchiveRepository: IChatArchiveRepository
|
||||
) {}
|
||||
|
||||
async execute(query: GetUserChatsQuery): Promise<ChatWithMetadata[]> {
|
||||
try {
|
||||
const result: ChatWithMetadata[] = [];
|
||||
|
||||
// Get active chats
|
||||
const activeChats = await this.chatRepository.findActiveChatsForUser(query.userId);
|
||||
result.push(...activeChats.map(chat => ({
|
||||
id: chat.id,
|
||||
type: chat.type,
|
||||
name: chat.name,
|
||||
gameId: chat.gameId,
|
||||
users: chat.users,
|
||||
lastActivity: chat.lastActivity,
|
||||
isArchived: false,
|
||||
messageCount: chat.messages.length,
|
||||
unreadCount: this.calculateUnreadMessages(chat, query.userId)
|
||||
})));
|
||||
|
||||
// Get archived chats if requested
|
||||
if (query.includeArchived) {
|
||||
const userActiveChats = await this.chatRepository.findByUserId(query.userId);
|
||||
const archivedChatIds = userActiveChats
|
||||
.filter(chat => chat.archiveDate !== null)
|
||||
.map(chat => chat.id);
|
||||
|
||||
const archives = await Promise.all(
|
||||
archivedChatIds.map(id => this.chatArchiveRepository.findByChatId(id))
|
||||
);
|
||||
|
||||
archives.forEach(archiveArray => {
|
||||
archiveArray.forEach(archive => {
|
||||
if (archive.participants.includes(query.userId)) {
|
||||
result.push({
|
||||
id: archive.chatId,
|
||||
type: archive.chatType,
|
||||
name: archive.chatName,
|
||||
gameId: archive.gameId,
|
||||
users: archive.participants,
|
||||
lastActivity: archive.archivedAt,
|
||||
isArchived: true,
|
||||
messageCount: archive.archivedMessages.length,
|
||||
unreadCount: 0 // Archived chats have no unread messages
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
logAuth('User chats retrieved', query.userId, {
|
||||
activeCount: activeChats.length,
|
||||
totalCount: result.length,
|
||||
includeArchived: query.includeArchived
|
||||
});
|
||||
|
||||
return result.sort((a, b) => {
|
||||
if (!a.lastActivity) return 1;
|
||||
if (!b.lastActivity) return -1;
|
||||
return new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logError('GetUserChatsQueryHandler error', error as Error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private calculateUnreadMessages(chat: ChatAggregate, userId: string): number {
|
||||
// Simple implementation - count messages from other users
|
||||
// In production, you'd store lastSeen timestamp per user per chat
|
||||
return chat.messages.filter(msg => msg.userid !== userId).length;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user