Files
gymflow/server/src/services/auth.service.ts

264 lines
9.3 KiB
TypeScript

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
}
});
}
}