213 lines
5.8 KiB
TypeScript
213 lines
5.8 KiB
TypeScript
import {
|
|
Injectable,
|
|
NotFoundException,
|
|
ConflictException,
|
|
BadRequestException,
|
|
} from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import * as bcrypt from 'bcrypt';
|
|
import { User, UserRole } from '../entities';
|
|
import {
|
|
CreateUserDto,
|
|
UpdateUserDto,
|
|
UserResponseDto,
|
|
ChangePasswordDto,
|
|
} from './user.dto';
|
|
|
|
@Injectable()
|
|
export class UserService {
|
|
constructor(
|
|
@InjectRepository(User)
|
|
private userRepository: Repository<User>,
|
|
) {}
|
|
|
|
async create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
|
// Check if email already exists
|
|
const existingEmail = await this.userRepository.findOne({
|
|
where: { email: createUserDto.email },
|
|
});
|
|
if (existingEmail) {
|
|
throw new ConflictException('Email already exists');
|
|
}
|
|
|
|
// Check if username already exists
|
|
const existingUsername = await this.userRepository.findOne({
|
|
where: { username: createUserDto.username },
|
|
});
|
|
if (existingUsername) {
|
|
throw new ConflictException('Username already exists');
|
|
}
|
|
|
|
// Hash password
|
|
const saltRounds = 10;
|
|
const passwordHash = await bcrypt.hash(createUserDto.password, saltRounds);
|
|
|
|
// Create user
|
|
const user = this.userRepository.create({
|
|
...createUserDto,
|
|
passwordHash,
|
|
});
|
|
|
|
const savedUser = await this.userRepository.save(user);
|
|
return new UserResponseDto(savedUser);
|
|
}
|
|
|
|
async findAll(): Promise<UserResponseDto[]> {
|
|
const users = await this.userRepository.find({
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
return users.map((user) => new UserResponseDto(user));
|
|
}
|
|
|
|
async findOne(id: string): Promise<UserResponseDto> {
|
|
const user = await this.userRepository.findOne({ where: { id } });
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
return new UserResponseDto(user);
|
|
}
|
|
|
|
async findByUsername(username: string): Promise<User | null> {
|
|
return this.userRepository.findOne({ where: { username } });
|
|
}
|
|
|
|
async findByEmail(email: string): Promise<User | null> {
|
|
return this.userRepository.findOne({ where: { email } });
|
|
}
|
|
|
|
async update(
|
|
id: string,
|
|
updateUserDto: UpdateUserDto,
|
|
): Promise<UserResponseDto> {
|
|
const user = await this.userRepository.findOne({ where: { id } });
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
// Check if email is being changed and if it already exists
|
|
if (updateUserDto.email && updateUserDto.email !== user.email) {
|
|
const existingEmail = await this.userRepository.findOne({
|
|
where: { email: updateUserDto.email },
|
|
});
|
|
if (existingEmail) {
|
|
throw new ConflictException('Email already exists');
|
|
}
|
|
}
|
|
|
|
// Check if username is being changed and if it already exists
|
|
if (updateUserDto.username && updateUserDto.username !== user.username) {
|
|
const existingUsername = await this.userRepository.findOne({
|
|
where: { username: updateUserDto.username },
|
|
});
|
|
if (existingUsername) {
|
|
throw new ConflictException('Username already exists');
|
|
}
|
|
}
|
|
|
|
// Hash new password if provided
|
|
if (updateUserDto.password) {
|
|
const saltRounds = 10;
|
|
updateUserDto.password = await bcrypt.hash(
|
|
updateUserDto.password,
|
|
saltRounds,
|
|
);
|
|
}
|
|
|
|
// Update user
|
|
Object.assign(user, updateUserDto);
|
|
if (updateUserDto.password) {
|
|
user.passwordHash = updateUserDto.password;
|
|
delete updateUserDto.password;
|
|
}
|
|
|
|
const updatedUser = await this.userRepository.save(user);
|
|
return new UserResponseDto(updatedUser);
|
|
}
|
|
|
|
async remove(id: string): Promise<void> {
|
|
const user = await this.userRepository.findOne({ where: { id } });
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
// Prevent deleting the last admin
|
|
if (user.role === UserRole.ADMIN) {
|
|
const adminCount = await this.userRepository.count({
|
|
where: { role: UserRole.ADMIN },
|
|
});
|
|
if (adminCount <= 1) {
|
|
throw new BadRequestException('Cannot delete the last admin user');
|
|
}
|
|
}
|
|
|
|
await this.userRepository.remove(user);
|
|
}
|
|
|
|
async changePassword(
|
|
id: string,
|
|
changePasswordDto: ChangePasswordDto,
|
|
): Promise<void> {
|
|
const user = await this.userRepository.findOne({ where: { id } });
|
|
if (!user) {
|
|
throw new NotFoundException('User not found');
|
|
}
|
|
|
|
// Verify current password
|
|
const isPasswordValid = await bcrypt.compare(
|
|
changePasswordDto.currentPassword,
|
|
user.passwordHash,
|
|
);
|
|
if (!isPasswordValid) {
|
|
throw new BadRequestException('Current password is incorrect');
|
|
}
|
|
|
|
// Hash new password
|
|
const saltRounds = 10;
|
|
user.passwordHash = await bcrypt.hash(
|
|
changePasswordDto.newPassword,
|
|
saltRounds,
|
|
);
|
|
|
|
await this.userRepository.save(user);
|
|
}
|
|
|
|
async validateUser(username: string, password: string): Promise<User | null> {
|
|
const user = await this.userRepository.findOne({
|
|
where: { username, isActive: true },
|
|
});
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
|
|
if (!isPasswordValid) {
|
|
return null;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
async createAdminIfNotExists(): Promise<void> {
|
|
const adminExists = await this.userRepository.findOne({
|
|
where: { role: UserRole.ADMIN },
|
|
});
|
|
|
|
if (!adminExists) {
|
|
const saltRounds = 10;
|
|
const passwordHash = await bcrypt.hash('admin123', saltRounds);
|
|
|
|
const adminUser = this.userRepository.create({
|
|
email: 'admin@placebo.mk',
|
|
username: 'admin',
|
|
passwordHash,
|
|
role: UserRole.ADMIN,
|
|
isActive: true,
|
|
});
|
|
|
|
await this.userRepository.save(adminUser);
|
|
console.log('Default admin user created: admin / admin123');
|
|
}
|
|
}
|
|
}
|