192 lines
4.9 KiB
TypeScript
192 lines
4.9 KiB
TypeScript
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<UserDTO> {
|
|
return mapEntity(await userRepo.findByID(id))
|
|
}
|
|
|
|
export async function getByPicture(picture: string): Promise<UserDTO> {
|
|
return mapEntity(await userRepo.findByPicture(picture))
|
|
}
|
|
|
|
export async function updateFullName(
|
|
id: string,
|
|
options: UserUpdateFullNameOptions
|
|
): Promise<UserDTO> {
|
|
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<UserDTO> {
|
|
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<UserDTO> {
|
|
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<UserDTO> {
|
|
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<UserDTO> {
|
|
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
|
|
}
|