diff --git a/packages/core/src/env/consts.ts b/packages/core/src/env/consts.ts index 750028e69..78a8d19f2 100644 --- a/packages/core/src/env/consts.ts +++ b/packages/core/src/env/consts.ts @@ -4,3 +4,4 @@ export const signIn = assertEnv('UI_SIGN_IN_ROUTE'); export const isProduction = getEnv('NODE_ENV') === 'production'; export const port = Number(getEnv('PORT', '3001')); export const mountedApps = Object.freeze(['api', 'oidc']); +export const developmentUserId = getEnv('DEVELOPMENT_USER_ID'); diff --git a/packages/core/src/middleware/koa-auth.ts b/packages/core/src/middleware/koa-auth.ts index 6e7d091ab..1ed6c599e 100644 --- a/packages/core/src/middleware/koa-auth.ts +++ b/packages/core/src/middleware/koa-auth.ts @@ -1,12 +1,14 @@ import assert from 'assert'; +import { IncomingHttpHeaders } from 'http'; import RequestError from '@/errors/RequestError'; -import { MiddlewareType } from 'koa'; +import { MiddlewareType, Request } from 'koa'; import { jwtVerify } from 'jose/jwt/verify'; import { publicKey, issuer, adminResource } from '@/oidc/consts'; import { IRouterParamContext } from 'koa-router'; import { UserInfo, userInfoSelectFields } from '@logto/schemas'; import { findUserById } from '@/queries/user'; import pick from 'lodash.pick'; +import { developmentUserId, isProduction } from '@/env/consts'; export type WithAuthContext = ContextT & { @@ -15,35 +17,45 @@ export type WithAuthContext { + assert( + authorization, + new RequestError({ code: 'auth.authorization_header_missing', status: 401 }) + ); + assert( + authorization.startsWith(bearerToken), + new RequestError( + { code: 'auth.authorization_type_not_supported', status: 401 }, + { supportedTypes: [bearerToken] } + ) + ); + return authorization.slice(bearerToken.length + 1); +}; + +const getUserIdFromRequest = async (request: Request) => { + if (!isProduction && developmentUserId) { + return developmentUserId; + } + + const { + payload: { sub }, + } = await jwtVerify(extractBearerTokenFromHeaders(request.headers), publicKey, { + issuer, + audience: adminResource, + }); + assert(sub); + return sub; +}; + export default function koaAuth< StateT, ContextT extends IRouterParamContext, ResponseBodyT >(): MiddlewareType, ResponseBodyT> { return async (ctx, next) => { - const { authorization } = ctx.request.headers; - assert( - authorization, - new RequestError({ code: 'auth.authorization_header_missing', status: 401 }) - ); - assert( - authorization.startsWith(bearerToken), - new RequestError( - { code: 'auth.authorization_type_not_supported', status: 401 }, - { supportedTypes: [bearerToken] } - ) - ); - const jwt = authorization.slice(bearerToken.length + 1); - try { - const { - payload: { sub }, - } = await jwtVerify(jwt, publicKey, { - issuer, - audience: adminResource, - }); - assert(sub); - const user = await findUserById(sub); + const userId = await getUserIdFromRequest(ctx.request); + const user = await findUserById(userId); ctx.user = pick(user, ...userInfoSelectFields); } catch { throw new RequestError({ code: 'auth.unauthorized', status: 401 }); diff --git a/packages/core/src/queries/application.ts b/packages/core/src/queries/application.ts index 29ecdc95b..0a2e931ef 100644 --- a/packages/core/src/queries/application.ts +++ b/packages/core/src/queries/application.ts @@ -1,6 +1,7 @@ import { buildInsertInto } from '@/database/insert'; import pool from '@/database/pool'; import { convertToIdentifiers } from '@/database/utils'; +import RequestError from '@/errors/RequestError'; import { ApplicationDBEntry, Applications } from '@logto/schemas'; import { sql } from 'slonik'; @@ -8,11 +9,21 @@ const { table, fields } = convertToIdentifiers(Applications); export const findApplicationById = async (id: string) => pool.one(sql` - select ${sql.join(Object.values(fields), sql`, `)} - from ${table} - where ${fields.id}=${id} -`); + select ${sql.join(Object.values(fields), sql`, `)} + from ${table} + where ${fields.id}=${id} + `); export const insertApplication = buildInsertInto(pool, Applications, { returning: true, }); + +export const deleteApplicationById = 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', name: Applications.tableSingular, id }); + } +}; diff --git a/packages/core/src/routes/application.ts b/packages/core/src/routes/application.ts index 5c4a28d7f..ff8402207 100644 --- a/packages/core/src/routes/application.ts +++ b/packages/core/src/routes/application.ts @@ -2,7 +2,7 @@ import Router from 'koa-router'; import { nativeEnum, object, string } from 'zod'; import { ApplicationType } from '@logto/schemas'; import koaGuard from '@/middleware/koa-guard'; -import { insertApplication } from '@/queries/application'; +import { deleteApplicationById, insertApplication } from '@/queries/application'; import { buildIdGenerator } from '@/utils/id'; import { generateOidcClientMetadata } from '@/oidc/utils'; @@ -29,4 +29,16 @@ export default function applicationRoutes(router: Router { + const { id } = ctx.guard.params; + // Note: will need delete cascade when application is joint with other tables + await deleteApplicationById(id); + ctx.status = 204; + return next(); + } + ); } diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts index eb7872cb9..89c6b2f4d 100644 --- a/packages/phrases/src/locales/en.ts +++ b/packages/phrases/src/locales/en.ts @@ -39,6 +39,7 @@ const errors = { }, entity: { create_failed: 'Failed to create {{name}}.', + not_exists: 'The {{name}} with ID `{{id}}` does not exist.', }, }; diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts index 02520a113..08c52ba9e 100644 --- a/packages/phrases/src/locales/zh-cn.ts +++ b/packages/phrases/src/locales/zh-cn.ts @@ -40,7 +40,8 @@ const errors = { invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。', }, entity: { - create_failed: '创建{{name}}失败。', + create_failed: '创建 {{name}} 失败。', + not_exists: 'ID 为 `{{id}}` 的 {{name}} 不存在。', }, };