import { ConflictException, Injectable } from '@nestjs/common'; import { Prisma, User } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { hashPassword, verifyPassword } from './password'; export const PASSWORD_PROVIDER = 'password'; export interface CreateUserInput { username: string; email: string; displayName: string; password: string; locale: string; } @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} /** * Creates the account plus its password identity in one transaction. * Uniqueness violations surface as field-level conflicts so the client * can highlight the right input. */ async createUser(input: CreateUserInput): Promise { try { return await this.prisma.$transaction(async (tx) => { const user = await tx.user.create({ data: { username: input.username, email: input.email.toLowerCase(), displayName: input.displayName, locale: input.locale, }, }); await tx.userIdentity.create({ data: { userId: user.id, provider: PASSWORD_PROVIDER, // The user id (not the username) is the stable subject: a // future username change must not orphan the identity. subject: user.id, credential: await hashPassword(input.password), }, }); return user; }); } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { const target = (error.meta?.target as string[] | undefined)?.[0] ?? 'username'; throw new ConflictException({ details: { [target]: ['validation.taken'] } }); } throw error; } } findById(id: string): Promise { return this.prisma.user.findUnique({ where: { id } }); } findByUsernameOrEmail(usernameOrEmail: string): Promise { const value = usernameOrEmail.trim(); return this.prisma.user.findFirst({ where: value.includes('@') ? { email: value.toLowerCase() } : { username: value }, }); } findByEmail(email: string): Promise { return this.prisma.user.findUnique({ where: { email: email.toLowerCase() } }); } async checkPassword(userId: string, password: string): Promise { const identity = await this.prisma.userIdentity.findUnique({ where: { provider_subject: { provider: PASSWORD_PROVIDER, subject: userId } }, }); if (!identity?.credential) return false; return verifyPassword(identity.credential, password); } async setPassword(userId: string, password: string): Promise { await this.prisma.userIdentity.update({ where: { provider_subject: { provider: PASSWORD_PROVIDER, subject: userId } }, data: { credential: await hashPassword(password) }, }); } async markEmailVerified(userId: string): Promise { return this.prisma.user.update({ where: { id: userId }, data: { status: 'ACTIVE', emailVerifiedAt: new Date() }, }); } async updateProfile( userId: string, data: { displayName?: string; locale?: string; autoWatchOwnPages?: boolean; autoWatchOnComment?: boolean; digestFrequency?: string; }, ): Promise { return this.prisma.user.update({ where: { id: userId }, data }); } }