This commit is contained in:
2024-04-21 14:42:52 +02:00
parent 4b69674ede
commit 8a25f53c99
10700 changed files with 55767 additions and 14201 deletions

View File

@ -0,0 +1,91 @@
import { Router, Request, Response, NextFunction } from 'express'
import { body, validationResult } from 'express-validator'
import { parseValidationError } from '@/infra/error'
import {
confirmEmail,
createUser,
resetPassword,
sendResetPasswordEmail,
AccountConfirmEmailOptions,
AccountCreateOptions,
AccountResetPasswordOptions,
AccountSendResetPasswordEmailOptions,
} from './service'
const router = Router()
router.post(
'/',
body('email').isEmail().isLength({ max: 255 }),
body('password').isStrongPassword().isLength({ max: 10000 }),
body('fullName').isString().notEmpty().trim().escape().isLength({ max: 255 }),
body('picture').optional().isBase64().isByteLength({ max: 3000000 }),
async (req: Request, res: Response, next: NextFunction) => {
try {
const result = validationResult(req)
if (!result.isEmpty()) {
throw parseValidationError(result)
}
res.json(await createUser(req.body as AccountCreateOptions))
} catch (err) {
next(err)
}
}
)
router.post(
'/reset_password',
body('token').isString().notEmpty().trim(),
body('newPassword').isStrongPassword(),
async (req: Request, res: Response, next: NextFunction) => {
try {
const result = validationResult(req)
if (!result.isEmpty()) {
throw parseValidationError(result)
}
await resetPassword(req.body as AccountResetPasswordOptions)
res.sendStatus(200)
} catch (err) {
next(err)
}
}
)
router.post(
'/confirm_email',
body('token').isString().notEmpty().trim(),
async (req: Request, res: Response, next: NextFunction) => {
try {
const result = validationResult(req)
if (!result.isEmpty()) {
throw parseValidationError(result)
}
await confirmEmail(req.body as AccountConfirmEmailOptions)
res.sendStatus(200)
} catch (err) {
next(err)
}
}
)
router.post(
'/send_reset_password_email',
body('email').isEmail().isLength({ max: 255 }),
async (req: Request, res: Response, next: NextFunction) => {
try {
const result = validationResult(req)
if (!result.isEmpty()) {
throw parseValidationError(result)
}
res.json(
await sendResetPasswordEmail(
req.body as AccountSendResetPasswordEmailOptions
)
)
} catch (err) {
next(err)
}
}
)
export default router

View File

@ -0,0 +1,118 @@
import { getConfig } from '@/config/config'
import { newDateTime } from '@/infra/date-time'
import { ErrorCode, newError } from '@/infra/error'
import { newHashId, newHyphenlessUuid } from '@/infra/id'
import { sendTemplateMail } from '@/infra/mail'
import { hashPassword } from '@/infra/password'
import search, { USER_SEARCH_INDEX } from '@/infra/search'
import userRepo, { User } from '@/user/repo'
import { mapEntity, UserDTO } from '@/user/service'
export type AccountCreateOptions = {
email: string
password: string
fullName: string
picture?: string
}
export type AccountResetPasswordOptions = {
token: string
newPassword: string
}
export type AccountConfirmEmailOptions = {
token: string
}
export type AccountSendResetPasswordEmailOptions = {
email: string
}
export async function createUser(
options: AccountCreateOptions
): Promise<UserDTO> {
const id = newHashId()
if (!(await userRepo.isUsernameAvailable(options.email))) {
throw newError({ code: ErrorCode.UsernameUnavailable })
}
try {
const emailConfirmationToken = newHyphenlessUuid()
const user = await userRepo.insert({
id,
username: options.email,
email: options.email,
fullName: options.fullName,
picture: options.picture,
passwordHash: hashPassword(options.password),
emailConfirmationToken,
createTime: newDateTime(),
})
await search.index(USER_SEARCH_INDEX).addDocuments([
{
id: user.id,
username: user.username,
email: user.email,
fullName: user.fullName,
isEmailConfirmed: user.isEmailConfirmed,
createTime: user.createTime,
},
])
await sendTemplateMail('email-confirmation', options.email, {
'UI_URL': getConfig().publicUIURL,
'TOKEN': emailConfirmationToken,
})
return mapEntity(user)
} catch (error) {
await userRepo.delete(id)
await search.index(USER_SEARCH_INDEX).deleteDocuments([id])
throw newError({ code: ErrorCode.InternalServerError, error })
}
}
export async function resetPassword(options: AccountResetPasswordOptions) {
const user = await userRepo.findByResetPasswordToken(options.token)
await userRepo.update({
id: user.id,
passwordHash: hashPassword(options.newPassword),
})
}
export async function confirmEmail(options: AccountConfirmEmailOptions) {
let user = await userRepo.findByEmailConfirmationToken(options.token)
user = await userRepo.update({
id: user.id,
isEmailConfirmed: true,
emailConfirmationToken: null,
})
await search.index(USER_SEARCH_INDEX).updateDocuments([
{
...user,
isEmailConfirmed: user.isEmailConfirmed,
},
])
}
export async function sendResetPasswordEmail(
options: AccountSendResetPasswordEmailOptions
) {
let user: User
try {
user = await userRepo.findByEmail(options.email)
user = await userRepo.update({
id: user.id,
resetPasswordToken: newHyphenlessUuid(),
})
} catch {
return
}
try {
await sendTemplateMail('reset-password', user.email, {
'UI_URL': getConfig().publicUIURL,
'TOKEN': user.resetPasswordToken,
})
} catch (error) {
const { id } = await userRepo.findByEmail(options.email)
await userRepo.update({ id, resetPasswordToken: null })
throw newError({ code: ErrorCode.InternalServerError, error })
}
}