elearning/Backend/src/services/user.service.ts

307 lines
12 KiB
TypeScript
Raw Normal View History

2026-01-13 17:55:00 +07:00
import { prisma } from '../config/database';
import { Prisma } from '@prisma/client';
import { config } from '../config';
import { logger } from '../config/logger';
import jwt from 'jsonwebtoken';
2026-01-14 14:06:09 +07:00
import bcrypt from 'bcrypt';
2026-01-13 17:55:00 +07:00
import {
UserResponse,
ProfileResponse,
ProfileUpdate,
ProfileUpdateResponse,
ChangePasswordRequest,
ChangePasswordResponse,
updateAvatarRequest,
updateAvatarResponse
2026-01-13 17:55:00 +07:00
} from '../types/user.types';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { uploadFile, deleteFile, getPresignedUrl } from '../config/minio';
2026-01-13 17:55:00 +07:00
export class UserService {
async getUserProfile(token: string): Promise<UserResponse> {
try {
// Decode JWT token to get user ID
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string };
const user = await prisma.user.findUnique({
where: {
id: decoded.id
},
include: {
profile: true,
role: true
}
});
if (!user) throw new UnauthorizedError("User not found");
2026-01-15 10:17:15 +07:00
// Check if account is deactivated
if (user.is_deactivated) {
logger.warn('Profile access attempt with deactivated account', { userId: user.id });
throw new ForbiddenError('This account has been deactivated');
}
2026-01-13 17:55:00 +07:00
return {
id: user.id,
username: user.username,
email: user.email,
updated_at: user.updated_at,
created_at: user.created_at,
role: {
code: user.role.code,
name: user.role.name as { th: string; en: string }
},
profile: user.profile ? {
prefix: user.profile.prefix as { th?: string; en?: string } | undefined,
first_name: user.profile.first_name,
last_name: user.profile.last_name,
avatar_url: user.profile.avatar_url ? await this.getAvatarPresignedUrl(user.profile.avatar_url) : null,
2026-01-13 17:55:00 +07:00
birth_date: user.profile.birth_date,
phone: user.profile.phone
} : undefined
};
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
logger.error('Invalid JWT token:', error);
throw new UnauthorizedError('Invalid token');
}
if (error instanceof jwt.TokenExpiredError) {
logger.error('JWT token expired:', error);
throw new UnauthorizedError('Token expired');
}
logger.error('Error fetching user profile:', error);
throw error;
}
};
2026-01-14 14:06:09 +07:00
/**
* Change user password
*/
async changePassword(token: string, oldPassword: string, newPassword: string): Promise<ChangePasswordResponse> {
try {
// Decode JWT token to get user ID
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string };
const user = await prisma.user.findUnique({ where: { id: decoded.id } });
if (!user) throw new UnauthorizedError('User not found');
2026-01-15 10:17:15 +07:00
// Check if account is deactivated
if (user.is_deactivated) {
logger.warn('Password change attempt with deactivated account', { userId: user.id });
throw new ForbiddenError('This account has been deactivated');
}
2026-01-14 14:06:09 +07:00
// Verify old password
const isPasswordValid = await bcrypt.compare(oldPassword, user.password);
if (!isPasswordValid) throw new UnauthorizedError('Invalid old password');
// Hash new password
const encryptedPassword = await bcrypt.hash(newPassword, 10);
// Update password
await prisma.user.update({
where: { id: user.id },
data: { password: encryptedPassword }
});
logger.info('Password changed successfully', { userId: user.id });
return {
code: 200,
message: 'Password changed successfully'
};
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
logger.error('Invalid JWT token:', error);
throw new UnauthorizedError('Invalid token');
}
if (error instanceof jwt.TokenExpiredError) {
logger.error('JWT token expired:', error);
throw new UnauthorizedError('Token expired');
}
logger.error('Failed to change password', { error });
throw error;
}
}
2026-01-14 16:29:18 +07:00
/**
* Update user profile
*/
async updateProfile(token: string, profile: ProfileUpdate): Promise<ProfileUpdateResponse> {
try {
// Decode JWT token to get user ID
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; username: string; email: string; roleCode: string };
const user = await prisma.user.findUnique({ where: { id: decoded.id } });
if (!user) throw new UnauthorizedError('User not found');
2026-01-15 10:17:15 +07:00
// Check if account is deactivated
if (user.is_deactivated) {
logger.warn('Profile update attempt with deactivated account', { userId: user.id });
throw new ForbiddenError('This account has been deactivated');
}
2026-01-14 16:29:18 +07:00
// Update profile
const updatedProfile = await prisma.userProfile.update({
where: { user_id: user.id },
data: profile
});
logger.info('Profile updated successfully', { userId: user.id });
return {
code: 200,
message: 'Profile updated successfully',
data: {
id: updatedProfile.id,
prefix: updatedProfile.prefix as { th?: string; en?: string } | undefined,
first_name: updatedProfile.first_name,
last_name: updatedProfile.last_name,
avatar_url: updatedProfile.avatar_url,
phone: updatedProfile.phone,
birth_date: updatedProfile.birth_date
}
};
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
logger.error('Invalid JWT token:', error);
throw new UnauthorizedError('Invalid token');
}
if (error instanceof jwt.TokenExpiredError) {
logger.error('JWT token expired:', error);
throw new UnauthorizedError('Token expired');
}
logger.error('Failed to update profile', { error });
throw error;
}
}
/**
* Upload avatar picture to MinIO
*/
async uploadAvatarPicture(token: string, file: Express.Multer.File): Promise<updateAvatarResponse> {
try {
const decoded = jwt.verify(token, config.jwt.secret) as { id: number };
// Check if user exists
const user = await prisma.user.findUnique({
where: { id: decoded.id },
include: { profile: true }
});
if (!user) {
throw new UnauthorizedError('User not found');
}
// Check if account is deactivated
if (user.is_deactivated) {
throw new ForbiddenError('This account has been deactivated');
}
// Generate unique filename
const timestamp = Date.now();
const uniqueId = Math.random().toString(36).substring(2, 15);
const fileName = file.originalname || 'avatar';
const extension = fileName.split('.').pop() || 'jpg';
const safeFilename = `${timestamp}-${uniqueId}.${extension}`;
const filePath = `avatars/${decoded.id}/${safeFilename}`;
// Delete old avatar if exists
if (user.profile?.avatar_url) {
try {
// Extract file path from URL
const urlParts = user.profile.avatar_url.split('/');
const oldFilePath = urlParts.slice(-2).join('/'); // Get avatars/{userId}/{filename}
await deleteFile(oldFilePath);
logger.info(`Deleted old avatar: ${oldFilePath}`);
} catch (error) {
logger.warn(`Failed to delete old avatar: ${error}`);
// Continue with upload even if delete fails
}
}
// Upload to MinIO
await uploadFile(filePath, file.buffer, file.mimetype || 'image/jpeg');
// Update user profile with avatar URL
const avatarUrl = `${config.s3.endpoint}/${config.s3.bucket}/${filePath}`;
// Update or create profile
if (user.profile) {
await prisma.userProfile.update({
where: { user_id: decoded.id },
data: { avatar_url: avatarUrl }
});
} else {
await prisma.userProfile.create({
data: {
user_id: decoded.id,
avatar_url: avatarUrl,
first_name: '',
last_name: ''
}
});
}
return {
code: 200,
message: 'Avatar uploaded successfully',
data: {
id: decoded.id,
avatar_url: avatarUrl
}
};
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
logger.error('Invalid JWT token:', error);
throw new UnauthorizedError('Invalid token');
}
if (error instanceof jwt.TokenExpiredError) {
logger.error('JWT token expired:', error);
throw new UnauthorizedError('Token expired');
}
logger.error('Failed to upload avatar', { error });
throw error;
}
}
/**
* Get presigned URL for avatar
*/
private async getAvatarPresignedUrl(avatarUrl: string): Promise<string | null> {
try {
// Extract file path from stored URL or path
const filePath = avatarUrl.includes('/')
? `avatars/${avatarUrl.split('/').slice(-2).join('/')}`
: avatarUrl;
return await getPresignedUrl(filePath, 3600); // 1 hour expiry
} catch (error) {
logger.warn(`Failed to generate presigned URL for avatar: ${error}`);
return null;
}
}
2026-01-15 10:17:15 +07:00
2026-01-13 17:55:00 +07:00
/**
* Format user response
*/
private formatUserResponse(user: any): UserResponse {
return {
id: user.id,
username: user.username,
email: user.email,
updated_at: user.updated_at,
created_at: user.created_at,
role: {
code: user.role.code,
name: user.role.name
},
profile: user.profile ? {
prefix: user.profile.prefix,
first_name: user.profile.first_name,
last_name: user.profile.last_name,
avatar_url: user.profile.avatar_url,
phone: user.profile.phone,
birth_date: user.profile.birth_date
} : undefined
};
}
}