264 lines
9.3 KiB
TypeScript
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
|
|
}
|
|
});
|
|
}
|
|
}
|