import fs from 'fs/promises' import { ErrorCode, newError } from '@/infra/error' import { hashPassword, verifyPassword } from '@/infra/password' import search, { USER_SEARCH_INDEX } from '@/infra/search' import userRepo, { User } from '@/user/repo' import { newHyphenlessUuid } from '@/infra/id' import { sendTemplateMail } from '@/infra/mail' import { getConfig } from '@/config/config' export type UserDTO = { id: string fullName: string picture: string email: string username: string pendingEmail?: string } export type UserUpdateEmailRequestOptions = { email: string } export type UserUpdateEmailConfirmationOptions = { token: string } export type UserUpdateFullNameOptions = { fullName: string } export type UserUpdatePasswordOptions = { currentPassword: string newPassword: string } export type UserDeleteOptions = { password: string } export async function getUser(id: string): Promise { return mapEntity(await userRepo.findByID(id)) } export async function getByPicture(picture: string): Promise { return mapEntity(await userRepo.findByPicture(picture)) } export async function updateFullName( id: string, options: UserUpdateFullNameOptions ): Promise { let user = await userRepo.findByID(id) user = await userRepo.update({ id: user.id, fullName: options.fullName }) await search.index(USER_SEARCH_INDEX).updateDocuments([ { ...user, fullName: user.fullName, }, ]) return mapEntity(user) } export async function updateEmailRequest( id: string, options: UserUpdateEmailRequestOptions ): Promise { let user = await userRepo.findByID(id) if (options.email === user.email) { user = await userRepo.update({ id: user.id, emailUpdateToken: null, emailUpdateValue: null, }) return mapEntity(user) } else { let usernameUnavailable = false try { await userRepo.findByUsername(options.email) usernameUnavailable = true } catch { // Ignored } if (usernameUnavailable) { throw newError({ code: ErrorCode.UsernameUnavailable }) } user = await userRepo.update({ id: user.id, emailUpdateToken: newHyphenlessUuid(), emailUpdateValue: options.email, }) try { await sendTemplateMail('email-update', options.email, { 'EMAIL': options.email, 'UI_URL': getConfig().publicUIURL, 'TOKEN': user.emailUpdateToken, }) return mapEntity(user) } catch (error) { await userRepo.update({ id, emailUpdateToken: null, emailUpdateValue: null, }) throw newError({ code: ErrorCode.InternalServerError, error }) } } } export async function updateEmailConfirmation( options: UserUpdateEmailConfirmationOptions ) { let user = await userRepo.findByEmailUpdateToken(options.token) user = await userRepo.update({ id: user.id, email: user.emailUpdateValue, username: user.emailUpdateValue, emailUpdateToken: null, emailUpdateValue: null, }) await search.index(USER_SEARCH_INDEX).updateDocuments([ { ...user, email: user.email, username: user.email, emailUpdateToken: null, emailUpdateValue: null, }, ]) return mapEntity(user) } export async function updatePassword( id: string, options: UserUpdatePasswordOptions ): Promise { let user = await userRepo.findByID(id) if (verifyPassword(options.currentPassword, user.passwordHash)) { user = await userRepo.update({ id: user.id, passwordHash: hashPassword(options.newPassword), }) return mapEntity(user) } else { throw newError({ code: ErrorCode.PasswordValidationFailed }) } } export async function updatePicture( id: string, path: string, contentType: string ): Promise { const picture = await fs.readFile(path, { encoding: 'base64' }) const { id: userId } = await userRepo.findByID(id) const user = await userRepo.update({ id: userId, picture: `data:${contentType};base64,${picture}`, }) return mapEntity(user) } export async function deletePicture(id: string): Promise { let user = await userRepo.findByID(id) user = await userRepo.update({ id: user.id, picture: null }) return mapEntity(user) } export async function deleteUser(id: string, options: UserDeleteOptions) { const user = await userRepo.findByID(id) if (verifyPassword(options.password, user.passwordHash)) { await userRepo.delete(user.id) await search.index(USER_SEARCH_INDEX).deleteDocuments([user.id]) } else { throw newError({ code: ErrorCode.InvalidPassword }) } } export function mapEntity(entity: User): UserDTO { const user: UserDTO = { id: entity.id, email: entity.email, username: entity.username, fullName: entity.fullName, picture: entity.picture, pendingEmail: entity.emailUpdateValue, } Object.keys(user).forEach( (index) => !user[index] && user[index] !== undefined && delete user[index] ) return user }