import prisma from '../lib/prisma'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import fs from 'fs'; import path from 'path'; import dotenv from 'dotenv'; const JWT_SECRET = process.env.JWT_SECRET || 'secret'; export class AuthService { static async getUser(userId: string) { const user = await prisma.user.findUnique({ where: { id: userId }, include: { profile: true } }); if (!user) return null; const { password: _, ...userSafe } = user; return userSafe; } static async login(email: string, password: string) { const user = await prisma.user.findUnique({ where: { email }, include: { profile: true } }); if (!user) { throw new Error('Invalid credentials'); } if (user.isBlocked) { throw new Error('Account is blocked'); } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { throw new Error('Invalid credentials'); } const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET); const { password: _, ...userSafe } = user; return { user: userSafe, token }; } static async register(email: string, password: string) { const existingUser = await prisma.user.findUnique({ where: { email } }); if (existingUser) { throw new Error('User already exists'); } const hashedPassword = await bcrypt.hash(password, 10); const user = await prisma.user.create({ data: { email, password: hashedPassword, role: 'USER', profile: { create: { weight: 70 } } }, include: { profile: true } }); const token = jwt.sign({ userId: user.id, role: user.role }, JWT_SECRET); const { password: _, ...userSafe } = user; return { user: userSafe, token }; } static async seedDefaultExercises(userId: string, language: string = 'en') { try { // Ensure env is loaded from root (in case server didn't restart) if (!process.env.DEFAULT_EXERCISES_CSV_PATH) { const rootEnv = path.resolve(process.cwd(), '../.env'); if (fs.existsSync(rootEnv)) { dotenv.config({ path: rootEnv, override: true }); } else { dotenv.config({ path: path.resolve(process.cwd(), '.env'), override: true }); } } const csvPath = process.env.DEFAULT_EXERCISES_CSV_PATH; if (csvPath) { let resolvedPath = path.resolve(process.cwd(), csvPath); // Try to handle if resolvedPath doesn't exist but relative to root does (if CWD is server) if (!fs.existsSync(resolvedPath) && !path.isAbsolute(csvPath)) { const altPath = path.resolve(process.cwd(), '..', csvPath); if (fs.existsSync(altPath)) { resolvedPath = altPath; } } if (fs.existsSync(resolvedPath)) { const content = fs.readFileSync(resolvedPath, 'utf-8'); const lines = content.split(/\r?\n/).filter(l => l.trim().length > 0); if (lines.length > 1) { const headers = lines[0].split(',').map(h => h.trim()); const exercisesToCreate = []; const nameColumn = language === 'ru' ? 'name_ru' : 'name'; for (let i = 1; i < lines.length; i++) { const cols = lines[i].split(',').map(c => c.trim()); if (cols.length < headers.length) continue; const row: any = {}; headers.forEach((h, idx) => row[h] = cols[idx]); const exerciseName = row[nameColumn] || row['name']; if (exerciseName && row.type) { exercisesToCreate.push({ userId, name: exerciseName, type: row.type, bodyWeightPercentage: row.bodyWeightPercentage ? parseFloat(row.bodyWeightPercentage) : 0, isUnilateral: row.isUnilateral === 'true', isArchived: false }); } } if (exercisesToCreate.length > 0) { // Check if exercises already exist for this user to avoid duplicates const existingCount = await prisma.exercise.count({ where: { userId } }); if (existingCount === 0) { await prisma.exercise.createMany({ data: exercisesToCreate }); console.log(`[AuthService] Seeded ${exercisesToCreate.length} exercises for user: ${userId}`); } else { console.log(`[AuthService] User ${userId} already has ${existingCount} exercises. Skipping seed.`); } } } } else { console.warn(`[AuthService] Default exercises CSV configured but not found at: ${resolvedPath}`); } } } catch (error) { try { fs.appendFileSync(path.join(process.cwd(), 'auth_debug_custom.log'), `[${new Date().toISOString()}] ERROR: ${error}\n`); } catch (e) { } console.error('[AuthService] Failed to seed default exercises:', error); // Non-blocking error } } static async initializeUser(userId: string, language: string, profileData: any = {}) { // Prepare profile update data const updateData: any = { language }; if (profileData.weight && !isNaN(parseFloat(profileData.weight))) updateData.weight = parseFloat(profileData.weight); if (profileData.height && !isNaN(parseFloat(profileData.height))) updateData.height = parseFloat(profileData.height); if (profileData.gender) updateData.gender = profileData.gender; if (profileData.birthDate && profileData.birthDate !== '') { const date = new Date(profileData.birthDate); if (!isNaN(date.getTime())) { updateData.birthDate = date; } } // Update profile language and other attributes await prisma.userProfile.upsert({ where: { userId }, update: updateData, create: { userId, ...updateData } }); // Seed exercises in that language await this.seedDefaultExercises(userId, language); // Mark as first login done await prisma.user.update({ where: { id: userId }, data: { isFirstLogin: false } }); // Return updated user return this.getUser(userId); } static async changePassword(userId: string, newPassword: string) { const hashed = await bcrypt.hash(newPassword, 10); await prisma.user.update({ where: { id: userId }, data: { password: hashed } }); } static async updateProfile(userId: string, data: any) { // Convert birthDate if needed if (data.birthDate) { data.birthDate = new Date(data.birthDate); } await prisma.userProfile.upsert({ where: { userId: userId }, update: { ...data }, create: { userId: userId, ...data } }); } static async getAllUsers() { const users = await prisma.user.findMany({ select: { id: true, email: true, role: true, isBlocked: true, isFirstLogin: true, profile: true }, orderBy: { email: 'asc' } }); return users; } static async deleteUser(adminId: string, targetId: string) { if (targetId === adminId) { throw new Error('Cannot delete yourself'); } await prisma.user.delete({ where: { id: targetId } }); } static async blockUser(adminId: string, targetId: string, block: boolean) { if (targetId === adminId) { throw new Error('Cannot block yourself'); } await prisma.user.update({ where: { id: targetId }, data: { isBlocked: block } }); } static async resetUserPassword(targetId: string, newPassword: string) { if (!newPassword || newPassword.length < 4) { throw new Error('Password too short'); } const hashed = await bcrypt.hash(newPassword, 10); await prisma.user.update({ where: { id: targetId }, data: { password: hashed, isFirstLogin: true } }); } }