diff --git a/packages/core/src/routes-me/init.ts b/packages/core/src/routes-me/init.ts index 5d7371cde..bff084f08 100644 --- a/packages/core/src/routes-me/init.ts +++ b/packages/core/src/routes-me/init.ts @@ -1,8 +1,4 @@ -import { - adminTenantId, - arbitraryObjectGuard, - getManagementApiResourceIndicator, -} from '@logto/schemas'; +import { adminTenantId, getManagementApiResourceIndicator } from '@logto/schemas'; import Koa from 'koa'; import Router from 'koa-router'; @@ -11,18 +7,18 @@ import RequestError from '#src/errors/RequestError/index.js'; import type { WithAuthContext } from '#src/middleware/koa-auth/index.js'; import koaAuth from '#src/middleware/koa-auth/index.js'; import koaCors from '#src/middleware/koa-cors.js'; -import koaGuard from '#src/middleware/koa-guard.js'; import type TenantContext from '#src/tenants/TenantContext.js'; import assertThat from '#src/utils/assert-that.js'; import socialRoutes from './social.js'; +import userRoutes from './user.js'; +import verificationCodeRoutes from './verification-code.js'; export default function initMeApis(tenant: TenantContext): Koa { if (tenant.id !== adminTenantId) { throw new Error('`/me` routes should only be initialized in the admin tenant.'); } - const { findUserById, updateUserById } = tenant.queries.users; const meRouter = new Router(); meRouter.use( @@ -37,38 +33,9 @@ export default function initMeApis(tenant: TenantContext): Koa { } ); - meRouter.get('/custom-data', async (ctx, next) => { - const { id: userId } = ctx.auth; - const user = await findUserById(userId); - - ctx.body = user.customData; - - return next(); - }); - - meRouter.patch( - '/custom-data', - koaGuard({ - body: arbitraryObjectGuard, - response: arbitraryObjectGuard, - }), - async (ctx, next) => { - const { id: userId } = ctx.auth; - const { body: customData } = ctx.guard; - - await findUserById(userId); - - const user = await updateUserById(userId, { - customData, - }); - - ctx.body = user.customData; - - return next(); - } - ); - + userRoutes(meRouter, tenant); socialRoutes(meRouter, tenant); + verificationCodeRoutes(meRouter, tenant); const meApp = new Koa(); meApp.use(koaCors(EnvSet.values.cloudUrlSet)); diff --git a/packages/core/src/routes-me/user.ts b/packages/core/src/routes-me/user.ts new file mode 100644 index 000000000..d731649d5 --- /dev/null +++ b/packages/core/src/routes-me/user.ts @@ -0,0 +1,85 @@ +import { passwordRegEx } from '@logto/core-kit'; +import { arbitraryObjectGuard } from '@logto/schemas'; +import { object, string } from 'zod'; + +import RequestError from '#src/errors/RequestError/index.js'; +import { encryptUserPassword, verifyUserPassword } from '#src/libraries/user.js'; +import koaGuard from '#src/middleware/koa-guard.js'; +import assertThat from '#src/utils/assert-that.js'; + +import type { RouterInitArgs } from '../routes/types.js'; +import type { AuthedMeRouter } from './types.js'; + +export default function userRoutes( + ...[router, tenant]: RouterInitArgs +) { + const { findUserById, updateUserById } = tenant.queries.users; + + router.get('/custom-data', async (ctx, next) => { + const { id: userId } = ctx.auth; + const user = await findUserById(userId); + + ctx.body = user.customData; + + return next(); + }); + + router.patch( + '/custom-data', + koaGuard({ + body: arbitraryObjectGuard, + response: arbitraryObjectGuard, + }), + async (ctx, next) => { + const { id: userId } = ctx.auth; + const { body: customData } = ctx.guard; + + await findUserById(userId); + + const user = await updateUserById(userId, { + customData, + }); + + ctx.body = user.customData; + + return next(); + } + ); + + router.post( + '/password/verify', + koaGuard({ + body: object({ password: string().regex(passwordRegEx) }), + }), + async (ctx, next) => { + const { id: userId } = ctx.auth; + const { password } = ctx.guard.body; + + const user = await findUserById(userId); + await verifyUserPassword(user, password); + + ctx.status = 204; + + return next(); + } + ); + + router.post( + '/password', + koaGuard({ body: object({ password: string().regex(passwordRegEx) }) }), + async (ctx, next) => { + const { id: userId } = ctx.auth; + const { password } = ctx.guard.body; + + const user = await findUserById(userId); + assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 })); + + const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password); + await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod }); + + ctx.status = 204; + + return next(); + } + ); +} diff --git a/packages/core/src/routes-me/verification-code.ts b/packages/core/src/routes-me/verification-code.ts new file mode 100644 index 000000000..27738c8f3 --- /dev/null +++ b/packages/core/src/routes-me/verification-code.ts @@ -0,0 +1,49 @@ +import { VerificationCodeType } from '@logto/connector-kit'; +import { + requestVerificationCodePayloadGuard, + verifyVerificationCodePayloadGuard, +} from '@logto/schemas'; + +import koaGuard from '#src/middleware/koa-guard.js'; +import type { RouterInitArgs } from '#src/routes/types.js'; + +import type { AuthedMeRouter } from './types.js'; + +export default function verificationCodeRoutes( + ...[router, tenant]: RouterInitArgs +) { + const codeType = VerificationCodeType.Generic; + const { + passcodes: { createPasscode, sendPasscode, verifyPasscode }, + } = tenant.libraries; + + router.post( + '/verification-codes', + koaGuard({ + body: requestVerificationCodePayloadGuard, + }), + async (ctx, next) => { + const code = await createPasscode(undefined, codeType, ctx.guard.body); + await sendPasscode(code); + + ctx.status = 204; + + return next(); + } + ); + + router.post( + '/verification-codes/verify', + koaGuard({ + body: verifyVerificationCodePayloadGuard, + }), + async (ctx, next) => { + const { verificationCode, ...identifier } = ctx.guard.body; + await verifyPasscode(undefined, codeType, verificationCode, identifier); + + ctx.status = 204; + + return next(); + } + ); +}