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
|
|
|
|
|
} from '../types/user.types';
|
|
|
|
|
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|