all
This commit is contained in:
91
Downloads/Voltaserve/idp/src/account/router.ts
Normal file
91
Downloads/Voltaserve/idp/src/account/router.ts
Normal 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
|
118
Downloads/Voltaserve/idp/src/account/service.ts
Normal file
118
Downloads/Voltaserve/idp/src/account/service.ts
Normal 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 })
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user