From 9e96eca03248a4c8befb13797c40e8450e3b5f0c Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 23 Oct 2024 17:56:47 +0200 Subject: [PATCH] refactor(database): :recycle: Move Applications to our custom ORM --- api/api/auth/login/index.test.ts | 43 +++--- api/api/auth/login/index.ts | 12 +- api/api/auth/reset/index.test.ts | 33 ++--- api/api/v1/apps/index.ts | 37 ++--- api/api/v1/apps/verify_credentials/index.ts | 22 +-- classes/functions/application.ts | 36 ----- classes/functions/status.ts | 4 +- classes/functions/user.ts | 6 +- packages/database-interface/application.ts | 155 ++++++++++++++++++++ packages/database-interface/note.ts | 7 +- packages/plugin-kit/exports/db.ts | 1 + plugins/openid/routes/authorize.test.ts | 93 ++++++------ plugins/openid/routes/authorize.ts | 15 +- plugins/openid/routes/jwks.test.ts | 27 ++-- plugins/openid/routes/oauth/revoke.test.ts | 56 ++++--- plugins/openid/routes/oauth/sso.ts | 6 +- plugins/openid/routes/oauth/token.test.ts | 71 +++++---- plugins/openid/routes/oauth/token.ts | 9 +- plugins/openid/routes/sso/index.ts | 34 ++--- tests/oauth-scopes.test.ts | 118 +++++++-------- types/api.ts | 2 +- utils/api.ts | 6 +- utils/oauth.ts | 12 +- 23 files changed, 424 insertions(+), 381 deletions(-) delete mode 100644 classes/functions/application.ts create mode 100644 packages/database-interface/application.ts diff --git a/api/api/auth/login/index.test.ts b/api/api/auth/login/index.test.ts index bac3beed..23a4f862 100644 --- a/api/api/auth/login/index.test.ts +++ b/api/api/auth/login/index.test.ts @@ -1,31 +1,24 @@ import { afterAll, describe, expect, test } from "bun:test"; import { randomString } from "@/math"; -import { eq } from "drizzle-orm"; -import { db } from "~/drizzle/db"; -import { Applications } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; +import { Application } from "~/packages/database-interface/application.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; import { meta } from "./index.ts"; const { users, deleteUsers, passwords } = await getTestUsers(1); // Create application -const application = ( - await db - .insert(Applications) - .values({ - name: "Test Application", - clientId: randomString(32, "hex"), - secret: "test", - redirectUri: "https://example.com", - scopes: "read write", - }) - .returning() -)[0]; +const application = await Application.insert({ + name: "Test Application", + clientId: randomString(32, "hex"), + secret: "test", + redirectUri: "https://example.com", + scopes: "read write", +}); afterAll(async () => { await deleteUsers(); - await db.delete(Applications).where(eq(Applications.id, application.id)); + await application.delete(); }); // /api/auth/login @@ -37,7 +30,7 @@ describe(meta.route, () => { formData.append("password", passwords[0]); const response = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, @@ -53,7 +46,7 @@ describe(meta.route, () => { expect(locationHeader.pathname).toBe("/oauth/consent"); expect(locationHeader.searchParams.get("client_id")).toBe( - application.clientId, + application.data.clientId, ); expect(locationHeader.searchParams.get("redirect_uri")).toBe( "https://example.com", @@ -71,7 +64,7 @@ describe(meta.route, () => { formData.append("password", passwords[0]); const response = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, @@ -87,7 +80,7 @@ describe(meta.route, () => { expect(locationHeader.pathname).toBe("/oauth/consent"); expect(locationHeader.searchParams.get("client_id")).toBe( - application.clientId, + application.data.clientId, ); expect(locationHeader.searchParams.get("redirect_uri")).toBe( "https://example.com", @@ -105,7 +98,7 @@ describe(meta.route, () => { formData.append("password", passwords[0]); const response = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write&state=abc`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write&state=abc`, { method: "POST", body: formData, @@ -121,7 +114,7 @@ describe(meta.route, () => { expect(locationHeader.pathname).toBe("/oauth/consent"); expect(locationHeader.searchParams.get("client_id")).toBe( - application.clientId, + application.data.clientId, ); expect(locationHeader.searchParams.get("redirect_uri")).toBe( "https://example.com", @@ -142,7 +135,7 @@ describe(meta.route, () => { formData.append("password", "password"); const response = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", @@ -175,7 +168,7 @@ describe(meta.route, () => { formData.append("password", "password"); const response = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, @@ -207,7 +200,7 @@ describe(meta.route, () => { formData.append("password", "password"); const response = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, diff --git a/api/api/auth/login/index.ts b/api/api/auth/login/index.ts index d26360a7..f82e5123 100644 --- a/api/api/auth/login/index.ts +++ b/api/api/auth/login/index.ts @@ -5,9 +5,9 @@ import { createRoute } from "@hono/zod-openapi"; import { eq, or } from "drizzle-orm"; import { SignJWT } from "jose"; import { z } from "zod"; -import { db } from "~/drizzle/db"; import { Users } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; +import { Application } from "~/packages/database-interface/application"; import { User } from "~/packages/database-interface/user"; export const meta = applyConfig({ @@ -197,20 +197,18 @@ export default apiRoute((app) => .setProtectedHeader({ alg: "EdDSA" }) .sign(privateKey); - const application = await db.query.Applications.findFirst({ - where: (app, { eq }) => eq(app.clientId, client_id), - }); + const application = await Application.fromClientId(client_id); if (!application) { return context.json({ error: "Invalid application" }, 400); } const searchParams = new URLSearchParams({ - application: application.name, + application: application.data.name, }); - if (application.website) { - searchParams.append("website", application.website); + if (application.data.website) { + searchParams.append("website", application.data.website); } // Add all data that is not undefined except email and password diff --git a/api/api/auth/reset/index.test.ts b/api/api/auth/reset/index.test.ts index 82215505..a6878008 100644 --- a/api/api/auth/reset/index.test.ts +++ b/api/api/auth/reset/index.test.ts @@ -1,9 +1,7 @@ import { afterAll, describe, expect, test } from "bun:test"; import { randomString } from "@/math"; -import { eq } from "drizzle-orm"; -import { db } from "~/drizzle/db"; -import { Applications } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; +import { Application } from "~/packages/database-interface/application.ts"; import { fakeRequest, getTestUsers } from "~/tests/utils"; import { meta } from "./index.ts"; @@ -12,22 +10,17 @@ const token = randomString(32, "hex"); const newPassword = randomString(16, "hex"); // Create application -const application = ( - await db - .insert(Applications) - .values({ - name: "Test Application", - clientId: randomString(32, "hex"), - secret: "test", - redirectUri: "https://example.com", - scopes: "read write", - }) - .returning() -)[0]; +const application = await Application.insert({ + name: "Test Application", + clientId: randomString(32, "hex"), + secret: "test", + redirectUri: "https://example.com", + scopes: "read write", +}); afterAll(async () => { await deleteUsers(); - await db.delete(Applications).where(eq(Applications.id, application.id)); + await application.delete(); }); // /api/auth/reset @@ -39,7 +32,7 @@ describe(meta.route, () => { formData.append("password", passwords[0]); const response = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", @@ -62,7 +55,7 @@ describe(meta.route, () => { formData.append("password", passwords[0]); const response = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, @@ -101,7 +94,7 @@ describe(meta.route, () => { loginFormData.append("password", newPassword); const loginResponse = await fakeRequest( - `/api/auth/login?client_id=${application.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: loginFormData, @@ -117,7 +110,7 @@ describe(meta.route, () => { expect(locationHeader.pathname).toBe("/oauth/consent"); expect(locationHeader.searchParams.get("client_id")).toBe( - application.clientId, + application.data.clientId, ); expect(locationHeader.searchParams.get("redirect_uri")).toBe( "https://example.com", diff --git a/api/api/v1/apps/index.ts b/api/api/v1/apps/index.ts index 62c2f4df..ff92e63b 100644 --- a/api/api/v1/apps/index.ts +++ b/api/api/v1/apps/index.ts @@ -2,8 +2,8 @@ import { apiRoute, applyConfig, jsonOrForm } from "@/api"; import { randomString } from "@/math"; import { createRoute } from "@hono/zod-openapi"; import { z } from "zod"; -import { db } from "~/drizzle/db"; -import { Applications, RolePermissions } from "~/drizzle/schema"; +import { RolePermissions } from "~/drizzle/schema"; +import { Application } from "~/packages/database-interface/application"; export const meta = applyConfig({ route: "/api/v1/apps", @@ -81,29 +81,24 @@ export default apiRoute((app) => const { client_name, redirect_uris, scopes, website } = context.req.valid("json"); - const app = ( - await db - .insert(Applications) - .values({ - name: client_name || "", - redirectUri: decodeURI(redirect_uris) || "", - scopes: scopes || "read", - website: website || null, - clientId: randomString(32, "base64url"), - secret: randomString(64, "base64url"), - }) - .returning() - )[0]; + const app = await Application.insert({ + name: client_name || "", + redirectUri: decodeURI(redirect_uris) || "", + scopes: scopes || "read", + website: website || null, + clientId: randomString(32, "base64url"), + secret: randomString(64, "base64url"), + }); return context.json( { id: app.id, - name: app.name, - website: app.website, - client_id: app.clientId, - client_secret: app.secret, - redirect_uri: app.redirectUri, - vapid_link: app.vapidKey, + name: app.data.name, + website: app.data.website, + client_id: app.data.clientId, + client_secret: app.data.secret, + redirect_uri: app.data.redirectUri, + vapid_link: app.data.vapidKey, }, 200, ); diff --git a/api/api/v1/apps/verify_credentials/index.ts b/api/api/v1/apps/verify_credentials/index.ts index ce16b753..cbd47375 100644 --- a/api/api/v1/apps/verify_credentials/index.ts +++ b/api/api/v1/apps/verify_credentials/index.ts @@ -1,7 +1,7 @@ import { apiRoute, applyConfig, auth } from "@/api"; -import { createRoute, z } from "@hono/zod-openapi"; -import { getFromToken } from "~/classes/functions/application"; +import { createRoute } from "@hono/zod-openapi"; import { RolePermissions } from "~/drizzle/schema"; +import { Application } from "~/packages/database-interface/application"; import { ErrorSchema } from "~/types/api"; export const meta = applyConfig({ @@ -29,13 +29,7 @@ const route = createRoute({ description: "Application", content: { "application/json": { - schema: z.object({ - name: z.string(), - website: z.string().nullable(), - vapid_key: z.string().nullable(), - redirect_uris: z.string(), - scopes: z.string(), - }), + schema: Application.schema, }, }, }, @@ -61,7 +55,7 @@ export default apiRoute((app) => return context.json({ error: "Unauthorized" }, 401); } - const application = await getFromToken(token); + const application = await Application.getFromToken(token); if (!application) { return context.json({ error: "Unauthorized" }, 401); @@ -69,11 +63,9 @@ export default apiRoute((app) => return context.json( { - name: application.name, - website: application.website, - vapid_key: application.vapidKey, - redirect_uris: application.redirectUri, - scopes: application.scopes, + ...application.toApi(), + redirect_uris: application.data.redirectUri, + scopes: application.data.scopes, }, 200, ); diff --git a/classes/functions/application.ts b/classes/functions/application.ts deleted file mode 100644 index c4f82de5..00000000 --- a/classes/functions/application.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Application as APIApplication } from "@versia/client/types"; -import type { InferSelectModel } from "drizzle-orm"; -import { db } from "~/drizzle/db"; -import type { Applications } from "~/drizzle/schema"; - -export type Application = InferSelectModel; - -/** - * Retrieves the application associated with the given access token. - * @param token The access token to retrieve the application for. - * @returns The application associated with the given access token, or null if no such application exists. - */ -export const getFromToken = async ( - token: string, -): Promise => { - const result = await db.query.Tokens.findFirst({ - where: (tokens, { eq }) => eq(tokens.accessToken, token), - with: { - application: true, - }, - }); - - return result?.application || null; -}; - -/** - * Converts this application to an API application. - * @returns The API application representation of this application. - */ -export const applicationToApi = (app: Application): APIApplication => { - return { - name: app.name, - website: app.website, - vapid_key: app.vapidKey, - }; -}; diff --git a/classes/functions/status.ts b/classes/functions/status.ts index cf0f8b15..61d0e98b 100644 --- a/classes/functions/status.ts +++ b/classes/functions/status.ts @@ -32,9 +32,9 @@ import { Users, } from "~/drizzle/schema"; import { config } from "~/packages/config-manager/index.ts"; +import type { ApplicationType } from "~/packages/database-interface/application.ts"; import type { EmojiWithInstance } from "~/packages/database-interface/emoji"; import { User } from "~/packages/database-interface/user"; -import type { Application } from "./application.ts"; import { type UserWithInstance, type UserWithRelations, @@ -53,7 +53,7 @@ export type StatusWithRelations = Status & { emojis: EmojiWithInstance[]; reply: Status | null; quote: Status | null; - application: Application | null; + application: ApplicationType | null; reblogCount: number; likeCount: number; replyCount: number; diff --git a/classes/functions/user.ts b/classes/functions/user.ts index 9466b7f2..69dd8752 100644 --- a/classes/functions/user.ts +++ b/classes/functions/user.ts @@ -12,9 +12,9 @@ import { Tokens, type Users, } from "~/drizzle/schema"; +import type { ApplicationType } from "~/packages/database-interface/application.ts"; import type { EmojiWithInstance } from "~/packages/database-interface/emoji"; import { User } from "~/packages/database-interface/user"; -import type { Application } from "./application.ts"; import type { Token } from "./token.ts"; export type UserType = InferSelectModel; @@ -99,7 +99,7 @@ export const userExtrasTemplate = (name: string) => ({ export interface AuthData { user: User | null; token: string; - application: Application | null; + application: ApplicationType | null; } export const getFromHeader = async (value: string): Promise => { @@ -216,7 +216,7 @@ export const retrieveUserAndApplicationFromToken = async ( accessToken: string, ): Promise<{ user: User | null; - application: Application | null; + application: ApplicationType | null; }> => { if (!accessToken) { return { user: null, application: null }; diff --git a/packages/database-interface/application.ts b/packages/database-interface/application.ts new file mode 100644 index 00000000..5ef0c691 --- /dev/null +++ b/packages/database-interface/application.ts @@ -0,0 +1,155 @@ +import type { Application as APIApplication } from "@versia/client/types"; +import { + type InferInsertModel, + type InferSelectModel, + type SQL, + desc, + eq, + inArray, +} from "drizzle-orm"; +import { z } from "zod"; +import { db } from "~/drizzle/db"; +import { Applications } from "~/drizzle/schema"; +import { BaseInterface } from "./base.ts"; + +export type ApplicationType = InferSelectModel; + +export class Application extends BaseInterface { + static schema: z.ZodType = z.object({ + name: z.string(), + website: z.string().url().optional().nullable(), + vapid_key: z.string().optional().nullable(), + redirect_uris: z.string().optional(), + scopes: z.string().optional(), + }); + + async reload(): Promise { + const reloaded = await Application.fromId(this.data.id); + + if (!reloaded) { + throw new Error("Failed to reload application"); + } + + this.data = reloaded.data; + } + + public static async fromId(id: string | null): Promise { + if (!id) { + return null; + } + + return await Application.fromSql(eq(Applications.id, id)); + } + + public static async fromIds(ids: string[]): Promise { + return await Application.manyFromSql(inArray(Applications.id, ids)); + } + + public static async fromSql( + sql: SQL | undefined, + orderBy: SQL | undefined = desc(Applications.id), + ): Promise { + const found = await db.query.Applications.findFirst({ + where: sql, + orderBy, + }); + + if (!found) { + return null; + } + return new Application(found); + } + + public static async manyFromSql( + sql: SQL | undefined, + orderBy: SQL | undefined = desc(Applications.id), + limit?: number, + offset?: number, + extra?: Parameters[0], + ): Promise { + const found = await db.query.Applications.findMany({ + where: sql, + orderBy, + limit, + offset, + with: extra?.with, + }); + + return found.map((s) => new Application(s)); + } + + public static async getFromToken( + token: string, + ): Promise { + const result = await db.query.Tokens.findFirst({ + where: (tokens, { eq }) => eq(tokens.accessToken, token), + with: { + application: true, + }, + }); + + return result?.application ? new Application(result.application) : null; + } + + public static fromClientId(clientId: string): Promise { + return Application.fromSql(eq(Applications.clientId, clientId)); + } + + async update( + newApplication: Partial, + ): Promise { + await db + .update(Applications) + .set(newApplication) + .where(eq(Applications.id, this.id)); + + const updated = await Application.fromId(this.data.id); + + if (!updated) { + throw new Error("Failed to update application"); + } + + this.data = updated.data; + return updated.data; + } + + save(): Promise { + return this.update(this.data); + } + + async delete(ids?: string[]): Promise { + if (Array.isArray(ids)) { + await db.delete(Applications).where(inArray(Applications.id, ids)); + } else { + await db.delete(Applications).where(eq(Applications.id, this.id)); + } + } + + public static async insert( + data: InferInsertModel, + ): Promise { + const inserted = ( + await db.insert(Applications).values(data).returning() + )[0]; + + const application = await Application.fromId(inserted.id); + + if (!application) { + throw new Error("Failed to insert application"); + } + + return application; + } + + get id() { + return this.data.id; + } + + public toApi(): APIApplication { + return { + name: this.data.name, + website: this.data.website, + vapid_key: this.data.vapidKey, + }; + } +} diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index 7f12d7c9..d400168f 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -27,10 +27,6 @@ import { import { htmlToText } from "html-to-text"; import { createRegExp, exactly, global } from "magic-regexp"; import { z } from "zod"; -import { - type Application, - applicationToApi, -} from "~/classes/functions/application"; import { type StatusWithRelations, contentToHtml, @@ -47,6 +43,7 @@ import { Users, } from "~/drizzle/schema"; import { config } from "~/packages/config-manager"; +import { Application } from "./application.ts"; import { Attachment } from "./attachment.ts"; import { BaseInterface } from "./base.ts"; import { Emoji } from "./emoji.ts"; @@ -904,7 +901,7 @@ export class Note extends BaseInterface { account: this.author.toApi(userFetching?.id === data.authorId), created_at: new Date(data.createdAt).toISOString(), application: data.application - ? applicationToApi(data.application) + ? new Application(data.application).toApi() : null, card: null, content: replacedContent, diff --git a/packages/plugin-kit/exports/db.ts b/packages/plugin-kit/exports/db.ts index 4385a008..89040fec 100644 --- a/packages/plugin-kit/exports/db.ts +++ b/packages/plugin-kit/exports/db.ts @@ -6,4 +6,5 @@ export { Emoji } from "~/packages/database-interface/emoji"; export { Instance } from "~/packages/database-interface/instance"; export { Note } from "~/packages/database-interface/note"; export { Timeline } from "~/packages/database-interface/timeline"; +export { Application } from "~/packages/database-interface/application"; export { db } from "~/drizzle/db"; diff --git a/plugins/openid/routes/authorize.test.ts b/plugins/openid/routes/authorize.test.ts index 94230090..32662156 100644 --- a/plugins/openid/routes/authorize.test.ts +++ b/plugins/openid/routes/authorize.test.ts @@ -1,17 +1,12 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { afterAll, describe, expect, test } from "bun:test"; import { randomString } from "@/math"; -import { db } from "@versia/kit/db"; -import { eq } from "@versia/kit/drizzle"; -import { Applications, RolePermissions } from "@versia/kit/tables"; +import { RolePermissions } from "@versia/kit/tables"; import { SignJWT } from "jose"; import { config } from "~/packages/config-manager"; +import { Application } from "~/packages/database-interface/application"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { deleteUsers, tokens, users } = await getTestUsers(1); -const clientId = "test-client-id"; -const redirectUri = "https://example.com/callback"; -const scope = "openid profile email"; -const secret = "test-secret"; const privateKey = await crypto.subtle.importKey( "pkcs8", Buffer.from( @@ -23,19 +18,17 @@ const privateKey = await crypto.subtle.importKey( ["sign"], ); -beforeAll(async () => { - await db.insert(Applications).values({ - clientId, - redirectUri, - scopes: scope, - name: "Test Application", - secret, - }); +const application = await Application.insert({ + clientId: "test-client-id", + redirectUri: "https://example.com/callback", + scopes: "openid profile email", + name: "Test Application", + secret: "test-secret", }); afterAll(async () => { await deleteUsers(); - await db.delete(Applications).where(eq(Applications.clientId, clientId)); + await application.delete(); }); describe("/oauth/authorize", () => { @@ -43,7 +36,7 @@ describe("/oauth/authorize", () => { const jwt = await new SignJWT({ sub: users[0].id, iss: new URL(config.http.base_url).origin, - aud: clientId, + aud: application.data.clientId, exp: Math.floor(Date.now() / 1000) + 60 * 60, iat: Math.floor(Date.now() / 1000), nbf: Math.floor(Date.now() / 1000), @@ -59,10 +52,10 @@ describe("/oauth/authorize", () => { Cookie: `jwt=${jwt}`, }, body: JSON.stringify({ - client_id: clientId, - redirect_uri: redirectUri, + client_id: application.data.clientId, + redirect_uri: application.data.redirectUri, response_type: "code", - scope, + scope: application.data.scopes, state: "test-state", code_challenge: randomString(43), code_challenge_method: "S256", @@ -75,7 +68,9 @@ describe("/oauth/authorize", () => { config.http.base_url, ); const params = new URLSearchParams(location.search); - expect(location.origin + location.pathname).toBe(redirectUri); + expect(location.origin + location.pathname).toBe( + application.data.redirectUri, + ); expect(params.get("code")).toBeTruthy(); expect(params.get("state")).toBe("test-state"); }); @@ -89,10 +84,10 @@ describe("/oauth/authorize", () => { Cookie: "jwt=invalid-jwt", }, body: JSON.stringify({ - client_id: clientId, - redirect_uri: redirectUri, + client_id: application.data.clientId, + redirect_uri: application.data.redirectUri, response_type: "code", - scope, + scope: application.data.scopes, state: "test-state", code_challenge: randomString(43), code_challenge_method: "S256", @@ -115,7 +110,7 @@ describe("/oauth/authorize", () => { const jwt = await new SignJWT({ sub: users[0].id, iss: new URL(config.http.base_url).origin, - aud: clientId, + aud: application.data.clientId, }) .setProtectedHeader({ alg: "EdDSA" }) .sign(privateKey); @@ -128,10 +123,10 @@ describe("/oauth/authorize", () => { Cookie: `jwt=${jwt}`, }, body: JSON.stringify({ - client_id: clientId, - redirect_uri: redirectUri, + client_id: application.data.clientId, + redirect_uri: application.data.redirectUri, response_type: "code", - scope, + scope: application.data.scopes, state: "test-state", code_challenge: randomString(43), code_challenge_method: "S256", @@ -153,7 +148,7 @@ describe("/oauth/authorize", () => { test("should return error for user not found", async () => { const jwt = await new SignJWT({ sub: "non-existent-user", - aud: clientId, + aud: application.data.clientId, exp: Math.floor(Date.now() / 1000) + 60 * 60, iss: new URL(config.http.base_url).origin, iat: Math.floor(Date.now() / 1000), @@ -170,10 +165,10 @@ describe("/oauth/authorize", () => { Cookie: `jwt=${jwt}`, }, body: JSON.stringify({ - client_id: clientId, - redirect_uri: redirectUri, + client_id: application.data.clientId, + redirect_uri: application.data.redirectUri, response_type: "code", - scope, + scope: application.data.scopes, state: "test-state", code_challenge: randomString(43), code_challenge_method: "S256", @@ -193,7 +188,7 @@ describe("/oauth/authorize", () => { const jwt2 = await new SignJWT({ sub: "23e42862-d5df-49a8-95b5-52d8c6a11aea", - aud: clientId, + aud: application.data.clientId, exp: Math.floor(Date.now() / 1000) + 60 * 60, iss: new URL(config.http.base_url).origin, iat: Math.floor(Date.now() / 1000), @@ -210,10 +205,10 @@ describe("/oauth/authorize", () => { Cookie: `jwt=${jwt2}`, }, body: JSON.stringify({ - client_id: clientId, - redirect_uri: redirectUri, + client_id: application.data.clientId, + redirect_uri: application.data.redirectUri, response_type: "code", - scope, + scope: application.data.scopes, state: "test-state", code_challenge: randomString(43), code_challenge_method: "S256", @@ -239,7 +234,7 @@ describe("/oauth/authorize", () => { const jwt = await new SignJWT({ sub: users[0].id, iss: new URL(config.http.base_url).origin, - aud: clientId, + aud: application.data.clientId, exp: Math.floor(Date.now() / 1000) + 60 * 60, iat: Math.floor(Date.now() / 1000), nbf: Math.floor(Date.now() / 1000), @@ -255,10 +250,10 @@ describe("/oauth/authorize", () => { Cookie: `jwt=${jwt}`, }, body: JSON.stringify({ - client_id: clientId, - redirect_uri: redirectUri, + client_id: application.data.clientId, + redirect_uri: application.data.redirectUri, response_type: "code", - scope, + scope: application.data.scopes, state: "test-state", code_challenge: randomString(43), code_challenge_method: "S256", @@ -300,9 +295,9 @@ describe("/oauth/authorize", () => { }, body: JSON.stringify({ client_id: "invalid-client-id", - redirect_uri: redirectUri, + redirect_uri: application.data.redirectUri, response_type: "code", - scope, + scope: application.data.scopes, state: "test-state", code_challenge: randomString(43), code_challenge_method: "S256", @@ -325,7 +320,7 @@ describe("/oauth/authorize", () => { const jwt = await new SignJWT({ sub: users[0].id, iss: new URL(config.http.base_url).origin, - aud: clientId, + aud: application.data.clientId, exp: Math.floor(Date.now() / 1000) + 60 * 60, iat: Math.floor(Date.now() / 1000), nbf: Math.floor(Date.now() / 1000), @@ -341,10 +336,10 @@ describe("/oauth/authorize", () => { Cookie: `jwt=${jwt}`, }, body: JSON.stringify({ - client_id: clientId, + client_id: application.data.clientId, redirect_uri: "https://invalid.com/callback", response_type: "code", - scope, + scope: application.data.scopes, state: "test-state", code_challenge: randomString(43), code_challenge_method: "S256", @@ -367,7 +362,7 @@ describe("/oauth/authorize", () => { const jwt = await new SignJWT({ sub: users[0].id, iss: new URL(config.http.base_url).origin, - aud: clientId, + aud: application.data.clientId, exp: Math.floor(Date.now() / 1000) + 60 * 60, iat: Math.floor(Date.now() / 1000), nbf: Math.floor(Date.now() / 1000), @@ -383,8 +378,8 @@ describe("/oauth/authorize", () => { Cookie: `jwt=${jwt}`, }, body: JSON.stringify({ - client_id: clientId, - redirect_uri: redirectUri, + client_id: application.data.clientId, + redirect_uri: application.data.redirectUri, response_type: "code", scope: "invalid-scope", state: "test-state", diff --git a/plugins/openid/routes/authorize.ts b/plugins/openid/routes/authorize.ts index 6faa0686..4d69170d 100644 --- a/plugins/openid/routes/authorize.ts +++ b/plugins/openid/routes/authorize.ts @@ -6,6 +6,7 @@ import { type JWTPayload, SignJWT, jwtVerify } from "jose"; import { JOSEError } from "jose/errors"; import { z } from "zod"; import { TokenType } from "~/classes/functions/token"; +import { Application } from "~/packages/database-interface/application.ts"; import { User } from "~/packages/database-interface/user"; import type { PluginType } from "../index.ts"; @@ -197,9 +198,7 @@ export default (plugin: PluginType) => ); } - const application = await db.query.Applications.findFirst({ - where: (app, { eq }) => eq(app.clientId, client_id), - }); + const application = await Application.fromClientId(client_id); if (!application) { errorSearchParams.append("error", "invalid_request"); @@ -213,7 +212,7 @@ export default (plugin: PluginType) => ); } - if (application.redirectUri !== redirect_uri) { + if (application.data.redirectUri !== redirect_uri) { errorSearchParams.append("error", "invalid_request"); errorSearchParams.append( "error_description", @@ -230,7 +229,7 @@ export default (plugin: PluginType) => scope && !scope .split(" ") - .every((s) => application.scopes.includes(s)) + .every((s) => application.data.scopes.includes(s)) ) { errorSearchParams.append("error", "invalid_scope"); errorSearchParams.append( @@ -288,10 +287,10 @@ export default (plugin: PluginType) => await db.insert(Tokens).values({ accessToken: randomString(64, "base64url"), code, - scope: scope ?? application.scopes, + scope: scope ?? application.data.scopes, tokenType: TokenType.Bearer, applicationId: application.id, - redirectUri: redirect_uri ?? application.redirectUri, + redirectUri: redirect_uri ?? application.data.redirectUri, expiresAt: new Date( Date.now() + 60 * 60 * 24 * 14, ).toISOString(), @@ -310,7 +309,7 @@ export default (plugin: PluginType) => "/oauth/code", context.get("config").http.base_url, ) - : new URL(redirect_uri ?? application.redirectUri); + : new URL(redirect_uri ?? application.data.redirectUri); redirectUri.searchParams.append("code", code); state && redirectUri.searchParams.append("state", state); diff --git a/plugins/openid/routes/jwks.test.ts b/plugins/openid/routes/jwks.test.ts index adf42c77..0e4039b2 100644 --- a/plugins/openid/routes/jwks.test.ts +++ b/plugins/openid/routes/jwks.test.ts @@ -1,26 +1,17 @@ -import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { db } from "@versia/kit/db"; -import { eq } from "drizzle-orm"; -import { Applications } from "~/drizzle/schema"; +import { afterAll, describe, expect, test } from "bun:test"; +import { Application } from "~/packages/database-interface/application"; import { fakeRequest } from "~/tests/utils"; -const clientId = "test-client-id"; -const redirectUri = "https://example.com/callback"; -const scope = "openid profile email"; -const secret = "test-secret"; - -beforeAll(async () => { - await db.insert(Applications).values({ - clientId, - redirectUri, - scopes: scope, - name: "Test Application", - secret, - }); +const application = await Application.insert({ + clientId: "test-client-id", + redirectUri: "https://example.com/callback", + scopes: "openid profile email", + secret: "test-secret", + name: "Test Application", }); afterAll(async () => { - await db.delete(Applications).where(eq(Applications.clientId, clientId)); + await application.delete(); }); describe("/.well-known/jwks", () => { diff --git a/plugins/openid/routes/oauth/revoke.test.ts b/plugins/openid/routes/oauth/revoke.test.ts index 2adbe2d3..3d556033 100644 --- a/plugins/openid/routes/oauth/revoke.test.ts +++ b/plugins/openid/routes/oauth/revoke.test.ts @@ -1,38 +1,30 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { db } from "@versia/kit/db"; import { eq } from "@versia/kit/drizzle"; -import { Applications, Tokens } from "@versia/kit/tables"; +import { Tokens } from "@versia/kit/tables"; +import { Application } from "~/packages/database-interface/application"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { deleteUsers, users } = await getTestUsers(1); -const clientId = "test-client-id"; -const redirectUri = "https://example.com/callback"; -const scope = "openid profile email"; -const secret = "test-secret"; + +const application = await Application.insert({ + clientId: "test-client-id", + redirectUri: "https://example.com/callback", + scopes: "openid profile email", + secret: "test-secret", + name: "Test Application", +}); beforeAll(async () => { - const application = ( - await db - .insert(Applications) - .values({ - clientId, - redirectUri, - scopes: scope, - name: "Test Application", - secret, - }) - .returning() - )[0]; - await db.insert(Tokens).values({ code: "test-code", - redirectUri, - clientId, + redirectUri: application.data.redirectUri, + clientId: application.data.clientId, accessToken: "test-access-token", expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), createdAt: new Date().toISOString(), tokenType: "Bearer", - scope, + scope: application.data.scopes, userId: users[0].id, applicationId: application.id, }); @@ -40,8 +32,10 @@ beforeAll(async () => { afterAll(async () => { await deleteUsers(); - await db.delete(Applications).where(eq(Applications.clientId, clientId)); - await db.delete(Tokens).where(eq(Tokens.clientId, clientId)); + await application.delete(); + await db + .delete(Tokens) + .where(eq(Tokens.clientId, application.data.clientId)); }); describe("/oauth/revoke", () => { @@ -52,8 +46,8 @@ describe("/oauth/revoke", () => { "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: clientId, - client_secret: secret, + client_id: application.data.clientId, + client_secret: application.data.secret, token: "test-access-token", }), }); @@ -70,8 +64,8 @@ describe("/oauth/revoke", () => { "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: clientId, - client_secret: secret, + client_id: application.data.clientId, + client_secret: application.data.secret, }), }); @@ -90,7 +84,7 @@ describe("/oauth/revoke", () => { "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: clientId, + client_id: application.data.clientId, client_secret: "invalid-secret", token: "test-access-token", }), @@ -111,8 +105,8 @@ describe("/oauth/revoke", () => { "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: clientId, - client_secret: secret, + client_id: application.data.clientId, + client_secret: application.data.secret, token: "invalid-token", }), }); @@ -133,7 +127,7 @@ describe("/oauth/revoke", () => { }, body: JSON.stringify({ client_id: "unauthorized-client-id", - client_secret: secret, + client_secret: application.data.secret, token: "test-access-token", }), }); diff --git a/plugins/openid/routes/oauth/sso.ts b/plugins/openid/routes/oauth/sso.ts index 5ea9600c..e0c6f874 100644 --- a/plugins/openid/routes/oauth/sso.ts +++ b/plugins/openid/routes/oauth/sso.ts @@ -7,6 +7,7 @@ import { generateRandomCodeVerifier, processDiscoveryResponse, } from "oauth4webapi"; +import { Application } from "~/packages/database-interface/application.ts"; import type { PluginType } from "../../index.ts"; import { oauthRedirectUri } from "../../utils.ts"; @@ -83,10 +84,7 @@ export default (plugin: PluginType) => { const codeVerifier = generateRandomCodeVerifier(); - const application = await db.query.Applications.findFirst({ - where: (application, { eq }) => - eq(application.clientId, client_id), - }); + const application = await Application.fromClientId(client_id); if (!application) { errorSearchParams.append("error", "invalid_request"); diff --git a/plugins/openid/routes/oauth/token.test.ts b/plugins/openid/routes/oauth/token.test.ts index 44455cb1..3b41699a 100644 --- a/plugins/openid/routes/oauth/token.test.ts +++ b/plugins/openid/routes/oauth/token.test.ts @@ -1,41 +1,40 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { db } from "@versia/kit/db"; import { eq } from "@versia/kit/drizzle"; -import { Applications, Tokens } from "@versia/kit/tables"; +import { Tokens } from "@versia/kit/tables"; +import { Application } from "~/packages/database-interface/application"; import { fakeRequest, getTestUsers } from "~/tests/utils"; const { deleteUsers, users } = await getTestUsers(1); -const clientId = "test-client-id"; -const redirectUri = "https://example.com/callback"; -const scope = "openid profile email"; -const secret = "test-secret"; + +const application = await Application.insert({ + clientId: "test-client-id", + redirectUri: "https://example.com/callback", + scopes: "openid profile email", + secret: "test-secret", + name: "Test Application", +}); beforeAll(async () => { - await db.insert(Applications).values({ - clientId, - redirectUri, - scopes: scope, - name: "Test Application", - secret, - }); - await db.insert(Tokens).values({ code: "test-code", - redirectUri, - clientId, + redirectUri: application.data.redirectUri, + clientId: application.data.clientId, accessToken: "test-access-token", expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), createdAt: new Date().toISOString(), tokenType: "Bearer", - scope, + scope: application.data.scopes, userId: users[0].id, }); }); afterAll(async () => { await deleteUsers(); - await db.delete(Applications).where(eq(Applications.clientId, clientId)); - await db.delete(Tokens).where(eq(Tokens.clientId, clientId)); + await application.delete(); + await db + .delete(Tokens) + .where(eq(Tokens.clientId, application.data.clientId)); }); describe("/oauth/token", () => { @@ -48,9 +47,9 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "test-code", - redirect_uri: redirectUri, - client_id: clientId, - client_secret: secret, + redirect_uri: application.data.redirectUri, + client_id: application.data.clientId, + client_secret: application.data.secret, }), }); @@ -69,9 +68,9 @@ describe("/oauth/token", () => { }, body: JSON.stringify({ grant_type: "authorization_code", - redirect_uri: redirectUri, - client_id: clientId, - client_secret: secret, + redirect_uri: application.data.redirectUri, + client_id: application.data.clientId, + client_secret: application.data.secret, }), }); @@ -90,8 +89,8 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "test-code", - client_id: clientId, - client_secret: secret, + client_id: application.data.clientId, + client_secret: application.data.secret, }), }); @@ -110,8 +109,8 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "test-code", - redirect_uri: redirectUri, - client_secret: secret, + redirect_uri: application.data.redirectUri, + client_secret: application.data.secret, }), }); @@ -130,8 +129,8 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "test-code", - redirect_uri: redirectUri, - client_id: clientId, + redirect_uri: application.data.redirectUri, + client_id: application.data.clientId, client_secret: "invalid-secret", }), }); @@ -151,9 +150,9 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "invalid-code", - redirect_uri: redirectUri, - client_id: clientId, - client_secret: secret, + redirect_uri: application.data.redirectUri, + client_id: application.data.clientId, + client_secret: application.data.secret, }), }); @@ -172,9 +171,9 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "refresh_token", code: "test-code", - redirect_uri: redirectUri, - client_id: clientId, - client_secret: secret, + redirect_uri: application.data.redirectUri, + client_id: application.data.clientId, + client_secret: application.data.secret, }), }); diff --git a/plugins/openid/routes/oauth/token.ts b/plugins/openid/routes/oauth/token.ts index 486b26f9..5ffadffb 100644 --- a/plugins/openid/routes/oauth/token.ts +++ b/plugins/openid/routes/oauth/token.ts @@ -3,6 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { db } from "@versia/kit/db"; import { eq } from "@versia/kit/drizzle"; import { Tokens } from "@versia/kit/tables"; +import { Application } from "~/packages/database-interface/application.ts"; import type { PluginType } from "../../index.ts"; export const schemas = { @@ -140,12 +141,10 @@ export default (plugin: PluginType) => { } // Verify the client_secret - const client = await db.query.Applications.findFirst({ - where: (application, { eq }) => - eq(application.clientId, client_id), - }); + const client = + await Application.fromClientId(client_id); - if (!client || client.secret !== client_secret) { + if (!client || client.data.secret !== client_secret) { return context.json( { error: "invalid_client", diff --git a/plugins/openid/routes/sso/index.ts b/plugins/openid/routes/sso/index.ts index fd7e39cc..bee04b62 100644 --- a/plugins/openid/routes/sso/index.ts +++ b/plugins/openid/routes/sso/index.ts @@ -1,10 +1,7 @@ import { auth } from "@/api"; import { db } from "@versia/kit/db"; -import { - Applications, - OpenIdLoginFlows, - RolePermissions, -} from "@versia/kit/tables"; +import { Application } from "@versia/kit/db"; +import { OpenIdLoginFlows, RolePermissions } from "@versia/kit/tables"; import { calculatePKCECodeChallenge, generateRandomCodeVerifier, @@ -169,22 +166,17 @@ export default (plugin: PluginType) => { issuerId, ); - const application = ( - await db - .insert(Applications) - .values({ - clientId: - user.id + - Buffer.from( - crypto.getRandomValues(new Uint8Array(32)), - ).toString("base64"), - name: "Versia", - redirectUri, - scopes: "openid profile email", - secret: "", - }) - .returning() - )[0]; + const application = await Application.insert({ + clientId: + user.id + + Buffer.from( + crypto.getRandomValues(new Uint8Array(32)), + ).toString("base64"), + name: "Versia", + redirectUri, + scopes: "openid profile email", + secret: "", + }); // Store into database const newFlow = ( diff --git a/tests/oauth-scopes.test.ts b/tests/oauth-scopes.test.ts index 15488b88..03478fd1 100644 --- a/tests/oauth-scopes.test.ts +++ b/tests/oauth-scopes.test.ts @@ -1,135 +1,121 @@ import { describe, expect, it } from "bun:test"; import { checkIfOauthIsValid } from "@/oauth"; -import type { Application } from "~/classes/functions/application"; +import { + Application, + type ApplicationType, +} from "~/packages/database-interface/application"; describe("checkIfOauthIsValid", () => { it("should return true when routeScopes and application.scopes are empty", () => { - const application = { scopes: "" }; + const application = new Application({ scopes: "" } as ApplicationType); const routeScopes: string[] = []; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(true); }); it("should return true when routeScopes is empty and application.scopes contains write:* or write", () => { - const application = { scopes: "write:*" }; + const application = new Application({ + scopes: "write:*", + } as ApplicationType); const routeScopes: string[] = []; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(true); }); it("should return true when routeScopes is empty and application.scopes contains read:* or read", () => { - const application = { scopes: "read:*" }; + const application = new Application({ + scopes: "read:*", + } as ApplicationType); const routeScopes: string[] = []; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(true); }); it("should return true when routeScopes contains only write: permissions and application.scopes contains write:* or write", () => { - const application = { scopes: "write:*" }; + const application = new Application({ + scopes: "write:*", + } as ApplicationType); const routeScopes = ["write:users", "write:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(true); }); it("should return true when routeScopes contains only read: permissions and application.scopes contains read:* or read", () => { - const application = { scopes: "read:*" }; + const application = new Application({ + scopes: "read:*", + } as ApplicationType); const routeScopes = ["read:users", "read:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(true); }); it("should return true when routeScopes contains both write: and read: permissions and application.scopes contains write:* or write and read:* or read", () => { - const application = { scopes: "write:* read:*" }; + const application = new Application({ + scopes: "write:* read:*", + } as ApplicationType); const routeScopes = ["write:users", "read:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(true); }); it("should return false when routeScopes contains write: permissions but application.scopes does not contain write:* or write", () => { - const application = { scopes: "read:*" }; + const application = new Application({ + scopes: "read:*", + } as ApplicationType); const routeScopes = ["write:users", "write:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(false); }); it("should return false when routeScopes contains read: permissions but application.scopes does not contain read:* or read", () => { - const application = { scopes: "write:*" }; + const application = new Application({ + scopes: "write:*", + } as ApplicationType); const routeScopes = ["read:users", "read:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(false); }); it("should return false when routeScopes contains both write: and read: permissions but application.scopes does not contain write:* or write and read:* or read", () => { - const application = { scopes: "" }; + const application = new Application({ scopes: "" } as ApplicationType); const routeScopes = ["write:users", "read:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(false); }); it("should return true when routeScopes contains a mix of valid and invalid permissions and application.scopes contains all the required permissions", () => { - const application = { scopes: "write:* read:*" }; + const application = new Application({ + scopes: "write:* read:*", + } as ApplicationType); const routeScopes = ["write:users", "invalid:permission", "read:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(true); }); it("should return false when routeScopes contains a mix of valid and invalid permissions but application.scopes does not contain all the required permissions", () => { - const application = { scopes: "write:*" }; + const application = new Application({ + scopes: "write:*", + } as ApplicationType); const routeScopes = ["write:users", "invalid:permission", "read:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(false); }); it("should return true when routeScopes contains a mix of valid write and read permissions and application.scopes contains all the required permissions", () => { - const application = { scopes: "write:* read:posts" }; + const application = new Application({ + scopes: "write:* read:posts", + } as ApplicationType); const routeScopes = ["write:users", "read:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(true); }); it("should return false when routeScopes contains a mix of valid write and read permissions but application.scopes does not contain all the required permissions", () => { - const application = { scopes: "write:*" }; + const application = new Application({ + scopes: "write:*", + } as ApplicationType); const routeScopes = ["write:users", "read:posts"]; - const result = checkIfOauthIsValid( - application as Application, - routeScopes, - ); + const result = checkIfOauthIsValid(application, routeScopes); expect(result).toBe(false); }); }); diff --git a/types/api.ts b/types/api.ts index df950741..907e78b3 100644 --- a/types/api.ts +++ b/types/api.ts @@ -13,9 +13,9 @@ import type { } from "@versia/federation/types"; import type { SocketAddress } from "bun"; import { z } from "zod"; -import type { Application } from "~/classes/functions/application"; import type { RolePermissions } from "~/drizzle/schema"; import type { Config } from "~/packages/config-manager"; +import type { Application } from "~/packages/database-interface/application"; import type { User as DatabaseUser } from "~/packages/database-interface/user"; export type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"; diff --git a/utils/api.ts b/utils/api.ts index 7489c833..4960c690 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -22,11 +22,11 @@ import { import { parse } from "qs"; import type { z } from "zod"; import { fromZodError } from "zod-validation-error"; -import type { Application } from "~/classes/functions/application"; import { type AuthData, getFromHeader } from "~/classes/functions/user"; import { db } from "~/drizzle/db"; import { Challenges } from "~/drizzle/schema"; import { config } from "~/packages/config-manager/index.ts"; +import { Application } from "~/packages/database-interface/application"; import type { User } from "~/packages/database-interface/user"; import type { ApiRouteMetadata, HonoEnv, HttpVerb } from "~/types/api"; @@ -185,7 +185,9 @@ const checkRouteNeedsAuth = ( return { user: auth.user as User, token: auth.token as string, - application: auth.application as Application | null, + application: auth.application + ? new Application(auth.application) + : null, }; } if ( diff --git a/utils/oauth.ts b/utils/oauth.ts index 21c44bb1..95882d93 100644 --- a/utils/oauth.ts +++ b/utils/oauth.ts @@ -1,4 +1,4 @@ -import type { Application } from "~/classes/functions/application"; +import type { Application } from "~/packages/database-interface/application"; /** * Check if an OAuth application is valid for a route @@ -15,12 +15,12 @@ export const checkIfOauthIsValid = ( } const hasAllWriteScopes = - application.scopes.split(" ").includes("write:*") || - application.scopes.split(" ").includes("write"); + application.data.scopes.split(" ").includes("write:*") || + application.data.scopes.split(" ").includes("write"); const hasAllReadScopes = - application.scopes.split(" ").includes("read:*") || - application.scopes.split(" ").includes("read"); + application.data.scopes.split(" ").includes("read:*") || + application.data.scopes.split(" ").includes("read"); if (hasAllWriteScopes && hasAllReadScopes) { return true; @@ -50,6 +50,6 @@ export const checkIfOauthIsValid = ( // If there are scopes left, check if they match return nonMatchedScopes.every((scope) => - application.scopes.split(" ").includes(scope), + application.data.scopes.split(" ").includes(scope), ); };