🎓 Initial commit: Math2 Platform - Plataforma de Álgebra Lineal PRO
Some checks failed
Test Suite / test-backend (push) Has been cancelled
Test Suite / test-frontend (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / coverage-check (push) Has been cancelled

 Características:
- 45 ejercicios universitarios (Basic → Advanced)
- Renderizado LaTeX profesional
- IA generativa (Z.ai/DashScope)
- Docker 9 servicios
- Tests 123/123 pasando
- Seguridad enterprise (JWT, XSS, Rate limiting)

🐳 Infraestructura:
- Next.js 14 + Node.js 20
- PostgreSQL 15 + Redis 7
- Docker Compose completo
- Nginx + SSL ready

📚 Documentación:
- 5 informes técnicos completos
- README profesional
- Scripts de deployment automatizados

Estado: Producción lista 
This commit is contained in:
Renato
2026-03-31 11:27:11 -03:00
commit bc43c9e772
309 changed files with 84845 additions and 0 deletions

View File

@@ -0,0 +1,295 @@
/**
* User Service
*
* Business logic for user profile management
*/
import { prisma } from '../../shared/database/prisma.client';
import { NotFoundError, ValidationError, ConflictError, AuthenticationError } from '../../shared/types';
import { logger } from '../../shared/utils/logger';
import type { User, UserRole } from '@prisma/client';
import bcrypt from 'bcrypt';
// ============================================
// SERVICE
// ============================================
class UserService {
/**
* Get user profile by ID
*/
async getUserProfile(userId: string): Promise<User> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
email: true,
username: true,
role: true,
telegramChatId: true,
isActive: true,
createdAt: true,
updatedAt: true,
lastLoginAt: true,
// Exclude passwordHash for security
},
});
if (!user) {
throw new NotFoundError('User');
}
return user as User;
}
/**
* Update user profile
*/
async updateProfile(userId: string, data: {
username?: string;
telegramChatId?: string | null;
}): Promise<User> {
// Validate username uniqueness if changing
if (data.username) {
const existingUser = await prisma.user.findFirst({
where: {
username: data.username,
NOT: { id: userId },
},
});
if (existingUser) {
throw new ConflictError('Username already taken');
}
}
// Validate telegramChatId uniqueness if setting
if (data.telegramChatId) {
const existingTelegram = await prisma.user.findFirst({
where: {
telegramChatId: data.telegramChatId,
NOT: { id: userId },
},
});
if (existingTelegram) {
throw new ConflictError('Telegram chat ID already linked to another account');
}
}
const updatedUser = await prisma.user.update({
where: { id: userId },
data: {
...data,
updatedAt: new Date(),
},
select: {
id: true,
email: true,
username: true,
role: true,
telegramChatId: true,
isActive: true,
createdAt: true,
updatedAt: true,
lastLoginAt: true,
},
});
logger.info({ userId, updates: data }, 'User profile updated');
return updatedUser as User;
}
/**
* Change user password
*/
async changePassword(userId: string, currentPassword: string, newPassword: string): Promise<User> {
// Validate input
if (!currentPassword || !newPassword) {
throw new ValidationError('Current and new password are required');
}
if (newPassword.length < 8) {
throw new ValidationError('New password must be at least 8 characters');
}
// Find user with password hash
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
passwordHash: true,
},
});
if (!user) {
throw new NotFoundError('User');
}
// Verify current password
const isPasswordValid = await bcrypt.compare(currentPassword, user.passwordHash);
if (!isPasswordValid) {
throw new AuthenticationError('Current password is incorrect');
}
// Hash the new password
const saltRounds = 12;
const newPasswordHash = await bcrypt.hash(newPassword, saltRounds);
// Update user's password
const updatedUser = await prisma.user.update({
where: { id: userId },
data: {
passwordHash: newPasswordHash,
updatedAt: new Date(),
},
select: {
id: true,
email: true,
username: true,
role: true,
telegramChatId: true,
isActive: true,
createdAt: true,
updatedAt: true,
lastLoginAt: true,
},
});
logger.info({ userId }, 'Password changed successfully');
return updatedUser as User;
}
/**
* Get user statistics
*/
async getUserStats(userId: string): Promise<{
totalExercisesCompleted: number;
totalPoints: number;
currentStreak: number;
achievementsUnlocked: number;
}> {
// Get progress stats
const progressStats = await prisma.progress.aggregate({
where: { userId },
_sum: { points: true, exercisesCompleted: true },
_count: { _all: true },
});
// Get ranking for streak info
const globalRanking = await prisma.ranking.findFirst({
where: { userId, moduleId: null },
select: { streak: true },
});
// Get achievements count
const achievementsCount = await prisma.userAchievement.count({
where: { userId, unlockedAt: { not: null } },
});
return {
totalExercisesCompleted: progressStats._sum.exercisesCompleted || 0,
totalPoints: progressStats._sum.points || 0,
currentStreak: globalRanking?.streak || 0,
achievementsUnlocked: achievementsCount,
};
}
/**
* Get all users (admin only)
*/
async listUsers(options?: {
role?: UserRole;
isActive?: boolean;
page?: number;
limit?: number;
}): Promise<{ users: User[]; total: number }> {
const page = options?.page || 1;
const limit = options?.limit || 50;
const skip = (page - 1) * limit;
const where: any = {};
if (options?.role) where.role = options.role;
if (options?.isActive !== undefined) where.isActive = options.isActive;
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
skip,
take: limit,
select: {
id: true,
email: true,
username: true,
role: true,
isActive: true,
createdAt: true,
lastLoginAt: true,
},
orderBy: { createdAt: 'desc' },
}),
prisma.user.count({ where }),
]);
return { users: users as User[], total };
}
/**
* Update user role (admin only)
*/
async updateUserRole(userId: string, role: UserRole): Promise<User> {
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new NotFoundError('User');
}
const updatedUser = await prisma.user.update({
where: { id: userId },
data: { role },
select: {
id: true,
email: true,
username: true,
role: true,
isActive: true,
},
});
logger.info({ userId, newRole: role }, 'User role updated');
return updatedUser as User;
}
/**
* Deactivate user (admin only)
*/
async deactivateUser(userId: string): Promise<User> {
const user = await prisma.user.update({
where: { id: userId },
data: { isActive: false },
select: {
id: true,
email: true,
username: true,
role: true,
isActive: true,
},
});
logger.info({ userId }, 'User deactivated');
return user as User;
}
}
// ============================================
// EXPORT
// ============================================
export const userService = new UserService();
export default userService;