diff --git a/packages/core/src/queries/user.ts b/packages/core/src/queries/user.ts index 7502a3901..efd3d3939 100644 --- a/packages/core/src/queries/user.ts +++ b/packages/core/src/queries/user.ts @@ -3,7 +3,9 @@ import { sql } from 'slonik'; import { buildInsertInto } from '@/database/insert-into'; import pool from '@/database/pool'; -import { convertToIdentifiers } from '@/database/utils'; +import { buildUpdateWhere } from '@/database/update-where'; +import { convertToIdentifiers, OmitAutoSetFields } from '@/database/utils'; +import RequestError from '@/errors/RequestError'; const { table, fields } = convertToIdentifiers(Users); @@ -36,3 +38,29 @@ export const hasUserWithId = async (id: string) => `); export const insertUser = buildInsertInto(pool, Users, { returning: true }); + +export const findAllUsers = async () => + pool.many(sql` + select ${sql.join(Object.values(fields), sql`, `)} + from ${table} + `); + +const updateUser = buildUpdateWhere(pool, Users, true); + +export const updateUserById = async (id: string, set: Partial>) => + updateUser({ set, where: { id } }); + +export const deleteUserById = async (id: string) => { + const { rowCount } = await pool.query(sql` + delete from ${table} + where id=${id} + `); + if (rowCount < 1) { + throw new RequestError({ + code: 'entity.not_exists_with_id', + name: Users.tableSingular, + id, + status: 404, + }); + } +}; diff --git a/packages/core/src/routes/user.ts b/packages/core/src/routes/user.ts index 960cbcd25..a9e4bd5aa 100644 --- a/packages/core/src/routes/user.ts +++ b/packages/core/src/routes/user.ts @@ -1,4 +1,5 @@ -import { PasswordEncryptionMethod } from '@logto/schemas'; +import { PasswordEncryptionMethod, userInfoSelectFields } from '@logto/schemas'; +import pick from 'lodash.pick'; import { nanoid } from 'nanoid'; import { Provider } from 'oidc-provider'; import { object, string } from 'zod'; @@ -6,7 +7,14 @@ import { object, string } from 'zod'; import RequestError from '@/errors/RequestError'; import { generateUserId } from '@/lib/user'; import koaGuard from '@/middleware/koa-guard'; -import { hasUser, insertUser } from '@/queries/user'; +import { + deleteUserById, + findAllUsers, + findUserById, + hasUser, + insertUser, + updateUserById, +} from '@/queries/user'; import { encryptPassword } from '@/utils/password'; import { AnonymousRouter } from './types'; @@ -58,4 +66,73 @@ export default function userRoutes(router: T, provide return next(); } ); + + router.get('/users', async (ctx, next) => { + const users = await findAllUsers(); + ctx.body = users.map((user) => pick(user, ...userInfoSelectFields)); + return next(); + }); + + router.get( + '/users/:userId', + koaGuard({ + params: object({ userId: string().min(1) }), + }), + async (ctx, next) => { + const { + params: { userId }, + } = ctx.guard; + const user = await findUserById(userId); + ctx.body = pick(user, ...userInfoSelectFields); + return next(); + } + ); + + router.patch( + '/users/:userId/password', + koaGuard({ + params: object({ userId: string().min(1) }), + body: object({ password: string().min(6) }), + }), + async (ctx, next) => { + const { + params: { userId }, + body: { password }, + } = ctx.guard; + + await findUserById(userId); + + const passwordEncryptionSalt = nanoid(); + const passwordEncryptionMethod = PasswordEncryptionMethod.SaltAndPepper; + const passwordEncrypted = encryptPassword( + userId, + password, + passwordEncryptionSalt, + passwordEncryptionMethod + ); + + await updateUserById(userId, { + passwordEncryptionSalt, + passwordEncrypted, + }); + const user = await findUserById(userId); + ctx.body = pick(user, ...userInfoSelectFields); + return next(); + } + ); + + router.delete( + '/users/:userId', + koaGuard({ + params: object({ userId: string().min(1) }), + }), + async (ctx, next) => { + const { + params: { userId }, + } = ctx.guard; + await deleteUserById(userId); + ctx.status = 204; + return next(); + } + ); }