From 4eae4cd0627434ebdd2c29ad50244d0047f6a9da Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 9 Aug 2025 17:15:05 +0200 Subject: [PATCH 01/11] feat: :lock: Harden Systemd unit config --- nix/module.nix | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/nix/module.nix b/nix/module.nix index dee7d70d..85cf910b 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -123,6 +123,28 @@ in { StandardError = "journal"; SyslogIdentifier = "${name}"; + # Hardening + CapabilityBoundingSet = [""]; + LockPersonality = true; + PrivateMounts = true; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + RemoveIPC = true; + NoNewPrivileges = true; + Environment = [ "CONFIG_LOCATION=${configFile}" ]; From 1bfc5fb013f27254f825ade738e64515fa51af2f Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 21 Aug 2025 00:45:58 +0200 Subject: [PATCH 02/11] refactor(api): :recycle: Rewrite full authentication code to go OpenID-only --- bun.lock | 14 +- cli/user/token.ts | 19 +- drizzle.config.ts | 14 +- package.json | 5 +- packages/api/package.json | 5 +- packages/api/plugins/openid/errors.ts | 45 - packages/api/plugins/openid/utils.ts | 217 -- .../api/routes/api/auth/login/index.test.ts | 26 +- packages/api/routes/api/auth/login/index.ts | 23 +- .../api/routes/api/auth/redirect/index.ts | 75 - .../api/routes/api/auth/reset/index.test.ts | 124 - packages/api/routes/api/auth/reset/index.ts | 80 - .../api/routes/api/oauth/authorize.test.ts | 394 --- packages/api/routes/api/oauth/authorize.ts | 277 -- packages/api/routes/api/oauth/revoke.test.ts | 21 +- packages/api/routes/api/oauth/revoke.ts | 2 +- packages/api/routes/api/oauth/sso.ts | 130 - .../routes/api/oauth/sso/[issuer]/callback.ts | 251 +- .../routes/api/oauth/sso/[issuer]/index.ts | 122 + packages/api/routes/api/oauth/token.test.ts | 35 +- packages/api/routes/api/oauth/token.ts | 216 +- packages/api/routes/api/v1/apps/index.ts | 12 +- packages/api/routes/api/v1/sso/index.ts | 66 +- packages/api/routes/well-known/jwks.test.ts | 35 - packages/api/routes/well-known/jwks.ts | 62 - .../well-known/openid-configuration/index.ts | 65 - packages/config/index.ts | 2 +- packages/kit/api.ts | 4 +- packages/kit/db/application.ts | 54 +- packages/kit/db/index.ts | 2 +- packages/kit/db/token.ts | 8 +- packages/kit/db/user.ts | 9 +- .../tables/migrations/0051_stiff_morbius.sql | 46 + .../tables/migrations/meta/0051_snapshot.json | 2439 +++++++++++++++++ .../kit/tables/migrations/meta/_journal.json | 7 + packages/kit/tables/schema.ts | 108 +- packages/tests/index.ts | 37 +- utils/bull-board.ts | 31 +- utils/lib.ts | 3 + 39 files changed, 3076 insertions(+), 2009 deletions(-) delete mode 100644 packages/api/plugins/openid/errors.ts delete mode 100644 packages/api/plugins/openid/utils.ts delete mode 100644 packages/api/routes/api/auth/redirect/index.ts delete mode 100644 packages/api/routes/api/auth/reset/index.test.ts delete mode 100644 packages/api/routes/api/auth/reset/index.ts delete mode 100644 packages/api/routes/api/oauth/authorize.test.ts delete mode 100644 packages/api/routes/api/oauth/authorize.ts delete mode 100644 packages/api/routes/api/oauth/sso.ts create mode 100644 packages/api/routes/api/oauth/sso/[issuer]/index.ts delete mode 100644 packages/api/routes/well-known/jwks.test.ts delete mode 100644 packages/api/routes/well-known/jwks.ts delete mode 100644 packages/api/routes/well-known/openid-configuration/index.ts create mode 100644 packages/kit/tables/migrations/0051_stiff_morbius.sql create mode 100644 packages/kit/tables/migrations/meta/0051_snapshot.json diff --git a/bun.lock b/bun.lock index f4c396b7..0ecacda7 100644 --- a/bun.lock +++ b/bun.lock @@ -39,7 +39,6 @@ "ioredis": "catalog:", "ip-matching": "catalog:", "iso-639-1": "catalog:", - "jose": "catalog:", "linkify-html": "catalog:", "linkify-string": "catalog:", "linkifyjs": "catalog:", @@ -51,7 +50,6 @@ "markdown-it-toc-done-right": "catalog:", "mime-types": "catalog:", "mitata": "catalog:", - "oauth4webapi": "catalog:", "ora": "catalog:", "qs": "catalog:", "sharp": "catalog:", @@ -108,8 +106,7 @@ "hono-rate-limiter": "catalog:", "ip-matching": "catalog:", "iso-639-1": "catalog:", - "jose": "catalog:", - "oauth4webapi": "catalog:", + "openid-client": "catalog:", "qs": "catalog:", "sharp": "catalog:", "string-comparison": "catalog:", @@ -270,7 +267,6 @@ "ioredis": "^5.6.1", "ip-matching": "^2.1.2", "iso-639-1": "^3.1.5", - "jose": "^6.0.11", "linkify-html": "^4.3.1", "linkify-string": "^4.3.1", "linkifyjs": "^4.3.1", @@ -284,7 +280,7 @@ "mime-types": "^3.0.1", "mitata": "^1.0.34", "mitt": "^3.0.1", - "oauth4webapi": "^3.5.5", + "openid-client": "^6.6.3", "ora": "^8.2.0", "qs": "^6.14.0", "sharp": "^0.34.2", @@ -1133,7 +1129,7 @@ "jake": ["jake@10.9.2", "", { "dependencies": { "async": "^3.2.3", "chalk": "^4.0.2", "filelist": "^1.0.4", "minimatch": "^3.1.2" }, "bin": { "jake": "bin/cli.js" } }, "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA=="], - "jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="], + "jose": ["jose@6.0.12", "", {}, "sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -1277,7 +1273,7 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "oauth4webapi": ["oauth4webapi@3.5.5", "", {}, "sha512-1K88D2GiAydGblHo39NBro5TebGXa+7tYoyIbxvqv3+haDDry7CBE1eSYuNbOSsYCCU6y0gdynVZAkm4YPw4hg=="], + "oauth4webapi": ["oauth4webapi@3.6.2", "", {}, "sha512-hwWLiyBYuqhVdcIUJMJVKdEvz+DCweOcbSfqDyIv9PuUwrNfqrzfHP2bypZgZdbYOS67QYqnAnvZa2BJwBBrHw=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -1287,6 +1283,8 @@ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "openid-client": ["openid-client@6.6.3", "", { "dependencies": { "jose": "^6.0.12", "oauth4webapi": "^3.6.1" } }, "sha512-sYYFJsyN21bjf/QepIU/t6w22tEUT+rYVPf1VZOSQwC+s1hAkyZpvAbFNLMrnrYMS/H74MctEHna2jPLvWbkCA=="], + "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], diff --git a/cli/user/token.ts b/cli/user/token.ts index 6c88327c..d65f364c 100644 --- a/cli/user/token.ts +++ b/cli/user/token.ts @@ -1,4 +1,4 @@ -import { Token } from "@versia-server/kit/db"; +import { Application, Token } from "@versia-server/kit/db"; import { randomUUIDv7 } from "bun"; import chalk from "chalk"; // @ts-expect-error - Root import is required or the Clec type definitions won't work @@ -22,13 +22,24 @@ export const generateTokenCommand = defineCommand( throw new Error(`User ${chalk.gray(username)} not found.`); } + const application = await Application.insert({ + id: + user.id + + Buffer.from( + crypto.getRandomValues(new Uint8Array(32)), + ).toString("base64"), + name: "Versia", + redirectUris: [], + scopes: ["openid", "profile", "email"], + secret: "", + }); + const token = await Token.insert({ id: randomUUIDv7(), accessToken: randomString(64, "base64url"), - code: null, - scope: "read write follow", - tokenType: "Bearer", + scopes: ["read", "write", "follow"], userId: user.id, + clientId: application.id, }); console.info( diff --git a/drizzle.config.ts b/drizzle.config.ts index 49d2ec86..faa519fe 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,4 +1,4 @@ -import { config } from "@versia-server/config"; +//import { config } from "@versia-server/config"; import type { Config } from "drizzle-kit"; /** @@ -7,19 +7,19 @@ import type { Config } from "drizzle-kit"; */ export default { dialect: "postgresql", - out: "./drizzle/migrations", - schema: "./drizzle/schema.ts", + out: "./packages/kit/tables/migrations", + schema: "./packages/kit/tables/schema.ts", dbCredentials: { - /* host: "localhost", + host: "localhost", port: 40000, user: "lysand", password: "lysand", - database: "lysand", */ - host: config.postgres.host, + database: "lysand", + /* host: config.postgres.host, port: config.postgres.port, user: config.postgres.username, password: config.postgres.password, - database: config.postgres.database, + database: config.postgres.database, */ }, // Print all statements verbose: true, diff --git a/package.json b/package.json index 306ffc4d..46495fad 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@logtape/otel": "^1.0.0", "@scalar/hono-api-reference": "^0.9.7", "@sentry/bun": "^9.35.0", + "openid-client": "^6.6.3", "altcha-lib": "^1.3.0", "blurhash": "^2.0.5", "bullmq": "^5.56.1", @@ -73,7 +74,6 @@ "ioredis": "^5.6.1", "ip-matching": "^2.1.2", "iso-639-1": "^3.1.5", - "jose": "^6.0.11", "linkify-html": "^4.3.1", "linkify-string": "^4.3.1", "linkifyjs": "^4.3.1", @@ -85,7 +85,6 @@ "markdown-it-toc-done-right": "^4.2.0", "mime-types": "^3.0.1", "mitata": "^1.0.34", - "oauth4webapi": "^3.5.5", "ora": "^8.2.0", "qs": "^6.14.0", "sharp": "^0.34.2", @@ -191,7 +190,6 @@ "ioredis": "catalog:", "ip-matching": "catalog:", "iso-639-1": "catalog:", - "jose": "catalog:", "linkify-html": "catalog:", "linkify-string": "catalog:", "linkifyjs": "catalog:", @@ -203,7 +201,6 @@ "markdown-it-toc-done-right": "catalog:", "mime-types": "catalog:", "mitata": "catalog:", - "oauth4webapi": "catalog:", "ora": "catalog:", "qs": "catalog:", "sharp": "catalog:", diff --git a/packages/api/package.json b/packages/api/package.json index 2e50ca7b..4053ea1a 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -55,6 +55,7 @@ "@versia-server/logging": "workspace:*", "@versia/client": "workspace:*", "@versia/sdk": "workspace:*", + "openid-client": "catalog:", "youch": "catalog:", "hono": "catalog:", "hono-openapi": "catalog:", @@ -66,7 +67,6 @@ "unicode-emoji-json": "catalog:", "sharp": "catalog:", "iso-639-1": "catalog:", - "jose": "catalog:", "zod-openapi": "catalog:", "@scalar/hono-api-reference": "catalog:", "hono-rate-limiter": "catalog:", @@ -75,7 +75,6 @@ "altcha-lib": "catalog:", "@hono/standard-validator": "catalog:", "zod-validation-error": "catalog:", - "confbox": "catalog:", - "oauth4webapi": "catalog:" + "confbox": "catalog:" } } diff --git a/packages/api/plugins/openid/errors.ts b/packages/api/plugins/openid/errors.ts deleted file mode 100644 index 72202032..00000000 --- a/packages/api/plugins/openid/errors.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Context, TypedResponse } from "hono"; - -export const errors = { - InvalidJWT: ["invalid_request", "Invalid JWT: could not verify"], - MissingJWTFields: [ - "invalid_request", - "Invalid JWT: missing required fields (aud, sub, exp, iss)", - ], - InvalidSub: ["invalid_request", "Invalid JWT: sub is not a valid user ID"], - UserNotFound: [ - "invalid_request", - "Invalid JWT, could not find associated user", - ], - MissingOauthPermission: [ - "unauthorized", - "User missing required 'oauth' permission", - ], - MissingApplication: [ - "invalid_request", - "Invalid client_id: no associated API application found", - ], - InvalidRedirectUri: [ - "invalid_request", - "Invalid redirect_uri: does not match API application's redirect_uri", - ], - InvalidScope: [ - "invalid_request", - "Invalid scope: not a subset of the application's scopes", - ], -}; - -export const errorRedirect = ( - context: Context, - error: (typeof errors)[keyof typeof errors], - extraParams?: URLSearchParams, -): Response & TypedResponse => { - const errorSearchParams = new URLSearchParams(extraParams); - - errorSearchParams.append("error", error[0]); - errorSearchParams.append("error_description", error[1]); - - return context.redirect( - `${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`, - ); -}; diff --git a/packages/api/plugins/openid/utils.ts b/packages/api/plugins/openid/utils.ts deleted file mode 100644 index 4f7afdc9..00000000 --- a/packages/api/plugins/openid/utils.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { type Application, db } from "@versia-server/kit/db"; -import type { OpenIdLoginFlows } from "@versia-server/kit/tables"; -import { eq, type InferSelectModel, type SQL } from "drizzle-orm"; -import { - type AuthorizationResponseError, - type AuthorizationServer, - authorizationCodeGrantRequest, - ClientSecretPost, - discoveryRequest, - expectNoState, - getValidatedIdTokenClaims, - processAuthorizationCodeResponse, - processDiscoveryResponse, - processUserInfoResponse, - type ResponseBodyError, - type TokenEndpointResponse, - type UserInfoResponse, - userInfoRequest, - validateAuthResponse, -} from "oauth4webapi"; - -export const oauthDiscoveryRequest = ( - issuerUrl: URL, -): Promise => { - return discoveryRequest(issuerUrl, { - algorithm: "oidc", - }).then((res) => processDiscoveryResponse(issuerUrl, res)); -}; - -export const oauthRedirectUri = (baseUrl: URL, issuer: string): URL => - new URL(`/oauth/sso/${issuer}/callback`, baseUrl); - -const getFlow = ( - flowId: string, -): Promise< - | (InferSelectModel & { - application?: typeof Application.$type | null; - }) - | undefined -> => { - return db.query.OpenIdLoginFlows.findFirst({ - where: (flow): SQL | undefined => eq(flow.id, flowId), - with: { - application: true, - }, - }); -}; - -const getAuthServer = (issuerUrl: URL): Promise => { - return discoveryRequest(issuerUrl, { - algorithm: "oidc", - }).then((res) => processDiscoveryResponse(issuerUrl, res)); -}; - -const getParameters = ( - authServer: AuthorizationServer, - clientId: string, - currentUrl: URL, -): URLSearchParams => { - return validateAuthResponse( - authServer, - { - client_id: clientId, - }, - currentUrl, - expectNoState, - ); -}; - -const getOIDCResponse = ( - authServer: AuthorizationServer, - clientId: string, - clientSecret: string, - redirectUri: URL, - codeVerifier: string, - parameters: URLSearchParams, -): Promise => { - return authorizationCodeGrantRequest( - authServer, - { - client_id: clientId, - }, - ClientSecretPost(clientSecret), - parameters, - redirectUri.toString(), - codeVerifier, - ); -}; - -const processOIDCResponse = ( - authServer: AuthorizationServer, - clientId: string, - oidcResponse: Response, -): Promise => { - return processAuthorizationCodeResponse( - authServer, - { - client_id: clientId, - }, - oidcResponse, - ); -}; - -const getUserInfo = ( - authServer: AuthorizationServer, - clientId: string, - accessToken: string, - sub: string, -): Promise => { - return userInfoRequest( - authServer, - { - client_id: clientId, - }, - accessToken, - ).then( - async (res) => - await processUserInfoResponse( - authServer, - { - client_id: clientId, - }, - sub, - res, - ), - ); -}; - -export const automaticOidcFlow = async ( - issuer: { - url: string; - client_id: string; - client_secret: string; - }, - flowId: string, - currentUrl: URL, - redirectUrl: URL, - errorFn: ( - error: string, - message: string, - flow: - | (InferSelectModel & { - application?: typeof Application.$type | null; - }) - | null, - ) => Response, -): Promise< - | Response - | { - userInfo: UserInfoResponse; - flow: InferSelectModel & { - application?: typeof Application.$type | null; - }; - claims: Record; - } -> => { - const flow = await getFlow(flowId); - - if (!flow) { - return errorFn("invalid_request", "Invalid flow", null); - } - - try { - const issuerUrl = new URL(issuer.url); - - const authServer = await getAuthServer(issuerUrl); - - const parameters = getParameters( - authServer, - issuer.client_id, - currentUrl, - ); - - const oidcResponse = await getOIDCResponse( - authServer, - issuer.client_id, - issuer.client_secret, - redirectUrl, - flow.codeVerifier, - parameters, - ); - - const result = await processOIDCResponse( - authServer, - issuer.client_id, - oidcResponse, - ); - - const { access_token } = result; - - const claims = getValidatedIdTokenClaims(result); - - if (!claims) { - return errorFn("invalid_request", "Invalid claims", flow); - } - - const { sub } = claims; - - // Validate `sub` - // Later, we'll use this to automatically set the user's data - const userInfo = await getUserInfo( - authServer, - issuer.client_id, - access_token, - sub, - ); - - return { - userInfo, - flow, - claims, - }; - } catch (e) { - const error = e as ResponseBodyError | AuthorizationResponseError; - return errorFn(error.error, error.error_description || "", flow); - } -}; diff --git a/packages/api/routes/api/auth/login/index.test.ts b/packages/api/routes/api/auth/login/index.test.ts index 1fe79f5d..5dc4c705 100644 --- a/packages/api/routes/api/auth/login/index.test.ts +++ b/packages/api/routes/api/auth/login/index.test.ts @@ -2,19 +2,17 @@ import { afterAll, describe, expect, test } from "bun:test"; import { config } from "@versia-server/config"; import { Application } from "@versia-server/kit/db"; import { fakeRequest, getTestUsers } from "@versia-server/tests"; -import { randomUUIDv7 } from "bun"; import { randomString } from "@/math"; const { users, deleteUsers, passwords } = await getTestUsers(1); // Create application const application = await Application.insert({ - id: randomUUIDv7(), + id: randomString(32, "hex"), name: "Test Application", - clientId: randomString(32, "hex"), secret: "test", - redirectUri: "https://example.com", - scopes: "read write", + redirectUris: ["https://example.com"], + scopes: ["read", "write"], }); afterAll(async () => { @@ -31,7 +29,7 @@ describe("/api/auth/login", () => { formData.append("password", passwords[0]); const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, @@ -47,7 +45,7 @@ describe("/api/auth/login", () => { expect(locationHeader.pathname).toBe("/oauth/consent"); expect(locationHeader.searchParams.get("client_id")).toBe( - application.data.clientId, + application.data.id, ); expect(locationHeader.searchParams.get("redirect_uri")).toBe( "https://example.com", @@ -65,7 +63,7 @@ describe("/api/auth/login", () => { formData.append("password", passwords[0]); const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, @@ -81,7 +79,7 @@ describe("/api/auth/login", () => { expect(locationHeader.pathname).toBe("/oauth/consent"); expect(locationHeader.searchParams.get("client_id")).toBe( - application.data.clientId, + application.data.id, ); expect(locationHeader.searchParams.get("redirect_uri")).toBe( "https://example.com", @@ -99,7 +97,7 @@ describe("/api/auth/login", () => { formData.append("password", passwords[0]); const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write&state=abc`, + `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write&state=abc`, { method: "POST", body: formData, @@ -115,7 +113,7 @@ describe("/api/auth/login", () => { expect(locationHeader.pathname).toBe("/oauth/consent"); expect(locationHeader.searchParams.get("client_id")).toBe( - application.data.clientId, + application.data.id, ); expect(locationHeader.searchParams.get("redirect_uri")).toBe( "https://example.com", @@ -136,7 +134,7 @@ describe("/api/auth/login", () => { formData.append("password", "password"); const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", @@ -169,7 +167,7 @@ describe("/api/auth/login", () => { formData.append("password", "password"); const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, @@ -201,7 +199,7 @@ describe("/api/auth/login", () => { formData.append("password", "password"); const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, + `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, { method: "POST", body: formData, diff --git a/packages/api/routes/api/auth/login/index.ts b/packages/api/routes/api/auth/login/index.ts index 1a3c9d19..8db55a49 100644 --- a/packages/api/routes/api/auth/login/index.ts +++ b/packages/api/routes/api/auth/login/index.ts @@ -7,8 +7,8 @@ import { password as bunPassword } from "bun"; import { eq, or } from "drizzle-orm"; import type { Context } from "hono"; import { setCookie } from "hono/cookie"; +import { sign } from "hono/jwt"; import { describeRoute, validator } from "hono-openapi"; -import { SignJWT } from "jose"; import { z } from "zod/v4"; const returnError = ( @@ -144,16 +144,17 @@ export default apiRoute((app) => } // Generate JWT - const jwt = await new SignJWT({ - sub: user.id, - iss: config.http.base_url.origin, - aud: client_id, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); + const jwt = await sign( + { + sub: user.id, + iss: config.http.base_url.origin, + aud: client_id, + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + nbf: Math.floor(Date.now() / 1000), + }, + config.authentication.keys.private, + ); const application = await Application.fromClientId(client_id); diff --git a/packages/api/routes/api/auth/redirect/index.ts b/packages/api/routes/api/auth/redirect/index.ts deleted file mode 100644 index 5e43431d..00000000 --- a/packages/api/routes/api/auth/redirect/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { config } from "@versia-server/config"; -import { apiRoute, handleZodError } from "@versia-server/kit/api"; -import { db } from "@versia-server/kit/db"; -import { Applications, Tokens } from "@versia-server/kit/tables"; -import { and, eq } from "drizzle-orm"; -import { describeRoute, validator } from "hono-openapi"; -import { z } from "zod/v4"; - -/** - * OAuth Code flow - */ -export default apiRoute((app) => - app.get( - "/api/auth/redirect", - describeRoute({ - summary: "OAuth Code flow", - description: - "Redirects to the application, or back to login if the code is invalid", - tags: ["OpenID"], - responses: { - 302: { - description: - "Redirects to the application, or back to login if the code is invalid", - }, - }, - }), - validator( - "query", - z.object({ - redirect_uri: z.url(), - client_id: z.string(), - code: z.string(), - }), - handleZodError, - ), - async (context) => { - const { redirect_uri, client_id, code } = - context.req.valid("query"); - - const redirectToLogin = (error: string): Response => - context.redirect( - `${config.frontend.routes.login}?${new URLSearchParams({ - ...context.req.query, - error: encodeURIComponent(error), - }).toString()}`, - ); - - const foundToken = await db - .select() - .from(Tokens) - .leftJoin( - Applications, - eq(Tokens.applicationId, Applications.id), - ) - .where( - and( - eq(Tokens.code, code), - eq(Applications.clientId, client_id), - ), - ) - .limit(1); - - if (!foundToken || foundToken.length <= 0) { - return redirectToLogin("Invalid code"); - } - - // Redirect back to application - return context.redirect( - `${redirect_uri}?${new URLSearchParams({ - code, - }).toString()}`, - ); - }, - ), -); diff --git a/packages/api/routes/api/auth/reset/index.test.ts b/packages/api/routes/api/auth/reset/index.test.ts deleted file mode 100644 index 2bb3ed5d..00000000 --- a/packages/api/routes/api/auth/reset/index.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { afterAll, describe, expect, test } from "bun:test"; -import { config } from "@versia-server/config"; -import { Application } from "@versia-server/kit/db"; -import { fakeRequest, getTestUsers } from "@versia-server/tests"; -import { randomUUIDv7 } from "bun"; -import { randomString } from "@/math"; - -const { users, deleteUsers, passwords } = await getTestUsers(1); -const token = randomString(32, "hex"); -const newPassword = randomString(16, "hex"); - -// Create application -const application = await Application.insert({ - id: randomUUIDv7(), - name: "Test Application", - clientId: randomString(32, "hex"), - secret: "test", - redirectUri: "https://example.com", - scopes: "read write", -}); - -afterAll(async () => { - await deleteUsers(); - await application.delete(); -}); - -// /api/auth/reset -describe("/api/auth/reset", () => { - test("should login with normal password", async () => { - const formData = new FormData(); - - formData.append("identifier", users[0]?.data.username ?? ""); - formData.append("password", passwords[0]); - - const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - }); - - test("should reset password and refuse login with old password", async () => { - await users[0]?.update({ - passwordResetToken: token, - }); - - const formData = new FormData(); - - formData.append("identifier", users[0]?.data.username ?? ""); - formData.append("password", passwords[0]); - - const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - - expect(locationHeader.pathname).toBe("/oauth/reset"); - expect(locationHeader.searchParams.get("token")).toBe(token); - }); - - test("should reset password and login with new password", async () => { - const formData = new FormData(); - - formData.append("token", token); - formData.append("password", newPassword); - formData.append("password2", newPassword); - - const response = await fakeRequest("/api/auth/reset", { - method: "POST", - body: formData, - }); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - - const loginFormData = new FormData(); - - loginFormData.append("identifier", users[0]?.data.username ?? ""); - loginFormData.append("password", newPassword); - - const loginResponse = await fakeRequest( - `/api/auth/login?client_id=${application.data.clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - { - method: "POST", - body: loginFormData, - }, - ); - - expect(loginResponse.status).toBe(302); - expect(loginResponse.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - loginResponse.headers.get("Location") ?? "", - config.http.base_url, - ); - - expect(locationHeader.pathname).toBe("/oauth/consent"); - expect(locationHeader.searchParams.get("client_id")).toBe( - application.data.clientId, - ); - expect(locationHeader.searchParams.get("redirect_uri")).toBe( - "https://example.com", - ); - expect(locationHeader.searchParams.get("response_type")).toBe("code"); - expect(locationHeader.searchParams.get("scope")).toBe("read write"); - - expect(loginResponse.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/); - }); -}); diff --git a/packages/api/routes/api/auth/reset/index.ts b/packages/api/routes/api/auth/reset/index.ts deleted file mode 100644 index f45a12ab..00000000 --- a/packages/api/routes/api/auth/reset/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { config } from "@versia-server/config"; -import { apiRoute, handleZodError } from "@versia-server/kit/api"; -import { User } from "@versia-server/kit/db"; -import { Users } from "@versia-server/kit/tables"; -import { password as bunPassword } from "bun"; -import { eq } from "drizzle-orm"; -import type { Context } from "hono"; -import { describeRoute, validator } from "hono-openapi"; -import { z } from "zod/v4"; - -const returnError = ( - context: Context, - token: string, - error: string, - description: string, -): Response => { - const searchParams = new URLSearchParams(); - - searchParams.append("error", error); - searchParams.append("error_description", description); - searchParams.append("token", token); - - return context.redirect( - new URL( - `${ - config.frontend.routes.password_reset - }?${searchParams.toString()}`, - config.http.base_url, - ).toString(), - ); -}; - -export default apiRoute((app) => - app.post( - "/api/auth/reset", - describeRoute({ - summary: "Reset password", - description: "Reset password", - responses: { - 302: { - description: - "Redirect to the password reset page with a message", - }, - }, - }), - validator( - "form", - z.object({ - token: z.string().min(1), - password: z.string().min(3).max(100), - }), - handleZodError, - ), - async (context) => { - const { token, password } = context.req.valid("form"); - - const user = await User.fromSql( - eq(Users.passwordResetToken, token), - ); - - if (!user) { - return returnError( - context, - token, - "invalid_token", - "Invalid token", - ); - } - - await user.update({ - password: await bunPassword.hash(password), - passwordResetToken: null, - }); - - return context.redirect( - `${config.frontend.routes.password_reset}?success=true`, - ); - }, - ), -); diff --git a/packages/api/routes/api/oauth/authorize.test.ts b/packages/api/routes/api/oauth/authorize.test.ts deleted file mode 100644 index 3c002d12..00000000 --- a/packages/api/routes/api/oauth/authorize.test.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { afterAll, describe, expect, test } from "bun:test"; -import { RolePermission } from "@versia/client/schemas"; -import { config } from "@versia-server/config"; -import { Application } from "@versia-server/kit/db"; -import { fakeRequest, getTestUsers } from "@versia-server/tests"; -import { randomUUIDv7 } from "bun"; -import { SignJWT } from "jose"; -import { randomString } from "@/math"; - -const { deleteUsers, tokens, users } = await getTestUsers(1); - -const application = await Application.insert({ - id: randomUUIDv7(), - clientId: "test-client-id", - redirectUri: "https://example.com/callback", - scopes: "openid profile email", - name: "Test Application", - secret: "test-secret", -}); - -afterAll(async () => { - await deleteUsers(); - await application.delete(); -}); - -describe("/oauth/authorize", () => { - test("should authorize and redirect with valid inputs", async () => { - const jwt = await new SignJWT({ - sub: users[0].id, - iss: config.http.base_url.origin, - aud: application.data.clientId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: `jwt=${jwt}`, - }, - body: JSON.stringify({ - client_id: application.data.clientId, - redirect_uri: application.data.redirectUri, - response_type: "code", - scope: application.data.scopes, - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response.status).toBe(302); - const location = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - const params = new URLSearchParams(location.search); - expect(location.origin + location.pathname).toBe( - application.data.redirectUri, - ); - expect(params.get("code")).toBeTruthy(); - expect(params.get("state")).toBe("test-state"); - }); - - test("should return error for invalid JWT", async () => { - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: "jwt=invalid-jwt", - }, - body: JSON.stringify({ - client_id: application.data.clientId, - redirect_uri: application.data.redirectUri, - response_type: "code", - scope: application.data.scopes, - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response.status).toBe(302); - const location = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - const params = new URLSearchParams(location.search); - expect(params.get("error")).toBe("invalid_request"); - expect(params.get("error_description")).toBe( - "Invalid JWT: could not verify", - ); - }); - - test("should return error for missing required fields in JWT", async () => { - const jwt = await new SignJWT({ - sub: users[0].id, - iss: config.http.base_url.origin, - aud: application.data.clientId, - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: `jwt=${jwt}`, - }, - body: JSON.stringify({ - client_id: application.data.clientId, - redirect_uri: application.data.redirectUri, - response_type: "code", - scope: application.data.scopes, - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response.status).toBe(302); - const location = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - const params = new URLSearchParams(location.search); - expect(params.get("error")).toBe("invalid_request"); - expect(params.get("error_description")).toBe( - "Invalid JWT: missing required fields (aud, sub, exp, iss)", - ); - }); - - test("should return error for user not found", async () => { - const jwt = await new SignJWT({ - sub: "non-existent-user", - aud: application.data.clientId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iss: config.http.base_url.origin, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: `jwt=${jwt}`, - }, - body: JSON.stringify({ - client_id: application.data.clientId, - redirect_uri: application.data.redirectUri, - response_type: "code", - scope: application.data.scopes, - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response.status).toBe(302); - const location = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - const params = new URLSearchParams(location.search); - expect(params.get("error")).toBe("invalid_request"); - expect(params.get("error_description")).toBe( - "Invalid JWT: sub is not a valid user ID", - ); - - const jwt2 = await new SignJWT({ - sub: "23e42862-d5df-49a8-95b5-52d8c6a11aea", - aud: application.data.clientId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iss: config.http.base_url.origin, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - const response2 = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: `jwt=${jwt2}`, - }, - body: JSON.stringify({ - client_id: application.data.clientId, - redirect_uri: application.data.redirectUri, - response_type: "code", - scope: application.data.scopes, - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response2.status).toBe(302); - const location2 = new URL( - response2.headers.get("Location") ?? "", - config.http.base_url, - ); - const params2 = new URLSearchParams(location2.search); - expect(params2.get("error")).toBe("invalid_request"); - expect(params2.get("error_description")).toBe( - "Invalid JWT, could not find associated user", - ); - }); - - test("should return error for user missing required permissions", async () => { - const oldPermissions = config.permissions.default; - config.permissions.default = []; - - const jwt = await new SignJWT({ - sub: users[0].id, - iss: config.http.base_url.origin, - aud: application.data.clientId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: `jwt=${jwt}`, - }, - body: JSON.stringify({ - client_id: application.data.clientId, - redirect_uri: application.data.redirectUri, - response_type: "code", - scope: application.data.scopes, - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response.status).toBe(302); - const location = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - const params = new URLSearchParams(location.search); - expect(params.get("error")).toBe("unauthorized"); - expect(params.get("error_description")).toBe( - `User missing required '${RolePermission.OAuth}' permission`, - ); - - config.permissions.default = oldPermissions; - }); - - test("should return error for invalid client_id", async () => { - const jwt = await new SignJWT({ - sub: users[0].id, - aud: "invalid-client-id", - iss: config.http.base_url.origin, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: `jwt=${jwt}`, - }, - body: JSON.stringify({ - client_id: "invalid-client-id", - redirect_uri: application.data.redirectUri, - response_type: "code", - scope: application.data.scopes, - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response.status).toBe(302); - const location = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - const params = new URLSearchParams(location.search); - expect(params.get("error")).toBe("invalid_request"); - expect(params.get("error_description")).toBe( - "Invalid client_id: no associated API application found", - ); - }); - - test("should return error for invalid redirect_uri", async () => { - const jwt = await new SignJWT({ - sub: users[0].id, - iss: config.http.base_url.origin, - aud: application.data.clientId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: `jwt=${jwt}`, - }, - body: JSON.stringify({ - client_id: application.data.clientId, - redirect_uri: "https://invalid.com/callback", - response_type: "code", - scope: application.data.scopes, - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response.status).toBe(302); - const location = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - const params = new URLSearchParams(location.search); - expect(params.get("error")).toBe("invalid_request"); - expect(params.get("error_description")).toBe( - "Invalid redirect_uri: does not match API application's redirect_uri", - ); - }); - - test("should return error for invalid scope", async () => { - const jwt = await new SignJWT({ - sub: users[0].id, - iss: config.http.base_url.origin, - aud: application.data.clientId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Authorization: `Bearer ${tokens[0].data.accessToken}`, - "Content-Type": "application/json", - Cookie: `jwt=${jwt}`, - }, - body: JSON.stringify({ - client_id: application.data.clientId, - redirect_uri: application.data.redirectUri, - response_type: "code", - scope: "invalid-scope", - state: "test-state", - code_challenge: randomString(43), - code_challenge_method: "S256", - }), - }); - - expect(response.status).toBe(302); - const location = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - const params = new URLSearchParams(location.search); - expect(params.get("error")).toBe("invalid_request"); - expect(params.get("error_description")).toBe( - "Invalid scope: not a subset of the application's scopes", - ); - }); -}); diff --git a/packages/api/routes/api/oauth/authorize.ts b/packages/api/routes/api/oauth/authorize.ts deleted file mode 100644 index 92e83657..00000000 --- a/packages/api/routes/api/oauth/authorize.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { RolePermission } from "@versia/client/schemas"; -import { config } from "@versia-server/config"; -import { - apiRoute, - auth, - handleZodError, - jsonOrForm, -} from "@versia-server/kit/api"; -import { Application, Token, User } from "@versia-server/kit/db"; -import { randomUUIDv7 } from "bun"; -import { describeRoute, validator } from "hono-openapi"; -import { type JWTPayload, jwtVerify, SignJWT } from "jose"; -import { JOSEError } from "jose/errors"; -import { z } from "zod/v4"; -import { randomString } from "@/math"; -import { errorRedirect, errors } from "../../../plugins/openid/errors.ts"; - -export default apiRoute((app) => - app.post( - "/oauth/authorize", - describeRoute({ - summary: "Main OpenID authorization endpoint", - tags: ["OpenID"], - responses: { - 302: { - description: "Redirect to the application", - }, - }, - }), - auth({ - auth: false, - }), - jsonOrForm(), - validator( - "query", - z.object({ - prompt: z - .enum(["none", "login", "consent", "select_account"]) - .optional() - .default("none"), - max_age: z.coerce - .number() - .int() - .optional() - .default(60 * 60 * 24 * 7), - }), - handleZodError, - ), - validator( - "json", - z - .object({ - scope: z.string().optional(), - redirect_uri: z - .url() - .optional() - .or(z.literal("urn:ietf:wg:oauth:2.0:oob")), - response_type: z.enum([ - "code", - "token", - "none", - "id_token", - "code id_token", - "code token", - "token id_token", - "code token id_token", - ]), - client_id: z.string(), - state: z.string().optional(), - code_challenge: z.string().optional(), - code_challenge_method: z.enum(["plain", "S256"]).optional(), - }) - .refine( - // Check if redirect_uri is valid for code flow - (data) => - data.response_type.includes("code") - ? data.redirect_uri - : true, - "redirect_uri is required for code flow", - ), - // Disable for Mastodon API compatibility - /* .refine( - // Check if code_challenge is valid for code flow - (data) => - data.response_type.includes("code") - ? data.code_challenge - : true, - "code_challenge is required for code flow", - ), */ - handleZodError, - ), - validator( - "cookie", - z.object({ - jwt: z.string(), - }), - handleZodError, - ), - async (context) => { - const { scope, redirect_uri, client_id, state } = - context.req.valid("json"); - - const { jwt } = context.req.valid("cookie"); - - const errorSearchParams = new URLSearchParams( - context.req.valid("json"), - ); - - const result = await jwtVerify( - jwt, - config.authentication.keys.public, - { - algorithms: ["EdDSA"], - audience: client_id, - issuer: new URL(context.get("config").http.base_url).origin, - }, - ).catch((error) => { - if (error instanceof JOSEError) { - return null; - } - - throw error; - }); - - if (!result) { - return errorRedirect( - context, - errors.InvalidJWT, - errorSearchParams, - ); - } - - const { - payload: { aud, sub, exp }, - } = result; - - if (!(aud && sub && exp)) { - return errorRedirect( - context, - errors.MissingJWTFields, - errorSearchParams, - ); - } - - if (!z.uuid().safeParse(sub).success) { - return errorRedirect( - context, - errors.InvalidSub, - errorSearchParams, - ); - } - - const user = await User.fromId(sub); - - if (!user) { - return errorRedirect( - context, - errors.UserNotFound, - errorSearchParams, - ); - } - - if (!user.hasPermission(RolePermission.OAuth)) { - return errorRedirect( - context, - errors.MissingOauthPermission, - errorSearchParams, - ); - } - - const application = await Application.fromClientId(client_id); - - if (!application) { - return errorRedirect( - context, - errors.MissingApplication, - errorSearchParams, - ); - } - - if (application.data.redirectUri !== redirect_uri) { - return errorRedirect( - context, - errors.InvalidRedirectUri, - errorSearchParams, - ); - } - - // Check that scopes are a subset of the application's scopes - if ( - scope && - !scope - .split(" ") - .every((s) => application.data.scopes.includes(s)) - ) { - return errorRedirect( - context, - errors.InvalidScope, - errorSearchParams, - ); - } - - const code = randomString(256, "base64url"); - - let payload: JWTPayload = {}; - - if (scope) { - if (scope.split(" ").includes("openid")) { - payload = { - ...payload, - sub: user.id, - iss: new URL(context.get("config").http.base_url) - .origin, - aud: client_id, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }; - } - if (scope.split(" ").includes("profile")) { - payload = { - ...payload, - name: user.data.displayName, - preferred_username: user.data.username, - picture: user.getAvatarUrl().href, - updated_at: new Date(user.data.updatedAt).toISOString(), - }; - } - if (scope.split(" ").includes("email")) { - payload = { - ...payload, - email: user.data.email, - // TODO: Add verification system - email_verified: true, - }; - } - } - - const idToken = await new SignJWT(payload) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); - - await Token.insert({ - id: randomUUIDv7(), - accessToken: randomString(64, "base64url"), - code, - scope: scope ?? application.data.scopes, - tokenType: "Bearer", - applicationId: application.id, - redirectUri: redirect_uri ?? application.data.redirectUri, - expiresAt: new Date( - Date.now() + 60 * 60 * 24 * 14, - ).toISOString(), - idToken: ["profile", "email", "openid"].some((s) => - scope?.split(" ").includes(s), - ) - ? idToken - : null, - clientId: client_id, - userId: user.id, - }); - - const redirectUri = - redirect_uri === "urn:ietf:wg:oauth:2.0:oob" - ? new URL( - "/oauth/code", - context.get("config").http.base_url, - ) - : new URL(redirect_uri ?? application.data.redirectUri); - - redirectUri.searchParams.append("code", code); - state && redirectUri.searchParams.append("state", state); - - return context.redirect(redirectUri.toString()); - }, - ), -); diff --git a/packages/api/routes/api/oauth/revoke.test.ts b/packages/api/routes/api/oauth/revoke.test.ts index 4d469b94..e7d19afb 100644 --- a/packages/api/routes/api/oauth/revoke.test.ts +++ b/packages/api/routes/api/oauth/revoke.test.ts @@ -7,25 +7,20 @@ const { deleteUsers, users } = await getTestUsers(1); const application = await Application.insert({ id: randomUUIDv7(), - clientId: "test-client-id", - redirectUri: "https://example.com/callback", - scopes: "openid profile email", + redirectUris: ["https://example.com/callback"], + scopes: ["openid", "profile", "email"], secret: "test-secret", name: "Test Application", }); const token = await Token.insert({ id: randomUUIDv7(), - code: "test-code", - redirectUri: application.data.redirectUri, - clientId: application.data.clientId, + clientId: application.id, accessToken: "test-access-token", expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), createdAt: new Date().toISOString(), - tokenType: "Bearer", - scope: application.data.scopes, + scopes: application.data.scopes, userId: users[0].id, - applicationId: application.id, }); afterAll(async () => { @@ -42,7 +37,7 @@ describe("/oauth/revoke", () => { "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: application.data.clientId, + client_id: application.data.id, client_secret: application.data.secret, token: "test-access-token", }), @@ -60,7 +55,7 @@ describe("/oauth/revoke", () => { "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: application.data.clientId, + client_id: application.data.id, client_secret: application.data.secret, }), }); @@ -80,7 +75,7 @@ describe("/oauth/revoke", () => { "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: application.data.clientId, + client_id: application.data.id, client_secret: "invalid-secret", token: "test-access-token", }), @@ -101,7 +96,7 @@ describe("/oauth/revoke", () => { "Content-Type": "application/json", }, body: JSON.stringify({ - client_id: application.data.clientId, + client_id: application.data.id, client_secret: application.data.secret, token: "invalid-token", }), diff --git a/packages/api/routes/api/oauth/revoke.ts b/packages/api/routes/api/oauth/revoke.ts index 36b0a4f2..a664d1ec 100644 --- a/packages/api/routes/api/oauth/revoke.ts +++ b/packages/api/routes/api/oauth/revoke.ts @@ -68,7 +68,7 @@ export default apiRoute((app) => { } // Check if the client secret is correct - if (foundToken.data.application?.secret !== client_secret) { + if (foundToken.data.client?.secret !== client_secret) { return context.json( { error: "unauthorized_client", diff --git a/packages/api/routes/api/oauth/sso.ts b/packages/api/routes/api/oauth/sso.ts deleted file mode 100644 index b58bcfe0..00000000 --- a/packages/api/routes/api/oauth/sso.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { config } from "@versia-server/config"; -import { apiRoute, handleZodError } from "@versia-server/kit/api"; -import { Application, db } from "@versia-server/kit/db"; -import { OpenIdLoginFlows } from "@versia-server/kit/tables"; -import { randomUUIDv7 } from "bun"; -import { describeRoute, validator } from "hono-openapi"; -import { - calculatePKCECodeChallenge, - discoveryRequest, - generateRandomCodeVerifier, - processDiscoveryResponse, -} from "oauth4webapi"; -import { z } from "zod/v4"; -import { oauthRedirectUri } from "../../../plugins/openid/utils.ts"; - -export default apiRoute((app) => { - app.get( - "/oauth/sso", - describeRoute({ - summary: "Initiate SSO login flow", - tags: ["OpenID"], - responses: { - 302: { - description: - "Redirect to SSO login, or redirect to login page with error", - }, - }, - }), - validator( - "query", - z.object({ - issuer: z.string(), - client_id: z.string().optional(), - redirect_uri: z.url().optional(), - scope: z.string().optional(), - response_type: z.enum(["code"]).optional(), - }), - handleZodError, - ), - async (context) => { - // This is the Versia client's client_id, not the external OAuth provider's client_id - const { issuer: issuerId, client_id } = context.req.valid("query"); - - const errorSearchParams = new URLSearchParams( - context.req.valid("query"), - ); - - if (!client_id || client_id === "undefined") { - errorSearchParams.append("error", "invalid_request"); - errorSearchParams.append( - "error_description", - "client_id is required", - ); - - return context.redirect( - `${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`, - ); - } - - const issuer = config.authentication.openid_providers.find( - (provider) => provider.id === issuerId, - ); - - if (!issuer) { - errorSearchParams.append("error", "invalid_request"); - errorSearchParams.append( - "error_description", - "issuer is invalid", - ); - - return context.redirect( - `${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`, - ); - } - - const issuerUrl = new URL(issuer.url); - - const authServer = await discoveryRequest(issuerUrl, { - algorithm: "oidc", - }).then((res) => processDiscoveryResponse(issuerUrl, res)); - - const codeVerifier = generateRandomCodeVerifier(); - - const application = await Application.fromClientId(client_id); - - if (!application) { - errorSearchParams.append("error", "invalid_request"); - errorSearchParams.append( - "error_description", - "client_id is invalid", - ); - - return context.redirect( - `${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`, - ); - } - - // Store into database - const newFlow = ( - await db - .insert(OpenIdLoginFlows) - .values({ - id: randomUUIDv7(), - codeVerifier, - applicationId: application.id, - issuerId, - }) - .returning() - )[0]; - - const codeChallenge = - await calculatePKCECodeChallenge(codeVerifier); - - return context.redirect( - `${authServer.authorization_endpoint}?${new URLSearchParams({ - client_id: issuer.client_id, - redirect_uri: `${oauthRedirectUri( - context.get("config").http.base_url, - issuerId, - )}?flow=${newFlow.id}`, - response_type: "code", - scope: "openid profile email", - // PKCE - code_challenge_method: "S256", - code_challenge: codeChallenge, - }).toString()}`, - ); - }, - ); -}); diff --git a/packages/api/routes/api/oauth/sso/[issuer]/callback.ts b/packages/api/routes/api/oauth/sso/[issuer]/callback.ts index b9c1a706..ef7b7b2d 100644 --- a/packages/api/routes/api/oauth/sso/[issuer]/callback.ts +++ b/packages/api/routes/api/oauth/sso/[issuer]/callback.ts @@ -6,17 +6,21 @@ import { import { config } from "@versia-server/config"; import { ApiError } from "@versia-server/kit"; import { apiRoute, handleZodError } from "@versia-server/kit/api"; -import { db, Media, Token, User } from "@versia-server/kit/db"; +import { db, Media, User } from "@versia-server/kit/db"; import { searchManager } from "@versia-server/kit/search"; -import { OpenIdAccounts, Users } from "@versia-server/kit/tables"; +import { + AuthorizationCodes, + OpenIdAccounts, + Users, +} from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { and, eq, isNull, type SQL } from "drizzle-orm"; import { setCookie } from "hono/cookie"; +import { sign } from "hono/jwt"; import { describeRoute, validator } from "hono-openapi"; -import { SignJWT } from "jose"; +import * as client from "openid-client"; import { z } from "zod/v4"; import { randomString } from "@/math.ts"; -import { automaticOidcFlow } from "../../../../../plugins/openid/utils.ts"; export default apiRoute((app) => { app.get( @@ -31,6 +35,7 @@ export default apiRoute((app) => { description: "Redirect to frontend's consent route, or redirect to login page with error", }, + 422: ApiError.validationFailed().schema, }, }), validator( @@ -43,103 +48,94 @@ export default apiRoute((app) => { validator( "query", z.object({ - client_id: z.string().optional(), flow: z.string(), - link: zBoolean.optional(), + link: zBoolean.default(false), user_id: z.uuid().optional(), }), handleZodError, ), async (context) => { - const currentUrl = new URL(context.req.url); - const redirectUrl = new URL(context.req.url); - - // Correct some reverse proxies incorrectly setting the protocol as http, even if the original request was https - // Looking at you, Traefik - if ( - new URL(context.get("config").http.base_url).protocol === - "https:" && - currentUrl.protocol === "http:" - ) { - currentUrl.protocol = "https:"; - redirectUrl.protocol = "https:"; - } - - // Remove state query parameter from URL - currentUrl.searchParams.delete("state"); - redirectUrl.searchParams.delete("state"); - // Remove issuer query parameter from URL (can cause redirect URI mismatches) - redirectUrl.searchParams.delete("iss"); - redirectUrl.searchParams.delete("code"); - const { issuer: issuerParam } = context.req.valid("param"); + const { issuer: issuerId } = context.req.valid("param"); const { flow: flowId, user_id, link } = context.req.valid("query"); const issuer = config.authentication.openid_providers.find( - (provider) => provider.id === issuerParam, + (provider) => provider.id === issuerId, ); if (!issuer) { - throw new ApiError(404, "Issuer not found"); + throw new ApiError(422, "Unknown or invalid issuer"); } - const userInfo = await automaticOidcFlow( - issuer, - flowId, - currentUrl, - redirectUrl, - (error, message, flow) => { - const errorSearchParams = new URLSearchParams( - Object.entries({ - redirect_uri: flow?.application?.redirectUri, - client_id: flow?.application?.clientId, - response_type: "code", - scope: flow?.application?.scopes, - }).filter(([_, value]) => value !== undefined) as [ - string, - string, - ][], - ); + const flow = await db.query.OpenIdLoginFlows.findFirst({ + where: (flow): SQL | undefined => eq(flow.id, flowId), + with: { + application: true, + }, + }); - errorSearchParams.append("error", error); - errorSearchParams.append("error_description", message); + const redirectWithMessage = ( + parameters: Record, + route = config.frontend.routes.login, + ) => { + const searchParams = new URLSearchParams( + Object.entries(parameters).filter( + ([_, value]) => value !== undefined, + ) as [string, string][], + ); - return context.redirect( - `${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`, - ); + return context.redirect(`${route}?${searchParams.toString()}`); + }; + + if (!flow) { + return redirectWithMessage({ + error: "invalid_request", + error_description: "Invalid flow", + }); + } + + const oidcConfig = await client.discovery( + issuer.url, + issuer.client_id, + issuer.client_secret, + ); + + const tokens = await client.authorizationCodeGrant( + oidcConfig, + context.req.raw, + { + pkceCodeVerifier: flow.codeVerifier, + expectedState: flow.state ?? undefined, + idTokenExpected: true, }, ); - if (userInfo instanceof Response) { - return userInfo; + const claims = tokens.claims(); + + if (!claims) { + return redirectWithMessage({ + error: "invalid_request", + error_description: "Missing or invalid ID token", + }); } - const { sub, email, preferred_username, picture } = - userInfo.userInfo; - const flow = userInfo.flow; - - const errorSearchParams = new URLSearchParams( - Object.entries({ - redirect_uri: flow.application?.redirectUri, - client_id: flow.application?.clientId, - response_type: "code", - scope: flow.application?.scopes, - }).filter(([_, value]) => value !== undefined) as [ - string, - string, - ][], + const userInfo = await client.fetchUserInfo( + oidcConfig, + tokens.access_token, + claims.sub, ); + const { sub, email, preferred_username, picture } = userInfo; + // If linking account if (link && user_id) { // Check if userId is equal to application.clientId - if (!flow.application?.clientId.startsWith(user_id)) { - return context.redirect( - `${context.get("config").http.base_url}${ - context.get("config").frontend.routes.home - }?${new URLSearchParams({ + if (!flow.application?.id.startsWith(user_id)) { + return redirectWithMessage( + { oidc_account_linking_error: "Account linking error", - oidc_account_linking_error_message: `User ID does not match application client ID (${user_id} != ${flow.application?.clientId})`, - })}`, + oidc_account_linking_error_message: `User ID does not match application client ID (${user_id} != ${flow.application?.id})`, + }, + config.frontend.routes.home, ); } @@ -153,15 +149,14 @@ export default apiRoute((app) => { }); if (account) { - return context.redirect( - `${context.get("config").http.base_url}${ - context.get("config").frontend.routes.home - }?${new URLSearchParams({ + return redirectWithMessage( + { oidc_account_linking_error: "Account already linked", oidc_account_linking_error_message: "This account has already been linked to this OpenID Connect provider.", - })}`, + }, + config.frontend.routes.home, ); } @@ -244,42 +239,27 @@ export default apiRoute((app) => { userId = user.id; } else { - errorSearchParams.append("error", "invalid_request"); - errorSearchParams.append( - "error_description", - "No user found with that account", - ); - - return context.redirect( - `${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`, - ); + return redirectWithMessage({ + error: "invalid_request", + error_description: "No user found with that account", + }); } } const user = await User.fromId(userId); if (!user) { - errorSearchParams.append("error", "invalid_request"); - errorSearchParams.append( - "error_description", - "No user found with that account", - ); - - return context.redirect( - `${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`, - ); + return redirectWithMessage({ + error: "invalid_request", + error_description: "No user found with that account", + }); } if (!user.hasPermission(RolePermission.OAuth)) { - errorSearchParams.append("error", "invalid_request"); - errorSearchParams.append( - "error_description", - `User does not have the '${RolePermission.OAuth}' permission`, - ); - - return context.redirect( - `${context.get("config").frontend.routes.login}?${errorSearchParams.toString()}`, - ); + return redirectWithMessage({ + error: "invalid_request", + error_description: `User does not have the '${RolePermission.OAuth}' permission`, + }); } if (!flow.application) { @@ -288,27 +268,26 @@ export default apiRoute((app) => { const code = randomString(32, "hex"); - await Token.insert({ - id: randomUUIDv7(), - accessToken: randomString(64, "base64url"), + await db.insert(AuthorizationCodes).values({ + clientId: flow.application.id, code, - scope: flow.application.scopes, - tokenType: "Bearer", + expiresAt: new Date(Date.now() + 60 * 1000).toISOString(), // 1 minute + redirectUri: flow.clientRedirectUri ?? undefined, userId: user.id, - applicationId: flow.application.id, + scopes: flow.clientScopes ?? [], }); - // Generate JWT - const jwt = await new SignJWT({ - sub: user.id, - iss: new URL(context.get("config").http.base_url).origin, - aud: flow.application.clientId, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }) - .setProtectedHeader({ alg: "EdDSA" }) - .sign(config.authentication.keys.private); + const jwt = await sign( + { + sub: user.id, + iss: new URL(context.get("config").http.base_url).origin, + aud: flow.application.id, + exp: Math.floor(Date.now() / 1000) + 60 * 60, + iat: Math.floor(Date.now() / 1000), + nbf: Math.floor(Date.now() / 1000), + }, + config.authentication.keys.private, + ); // Redirect back to application setCookie(context, "jwt", jwt, { @@ -320,21 +299,17 @@ export default apiRoute((app) => { maxAge: 60 * 60 * 24 * 14, }); - return context.redirect( - new URL( - `${context.get("config").frontend.routes.consent}?${new URLSearchParams( - { - redirect_uri: flow.application.redirectUri, - code, - client_id: flow.application.clientId, - application: flow.application.name, - website: flow.application.website ?? "", - scope: flow.application.scopes, - response_type: "code", - }, - ).toString()}`, - context.get("config").http.base_url, - ).toString(), + return redirectWithMessage( + { + redirect_uri: flow.clientRedirectUri ?? undefined, + code, + client_id: flow.application.id, + application: flow.application.name, + website: flow.application.website ?? "", + scope: flow.clientScopes?.join(" "), + state: flow.clientState ?? undefined, + }, + config.frontend.routes.consent, ); }, ); diff --git a/packages/api/routes/api/oauth/sso/[issuer]/index.ts b/packages/api/routes/api/oauth/sso/[issuer]/index.ts new file mode 100644 index 00000000..4b4bf9df --- /dev/null +++ b/packages/api/routes/api/oauth/sso/[issuer]/index.ts @@ -0,0 +1,122 @@ +import { config } from "@versia-server/config"; +import { ApiError } from "@versia-server/kit"; +import { apiRoute, handleZodError } from "@versia-server/kit/api"; +import { Application, db } from "@versia-server/kit/db"; +import { OpenIdLoginFlows } from "@versia-server/kit/tables"; +import { randomUUIDv7 } from "bun"; +import { describeRoute, validator } from "hono-openapi"; +import * as client from "openid-client"; +import { z } from "zod/v4"; +import { oauthRedirectUri } from "@/lib"; + +export default apiRoute((app) => { + app.post( + "/oauth/sso/:issuer", + describeRoute({ + summary: "Initiate SSO login flow", + tags: ["OpenID"], + responses: { + 302: { + description: + "Redirect to SSO provider's authorization endpoint", + }, + 422: ApiError.validationFailed().schema, + }, + }), + validator( + "param", + z.object({ + issuer: z.string(), + }), + handleZodError, + ), + validator( + "json", + z.object({ + client_id: z.string(), + redirect_uri: z.url(), + scopes: z.string().array().default(["read"]), + state: z.string().optional(), + }), + handleZodError, + ), + async (context) => { + // This is the Versia client's client_id, not the external OAuth provider's client_id + const { client_id, redirect_uri, scopes, state } = + context.req.valid("json"); + const { issuer: issuerId } = context.req.valid("param"); + + const issuer = config.authentication.openid_providers.find( + (provider) => provider.id === issuerId, + ); + + if (!issuer) { + throw new ApiError(422, "Unknown or invalid issuer"); + } + + const application = await Application.fromClientId(client_id); + + if (!application) { + throw new ApiError(422, "Unknown or invalid client_id"); + } + + if (!application.data.redirectUris.includes(redirect_uri)) { + throw new ApiError( + 422, + "redirect_uri is not a subset of application's redirect_uris", + ); + } + // TODO: Validate oauth scopes + + const oidcConfig = await client.discovery( + issuer.url, + issuer.client_id, + issuer.client_secret, + ); + const codeVerifier = client.randomPKCECodeVerifier(); + const codeChallenge = + await client.calculatePKCECodeChallenge(codeVerifier); + + const parameters: Record = { + scope: "openid profile email", + code_challenge: codeChallenge, + code_challenge_method: "S256", + }; + + if (!oidcConfig.serverMetadata().supportsPKCE()) { + parameters.state = client.randomState(); + } + + // Store into database + const newFlow = ( + await db + .insert(OpenIdLoginFlows) + .values({ + id: randomUUIDv7(), + codeVerifier, + state: parameters.state, + clientState: state, + clientRedirectUri: redirect_uri, + clientScopes: scopes, + applicationId: application.id, + issuerId, + }) + .returning() + )[0]; + + parameters.redirect_uri = `${oauthRedirectUri( + context.get("config").http.base_url, + issuerId, + )}?${new URLSearchParams({ + flow: newFlow.id, + })}`; + + const redirectTo = client.buildAuthorizationUrl( + oidcConfig, + parameters, + ); + + return context.redirect(redirectTo); + }, + ); +}); diff --git a/packages/api/routes/api/oauth/token.test.ts b/packages/api/routes/api/oauth/token.test.ts index cb53156e..27d5f23c 100644 --- a/packages/api/routes/api/oauth/token.test.ts +++ b/packages/api/routes/api/oauth/token.test.ts @@ -7,23 +7,18 @@ const { deleteUsers, users } = await getTestUsers(1); const application = await Application.insert({ id: randomUUIDv7(), - clientId: "test-client-id", - redirectUri: "https://example.com/callback", - scopes: "openid profile email", + redirectUris: ["https://example.com/callback"], + scopes: ["openid", "profile", "email"], secret: "test-secret", name: "Test Application", }); const token = await Token.insert({ id: randomUUIDv7(), - code: "test-code", - redirectUri: application.data.redirectUri, - clientId: application.data.clientId, + clientId: application.data.id, accessToken: "test-access-token", expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), createdAt: new Date().toISOString(), - tokenType: "Bearer", - scope: application.data.scopes, userId: users[0].id, }); @@ -43,8 +38,8 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "test-code", - redirect_uri: application.data.redirectUri, - client_id: application.data.clientId, + redirect_uri: application.data.redirectUris[0], + client_id: application.data.id, client_secret: application.data.secret, }), }); @@ -64,8 +59,8 @@ describe("/oauth/token", () => { }, body: JSON.stringify({ grant_type: "authorization_code", - redirect_uri: application.data.redirectUri, - client_id: application.data.clientId, + redirect_uri: application.data.redirectUris[0], + client_id: application.data.id, client_secret: application.data.secret, }), }); @@ -85,7 +80,7 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "test-code", - client_id: application.data.clientId, + client_id: application.data.id, client_secret: application.data.secret, }), }); @@ -105,7 +100,7 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "test-code", - redirect_uri: application.data.redirectUri, + redirect_uri: application.data.redirectUris[0], client_secret: application.data.secret, }), }); @@ -125,8 +120,8 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "test-code", - redirect_uri: application.data.redirectUri, - client_id: application.data.clientId, + redirect_uri: application.data.redirectUris[0], + client_id: application.data.id, client_secret: "invalid-secret", }), }); @@ -146,8 +141,8 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "authorization_code", code: "invalid-code", - redirect_uri: application.data.redirectUri, - client_id: application.data.clientId, + redirect_uri: application.data.redirectUris[0], + client_id: application.data.id, client_secret: application.data.secret, }), }); @@ -167,8 +162,8 @@ describe("/oauth/token", () => { body: JSON.stringify({ grant_type: "refresh_token", code: "test-code", - redirect_uri: application.data.redirectUri, - client_id: application.data.clientId, + redirect_uri: application.data.redirectUris[0], + client_id: application.data.id, client_secret: application.data.secret, }), }); diff --git a/packages/api/routes/api/oauth/token.ts b/packages/api/routes/api/oauth/token.ts index cd333ee6..ac78b130 100644 --- a/packages/api/routes/api/oauth/token.ts +++ b/packages/api/routes/api/oauth/token.ts @@ -1,43 +1,35 @@ +import { Token as TokenSchema } from "@versia/client/schemas"; import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api"; -import { Application, Token } from "@versia-server/kit/db"; -import { Tokens } from "@versia-server/kit/tables"; +import { Application, db, Token } from "@versia-server/kit/db"; +import { AuthorizationCodes } from "@versia-server/kit/tables"; +import { randomUUIDv7 } from "bun"; import { and, eq } from "drizzle-orm"; import { describeRoute, resolver, validator } from "hono-openapi"; import { z } from "zod/v4"; +import { randomString } from "@/math"; export default apiRoute((app) => { app.post( "/oauth/token", describeRoute({ - summary: "Get token", + summary: "Obtain a token", + description: + "Obtain an access token, to be used during API calls that are not public.", + externalDocs: { + url: "https://docs.joinmastodon.org/methods/oauth/#token", + }, tags: ["OpenID"], responses: { 200: { description: "Token", content: { "application/json": { - schema: resolver( - z.object({ - access_token: z.string(), - token_type: z.string(), - expires_in: z - .number() - .optional() - .nullable(), - id_token: z.string().optional().nullable(), - refresh_token: z - .string() - .optional() - .nullable(), - scope: z.string().optional(), - created_at: z.number(), - }), - ), + schema: resolver(TokenSchema), }, }, }, 401: { - description: "Authorization error", + description: "Invalid grant", content: { "application/json": { schema: resolver( @@ -55,135 +47,87 @@ export default apiRoute((app) => { validator( "json", z.object({ - code: z.string().optional(), + code: z.string(), + grant_type: z.enum([ + "authorization_code", + "refresh_token", + "client_credentials", + ]), code_verifier: z.string().optional(), - grant_type: z - .enum([ - "authorization_code", - "refresh_token", - "client_credentials", - "password", - "urn:ietf:params:oauth:grant-type:device_code", - "urn:ietf:params:oauth:grant-type:token-exchange", - "urn:ietf:params:oauth:grant-type:saml2-bearer", - "urn:openid:params:grant-type:ciba", - ]) - .default("authorization_code"), - client_id: z.string().optional(), - client_secret: z.string().optional(), - username: z.string().trim().optional(), - password: z.string().trim().optional(), - redirect_uri: z.url().optional(), + client_id: z.string(), + client_secret: z.string(), + redirect_uri: z.url(), refresh_token: z.string().optional(), - scope: z.string().optional(), - assertion: z.string().optional(), - audience: z.string().optional(), - subject_token_type: z.string().optional(), - subject_token: z.string().optional(), - actor_token_type: z.string().optional(), - actor_token: z.string().optional(), - auth_req_id: z.string().optional(), + scope: z.string().default("read"), }), handleZodError, ), async (context) => { - const { grant_type, code, redirect_uri, client_id, client_secret } = + const { code, client_id, client_secret, redirect_uri } = context.req.valid("json"); - switch (grant_type) { - case "authorization_code": { - if (!code) { - return context.json( - { - error: "invalid_request", - error_description: "Code is required", - }, - 401, - ); - } + // Verify the client_secret + const client = await Application.fromClientId(client_id); - if (!redirect_uri) { - return context.json( - { - error: "invalid_request", - error_description: "Redirect URI is required", - }, - 401, - ); - } - - if (!client_id) { - return context.json( - { - error: "invalid_request", - error_description: "Client ID is required", - }, - 401, - ); - } - - // Verify the client_secret - const client = await Application.fromClientId(client_id); - - if (!client || client.data.secret !== client_secret) { - return context.json( - { - error: "invalid_client", - error_description: "Invalid client credentials", - }, - 401, - ); - } - - const token = await Token.fromSql( - and( - eq(Tokens.code, code), - eq(Tokens.redirectUri, decodeURI(redirect_uri)), - eq(Tokens.clientId, client_id), - ), - ); - - if (!token) { - return context.json( - { - error: "invalid_grant", - error_description: "Code not found", - }, - 401, - ); - } - - // Invalidate the code - await token.update({ code: null }); - - return context.json( - { - ...token.toApi(), - expires_in: token.data.expiresAt - ? Math.floor( - (new Date( - token.data.expiresAt, - ).getTime() - - Date.now()) / - 1000, - ) - : null, - id_token: token.data.idToken, - refresh_token: null, - }, - 200, - ); - } - - default: + if (!client || client.data.secret !== client_secret) { + return context.json( + { + error: "invalid_client", + error_description: "Invalid client credentials", + }, + 401, + ); } + const authorizationCode = + await db.query.AuthorizationCodes.findFirst({ + where: (codeTable) => + and( + eq(codeTable.code, code), + eq(codeTable.redirectUri, redirect_uri), + eq(codeTable.clientId, client.id), + ), + }); + + if ( + !authorizationCode || + new Date(authorizationCode.expiresAt).getTime() < Date.now() + ) { + return context.json( + { + error: "invalid_grant", + error_description: + "Authorization code not found or expired", + }, + 404, + ); + } + + const token = await Token.insert({ + accessToken: randomString(64, "base64url"), + clientId: client.id, + id: randomUUIDv7(), + userId: authorizationCode.userId, + }); + + // Invalidate the code + await db + .delete(AuthorizationCodes) + .where(eq(AuthorizationCodes.code, authorizationCode.code)); + return context.json( { - error: "unsupported_grant_type", - error_description: "Unsupported grant type", + ...token.toApi(), + expires_in: token.data.expiresAt + ? Math.floor( + (new Date(token.data.expiresAt).getTime() - + Date.now()) / + 1000, + ) + : null, + refresh_token: null, }, - 401, + 200, ); }, ); diff --git a/packages/api/routes/api/v1/apps/index.ts b/packages/api/routes/api/v1/apps/index.ts index 0f2dbd2b..818a93b1 100644 --- a/packages/api/routes/api/v1/apps/index.ts +++ b/packages/api/routes/api/v1/apps/index.ts @@ -5,7 +5,6 @@ import { import { ApiError } from "@versia-server/kit"; import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api"; import { Application } from "@versia-server/kit/db"; -import { randomUUIDv7 } from "bun"; import { describeRoute, resolver, validator } from "hono-openapi"; import { z } from "zod/v4"; import { randomString } from "@/math"; @@ -64,14 +63,13 @@ export default apiRoute((app) => context.req.valid("json"); const app = await Application.insert({ - id: randomUUIDv7(), + id: randomString(32, "base64url"), name: client_name, - redirectUri: Array.isArray(redirect_uris) - ? redirect_uris.join("\n") - : redirect_uris, - scopes, + redirectUris: Array.isArray(redirect_uris) + ? redirect_uris + : [redirect_uris], + scopes: scopes.split(" "), website: website || undefined, - clientId: randomString(32, "base64url"), secret: randomString(64, "base64url"), }); diff --git a/packages/api/routes/api/v1/sso/index.ts b/packages/api/routes/api/v1/sso/index.ts index 86baa4b0..c1fb1af3 100644 --- a/packages/api/routes/api/v1/sso/index.ts +++ b/packages/api/routes/api/v1/sso/index.ts @@ -6,15 +6,9 @@ import { Application, db } from "@versia-server/kit/db"; import { OpenIdLoginFlows } from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { describeRoute, resolver, validator } from "hono-openapi"; -import { - calculatePKCECodeChallenge, - generateRandomCodeVerifier, -} from "oauth4webapi"; +import * as client from "openid-client"; import { z } from "zod/v4"; -import { - oauthDiscoveryRequest, - oauthRedirectUri, -} from "../../../../plugins/openid/utils.ts"; +import { oauthRedirectUri } from "@/lib"; export default apiRoute((app) => { app.get( @@ -105,9 +99,24 @@ export default apiRoute((app) => { ); } - const authServer = await oauthDiscoveryRequest(new URL(issuer.url)); + const oidcConfig = await client.discovery( + issuer.url, + issuer.client_id, + issuer.client_secret, + ); + const codeVerifier = client.randomPKCECodeVerifier(); + const codeChallenge = + await client.calculatePKCECodeChallenge(codeVerifier); - const codeVerifier = generateRandomCodeVerifier(); + const parameters: Record = { + scope: "openid profile email", + code_challenge: codeChallenge, + code_challenge_method: "S256", + }; + + if (!oidcConfig.serverMetadata().supportsPKCE()) { + parameters.state = client.randomState(); + } const redirectUri = oauthRedirectUri( context.get("config").http.base_url, @@ -115,15 +124,14 @@ export default apiRoute((app) => { ); const application = await Application.insert({ - id: randomUUIDv7(), - clientId: + id: user.id + Buffer.from( crypto.getRandomValues(new Uint8Array(32)), ).toString("base64"), name: "Versia", - redirectUri: redirectUri.toString(), - scopes: "openid profile email", + redirectUris: [redirectUri.href], + scopes: ["openid", "profile", "email"], secret: "", }); @@ -134,30 +142,28 @@ export default apiRoute((app) => { .values({ id: randomUUIDv7(), codeVerifier, + state: parameters.state, issuerId, applicationId: application.id, }) .returning() )[0]; - const codeChallenge = - await calculatePKCECodeChallenge(codeVerifier); + parameters.redirect_uri = `${oauthRedirectUri( + config.http.base_url, + issuerId, + )}?${new URLSearchParams({ + flow: newFlow.id, + link: "true", + user_id: user.id, + })}`; - return context.redirect( - `${authServer.authorization_endpoint}?${new URLSearchParams({ - client_id: issuer.client_id, - redirect_uri: `${redirectUri}?${new URLSearchParams({ - flow: newFlow.id, - link: "true", - user_id: user.id, - })}`, - response_type: "code", - scope: "openid profile email", - // PKCE - code_challenge_method: "S256", - code_challenge: codeChallenge, - }).toString()}`, + const redirectTo = client.buildAuthorizationUrl( + oidcConfig, + parameters, ); + + return context.redirect(redirectTo); }, ); }); diff --git a/packages/api/routes/well-known/jwks.test.ts b/packages/api/routes/well-known/jwks.test.ts deleted file mode 100644 index 2d97acf5..00000000 --- a/packages/api/routes/well-known/jwks.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { afterAll, describe, expect, test } from "bun:test"; -import { Application } from "@versia-server/kit/db"; -import { fakeRequest } from "@versia-server/tests"; -import { randomUUIDv7 } from "bun"; - -const application = await Application.insert({ - id: randomUUIDv7(), - clientId: "test-client-id", - redirectUri: "https://example.com/callback", - scopes: "openid profile email", - secret: "test-secret", - name: "Test Application", -}); - -afterAll(async () => { - await application.delete(); -}); - -describe("/.well-known/jwks", () => { - test("should return JWK set with valid inputs", async () => { - const response = await fakeRequest("/.well-known/jwks", { - method: "GET", - }); - - expect(response.status).toBe(200); - const body = await response.json(); - expect(body.keys).toHaveLength(1); - expect(body.keys[0].kty).toBe("OKP"); - expect(body.keys[0].use).toBe("sig"); - expect(body.keys[0].alg).toBe("EdDSA"); - expect(body.keys[0].kid).toBe("1"); - expect(body.keys[0].crv).toBe("Ed25519"); - expect(body.keys[0].x).toBeString(); - }); -}); diff --git a/packages/api/routes/well-known/jwks.ts b/packages/api/routes/well-known/jwks.ts deleted file mode 100644 index a8b182a3..00000000 --- a/packages/api/routes/well-known/jwks.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { config } from "@versia-server/config"; -import { apiRoute, auth } from "@versia-server/kit/api"; -import { describeRoute, resolver } from "hono-openapi"; -import { exportJWK } from "jose"; -import { z } from "zod/v4"; - -export default apiRoute((app) => { - app.get( - "/.well-known/jwks", - describeRoute({ - summary: "JWK Set", - tags: ["OpenID"], - responses: { - 200: { - description: "JWK Set", - content: { - "application/json": { - schema: resolver( - z.object({ - keys: z.array( - z.object({ - kty: z.string().optional(), - use: z.string(), - alg: z.string(), - kid: z.string(), - crv: z.string().optional(), - x: z.string().optional(), - y: z.string().optional(), - }), - ), - }), - ), - }, - }, - }, - }, - }), - auth({ - auth: false, - }), - async (context) => { - const jwk = await exportJWK(config.authentication.keys.private); - - // Remove the private key 💀 - jwk.d = undefined; - - return context.json( - { - keys: [ - { - ...jwk, - use: "sig", - alg: "EdDSA", - kid: "1", - }, - ], - }, - 200, - ); - }, - ); -}); diff --git a/packages/api/routes/well-known/openid-configuration/index.ts b/packages/api/routes/well-known/openid-configuration/index.ts deleted file mode 100644 index 9917997b..00000000 --- a/packages/api/routes/well-known/openid-configuration/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { config } from "@versia-server/config"; -import { apiRoute } from "@versia-server/kit/api"; -import { describeRoute, resolver } from "hono-openapi"; -import { z } from "zod/v4"; - -export default apiRoute((app) => - app.get( - "/.well-known/openid-configuration", - describeRoute({ - summary: "OpenID Configuration", - tags: ["OpenID"], - responses: { - 200: { - description: "OpenID Configuration", - content: { - "application/json": { - schema: resolver( - z.object({ - issuer: z.string(), - authorization_endpoint: z.string(), - token_endpoint: z.string(), - userinfo_endpoint: z.string(), - jwks_uri: z.string(), - response_types_supported: z.array( - z.string(), - ), - subject_types_supported: z.array( - z.string(), - ), - id_token_signing_alg_values_supported: - z.array(z.string()), - scopes_supported: z.array(z.string()), - token_endpoint_auth_methods_supported: - z.array(z.string()), - claims_supported: z.array(z.string()), - }), - ), - }, - }, - }, - }, - }), - (context) => { - const baseUrl = config.http.base_url; - return context.json( - { - issuer: baseUrl.origin.toString(), - authorization_endpoint: `${baseUrl.origin}/oauth/authorize`, - token_endpoint: `${baseUrl.origin}/oauth/token`, - userinfo_endpoint: `${baseUrl.origin}/api/v1/accounts/verify_credentials`, - jwks_uri: `${baseUrl.origin}/.well-known/jwks`, - response_types_supported: ["code"], - subject_types_supported: ["public"], - id_token_signing_alg_values_supported: ["EdDSA"], - scopes_supported: ["openid", "profile", "email"], - token_endpoint_auth_methods_supported: [ - "client_secret_basic", - ], - claims_supported: ["sub"], - }, - 200, - ); - }, - ), -); diff --git a/packages/config/index.ts b/packages/config/index.ts index f4b50b96..8b54e7f0 100644 --- a/packages/config/index.ts +++ b/packages/config/index.ts @@ -799,7 +799,7 @@ export const ConfigSchema = z z.strictObject({ name: z.string().min(1), id: z.string().min(1), - url: z.string().min(1), + url, client_id: z.string().min(1), client_secret: sensitiveString, icon: url.optional(), diff --git a/packages/kit/api.ts b/packages/kit/api.ts index f31e6a7f..876b6f80 100644 --- a/packages/kit/api.ts +++ b/packages/kit/api.ts @@ -169,8 +169,8 @@ export const auth = (options: { const auth: AuthData = { token, - application: token?.data.application - ? new Application(token?.data.application) + application: token?.data.client + ? new Application(token?.data.client) : null, user: (await token?.getUser()) ?? null, }; diff --git a/packages/kit/db/application.ts b/packages/kit/db/application.ts index 15314c66..84920d41 100644 --- a/packages/kit/db/application.ts +++ b/packages/kit/db/application.ts @@ -12,13 +12,13 @@ import { } from "drizzle-orm"; import type { z } from "zod/v4"; import { db } from "../tables/db.ts"; -import { Applications } from "../tables/schema.ts"; +import { Clients } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; import { Token } from "./token.ts"; -type ApplicationType = InferSelectModel; +type ApplicationType = InferSelectModel; -export class Application extends BaseInterface { +export class Application extends BaseInterface { public static $type: ApplicationType; public async reload(): Promise { @@ -36,18 +36,18 @@ export class Application extends BaseInterface { return null; } - return await Application.fromSql(eq(Applications.id, id)); + return await Application.fromSql(eq(Clients.id, id)); } public static async fromIds(ids: string[]): Promise { - return await Application.manyFromSql(inArray(Applications.id, ids)); + return await Application.manyFromSql(inArray(Clients.id, ids)); } public static async fromSql( sql: SQL | undefined, - orderBy: SQL | undefined = desc(Applications.id), + orderBy: SQL | undefined = desc(Clients.id), ): Promise { - const found = await db.query.Applications.findFirst({ + const found = await db.query.Clients.findFirst({ where: sql, orderBy, }); @@ -60,12 +60,12 @@ export class Application extends BaseInterface { public static async manyFromSql( sql: SQL | undefined, - orderBy: SQL | undefined = desc(Applications.id), + orderBy: SQL | undefined = desc(Clients.id), limit?: number, offset?: number, - extra?: Parameters[0], + extra?: Parameters[0], ): Promise { - const found = await db.query.Applications.findMany({ + const found = await db.query.Clients.findMany({ where: sql, orderBy, limit, @@ -81,22 +81,20 @@ export class Application extends BaseInterface { ): Promise { const result = await Token.fromAccessToken(token); - return result?.data.application - ? new Application(result.data.application) - : null; + return result?.data.client ? new Application(result.data.client) : null; } public static fromClientId(clientId: string): Promise { - return Application.fromSql(eq(Applications.clientId, clientId)); + return Application.fromSql(eq(Clients.id, clientId)); } public async update( newApplication: Partial, ): Promise { await db - .update(Applications) + .update(Clients) .set(newApplication) - .where(eq(Applications.id, this.id)); + .where(eq(Clients.id, this.id)); const updated = await Application.fromId(this.data.id); @@ -114,18 +112,16 @@ export class Application extends BaseInterface { public async delete(ids?: string[]): Promise { if (Array.isArray(ids)) { - await db.delete(Applications).where(inArray(Applications.id, ids)); + await db.delete(Clients).where(inArray(Clients.id, ids)); } else { - await db.delete(Applications).where(eq(Applications.id, this.id)); + await db.delete(Clients).where(eq(Clients.id, this.id)); } } public static async insert( - data: InferInsertModel, + data: InferInsertModel, ): Promise { - const inserted = ( - await db.insert(Applications).values(data).returning() - )[0]; + const inserted = (await db.insert(Clients).values(data).returning())[0]; const application = await Application.fromId(inserted.id); @@ -144,9 +140,9 @@ export class Application extends BaseInterface { return { name: this.data.name, website: this.data.website, - scopes: this.data.scopes.split(" "), - redirect_uri: this.data.redirectUri, - redirect_uris: this.data.redirectUri.split("\n"), + scopes: this.data.scopes, + redirect_uri: this.data.redirectUris.join(" "), + redirect_uris: this.data.redirectUris, }; } @@ -154,12 +150,12 @@ export class Application extends BaseInterface { return { name: this.data.name, website: this.data.website, - client_id: this.data.clientId, + client_id: this.data.id, client_secret: this.data.secret, client_secret_expires_at: "0", - scopes: this.data.scopes.split(" "), - redirect_uri: this.data.redirectUri, - redirect_uris: this.data.redirectUri.split("\n"), + scopes: this.data.scopes, + redirect_uri: this.data.redirectUris.join(" "), + redirect_uris: this.data.redirectUris, }; } } diff --git a/packages/kit/db/index.ts b/packages/kit/db/index.ts index 1cc2359d..67f52bcf 100644 --- a/packages/kit/db/index.ts +++ b/packages/kit/db/index.ts @@ -12,4 +12,4 @@ export { Relationship } from "./relationship.ts"; export { Role } from "./role.ts"; export { Timeline } from "./timeline.ts"; export { Token } from "./token.ts"; -export { User } from "./user.ts"; +export { transformOutputToUserWithRelations, User } from "./user.ts"; diff --git a/packages/kit/db/token.ts b/packages/kit/db/token.ts index fdfd9e43..7fd4620d 100644 --- a/packages/kit/db/token.ts +++ b/packages/kit/db/token.ts @@ -15,7 +15,7 @@ import { BaseInterface } from "./base.ts"; import { User } from "./user.ts"; type TokenType = InferSelectModel & { - application: typeof Application.$type | null; + client: typeof Application.$type; }; export class Token extends BaseInterface { @@ -51,7 +51,7 @@ export class Token extends BaseInterface { where: sql, orderBy, with: { - application: true, + client: true, }, }); @@ -74,7 +74,7 @@ export class Token extends BaseInterface { limit, offset, with: { - application: true, + client: true, ...extra?.with, }, }); @@ -159,7 +159,7 @@ export class Token extends BaseInterface { return { access_token: this.data.accessToken, token_type: "Bearer", - scope: this.data.scope, + scope: this.data.scopes.join(" "), created_at: Math.floor( new Date(this.data.createdAt).getTime() / 1000, ), diff --git a/packages/kit/db/user.ts b/packages/kit/db/user.ts index 30130e32..4551de82 100644 --- a/packages/kit/db/user.ts +++ b/packages/kit/db/user.ts @@ -77,6 +77,7 @@ export const userRelations = { }, } as const; +// TODO: Remove this function and use what drizzle outputs directly instead of transforming it export const transformOutputToUserWithRelations = ( user: Omit, "endpoints"> & { followerCount: unknown; @@ -525,15 +526,15 @@ export class User extends BaseInterface { providers: { id: string; name: string; - url: string; + url: ProxiableUrl; icon?: ProxiableUrl; }[], ): Promise< { id: string; name: string; - url: string; - icon?: string | undefined; + url: ProxiableUrl; + icon?: ProxiableUrl; server_id: string; }[] > { @@ -556,7 +557,7 @@ export class User extends BaseInterface { id: issuer.id, name: issuer.name, url: issuer.url, - icon: issuer.icon?.proxied, + icon: issuer.icon, server_id: account.serverId, }; }) diff --git a/packages/kit/tables/migrations/0051_stiff_morbius.sql b/packages/kit/tables/migrations/0051_stiff_morbius.sql new file mode 100644 index 00000000..23745656 --- /dev/null +++ b/packages/kit/tables/migrations/0051_stiff_morbius.sql @@ -0,0 +1,46 @@ +CREATE TABLE "AuthorizationCodes" ( + "code" text PRIMARY KEY NOT NULL, + "scopes" text[] DEFAULT ARRAY[]::text[] NOT NULL, + "redirect_uri" text, + "expires_at" timestamp(3) NOT NULL, + "created_at" timestamp(3) DEFAULT now() NOT NULL, + "code_challenge" text, + "code_challenge_method" text, + "userId" uuid NOT NULL, + "clientId" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "Tokens" RENAME COLUMN "applicationId" TO "clientId";--> statement-breakpoint +--ALTER TABLE "Notes" DROP CONSTRAINT "Notes_applicationId_Applications_id_fk"; +--> statement-breakpoint +--ALTER TABLE "OpenIdLoginFlows" DROP CONSTRAINT "OpenIdLoginFlows_applicationId_Applications_id_fk"; +--> statement-breakpoint +--ALTER TABLE "Tokens" DROP CONSTRAINT "Tokens_applicationId_Applications_id_fk"; +--> statement-breakpoint +DROP INDEX "Applications_client_id_index";--> statement-breakpoint +ALTER TABLE "Applications" ADD PRIMARY KEY ("client_id");--> statement-breakpoint +ALTER TABLE "Applications" ALTER COLUMN "scopes" SET DATA TYPE text[] USING (string_to_array("scopes", ' ')::text[]);--> statement-breakpoint +ALTER TABLE "Applications" ALTER COLUMN "scopes" SET DEFAULT ARRAY[]::text[];--> statement-breakpoint +ALTER TABLE "Notes" ALTER COLUMN "applicationId" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" ALTER COLUMN "applicationId" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "Applications" ADD COLUMN "redirect_uris" text[] DEFAULT ARRAY[]::text[] NOT NULL;--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" ADD COLUMN "state" text;--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" ADD COLUMN "client_state" text;--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" ADD COLUMN "client_redirect_uri" text;--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" ADD COLUMN "client_scopes" text[];--> statement-breakpoint +ALTER TABLE "Tokens" ADD COLUMN "scopes" text[] DEFAULT ARRAY[]::text[] NOT NULL;--> statement-breakpoint +ALTER TABLE "AuthorizationCodes" ADD CONSTRAINT "AuthorizationCodes_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."Users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "AuthorizationCodes" ADD CONSTRAINT "AuthorizationCodes_clientId_Applications_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."Applications"("client_id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Notes" ADD CONSTRAINT "Notes_applicationId_Applications_client_id_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."Applications"("client_id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" ADD CONSTRAINT "OpenIdLoginFlows_applicationId_Applications_client_id_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."Applications"("client_id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Tokens" ALTER COLUMN "clientId" SET DATA TYPE text;--> statement-breakpoint +ALTER TABLE "Tokens" ADD CONSTRAINT "Tokens_clientId_Applications_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."Applications"("client_id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Applications" DROP COLUMN "id";--> statement-breakpoint +ALTER TABLE "Applications" DROP COLUMN "vapid_key";--> statement-breakpoint +ALTER TABLE "Applications" DROP COLUMN "redirect_uri";--> statement-breakpoint +ALTER TABLE "Tokens" DROP COLUMN "token_type";--> statement-breakpoint +ALTER TABLE "Tokens" DROP COLUMN "scope";--> statement-breakpoint +ALTER TABLE "Tokens" DROP COLUMN "code";--> statement-breakpoint +ALTER TABLE "Tokens" DROP COLUMN "client_id";--> statement-breakpoint +ALTER TABLE "Tokens" DROP COLUMN "redirect_uri";--> statement-breakpoint +ALTER TABLE "Tokens" DROP COLUMN "id_token"; diff --git a/packages/kit/tables/migrations/meta/0051_snapshot.json b/packages/kit/tables/migrations/meta/0051_snapshot.json new file mode 100644 index 00000000..6f8fb3cd --- /dev/null +++ b/packages/kit/tables/migrations/meta/0051_snapshot.json @@ -0,0 +1,2439 @@ +{ + "id": "62ba1f05-306f-4b31-a4c0-935d6946634e", + "prevId": "13fdf09c-59ef-4e1e-9e59-f87676154810", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.AuthorizationCodes": { + "name": "AuthorizationCodes", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "AuthorizationCodes_userId_Users_id_fk": { + "name": "AuthorizationCodes_userId_Users_id_fk", + "tableFrom": "AuthorizationCodes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "AuthorizationCodes_clientId_Applications_client_id_fk": { + "name": "AuthorizationCodes_clientId_Applications_client_id_fk", + "tableFrom": "AuthorizationCodes", + "tableTo": "Applications", + "columnsFrom": ["clientId"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Challenges": { + "name": "Challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "challenge": { + "name": "challenge", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "NOW() + INTERVAL '5 minutes'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Applications": { + "name": "Applications", + "schema": "", + "columns": { + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EmojiToNote": { + "name": "EmojiToNote", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToNote_emojiId_noteId_index": { + "name": "EmojiToNote_emojiId_noteId_index", + "columns": [ + { + "expression": "emojiId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EmojiToNote_noteId_index": { + "name": "EmojiToNote_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EmojiToNote_emojiId_Emojis_id_fk": { + "name": "EmojiToNote_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToNote_noteId_Notes_id_fk": { + "name": "EmojiToNote_noteId_Notes_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EmojiToUser": { + "name": "EmojiToUser", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToUser_emojiId_userId_index": { + "name": "EmojiToUser_emojiId_userId_index", + "columns": [ + { + "expression": "emojiId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EmojiToUser_userId_index": { + "name": "EmojiToUser_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EmojiToUser_emojiId_Emojis_id_fk": { + "name": "EmojiToUser_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToUser_userId_Users_id_fk": { + "name": "EmojiToUser_userId_Users_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Emojis": { + "name": "Emojis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mediaId": { + "name": "mediaId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visible_in_picker": { + "name": "visible_in_picker", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Emojis_mediaId_Medias_id_fk": { + "name": "Emojis_mediaId_Medias_id_fk", + "tableFrom": "Emojis", + "tableTo": "Medias", + "columnsFrom": ["mediaId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Emojis_instanceId_Instances_id_fk": { + "name": "Emojis_instanceId_Instances_id_fk", + "tableFrom": "Emojis", + "tableTo": "Instances", + "columnsFrom": ["instanceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Emojis_ownerId_Users_id_fk": { + "name": "Emojis_ownerId_Users_id_fk", + "tableFrom": "Emojis", + "tableTo": "Users", + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.FilterKeywords": { + "name": "FilterKeywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "filterId": { + "name": "filterId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "whole_word": { + "name": "whole_word", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "FilterKeywords_filterId_Filters_id_fk": { + "name": "FilterKeywords_filterId_Filters_id_fk", + "tableFrom": "FilterKeywords", + "tableTo": "Filters", + "columnsFrom": ["filterId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Filters": { + "name": "Filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter_action": { + "name": "filter_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Filters_userId_Users_id_fk": { + "name": "Filters_userId_Users_id_fk", + "tableFrom": "Filters", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Flags": { + "name": "Flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "flag_type": { + "name": "flag_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Flags_noteId_Notes_id_fk": { + "name": "Flags_noteId_Notes_id_fk", + "tableFrom": "Flags", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Flags_userId_Users_id_fk": { + "name": "Flags_userId_Users_id_fk", + "tableFrom": "Flags", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Instances": { + "name": "Instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'versia'" + }, + "inbox": { + "name": "inbox", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "extensions": { + "name": "extensions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Likes": { + "name": "Likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "likerId": { + "name": "likerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "likedId": { + "name": "likedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Likes_likerId_Users_id_fk": { + "name": "Likes_likerId_Users_id_fk", + "tableFrom": "Likes", + "tableTo": "Users", + "columnsFrom": ["likerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Likes_likedId_Notes_id_fk": { + "name": "Likes_likedId_Notes_id_fk", + "tableFrom": "Likes", + "tableTo": "Notes", + "columnsFrom": ["likedId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Likes_uri_unique": { + "name": "Likes_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Markers": { + "name": "Markers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notificationId": { + "name": "notificationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Markers_noteId_Notes_id_fk": { + "name": "Markers_noteId_Notes_id_fk", + "tableFrom": "Markers", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_notificationId_Notifications_id_fk": { + "name": "Markers_notificationId_Notifications_id_fk", + "tableFrom": "Markers", + "tableTo": "Notifications", + "columnsFrom": ["notificationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_userId_Users_id_fk": { + "name": "Markers_userId_Users_id_fk", + "tableFrom": "Markers", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Medias": { + "name": "Medias", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "original_content": { + "name": "original_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "blurhash": { + "name": "blurhash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.MediasToNote": { + "name": "MediasToNote", + "schema": "", + "columns": { + "mediaId": { + "name": "mediaId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "MediasToNote_mediaId_index": { + "name": "MediasToNote_mediaId_index", + "columns": [ + { + "expression": "mediaId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MediasToNote_noteId_index": { + "name": "MediasToNote_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MediasToNote_mediaId_Medias_id_fk": { + "name": "MediasToNote_mediaId_Medias_id_fk", + "tableFrom": "MediasToNote", + "tableTo": "Medias", + "columnsFrom": ["mediaId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MediasToNote_noteId_Notes_id_fk": { + "name": "MediasToNote_noteId_Notes_id_fk", + "tableFrom": "MediasToNote", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ModNotes": { + "name": "ModNotes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModNotes_noteId_Notes_id_fk": { + "name": "ModNotes_noteId_Notes_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_userId_Users_id_fk": { + "name": "ModNotes_userId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_modId_Users_id_fk": { + "name": "ModNotes_modId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": ["modId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ModTags": { + "name": "ModTags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModTags_noteId_Notes_id_fk": { + "name": "ModTags_noteId_Notes_id_fk", + "tableFrom": "ModTags", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_userId_Users_id_fk": { + "name": "ModTags_userId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_modId_Users_id_fk": { + "name": "ModTags_modId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": ["modId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.NoteToMentions": { + "name": "NoteToMentions", + "schema": "", + "columns": { + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "NoteToMentions_noteId_userId_index": { + "name": "NoteToMentions_noteId_userId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "NoteToMentions_userId_index": { + "name": "NoteToMentions_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "NoteToMentions_noteId_Notes_id_fk": { + "name": "NoteToMentions_noteId_Notes_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "NoteToMentions_userId_Users_id_fk": { + "name": "NoteToMentions_userId_Users_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Notes": { + "name": "Notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reblogId": { + "name": "reblogId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text/plain'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reblog_count": { + "name": "reblog_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "like_count": { + "name": "like_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reply_count": { + "name": "reply_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "replyId": { + "name": "replyId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quoteId": { + "name": "quoteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spoiler_text": { + "name": "spoiler_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "Notes_authorId_Users_id_fk": { + "name": "Notes_authorId_Users_id_fk", + "tableFrom": "Notes", + "tableTo": "Users", + "columnsFrom": ["authorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_reblogId_Notes_id_fk": { + "name": "Notes_reblogId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["reblogId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_replyId_Notes_id_fk": { + "name": "Notes_replyId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["replyId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_quoteId_Notes_id_fk": { + "name": "Notes_quoteId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["quoteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_applicationId_Applications_client_id_fk": { + "name": "Notes_applicationId_Applications_client_id_fk", + "tableFrom": "Notes", + "tableTo": "Applications", + "columnsFrom": ["applicationId"], + "columnsTo": ["client_id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Notes_uri_unique": { + "name": "Notes_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Notifications": { + "name": "Notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notifiedId": { + "name": "notifiedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dismissed": { + "name": "dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "Notifications_notifiedId_Users_id_fk": { + "name": "Notifications_notifiedId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": ["notifiedId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_accountId_Users_id_fk": { + "name": "Notifications_accountId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": ["accountId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_noteId_Notes_id_fk": { + "name": "Notifications_noteId_Notes_id_fk", + "tableFrom": "Notifications", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpenIdAccounts": { + "name": "OpenIdAccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdAccounts_userId_Users_id_fk": { + "name": "OpenIdAccounts_userId_Users_id_fk", + "tableFrom": "OpenIdAccounts", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpenIdLoginFlows": { + "name": "OpenIdLoginFlows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_state": { + "name": "client_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_redirect_uri": { + "name": "client_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_scopes": { + "name": "client_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdLoginFlows_applicationId_Applications_client_id_fk": { + "name": "OpenIdLoginFlows_applicationId_Applications_client_id_fk", + "tableFrom": "OpenIdLoginFlows", + "tableTo": "Applications", + "columnsFrom": ["applicationId"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.PushSubscriptions": { + "name": "PushSubscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_secret": { + "name": "auth_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alerts": { + "name": "alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tokenId": { + "name": "tokenId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "PushSubscriptions_tokenId_Tokens_id_fk": { + "name": "PushSubscriptions_tokenId_Tokens_id_fk", + "tableFrom": "PushSubscriptions", + "tableTo": "Tokens", + "columnsFrom": ["tokenId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "PushSubscriptions_tokenId_unique": { + "name": "PushSubscriptions_tokenId_unique", + "nullsNotDistinct": false, + "columns": ["tokenId"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Reaction": { + "name": "Reaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "emoji_text": { + "name": "emoji_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Reaction_emojiId_Emojis_id_fk": { + "name": "Reaction_emojiId_Emojis_id_fk", + "tableFrom": "Reaction", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Reaction_noteId_Notes_id_fk": { + "name": "Reaction_noteId_Notes_id_fk", + "tableFrom": "Reaction", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Reaction_authorId_Users_id_fk": { + "name": "Reaction_authorId_Users_id_fk", + "tableFrom": "Reaction", + "tableTo": "Users", + "columnsFrom": ["authorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Reaction_uri_unique": { + "name": "Reaction_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Relationships": { + "name": "Relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subjectId": { + "name": "subjectId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "showing_reblogs": { + "name": "showing_reblogs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "notifying": { + "name": "notifying", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blocking": { + "name": "blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting": { + "name": "muting", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting_notifications": { + "name": "muting_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "requested": { + "name": "requested", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "domain_blocking": { + "name": "domain_blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "endorsed": { + "name": "endorsed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Relationships_ownerId_Users_id_fk": { + "name": "Relationships_ownerId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Relationships_subjectId_Users_id_fk": { + "name": "Relationships_subjectId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": ["subjectId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RoleToUsers": { + "name": "RoleToUsers", + "schema": "", + "columns": { + "roleId": { + "name": "roleId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "RoleToUsers_roleId_Roles_id_fk": { + "name": "RoleToUsers_roleId_Roles_id_fk", + "tableFrom": "RoleToUsers", + "tableTo": "Roles", + "columnsFrom": ["roleId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RoleToUsers_userId_Users_id_fk": { + "name": "RoleToUsers_userId_Users_id_fk", + "tableFrom": "RoleToUsers", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Roles": { + "name": "Roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Tokens": { + "name": "Tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Tokens_userId_Users_id_fk": { + "name": "Tokens_userId_Users_id_fk", + "tableFrom": "Tokens", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Tokens_clientId_Applications_client_id_fk": { + "name": "Tokens_clientId_Applications_client_id_fk", + "tableFrom": "Tokens", + "tableTo": "Applications", + "columnsFrom": ["clientId"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.UserToPinnedNotes": { + "name": "UserToPinnedNotes", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UserToPinnedNotes_userId_noteId_index": { + "name": "UserToPinnedNotes_userId_noteId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UserToPinnedNotes_noteId_index": { + "name": "UserToPinnedNotes_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "UserToPinnedNotes_userId_Users_id_fk": { + "name": "UserToPinnedNotes_userId_Users_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "UserToPinnedNotes_noteId_Notes_id_fk": { + "name": "UserToPinnedNotes_noteId_Notes_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Users": { + "name": "Users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verification_token": { + "name": "email_verification_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_reset_token": { + "name": "password_reset_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "endpoints": { + "name": "endpoints", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "avatarId": { + "name": "avatarId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "headerId": { + "name": "headerId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "follower_count": { + "name": "follower_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "following_count": { + "name": "following_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status_count": { + "name": "status_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_discoverable": { + "name": "is_discoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_hiding_collections": { + "name": "is_hiding_collections", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_indexable": { + "name": "is_indexable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sanctions": { + "name": "sanctions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Users_uri_index": { + "name": "Users_uri_index", + "columns": [ + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Users_username_index": { + "name": "Users_username_index", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Users_email_index": { + "name": "Users_email_index", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Users_avatarId_Medias_id_fk": { + "name": "Users_avatarId_Medias_id_fk", + "tableFrom": "Users", + "tableTo": "Medias", + "columnsFrom": ["avatarId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Users_headerId_Medias_id_fk": { + "name": "Users_headerId_Medias_id_fk", + "tableFrom": "Users", + "tableTo": "Medias", + "columnsFrom": ["headerId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Users_instanceId_Instances_id_fk": { + "name": "Users_instanceId_Instances_id_fk", + "tableFrom": "Users", + "tableTo": "Instances", + "columnsFrom": ["instanceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Users_uri_unique": { + "name": "Users_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/kit/tables/migrations/meta/_journal.json b/packages/kit/tables/migrations/meta/_journal.json index b4ba6c21..180cdd3f 100644 --- a/packages/kit/tables/migrations/meta/_journal.json +++ b/packages/kit/tables/migrations/meta/_journal.json @@ -358,6 +358,13 @@ "when": 1746368175263, "tag": "0050_thick_lester", "breakpoints": true + }, + { + "idx": 51, + "version": "7", + "when": 1755729662013, + "tag": "0051_stiff_morbius", + "breakpoints": true } ] } diff --git a/packages/kit/tables/schema.ts b/packages/kit/tables/schema.ts index 7f0c8e17..5d243179 100644 --- a/packages/kit/tables/schema.ts +++ b/packages/kit/tables/schema.ts @@ -28,6 +28,7 @@ import { import type { z } from "zod/v4"; const createdAt = () => + // TODO: Change mode to Date timestamp("created_at", { precision: 3, mode: "string" }) .defaultNow() .notNull(); @@ -39,7 +40,7 @@ const updatedAt = () => const uri = () => text("uri").unique(); -const id = () => uuid("id").primaryKey().notNull(); +const id = () => uuid("id").primaryKey(); export const Challenges = pgTable("Challenges", { id: id(), @@ -308,47 +309,41 @@ export const RelationshipsRelations = relations(Relationships, ({ one }) => ({ }), })); -export const Applications = pgTable( - "Applications", - { - id: id(), - name: text("name").notNull(), - website: text("website"), - vapidKey: text("vapid_key"), - clientId: text("client_id").notNull(), - secret: text("secret").notNull(), - scopes: text("scopes").notNull(), - redirectUri: text("redirect_uri").notNull(), - }, - (table) => [uniqueIndex().on(table.clientId)], -); +export const Clients = pgTable("Applications", { + id: text("client_id").primaryKey(), + secret: text("secret").notNull(), + redirectUris: text("redirect_uris") + .array() + .notNull() + .default(sql`ARRAY[]::text[]`), + scopes: text("scopes").array().notNull().default(sql`ARRAY[]::text[]`), + name: text("name").notNull(), + website: text("website"), +}); -export const ApplicationsRelations = relations(Applications, ({ many }) => ({ +export const ClientsRelations = relations(Clients, ({ many }) => ({ tokens: many(Tokens), loginFlows: many(OpenIdLoginFlows), })); export const Tokens = pgTable("Tokens", { id: id(), - tokenType: text("token_type").notNull(), - scope: text("scope").notNull(), + scopes: text("scopes").array().notNull().default(sql`ARRAY[]::text[]`), accessToken: text("access_token").notNull(), - code: text("code"), expiresAt: timestamp("expires_at", { precision: 3, mode: "string" }), createdAt: createdAt(), - clientId: text("client_id").notNull().default(""), - redirectUri: text("redirect_uri").notNull().default(""), - idToken: text("id_token"), userId: uuid("userId") .references(() => Users.id, { onDelete: "cascade", onUpdate: "cascade", }) .notNull(), - applicationId: uuid("applicationId").references(() => Applications.id, { - onDelete: "cascade", - onUpdate: "cascade", - }), + clientId: text("clientId") + .references(() => Clients.id, { + onDelete: "cascade", + onUpdate: "cascade", + }) + .notNull(), }); export const TokensRelations = relations(Tokens, ({ one }) => ({ @@ -356,12 +351,51 @@ export const TokensRelations = relations(Tokens, ({ one }) => ({ fields: [Tokens.userId], references: [Users.id], }), - application: one(Applications, { - fields: [Tokens.applicationId], - references: [Applications.id], + client: one(Clients, { + fields: [Tokens.clientId], + references: [Clients.id], }), })); +export const AuthorizationCodes = pgTable("AuthorizationCodes", { + code: text("code").primaryKey(), + scopes: text("scopes").array().notNull().default(sql`ARRAY[]::text[]`), + redirectUri: text("redirect_uri"), + expiresAt: timestamp("expires_at", { + precision: 3, + mode: "string", + }).notNull(), + createdAt: createdAt(), + codeChallenge: text("code_challenge"), + codeChallengeMethod: text("code_challenge_method"), + userId: uuid("userId") + .references(() => Users.id, { + onDelete: "cascade", + onUpdate: "cascade", + }) + .notNull(), + clientId: text("clientId") + .references(() => Clients.id, { + onDelete: "cascade", + onUpdate: "cascade", + }) + .notNull(), +}); + +export const AuthorizationCodesRelations = relations( + AuthorizationCodes, + ({ one }) => ({ + user: one(Users, { + fields: [AuthorizationCodes.userId], + references: [Users.id], + }), + client: one(Clients, { + fields: [AuthorizationCodes.clientId], + references: [Clients.id], + }), + }), +); + export const Medias = pgTable("Medias", { id: id(), content: jsonb("content") @@ -460,7 +494,7 @@ export const Notes = pgTable("Notes", { }), sensitive: boolean("sensitive").notNull().default(false), spoilerText: text("spoiler_text").default("").notNull(), - applicationId: uuid("applicationId").references(() => Applications.id, { + applicationId: text("applicationId").references(() => Clients.id, { onDelete: "set null", onUpdate: "cascade", }), @@ -494,9 +528,9 @@ export const NotesRelations = relations(Notes, ({ many, one }) => ({ references: [Notes.id], relationName: "NoteToQuotes", }), - application: one(Applications, { + application: one(Clients, { fields: [Notes.applicationId], - references: [Applications.id], + references: [Clients.id], }), quotes: many(Notes, { relationName: "NoteToQuotes", @@ -665,7 +699,11 @@ export const UsersRelations = relations(Users, ({ many, one }) => ({ export const OpenIdLoginFlows = pgTable("OpenIdLoginFlows", { id: id(), codeVerifier: text("code_verifier").notNull(), - applicationId: uuid("applicationId").references(() => Applications.id, { + state: text("state"), + clientState: text("client_state"), + clientRedirectUri: text("client_redirect_uri"), + clientScopes: text("client_scopes").array(), + applicationId: text("applicationId").references(() => Clients.id, { onDelete: "cascade", onUpdate: "cascade", }), @@ -675,9 +713,9 @@ export const OpenIdLoginFlows = pgTable("OpenIdLoginFlows", { export const OpenIdLoginFlowsRelations = relations( OpenIdLoginFlows, ({ one }) => ({ - application: one(Applications, { + application: one(Clients, { fields: [OpenIdLoginFlows.applicationId], - references: [Applications.id], + references: [Clients.id], }), }), ); diff --git a/packages/tests/index.ts b/packages/tests/index.ts index 96468b82..c64cefa4 100644 --- a/packages/tests/index.ts +++ b/packages/tests/index.ts @@ -1,7 +1,14 @@ import { mock } from "bun:test"; import { Client as VersiaClient } from "@versia/client"; import { config } from "@versia-server/config"; -import { db, Note, setupDatabase, Token, User } from "@versia-server/kit/db"; +import { + Application, + db, + Note, + setupDatabase, + Token, + User, +} from "@versia-server/kit/db"; import { searchManager } from "@versia-server/kit/search"; import { Notes, Users } from "@versia-server/kit/tables"; import { solveChallenge } from "altcha-lib"; @@ -43,15 +50,21 @@ export const generateClient = async ( dbToken: Token; } > => { + const application = await Application.insert({ + id: randomUUIDv7(), + name: "Versia", + redirectUris: [], + scopes: ["openid", "profile", "email"], + secret: "", + }); + const token = user ? await Token.insert({ id: randomUUIDv7(), accessToken: randomString(32, "hex"), - tokenType: "bearer", userId: user.id, - applicationId: null, - code: randomString(32, "hex"), - scope: "read write follow push", + clientId: application.id, + scopes: ["read", "write", "follow", "push"], }) : null; @@ -71,6 +84,7 @@ export const generateClient = async ( // @ts-expect-error This is REAL monkeypatching done by REAL programmers, BITCH! client[Symbol.asyncDispose] = async (): Promise => { await token?.delete(); + await application.delete(); }; // @ts-expect-error More monkeypatching @@ -97,6 +111,14 @@ export const getTestUsers = async ( const users: User[] = []; const passwords: string[] = []; + const application = await Application.insert({ + id: randomUUIDv7(), + name: "Versia", + redirectUris: [], + scopes: ["openid", "profile", "email"], + secret: "", + }); + for (let i = 0; i < count; i++) { const password = randomString(32, "hex"); @@ -119,9 +141,9 @@ export const getTestUsers = async ( accessToken: randomString(32, "hex"), tokenType: "bearer", userId: u.id, - applicationId: null, + clientId: application.id, code: randomString(32, "hex"), - scope: "read write follow push", + scopes: ["read", "write", "follow", "push"], })), ); @@ -140,6 +162,7 @@ export const getTestUsers = async ( users.map((u) => u.id), ), ); + await application.delete(); }, }; }; diff --git a/utils/bull-board.ts b/utils/bull-board.ts index 56603839..5abcbeb2 100644 --- a/utils/bull-board.ts +++ b/utils/bull-board.ts @@ -14,8 +14,7 @@ import { relationshipQueue } from "@versia-server/kit/queues/relationships"; import type { Hono } from "hono"; import { serveStatic } from "hono/bun"; import { getCookie } from "hono/cookie"; -import { jwtVerify } from "jose"; -import { JOSEError, JWTExpired } from "jose/errors"; +import { verify } from "hono/jwt"; import type { HonoEnv } from "~/types/api"; import pkg from "../package.json" with { type: "json" }; @@ -58,38 +57,18 @@ export const applyToHono = (app: Hono): void => { throw new ApiError(401, "Missing JWT cookie"); } - const result = await jwtVerify( + const result = await verify( jwtCookie, config.authentication.keys.public, - { - algorithms: ["EdDSA"], - issuer: new URL(context.get("config").http.base_url).origin, - }, - ).catch((error) => { - if (error instanceof JOSEError) { - return error; - } + ); - throw error; - }); - - if (result instanceof JOSEError) { - if (result instanceof JWTExpired) { - throw new ApiError(401, "JWT has expired"); - } - - throw new ApiError(401, "Invalid JWT"); - } - - const { - payload: { sub }, - } = result; + const { sub } = result; if (!sub) { throw new ApiError(401, "Invalid JWT (no sub)"); } - const user = await User.fromId(sub); + const user = await User.fromId(sub as string); if (!user?.hasPermission(RolePermission.ManageInstanceFederation)) { throw new ApiError( diff --git a/utils/lib.ts b/utils/lib.ts index a5cb0dac..fe265bb8 100644 --- a/utils/lib.ts +++ b/utils/lib.ts @@ -9,3 +9,6 @@ export const mergeAndDeduplicate = ( (element, index, self) => index === self.findIndex((t) => t.id === element.id), ); + +export const oauthRedirectUri = (baseUrl: URL, issuer: string): URL => + new URL(`/oauth/sso/${issuer}/callback`, baseUrl); From 6f97903f3b7420f0b0b42631224bb95aba786f20 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 21 Aug 2025 01:15:38 +0200 Subject: [PATCH 03/11] fix(api): :white_check_mark: Fix all failing tests --- config/config.example.toml | 6 +- packages/api/routes/api/auth/login/index.ts | 2 +- packages/api/routes/oauth.test.ts | 92 +-------------- .../api/routes/{api => }/oauth/revoke.test.ts | 0 packages/api/routes/{api => }/oauth/revoke.ts | 0 .../{api => }/oauth/sso/[issuer]/callback.ts | 2 +- .../{api => }/oauth/sso/[issuer]/index.ts | 0 .../api/routes/{api => }/oauth/token.test.ts | 64 ++++++----- packages/api/routes/{api => }/oauth/token.ts | 13 ++- packages/config/index.ts | 106 +++++++++--------- utils/bull-board.ts | 5 +- 11 files changed, 111 insertions(+), 179 deletions(-) rename packages/api/routes/{api => }/oauth/revoke.test.ts (100%) rename packages/api/routes/{api => }/oauth/revoke.ts (100%) rename packages/api/routes/{api => }/oauth/sso/[issuer]/callback.ts (99%) rename packages/api/routes/{api => }/oauth/sso/[issuer]/index.ts (100%) rename packages/api/routes/{api => }/oauth/token.test.ts (77%) rename packages/api/routes/{api => }/oauth/token.ts (92%) diff --git a/config/config.example.toml b/config/config.example.toml index f19574a6..f7b231a7 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -466,10 +466,8 @@ forced_openid = false # If signups.registration is false, it will only be possible to register with OpenID openid_registration = true -# [authentication.keys] -# Run Versia Server with those values missing to generate a new key -# public = "" -# private = "" +# Run Versia Server with this value missing to generate a new key +# key = "" # The provider MUST support OpenID Connect with .well-known discovery # Most notably, GitHub does not support this diff --git a/packages/api/routes/api/auth/login/index.ts b/packages/api/routes/api/auth/login/index.ts index 8db55a49..5d74a943 100644 --- a/packages/api/routes/api/auth/login/index.ts +++ b/packages/api/routes/api/auth/login/index.ts @@ -153,7 +153,7 @@ export default apiRoute((app) => iat: Math.floor(Date.now() / 1000), nbf: Math.floor(Date.now() / 1000), }, - config.authentication.keys.private, + config.authentication.key, ); const application = await Application.fromClientId(client_id); diff --git a/packages/api/routes/oauth.test.ts b/packages/api/routes/oauth.test.ts index ec96a0bc..153647a2 100644 --- a/packages/api/routes/oauth.test.ts +++ b/packages/api/routes/oauth.test.ts @@ -1,17 +1,11 @@ import { afterAll, describe, expect, test } from "bun:test"; -import type { Token } from "@versia/client/schemas"; import { fakeRequest, generateClient, getTestUsers, } from "@versia-server/tests"; -import type { z } from "zod/v4"; let clientId: string; -let clientSecret: string; -let code: string; -let jwt: string; -let token: z.infer; const { users, passwords, deleteUsers } = await getTestUsers(1); afterAll(async () => { @@ -41,7 +35,6 @@ describe("Login flow", () => { }); clientId = data.client_id; - clientSecret = data.client_secret; }); test("should get a JWT", async () => { @@ -60,89 +53,8 @@ describe("Login flow", () => { expect(response.status).toBe(302); - jwt = - response.headers.get("Set-Cookie")?.match(/jwt=([^;]+);/)?.[1] ?? - ""; + //jwt = response.headers.get("Set-Cookie")?.match(/jwt=([^;]+);/)?.[1] ?? ""; }); - test("should get a code", async () => { - const response = await fakeRequest("/oauth/authorize", { - method: "POST", - headers: { - Cookie: `jwt=${jwt}`, - }, - body: new URLSearchParams({ - client_id: clientId, - client_secret: clientSecret, - redirect_uri: "https://example.com", - response_type: "code", - scope: "read write", - max_age: "604800", - }), - }); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - response.headers.get("Location") ?? "", - "", - ); - - expect(locationHeader.origin).toBe("https://example.com"); - - code = locationHeader.searchParams.get("code") ?? ""; - }); - - test("should get an access token", async () => { - const response = await fakeRequest("/oauth/token", { - method: "POST", - headers: { - Authorization: `Bearer ${jwt}`, - "Content-Type": "application/x-www-form-urlencoded", - }, - body: new URLSearchParams({ - grant_type: "authorization_code", - code, - redirect_uri: "https://example.com", - client_id: clientId, - client_secret: clientSecret, - scope: "read write", - }), - }); - - const json = await response.json(); - - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toContain( - "application/json", - ); - expect(json).toEqual({ - access_token: expect.any(String), - token_type: "Bearer", - scope: "read write", - created_at: expect.any(Number), - expires_in: expect.any(Number), - id_token: null, - refresh_token: null, - }); - - token = json; - }); - - test("should return the authenticated application's credentials", async () => { - const client = await generateClient(users[0]); - - const { ok, data } = await client.verifyAppCredentials({ - headers: { - Authorization: `Bearer ${token.access_token}`, - }, - }); - - expect(ok).toBe(true); - - const credentials = data; - - expect(credentials.name).toBe("Test Application"); - expect(credentials.website).toBe("https://example.com"); - }); + // TODO: Test full flow including OpenID part }); diff --git a/packages/api/routes/api/oauth/revoke.test.ts b/packages/api/routes/oauth/revoke.test.ts similarity index 100% rename from packages/api/routes/api/oauth/revoke.test.ts rename to packages/api/routes/oauth/revoke.test.ts diff --git a/packages/api/routes/api/oauth/revoke.ts b/packages/api/routes/oauth/revoke.ts similarity index 100% rename from packages/api/routes/api/oauth/revoke.ts rename to packages/api/routes/oauth/revoke.ts diff --git a/packages/api/routes/api/oauth/sso/[issuer]/callback.ts b/packages/api/routes/oauth/sso/[issuer]/callback.ts similarity index 99% rename from packages/api/routes/api/oauth/sso/[issuer]/callback.ts rename to packages/api/routes/oauth/sso/[issuer]/callback.ts index ef7b7b2d..7cf1757e 100644 --- a/packages/api/routes/api/oauth/sso/[issuer]/callback.ts +++ b/packages/api/routes/oauth/sso/[issuer]/callback.ts @@ -286,7 +286,7 @@ export default apiRoute((app) => { iat: Math.floor(Date.now() / 1000), nbf: Math.floor(Date.now() / 1000), }, - config.authentication.keys.private, + config.authentication.key, ); // Redirect back to application diff --git a/packages/api/routes/api/oauth/sso/[issuer]/index.ts b/packages/api/routes/oauth/sso/[issuer]/index.ts similarity index 100% rename from packages/api/routes/api/oauth/sso/[issuer]/index.ts rename to packages/api/routes/oauth/sso/[issuer]/index.ts diff --git a/packages/api/routes/api/oauth/token.test.ts b/packages/api/routes/oauth/token.test.ts similarity index 77% rename from packages/api/routes/api/oauth/token.test.ts rename to packages/api/routes/oauth/token.test.ts index 27d5f23c..ebba81ff 100644 --- a/packages/api/routes/api/oauth/token.test.ts +++ b/packages/api/routes/oauth/token.test.ts @@ -1,7 +1,10 @@ import { afterAll, describe, expect, test } from "bun:test"; -import { Application, Token } from "@versia-server/kit/db"; +import { Application, db } from "@versia-server/kit/db"; import { fakeRequest, getTestUsers } from "@versia-server/tests"; import { randomUUIDv7 } from "bun"; +import { eq } from "drizzle-orm"; +import { randomString } from "@/math"; +import { AuthorizationCodes } from "~/packages/kit/tables/schema"; const { deleteUsers, users } = await getTestUsers(1); @@ -13,19 +16,25 @@ const application = await Application.insert({ name: "Test Application", }); -const token = await Token.insert({ - id: randomUUIDv7(), - clientId: application.data.id, - accessToken: "test-access-token", - expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(), - createdAt: new Date().toISOString(), - userId: users[0].id, -}); +const authorizationCode = ( + await db + .insert(AuthorizationCodes) + .values({ + clientId: application.id, + code: randomString(10), + redirectUri: application.data.redirectUris[0], + userId: users[0].id, + expiresAt: new Date(Date.now() + 300 * 1000).toISOString(), + }) + .returning() +)[0]; afterAll(async () => { await deleteUsers(); await application.delete(); - await token.delete(); + await db + .delete(AuthorizationCodes) + .where(eq(AuthorizationCodes.code, authorizationCode.code)); }); describe("/oauth/token", () => { @@ -37,7 +46,7 @@ describe("/oauth/token", () => { }, body: JSON.stringify({ grant_type: "authorization_code", - code: "test-code", + code: authorizationCode.code, redirect_uri: application.data.redirectUris[0], client_id: application.data.id, client_secret: application.data.secret, @@ -46,9 +55,9 @@ describe("/oauth/token", () => { expect(response.status).toBe(200); const body = await response.json(); - expect(body.access_token).toBe("test-access-token"); + expect(body.access_token).toBeString(); expect(body.token_type).toBe("Bearer"); - expect(body.expires_in).toBeGreaterThan(0); + expect(body.expires_in).toBeNull(); }); test("should return error for missing code", async () => { @@ -65,10 +74,9 @@ describe("/oauth/token", () => { }), }); - expect(response.status).toBe(401); + expect(response.status).toBe(422); const body = await response.json(); - expect(body.error).toBe("invalid_request"); - expect(body.error_description).toBe("Code is required"); + expect(body.error).toInclude(`Expected string at "code"`); }); test("should return error for missing redirect_uri", async () => { @@ -79,16 +87,15 @@ describe("/oauth/token", () => { }, body: JSON.stringify({ grant_type: "authorization_code", - code: "test-code", + code: authorizationCode.code, client_id: application.data.id, client_secret: application.data.secret, }), }); - expect(response.status).toBe(401); + expect(response.status).toBe(422); const body = await response.json(); - expect(body.error).toBe("invalid_request"); - expect(body.error_description).toBe("Redirect URI is required"); + expect(body.error).toInclude(`Expected string at "redirect_uri"`); }); test("should return error for missing client_id", async () => { @@ -99,16 +106,15 @@ describe("/oauth/token", () => { }, body: JSON.stringify({ grant_type: "authorization_code", - code: "test-code", + code: authorizationCode.code, redirect_uri: application.data.redirectUris[0], client_secret: application.data.secret, }), }); - expect(response.status).toBe(401); + expect(response.status).toBe(422); const body = await response.json(); - expect(body.error).toBe("invalid_request"); - expect(body.error_description).toBe("Client ID is required"); + expect(body.error).toInclude(`Expected string at "client_id"`); }); test("should return error for invalid client credentials", async () => { @@ -119,7 +125,7 @@ describe("/oauth/token", () => { }, body: JSON.stringify({ grant_type: "authorization_code", - code: "test-code", + code: authorizationCode.code, redirect_uri: application.data.redirectUris[0], client_id: application.data.id, client_secret: "invalid-secret", @@ -147,10 +153,12 @@ describe("/oauth/token", () => { }), }); - expect(response.status).toBe(401); + expect(response.status).toBe(404); const body = await response.json(); expect(body.error).toBe("invalid_grant"); - expect(body.error_description).toBe("Code not found"); + expect(body.error_description).toBe( + "Authorization code not found or expired", + ); }); test("should return error for unsupported grant type", async () => { @@ -161,7 +169,7 @@ describe("/oauth/token", () => { }, body: JSON.stringify({ grant_type: "refresh_token", - code: "test-code", + code: authorizationCode.code, redirect_uri: application.data.redirectUris[0], client_id: application.data.id, client_secret: application.data.secret, diff --git a/packages/api/routes/api/oauth/token.ts b/packages/api/routes/oauth/token.ts similarity index 92% rename from packages/api/routes/api/oauth/token.ts rename to packages/api/routes/oauth/token.ts index ac78b130..f6d0d0e8 100644 --- a/packages/api/routes/api/oauth/token.ts +++ b/packages/api/routes/oauth/token.ts @@ -63,9 +63,19 @@ export default apiRoute((app) => { handleZodError, ), async (context) => { - const { code, client_id, client_secret, redirect_uri } = + const { code, client_id, client_secret, redirect_uri, grant_type } = context.req.valid("json"); + if (grant_type !== "authorization_code") { + return context.json( + { + error: "unsupported_grant_type", + error_description: "Unsupported grant type", + }, + 401, + ); + } + // Verify the client_secret const client = await Application.fromClientId(client_id); @@ -108,6 +118,7 @@ export default apiRoute((app) => { clientId: client.id, id: randomUUIDv7(), userId: authorizationCode.userId, + expiresAt: null, }); // Invalidate the code diff --git a/packages/config/index.ts b/packages/config/index.ts index 8b54e7f0..664f1e02 100644 --- a/packages/config/index.ts +++ b/packages/config/index.ts @@ -6,7 +6,6 @@ import ISO6391 from "iso-639-1"; import { types as mimeTypes } from "mime-types"; import { generateVAPIDKeys } from "web-push"; import { z } from "zod/v4"; -import { fromZodError } from "zod-validation-error"; export class ProxiableUrl extends URL { private isAllowedOrigin(): boolean { @@ -174,9 +173,10 @@ export const keyPair = z await crypto.subtle.exportKey("spki", keys.publicKey), ).toString("base64"); - ctx.addIssue({ + ctx.issues.push({ code: "custom", - error: `Public and private keys are not set. Here are generated keys for you to copy.\n\nPublic: ${publicKey}\nPrivate: ${privateKey}`, + message: `Public and private keys are not set. Here are generated keys for you to copy.\n\nPublic: ${publicKey}\nPrivate: ${privateKey}`, + input: k, }); return z.NEVER; @@ -194,9 +194,10 @@ export const keyPair = z ["verify"], ); } catch { - ctx.addIssue({ + ctx.issues.push({ code: "custom", - error: "Public key is invalid", + message: "Public key is invalid", + input: k, }); return z.NEVER; @@ -211,9 +212,10 @@ export const keyPair = z ["sign"], ); } catch { - ctx.addIssue({ + ctx.issues.push({ code: "custom", - error: "Private key is invalid", + message: "Private key is invalid", + input: k, }); return z.NEVER; @@ -235,9 +237,10 @@ export const vapidKeyPair = z if (!(k?.public && k?.private)) { const keys = generateVAPIDKeys(); - ctx.addIssue({ + ctx.issues.push({ code: "custom", - error: `VAPID keys are not set. Here are generated keys for you to copy.\n\nPublic: ${keys.publicKey}\nPrivate: ${keys.privateKey}`, + message: `VAPID keys are not set. Here are generated keys for you to copy.\n\nPublic: ${keys.publicKey}\nPrivate: ${keys.privateKey}`, + input: k, }); return z.NEVER; @@ -246,51 +249,55 @@ export const vapidKeyPair = z return k; }); -export const hmacKey = sensitiveString.transform(async (text, ctx) => { - if (!text) { - const key = await crypto.subtle.generateKey( - { - name: "HMAC", - hash: "SHA-256", - }, - true, - ["sign"], - ); +export const hmacKey = sensitiveString + .optional() + .transform(async (text, ctx) => { + if (!text) { + const key = await crypto.subtle.generateKey( + { + name: "HMAC", + hash: "SHA-256", + }, + true, + ["sign"], + ); - const exported = await crypto.subtle.exportKey("raw", key); + const exported = await crypto.subtle.exportKey("raw", key); - const base64 = Buffer.from(exported).toString("base64"); + const base64 = Buffer.from(exported).toString("base64"); - ctx.addIssue({ - code: "custom", - error: `HMAC key is not set. Here is a generated key for you to copy: ${base64}`, - }); + ctx.issues.push({ + code: "custom", + message: `HMAC key is not set. Here is a generated key for you to copy: ${base64}`, + input: text, + }); - return z.NEVER; - } + return z.NEVER; + } - try { - await crypto.subtle.importKey( - "raw", - Buffer.from(text, "base64"), - { - name: "HMAC", - hash: "SHA-256", - }, - true, - ["sign"], - ); - } catch { - ctx.addIssue({ - code: "custom", - error: "HMAC key is invalid", - }); + try { + await crypto.subtle.importKey( + "raw", + Buffer.from(text, "base64"), + { + name: "HMAC", + hash: "SHA-256", + }, + true, + ["sign"], + ); + } catch { + ctx.issues.push({ + code: "custom", + message: "HMAC key is invalid", + input: text, + }); - return z.NEVER; - } + return z.NEVER; + } - return text; -}); + return text; + }); export const ConfigSchema = z .strictObject({ @@ -807,7 +814,7 @@ export const ConfigSchema = z ) .default([]), openid_registration: z.boolean().default(true), - keys: keyPair, + key: hmacKey, }), }) .refine( @@ -840,9 +847,8 @@ if (!parsed.success) { console.error( "⚠ Here is the error message, please fix the configuration file accordingly:", ); - const errorMessage = fromZodError(parsed.error).message; - console.info(errorMessage); + console.info(z.prettifyError(parsed.error)); throw new Error("Configuration file is invalid."); } diff --git a/utils/bull-board.ts b/utils/bull-board.ts index 5abcbeb2..ab778c67 100644 --- a/utils/bull-board.ts +++ b/utils/bull-board.ts @@ -57,10 +57,7 @@ export const applyToHono = (app: Hono): void => { throw new ApiError(401, "Missing JWT cookie"); } - const result = await verify( - jwtCookie, - config.authentication.keys.public, - ); + const result = await verify(jwtCookie, config.authentication.key); const { sub } = result; From 1a0a27bee1a8db102d4648147de7b76e568c951b Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Thu, 21 Aug 2025 01:21:32 +0200 Subject: [PATCH 04/11] refactor(database): :truck: Rename Application to Client everywhere --- cli/user/token.ts | 4 +- drizzle.config.ts | 10 +- .../api/routes/api/auth/login/index.test.ts | 6 +- packages/api/routes/api/auth/login/index.ts | 4 +- packages/api/routes/api/v1/apps/index.ts | 4 +- .../api/v1/apps/verify_credentials/index.ts | 4 +- packages/api/routes/api/v1/sso/index.ts | 6 +- packages/api/routes/api/v1/statuses/index.ts | 2 +- packages/api/routes/oauth.test.ts | 6 +- packages/api/routes/oauth/revoke.test.ts | 4 +- .../api/routes/oauth/sso/[issuer]/callback.ts | 18 +- .../api/routes/oauth/sso/[issuer]/index.ts | 6 +- packages/api/routes/oauth/token.test.ts | 4 +- packages/api/routes/oauth/token.ts | 4 +- packages/kit/api.ts | 4 +- packages/kit/db/application.ts | 48 +- packages/kit/db/index.ts | 2 +- packages/kit/db/note.ts | 12 +- packages/kit/db/token.ts | 4 +- .../0052_complete_hellfire_club.sql | 15 + .../tables/migrations/meta/0052_snapshot.json | 2439 +++++++++++++++++ .../kit/tables/migrations/meta/_journal.json | 7 + packages/kit/tables/schema.ts | 14 +- packages/tests/index.ts | 8 +- types/api.ts | 4 +- 25 files changed, 2549 insertions(+), 90 deletions(-) create mode 100644 packages/kit/tables/migrations/0052_complete_hellfire_club.sql create mode 100644 packages/kit/tables/migrations/meta/0052_snapshot.json diff --git a/cli/user/token.ts b/cli/user/token.ts index d65f364c..6e1109e0 100644 --- a/cli/user/token.ts +++ b/cli/user/token.ts @@ -1,4 +1,4 @@ -import { Application, Token } from "@versia-server/kit/db"; +import { Client, Token } from "@versia-server/kit/db"; import { randomUUIDv7 } from "bun"; import chalk from "chalk"; // @ts-expect-error - Root import is required or the Clec type definitions won't work @@ -22,7 +22,7 @@ export const generateTokenCommand = defineCommand( throw new Error(`User ${chalk.gray(username)} not found.`); } - const application = await Application.insert({ + const application = await Client.insert({ id: user.id + Buffer.from( diff --git a/drizzle.config.ts b/drizzle.config.ts index faa519fe..7aaaacaf 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,4 +1,4 @@ -//import { config } from "@versia-server/config"; +import { config } from "@versia-server/config"; import type { Config } from "drizzle-kit"; /** @@ -10,16 +10,16 @@ export default { out: "./packages/kit/tables/migrations", schema: "./packages/kit/tables/schema.ts", dbCredentials: { - host: "localhost", + /* host: "localhost", port: 40000, user: "lysand", password: "lysand", - database: "lysand", - /* host: config.postgres.host, + database: "lysand", */ + host: config.postgres.host, port: config.postgres.port, user: config.postgres.username, password: config.postgres.password, - database: config.postgres.database, */ + database: config.postgres.database, }, // Print all statements verbose: true, diff --git a/packages/api/routes/api/auth/login/index.test.ts b/packages/api/routes/api/auth/login/index.test.ts index 5dc4c705..dc0f08c0 100644 --- a/packages/api/routes/api/auth/login/index.test.ts +++ b/packages/api/routes/api/auth/login/index.test.ts @@ -1,15 +1,15 @@ import { afterAll, describe, expect, test } from "bun:test"; import { config } from "@versia-server/config"; -import { Application } from "@versia-server/kit/db"; +import { Client } from "@versia-server/kit/db"; import { fakeRequest, getTestUsers } from "@versia-server/tests"; import { randomString } from "@/math"; const { users, deleteUsers, passwords } = await getTestUsers(1); // Create application -const application = await Application.insert({ +const application = await Client.insert({ id: randomString(32, "hex"), - name: "Test Application", + name: "Test Client", secret: "test", redirectUris: ["https://example.com"], scopes: ["read", "write"], diff --git a/packages/api/routes/api/auth/login/index.ts b/packages/api/routes/api/auth/login/index.ts index 5d74a943..f953fc0d 100644 --- a/packages/api/routes/api/auth/login/index.ts +++ b/packages/api/routes/api/auth/login/index.ts @@ -1,7 +1,7 @@ import { config } from "@versia-server/config"; import { ApiError } from "@versia-server/kit"; import { apiRoute, handleZodError } from "@versia-server/kit/api"; -import { Application, User } from "@versia-server/kit/db"; +import { Client, User } from "@versia-server/kit/db"; import { Users } from "@versia-server/kit/tables"; import { password as bunPassword } from "bun"; import { eq, or } from "drizzle-orm"; @@ -156,7 +156,7 @@ export default apiRoute((app) => config.authentication.key, ); - const application = await Application.fromClientId(client_id); + const application = await Client.fromClientId(client_id); if (!application) { throw new ApiError(400, "Invalid application"); diff --git a/packages/api/routes/api/v1/apps/index.ts b/packages/api/routes/api/v1/apps/index.ts index 818a93b1..b8f6fb29 100644 --- a/packages/api/routes/api/v1/apps/index.ts +++ b/packages/api/routes/api/v1/apps/index.ts @@ -4,7 +4,7 @@ import { } from "@versia/client/schemas"; import { ApiError } from "@versia-server/kit"; import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api"; -import { Application } from "@versia-server/kit/db"; +import { Client } from "@versia-server/kit/db"; import { describeRoute, resolver, validator } from "hono-openapi"; import { z } from "zod/v4"; import { randomString } from "@/math"; @@ -62,7 +62,7 @@ export default apiRoute((app) => const { client_name, redirect_uris, scopes, website } = context.req.valid("json"); - const app = await Application.insert({ + const app = await Client.insert({ id: randomString(32, "base64url"), name: client_name, redirectUris: Array.isArray(redirect_uris) diff --git a/packages/api/routes/api/v1/apps/verify_credentials/index.ts b/packages/api/routes/api/v1/apps/verify_credentials/index.ts index 9f42f8dd..336f94ad 100644 --- a/packages/api/routes/api/v1/apps/verify_credentials/index.ts +++ b/packages/api/routes/api/v1/apps/verify_credentials/index.ts @@ -4,7 +4,7 @@ import { } from "@versia/client/schemas"; import { ApiError } from "@versia-server/kit"; import { apiRoute, auth } from "@versia-server/kit/api"; -import { Application } from "@versia-server/kit/db"; +import { Client } from "@versia-server/kit/db"; import { describeRoute, resolver } from "hono-openapi"; export default apiRoute((app) => @@ -38,7 +38,7 @@ export default apiRoute((app) => async (context) => { const { token } = context.get("auth"); - const application = await Application.getFromToken( + const application = await Client.getFromToken( token.data.accessToken, ); diff --git a/packages/api/routes/api/v1/sso/index.ts b/packages/api/routes/api/v1/sso/index.ts index c1fb1af3..0f88bfe4 100644 --- a/packages/api/routes/api/v1/sso/index.ts +++ b/packages/api/routes/api/v1/sso/index.ts @@ -2,7 +2,7 @@ import { RolePermission } from "@versia/client/schemas"; import { config } from "@versia-server/config"; import { ApiError } from "@versia-server/kit"; import { apiRoute, auth, handleZodError } from "@versia-server/kit/api"; -import { Application, db } from "@versia-server/kit/db"; +import { Client, db } from "@versia-server/kit/db"; import { OpenIdLoginFlows } from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { describeRoute, resolver, validator } from "hono-openapi"; @@ -123,7 +123,7 @@ export default apiRoute((app) => { issuerId, ); - const application = await Application.insert({ + const application = await Client.insert({ id: user.id + Buffer.from( @@ -144,7 +144,7 @@ export default apiRoute((app) => { codeVerifier, state: parameters.state, issuerId, - applicationId: application.id, + clientId: application.id, }) .returning() )[0]; diff --git a/packages/api/routes/api/v1/statuses/index.ts b/packages/api/routes/api/v1/statuses/index.ts index 7b2a52ca..cbd97496 100644 --- a/packages/api/routes/api/v1/statuses/index.ts +++ b/packages/api/routes/api/v1/statuses/index.ts @@ -249,7 +249,7 @@ export default apiRoute((app) => spoilerText: sanitizedSpoilerText, replyId: in_reply_to_id ?? undefined, quotingId: quote_id ?? undefined, - applicationId: application?.id, + clientId: application?.id, contentSource: status, contentType: content_type, }); diff --git a/packages/api/routes/oauth.test.ts b/packages/api/routes/oauth.test.ts index 153647a2..66f79e4e 100644 --- a/packages/api/routes/oauth.test.ts +++ b/packages/api/routes/oauth.test.ts @@ -13,10 +13,10 @@ afterAll(async () => { }); describe("Login flow", () => { - test("should create an application", async () => { + test("should create a client", async () => { const client = await generateClient(users[0]); - const { ok, data } = await client.createApp("Test Application", { + const { ok, data } = await client.createApp("Test Client", { redirect_uris: "https://example.com", website: "https://example.com", scopes: ["read", "write"], @@ -24,7 +24,7 @@ describe("Login flow", () => { expect(ok).toBe(true); expect(data).toEqual({ - name: "Test Application", + name: "Test Client", website: "https://example.com", client_id: expect.any(String), client_secret: expect.any(String), diff --git a/packages/api/routes/oauth/revoke.test.ts b/packages/api/routes/oauth/revoke.test.ts index e7d19afb..836c514b 100644 --- a/packages/api/routes/oauth/revoke.test.ts +++ b/packages/api/routes/oauth/revoke.test.ts @@ -1,11 +1,11 @@ import { afterAll, describe, expect, test } from "bun:test"; -import { Application, Token } from "@versia-server/kit/db"; +import { Client, Token } from "@versia-server/kit/db"; import { fakeRequest, getTestUsers } from "@versia-server/tests"; import { randomUUIDv7 } from "bun"; const { deleteUsers, users } = await getTestUsers(1); -const application = await Application.insert({ +const application = await Client.insert({ id: randomUUIDv7(), redirectUris: ["https://example.com/callback"], scopes: ["openid", "profile", "email"], diff --git a/packages/api/routes/oauth/sso/[issuer]/callback.ts b/packages/api/routes/oauth/sso/[issuer]/callback.ts index 7cf1757e..c3a23e50 100644 --- a/packages/api/routes/oauth/sso/[issuer]/callback.ts +++ b/packages/api/routes/oauth/sso/[issuer]/callback.ts @@ -69,7 +69,7 @@ export default apiRoute((app) => { const flow = await db.query.OpenIdLoginFlows.findFirst({ where: (flow): SQL | undefined => eq(flow.id, flowId), with: { - application: true, + client: true, }, }); @@ -129,11 +129,11 @@ export default apiRoute((app) => { // If linking account if (link && user_id) { // Check if userId is equal to application.clientId - if (!flow.application?.id.startsWith(user_id)) { + if (!flow.client?.id.startsWith(user_id)) { return redirectWithMessage( { oidc_account_linking_error: "Account linking error", - oidc_account_linking_error_message: `User ID does not match application client ID (${user_id} != ${flow.application?.id})`, + oidc_account_linking_error_message: `User ID does not match application client ID (${user_id} != ${flow.client?.id})`, }, config.frontend.routes.home, ); @@ -262,14 +262,14 @@ export default apiRoute((app) => { }); } - if (!flow.application) { + if (!flow.client) { throw new ApiError(500, "Application not found"); } const code = randomString(32, "hex"); await db.insert(AuthorizationCodes).values({ - clientId: flow.application.id, + clientId: flow.client.id, code, expiresAt: new Date(Date.now() + 60 * 1000).toISOString(), // 1 minute redirectUri: flow.clientRedirectUri ?? undefined, @@ -281,7 +281,7 @@ export default apiRoute((app) => { { sub: user.id, iss: new URL(context.get("config").http.base_url).origin, - aud: flow.application.id, + aud: flow.client.id, exp: Math.floor(Date.now() / 1000) + 60 * 60, iat: Math.floor(Date.now() / 1000), nbf: Math.floor(Date.now() / 1000), @@ -303,9 +303,9 @@ export default apiRoute((app) => { { redirect_uri: flow.clientRedirectUri ?? undefined, code, - client_id: flow.application.id, - application: flow.application.name, - website: flow.application.website ?? "", + client_id: flow.client.id, + application: flow.client.name, + website: flow.client.website ?? "", scope: flow.clientScopes?.join(" "), state: flow.clientState ?? undefined, }, diff --git a/packages/api/routes/oauth/sso/[issuer]/index.ts b/packages/api/routes/oauth/sso/[issuer]/index.ts index 4b4bf9df..79eb7ec5 100644 --- a/packages/api/routes/oauth/sso/[issuer]/index.ts +++ b/packages/api/routes/oauth/sso/[issuer]/index.ts @@ -1,7 +1,7 @@ import { config } from "@versia-server/config"; import { ApiError } from "@versia-server/kit"; import { apiRoute, handleZodError } from "@versia-server/kit/api"; -import { Application, db } from "@versia-server/kit/db"; +import { Client, db } from "@versia-server/kit/db"; import { OpenIdLoginFlows } from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { describeRoute, validator } from "hono-openapi"; @@ -54,7 +54,7 @@ export default apiRoute((app) => { throw new ApiError(422, "Unknown or invalid issuer"); } - const application = await Application.fromClientId(client_id); + const application = await Client.fromClientId(client_id); if (!application) { throw new ApiError(422, "Unknown or invalid client_id"); @@ -98,7 +98,7 @@ export default apiRoute((app) => { clientState: state, clientRedirectUri: redirect_uri, clientScopes: scopes, - applicationId: application.id, + clientId: application.id, issuerId, }) .returning() diff --git a/packages/api/routes/oauth/token.test.ts b/packages/api/routes/oauth/token.test.ts index ebba81ff..d0dd74c1 100644 --- a/packages/api/routes/oauth/token.test.ts +++ b/packages/api/routes/oauth/token.test.ts @@ -1,5 +1,5 @@ import { afterAll, describe, expect, test } from "bun:test"; -import { Application, db } from "@versia-server/kit/db"; +import { Client, db } from "@versia-server/kit/db"; import { fakeRequest, getTestUsers } from "@versia-server/tests"; import { randomUUIDv7 } from "bun"; import { eq } from "drizzle-orm"; @@ -8,7 +8,7 @@ import { AuthorizationCodes } from "~/packages/kit/tables/schema"; const { deleteUsers, users } = await getTestUsers(1); -const application = await Application.insert({ +const application = await Client.insert({ id: randomUUIDv7(), redirectUris: ["https://example.com/callback"], scopes: ["openid", "profile", "email"], diff --git a/packages/api/routes/oauth/token.ts b/packages/api/routes/oauth/token.ts index f6d0d0e8..de7a4ca3 100644 --- a/packages/api/routes/oauth/token.ts +++ b/packages/api/routes/oauth/token.ts @@ -1,6 +1,6 @@ import { Token as TokenSchema } from "@versia/client/schemas"; import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api"; -import { Application, db, Token } from "@versia-server/kit/db"; +import { Client, db, Token } from "@versia-server/kit/db"; import { AuthorizationCodes } from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { and, eq } from "drizzle-orm"; @@ -77,7 +77,7 @@ export default apiRoute((app) => { } // Verify the client_secret - const client = await Application.fromClientId(client_id); + const client = await Client.fromClientId(client_id); if (!client || client.data.secret !== client_secret) { return context.json( diff --git a/packages/kit/api.ts b/packages/kit/api.ts index 876b6f80..72baff53 100644 --- a/packages/kit/api.ts +++ b/packages/kit/api.ts @@ -14,7 +14,7 @@ import { type ZodAny, ZodError, z } from "zod/v4"; import { fromZodError } from "zod-validation-error"; import type { AuthData, HonoEnv } from "~/types/api"; import { ApiError } from "./api-error.ts"; -import { Application } from "./db/application.ts"; +import { Client } from "./db/application.ts"; import { Emoji } from "./db/emoji.ts"; import { Note } from "./db/note.ts"; import { Token } from "./db/token.ts"; @@ -170,7 +170,7 @@ export const auth = (options: { const auth: AuthData = { token, application: token?.data.client - ? new Application(token?.data.client) + ? new Client(token?.data.client) : null, user: (await token?.getUser()) ?? null, }; diff --git a/packages/kit/db/application.ts b/packages/kit/db/application.ts index 84920d41..b23697af 100644 --- a/packages/kit/db/application.ts +++ b/packages/kit/db/application.ts @@ -16,37 +16,37 @@ import { Clients } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; import { Token } from "./token.ts"; -type ApplicationType = InferSelectModel; +type ClientType = InferSelectModel; -export class Application extends BaseInterface { - public static $type: ApplicationType; +export class Client extends BaseInterface { + public static $type: ClientType; public async reload(): Promise { - const reloaded = await Application.fromId(this.data.id); + const reloaded = await Client.fromId(this.data.id); if (!reloaded) { - throw new Error("Failed to reload application"); + throw new Error("Failed to reload client"); } this.data = reloaded.data; } - public static async fromId(id: string | null): Promise { + public static async fromId(id: string | null): Promise { if (!id) { return null; } - return await Application.fromSql(eq(Clients.id, id)); + return await Client.fromSql(eq(Clients.id, id)); } - public static async fromIds(ids: string[]): Promise { - return await Application.manyFromSql(inArray(Clients.id, ids)); + public static async fromIds(ids: string[]): Promise { + return await Client.manyFromSql(inArray(Clients.id, ids)); } public static async fromSql( sql: SQL | undefined, orderBy: SQL | undefined = desc(Clients.id), - ): Promise { + ): Promise { const found = await db.query.Clients.findFirst({ where: sql, orderBy, @@ -55,7 +55,7 @@ export class Application extends BaseInterface { if (!found) { return null; } - return new Application(found); + return new Client(found); } public static async manyFromSql( @@ -64,7 +64,7 @@ export class Application extends BaseInterface { limit?: number, offset?: number, extra?: Parameters[0], - ): Promise { + ): Promise { const found = await db.query.Clients.findMany({ where: sql, orderBy, @@ -73,30 +73,28 @@ export class Application extends BaseInterface { with: extra?.with, }); - return found.map((s) => new Application(s)); + return found.map((s) => new Client(s)); } - public static async getFromToken( - token: string, - ): Promise { + public static async getFromToken(token: string): Promise { const result = await Token.fromAccessToken(token); - return result?.data.client ? new Application(result.data.client) : null; + return result?.data.client ? new Client(result.data.client) : null; } - public static fromClientId(clientId: string): Promise { - return Application.fromSql(eq(Clients.id, clientId)); + public static fromClientId(clientId: string): Promise { + return Client.fromSql(eq(Clients.id, clientId)); } public async update( - newApplication: Partial, - ): Promise { + newApplication: Partial, + ): Promise { await db .update(Clients) .set(newApplication) .where(eq(Clients.id, this.id)); - const updated = await Application.fromId(this.data.id); + const updated = await Client.fromId(this.data.id); if (!updated) { throw new Error("Failed to update application"); @@ -106,7 +104,7 @@ export class Application extends BaseInterface { return updated.data; } - public save(): Promise { + public save(): Promise { return this.update(this.data); } @@ -120,10 +118,10 @@ export class Application extends BaseInterface { public static async insert( data: InferInsertModel, - ): Promise { + ): Promise { const inserted = (await db.insert(Clients).values(data).returning())[0]; - const application = await Application.fromId(inserted.id); + const application = await Client.fromId(inserted.id); if (!application) { throw new Error("Failed to insert application"); diff --git a/packages/kit/db/index.ts b/packages/kit/db/index.ts index 67f52bcf..827d8199 100644 --- a/packages/kit/db/index.ts +++ b/packages/kit/db/index.ts @@ -1,5 +1,5 @@ export { db, setupDatabase } from "../tables/db.ts"; -export { Application } from "./application.ts"; +export { Client } from "./application.ts"; export { Emoji } from "./emoji.ts"; export { Instance } from "./instance.ts"; export { Like } from "./like.ts"; diff --git a/packages/kit/db/note.ts b/packages/kit/db/note.ts index 73589953..68dc0880 100644 --- a/packages/kit/db/note.ts +++ b/packages/kit/db/note.ts @@ -36,7 +36,7 @@ import { Notifications, Users, } from "../tables/schema.ts"; -import { Application } from "./application.ts"; +import { Client } from "./application.ts"; import { BaseInterface } from "./base.ts"; import { Emoji } from "./emoji.ts"; import { Instance } from "./instance.ts"; @@ -129,7 +129,7 @@ const findManyNotes = async ( }, }, likes: true, - application: true, + client: true, mentions: { with: { user: { @@ -238,7 +238,7 @@ type NoteTypeWithRelations = NoteType & { emojis: (typeof Emoji.$type)[]; reply: NoteType | null; quote: NoteType | null; - application: typeof Application.$type | null; + client: typeof Client.$type | null; pinned: boolean; reblogged: boolean; muted: boolean; @@ -514,7 +514,7 @@ export class Note extends BaseInterface { visibility, sensitive: false, updatedAt: new Date().toISOString(), - applicationId: null, + clientId: null, uri: uri?.href, }); @@ -1162,8 +1162,8 @@ export class Note extends BaseInterface { in_reply_to_account_id: data.reply?.authorId || null, account: this.author.toApi(userFetching?.id === data.authorId), created_at: new Date(data.createdAt).toISOString(), - application: data.application - ? new Application(data.application).toApi() + application: data.client + ? new Client(data.client).toApi() : undefined, card: null, content: replacedContent, diff --git a/packages/kit/db/token.ts b/packages/kit/db/token.ts index 7fd4620d..a0ec9900 100644 --- a/packages/kit/db/token.ts +++ b/packages/kit/db/token.ts @@ -10,12 +10,12 @@ import { import type { z } from "zod/v4"; import { db } from "../tables/db.ts"; import { Tokens } from "../tables/schema.ts"; -import type { Application } from "./application.ts"; +import type { Client } from "./application.ts"; import { BaseInterface } from "./base.ts"; import { User } from "./user.ts"; type TokenType = InferSelectModel & { - client: typeof Application.$type; + client: typeof Client.$type; }; export class Token extends BaseInterface { diff --git a/packages/kit/tables/migrations/0052_complete_hellfire_club.sql b/packages/kit/tables/migrations/0052_complete_hellfire_club.sql new file mode 100644 index 00000000..eb5a69cc --- /dev/null +++ b/packages/kit/tables/migrations/0052_complete_hellfire_club.sql @@ -0,0 +1,15 @@ +ALTER TABLE "Applications" RENAME TO "Clients";--> statement-breakpoint +ALTER TABLE "Notes" RENAME COLUMN "applicationId" TO "clientId";--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" RENAME COLUMN "applicationId" TO "clientId";--> statement-breakpoint +ALTER TABLE "AuthorizationCodes" DROP CONSTRAINT "AuthorizationCodes_clientId_Applications_client_id_fk"; +--> statement-breakpoint +ALTER TABLE "Notes" DROP CONSTRAINT "Notes_applicationId_Applications_client_id_fk"; +--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" DROP CONSTRAINT "OpenIdLoginFlows_applicationId_Applications_client_id_fk"; +--> statement-breakpoint +ALTER TABLE "Tokens" DROP CONSTRAINT "Tokens_clientId_Applications_client_id_fk"; +--> statement-breakpoint +ALTER TABLE "AuthorizationCodes" ADD CONSTRAINT "AuthorizationCodes_clientId_Clients_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."Clients"("client_id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Notes" ADD CONSTRAINT "Notes_clientId_Clients_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."Clients"("client_id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "OpenIdLoginFlows" ADD CONSTRAINT "OpenIdLoginFlows_clientId_Clients_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."Clients"("client_id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint +ALTER TABLE "Tokens" ADD CONSTRAINT "Tokens_clientId_Clients_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."Clients"("client_id") ON DELETE cascade ON UPDATE cascade; \ No newline at end of file diff --git a/packages/kit/tables/migrations/meta/0052_snapshot.json b/packages/kit/tables/migrations/meta/0052_snapshot.json new file mode 100644 index 00000000..70868abc --- /dev/null +++ b/packages/kit/tables/migrations/meta/0052_snapshot.json @@ -0,0 +1,2439 @@ +{ + "id": "a8476829-ca99-441b-9291-24259d57c2ac", + "prevId": "62ba1f05-306f-4b31-a4c0-935d6946634e", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.AuthorizationCodes": { + "name": "AuthorizationCodes", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "redirect_uri": { + "name": "redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "AuthorizationCodes_userId_Users_id_fk": { + "name": "AuthorizationCodes_userId_Users_id_fk", + "tableFrom": "AuthorizationCodes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "AuthorizationCodes_clientId_Clients_client_id_fk": { + "name": "AuthorizationCodes_clientId_Clients_client_id_fk", + "tableFrom": "AuthorizationCodes", + "tableTo": "Clients", + "columnsFrom": ["clientId"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Challenges": { + "name": "Challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "challenge": { + "name": "challenge", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "NOW() + INTERVAL '5 minutes'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Clients": { + "name": "Clients", + "schema": "", + "columns": { + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EmojiToNote": { + "name": "EmojiToNote", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToNote_emojiId_noteId_index": { + "name": "EmojiToNote_emojiId_noteId_index", + "columns": [ + { + "expression": "emojiId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EmojiToNote_noteId_index": { + "name": "EmojiToNote_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EmojiToNote_emojiId_Emojis_id_fk": { + "name": "EmojiToNote_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToNote_noteId_Notes_id_fk": { + "name": "EmojiToNote_noteId_Notes_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EmojiToUser": { + "name": "EmojiToUser", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToUser_emojiId_userId_index": { + "name": "EmojiToUser_emojiId_userId_index", + "columns": [ + { + "expression": "emojiId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "EmojiToUser_userId_index": { + "name": "EmojiToUser_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "EmojiToUser_emojiId_Emojis_id_fk": { + "name": "EmojiToUser_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToUser_userId_Users_id_fk": { + "name": "EmojiToUser_userId_Users_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Emojis": { + "name": "Emojis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mediaId": { + "name": "mediaId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visible_in_picker": { + "name": "visible_in_picker", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Emojis_mediaId_Medias_id_fk": { + "name": "Emojis_mediaId_Medias_id_fk", + "tableFrom": "Emojis", + "tableTo": "Medias", + "columnsFrom": ["mediaId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Emojis_instanceId_Instances_id_fk": { + "name": "Emojis_instanceId_Instances_id_fk", + "tableFrom": "Emojis", + "tableTo": "Instances", + "columnsFrom": ["instanceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Emojis_ownerId_Users_id_fk": { + "name": "Emojis_ownerId_Users_id_fk", + "tableFrom": "Emojis", + "tableTo": "Users", + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.FilterKeywords": { + "name": "FilterKeywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "filterId": { + "name": "filterId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "whole_word": { + "name": "whole_word", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "FilterKeywords_filterId_Filters_id_fk": { + "name": "FilterKeywords_filterId_Filters_id_fk", + "tableFrom": "FilterKeywords", + "tableTo": "Filters", + "columnsFrom": ["filterId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Filters": { + "name": "Filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter_action": { + "name": "filter_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Filters_userId_Users_id_fk": { + "name": "Filters_userId_Users_id_fk", + "tableFrom": "Filters", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Flags": { + "name": "Flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "flag_type": { + "name": "flag_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Flags_noteId_Notes_id_fk": { + "name": "Flags_noteId_Notes_id_fk", + "tableFrom": "Flags", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Flags_userId_Users_id_fk": { + "name": "Flags_userId_Users_id_fk", + "tableFrom": "Flags", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Instances": { + "name": "Instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "protocol": { + "name": "protocol", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'versia'" + }, + "inbox": { + "name": "inbox", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "extensions": { + "name": "extensions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Likes": { + "name": "Likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "likerId": { + "name": "likerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "likedId": { + "name": "likedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Likes_likerId_Users_id_fk": { + "name": "Likes_likerId_Users_id_fk", + "tableFrom": "Likes", + "tableTo": "Users", + "columnsFrom": ["likerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Likes_likedId_Notes_id_fk": { + "name": "Likes_likedId_Notes_id_fk", + "tableFrom": "Likes", + "tableTo": "Notes", + "columnsFrom": ["likedId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Likes_uri_unique": { + "name": "Likes_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Markers": { + "name": "Markers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notificationId": { + "name": "notificationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Markers_noteId_Notes_id_fk": { + "name": "Markers_noteId_Notes_id_fk", + "tableFrom": "Markers", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_notificationId_Notifications_id_fk": { + "name": "Markers_notificationId_Notifications_id_fk", + "tableFrom": "Markers", + "tableTo": "Notifications", + "columnsFrom": ["notificationId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_userId_Users_id_fk": { + "name": "Markers_userId_Users_id_fk", + "tableFrom": "Markers", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Medias": { + "name": "Medias", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "original_content": { + "name": "original_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "thumbnail": { + "name": "thumbnail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "blurhash": { + "name": "blurhash", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.MediasToNote": { + "name": "MediasToNote", + "schema": "", + "columns": { + "mediaId": { + "name": "mediaId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "MediasToNote_mediaId_index": { + "name": "MediasToNote_mediaId_index", + "columns": [ + { + "expression": "mediaId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "MediasToNote_noteId_index": { + "name": "MediasToNote_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "MediasToNote_mediaId_Medias_id_fk": { + "name": "MediasToNote_mediaId_Medias_id_fk", + "tableFrom": "MediasToNote", + "tableTo": "Medias", + "columnsFrom": ["mediaId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "MediasToNote_noteId_Notes_id_fk": { + "name": "MediasToNote_noteId_Notes_id_fk", + "tableFrom": "MediasToNote", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ModNotes": { + "name": "ModNotes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModNotes_noteId_Notes_id_fk": { + "name": "ModNotes_noteId_Notes_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_userId_Users_id_fk": { + "name": "ModNotes_userId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_modId_Users_id_fk": { + "name": "ModNotes_modId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": ["modId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ModTags": { + "name": "ModTags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModTags_noteId_Notes_id_fk": { + "name": "ModTags_noteId_Notes_id_fk", + "tableFrom": "ModTags", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_userId_Users_id_fk": { + "name": "ModTags_userId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_modId_Users_id_fk": { + "name": "ModTags_modId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": ["modId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.NoteToMentions": { + "name": "NoteToMentions", + "schema": "", + "columns": { + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "NoteToMentions_noteId_userId_index": { + "name": "NoteToMentions_noteId_userId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "NoteToMentions_userId_index": { + "name": "NoteToMentions_userId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "NoteToMentions_noteId_Notes_id_fk": { + "name": "NoteToMentions_noteId_Notes_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "NoteToMentions_userId_Users_id_fk": { + "name": "NoteToMentions_userId_Users_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Notes": { + "name": "Notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reblogId": { + "name": "reblogId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text/plain'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reblog_count": { + "name": "reblog_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "like_count": { + "name": "like_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reply_count": { + "name": "reply_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "replyId": { + "name": "replyId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quoteId": { + "name": "quoteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spoiler_text": { + "name": "spoiler_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "Notes_authorId_Users_id_fk": { + "name": "Notes_authorId_Users_id_fk", + "tableFrom": "Notes", + "tableTo": "Users", + "columnsFrom": ["authorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_reblogId_Notes_id_fk": { + "name": "Notes_reblogId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["reblogId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_replyId_Notes_id_fk": { + "name": "Notes_replyId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["replyId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_quoteId_Notes_id_fk": { + "name": "Notes_quoteId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": ["quoteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_clientId_Clients_client_id_fk": { + "name": "Notes_clientId_Clients_client_id_fk", + "tableFrom": "Notes", + "tableTo": "Clients", + "columnsFrom": ["clientId"], + "columnsTo": ["client_id"], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Notes_uri_unique": { + "name": "Notes_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Notifications": { + "name": "Notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notifiedId": { + "name": "notifiedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dismissed": { + "name": "dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "Notifications_notifiedId_Users_id_fk": { + "name": "Notifications_notifiedId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": ["notifiedId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_accountId_Users_id_fk": { + "name": "Notifications_accountId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": ["accountId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_noteId_Notes_id_fk": { + "name": "Notifications_noteId_Notes_id_fk", + "tableFrom": "Notifications", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpenIdAccounts": { + "name": "OpenIdAccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdAccounts_userId_Users_id_fk": { + "name": "OpenIdAccounts_userId_Users_id_fk", + "tableFrom": "OpenIdAccounts", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.OpenIdLoginFlows": { + "name": "OpenIdLoginFlows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_state": { + "name": "client_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_redirect_uri": { + "name": "client_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_scopes": { + "name": "client_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdLoginFlows_clientId_Clients_client_id_fk": { + "name": "OpenIdLoginFlows_clientId_Clients_client_id_fk", + "tableFrom": "OpenIdLoginFlows", + "tableTo": "Clients", + "columnsFrom": ["clientId"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.PushSubscriptions": { + "name": "PushSubscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_secret": { + "name": "auth_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alerts": { + "name": "alerts", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "tokenId": { + "name": "tokenId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "PushSubscriptions_tokenId_Tokens_id_fk": { + "name": "PushSubscriptions_tokenId_Tokens_id_fk", + "tableFrom": "PushSubscriptions", + "tableTo": "Tokens", + "columnsFrom": ["tokenId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "PushSubscriptions_tokenId_unique": { + "name": "PushSubscriptions_tokenId_unique", + "nullsNotDistinct": false, + "columns": ["tokenId"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Reaction": { + "name": "Reaction", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "emoji_text": { + "name": "emoji_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Reaction_emojiId_Emojis_id_fk": { + "name": "Reaction_emojiId_Emojis_id_fk", + "tableFrom": "Reaction", + "tableTo": "Emojis", + "columnsFrom": ["emojiId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Reaction_noteId_Notes_id_fk": { + "name": "Reaction_noteId_Notes_id_fk", + "tableFrom": "Reaction", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Reaction_authorId_Users_id_fk": { + "name": "Reaction_authorId_Users_id_fk", + "tableFrom": "Reaction", + "tableTo": "Users", + "columnsFrom": ["authorId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Reaction_uri_unique": { + "name": "Reaction_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Relationships": { + "name": "Relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subjectId": { + "name": "subjectId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "showing_reblogs": { + "name": "showing_reblogs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "notifying": { + "name": "notifying", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blocking": { + "name": "blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting": { + "name": "muting", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting_notifications": { + "name": "muting_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "requested": { + "name": "requested", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "domain_blocking": { + "name": "domain_blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "endorsed": { + "name": "endorsed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Relationships_ownerId_Users_id_fk": { + "name": "Relationships_ownerId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": ["ownerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Relationships_subjectId_Users_id_fk": { + "name": "Relationships_subjectId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": ["subjectId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RoleToUsers": { + "name": "RoleToUsers", + "schema": "", + "columns": { + "roleId": { + "name": "roleId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "RoleToUsers_roleId_Roles_id_fk": { + "name": "RoleToUsers_roleId_Roles_id_fk", + "tableFrom": "RoleToUsers", + "tableTo": "Roles", + "columnsFrom": ["roleId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RoleToUsers_userId_Users_id_fk": { + "name": "RoleToUsers_userId_Users_id_fk", + "tableFrom": "RoleToUsers", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Roles": { + "name": "Roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visible": { + "name": "visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Tokens": { + "name": "Tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY[]::text[]" + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "clientId": { + "name": "clientId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Tokens_userId_Users_id_fk": { + "name": "Tokens_userId_Users_id_fk", + "tableFrom": "Tokens", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Tokens_clientId_Clients_client_id_fk": { + "name": "Tokens_clientId_Clients_client_id_fk", + "tableFrom": "Tokens", + "tableTo": "Clients", + "columnsFrom": ["clientId"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.UserToPinnedNotes": { + "name": "UserToPinnedNotes", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UserToPinnedNotes_userId_noteId_index": { + "name": "UserToPinnedNotes_userId_noteId_index", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "UserToPinnedNotes_noteId_index": { + "name": "UserToPinnedNotes_noteId_index", + "columns": [ + { + "expression": "noteId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "UserToPinnedNotes_userId_Users_id_fk": { + "name": "UserToPinnedNotes_userId_Users_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Users", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "UserToPinnedNotes_noteId_Notes_id_fk": { + "name": "UserToPinnedNotes_noteId_Notes_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Notes", + "columnsFrom": ["noteId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Users": { + "name": "Users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verification_token": { + "name": "email_verification_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password_reset_token": { + "name": "password_reset_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "endpoints": { + "name": "endpoints", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "avatarId": { + "name": "avatarId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "headerId": { + "name": "headerId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "follower_count": { + "name": "follower_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "following_count": { + "name": "following_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status_count": { + "name": "status_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_discoverable": { + "name": "is_discoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_hiding_collections": { + "name": "is_hiding_collections", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_indexable": { + "name": "is_indexable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sanctions": { + "name": "sanctions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Users_uri_index": { + "name": "Users_uri_index", + "columns": [ + { + "expression": "uri", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Users_username_index": { + "name": "Users_username_index", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "Users_email_index": { + "name": "Users_email_index", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "Users_avatarId_Medias_id_fk": { + "name": "Users_avatarId_Medias_id_fk", + "tableFrom": "Users", + "tableTo": "Medias", + "columnsFrom": ["avatarId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Users_headerId_Medias_id_fk": { + "name": "Users_headerId_Medias_id_fk", + "tableFrom": "Users", + "tableTo": "Medias", + "columnsFrom": ["headerId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Users_instanceId_Instances_id_fk": { + "name": "Users_instanceId_Instances_id_fk", + "tableFrom": "Users", + "tableTo": "Instances", + "columnsFrom": ["instanceId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "Users_uri_unique": { + "name": "Users_uri_unique", + "nullsNotDistinct": false, + "columns": ["uri"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/kit/tables/migrations/meta/_journal.json b/packages/kit/tables/migrations/meta/_journal.json index 180cdd3f..4e7ec191 100644 --- a/packages/kit/tables/migrations/meta/_journal.json +++ b/packages/kit/tables/migrations/meta/_journal.json @@ -365,6 +365,13 @@ "when": 1755729662013, "tag": "0051_stiff_morbius", "breakpoints": true + }, + { + "idx": 52, + "version": "7", + "when": 1755732000165, + "tag": "0052_complete_hellfire_club", + "breakpoints": true } ] } diff --git a/packages/kit/tables/schema.ts b/packages/kit/tables/schema.ts index 5d243179..e917f625 100644 --- a/packages/kit/tables/schema.ts +++ b/packages/kit/tables/schema.ts @@ -309,7 +309,7 @@ export const RelationshipsRelations = relations(Relationships, ({ one }) => ({ }), })); -export const Clients = pgTable("Applications", { +export const Clients = pgTable("Clients", { id: text("client_id").primaryKey(), secret: text("secret").notNull(), redirectUris: text("redirect_uris") @@ -494,7 +494,7 @@ export const Notes = pgTable("Notes", { }), sensitive: boolean("sensitive").notNull().default(false), spoilerText: text("spoiler_text").default("").notNull(), - applicationId: text("applicationId").references(() => Clients.id, { + clientId: text("clientId").references(() => Clients.id, { onDelete: "set null", onUpdate: "cascade", }), @@ -528,8 +528,8 @@ export const NotesRelations = relations(Notes, ({ many, one }) => ({ references: [Notes.id], relationName: "NoteToQuotes", }), - application: one(Clients, { - fields: [Notes.applicationId], + client: one(Clients, { + fields: [Notes.clientId], references: [Clients.id], }), quotes: many(Notes, { @@ -703,7 +703,7 @@ export const OpenIdLoginFlows = pgTable("OpenIdLoginFlows", { clientState: text("client_state"), clientRedirectUri: text("client_redirect_uri"), clientScopes: text("client_scopes").array(), - applicationId: text("applicationId").references(() => Clients.id, { + clientId: text("clientId").references(() => Clients.id, { onDelete: "cascade", onUpdate: "cascade", }), @@ -713,8 +713,8 @@ export const OpenIdLoginFlows = pgTable("OpenIdLoginFlows", { export const OpenIdLoginFlowsRelations = relations( OpenIdLoginFlows, ({ one }) => ({ - application: one(Clients, { - fields: [OpenIdLoginFlows.applicationId], + client: one(Clients, { + fields: [OpenIdLoginFlows.clientId], references: [Clients.id], }), }), diff --git a/packages/tests/index.ts b/packages/tests/index.ts index c64cefa4..ef8253dd 100644 --- a/packages/tests/index.ts +++ b/packages/tests/index.ts @@ -2,7 +2,7 @@ import { mock } from "bun:test"; import { Client as VersiaClient } from "@versia/client"; import { config } from "@versia-server/config"; import { - Application, + Client, db, Note, setupDatabase, @@ -50,7 +50,7 @@ export const generateClient = async ( dbToken: Token; } > => { - const application = await Application.insert({ + const application = await Client.insert({ id: randomUUIDv7(), name: "Versia", redirectUris: [], @@ -111,7 +111,7 @@ export const getTestUsers = async ( const users: User[] = []; const passwords: string[] = []; - const application = await Application.insert({ + const application = await Client.insert({ id: randomUUIDv7(), name: "Versia", redirectUris: [], @@ -182,7 +182,7 @@ export const getTestStatuses = async ( sensitive: false, updatedAt: new Date().toISOString(), visibility: "public", - applicationId: null, + clientId: null, ...partial, }); diff --git a/types/api.ts b/types/api.ts index ce9736c2..f708cb48 100644 --- a/types/api.ts +++ b/types/api.ts @@ -1,6 +1,6 @@ import type * as VersiaEntities from "@versia/sdk/entities"; import type { ConfigSchema } from "@versia-server/config"; -import type { Application, Token, User } from "@versia-server/kit/db"; +import type { Client, Token, User } from "@versia-server/kit/db"; import type { SocketAddress } from "bun"; import type { Hono } from "hono"; import type { RouterRoute } from "hono/types"; @@ -11,7 +11,7 @@ export type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"; export interface AuthData { user: User | null; token: Token | null; - application: Application | null; + application: Client | null; } export type HonoEnv = { From bfa7a0695873ea00c192d6c36af584215b9d0f34 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 24 Oct 2025 18:19:22 +0200 Subject: [PATCH 05/11] fix(database): :bug: Fix applications table not getting deleted correctly during migration --- packages/kit/tables/migrations/0051_stiff_morbius.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/tables/migrations/0051_stiff_morbius.sql b/packages/kit/tables/migrations/0051_stiff_morbius.sql index 23745656..cd9644b8 100644 --- a/packages/kit/tables/migrations/0051_stiff_morbius.sql +++ b/packages/kit/tables/migrations/0051_stiff_morbius.sql @@ -18,6 +18,7 @@ ALTER TABLE "Tokens" RENAME COLUMN "applicationId" TO "clientId";--> statement-b --ALTER TABLE "Tokens" DROP CONSTRAINT "Tokens_applicationId_Applications_id_fk"; --> statement-breakpoint DROP INDEX "Applications_client_id_index";--> statement-breakpoint +ALTER TABLE "Applications" DROP COLUMN "id";--> statement-breakpoint ALTER TABLE "Applications" ADD PRIMARY KEY ("client_id");--> statement-breakpoint ALTER TABLE "Applications" ALTER COLUMN "scopes" SET DATA TYPE text[] USING (string_to_array("scopes", ' ')::text[]);--> statement-breakpoint ALTER TABLE "Applications" ALTER COLUMN "scopes" SET DEFAULT ARRAY[]::text[];--> statement-breakpoint @@ -35,7 +36,6 @@ ALTER TABLE "Notes" ADD CONSTRAINT "Notes_applicationId_Applications_client_id_f ALTER TABLE "OpenIdLoginFlows" ADD CONSTRAINT "OpenIdLoginFlows_applicationId_Applications_client_id_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."Applications"("client_id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint ALTER TABLE "Tokens" ALTER COLUMN "clientId" SET DATA TYPE text;--> statement-breakpoint ALTER TABLE "Tokens" ADD CONSTRAINT "Tokens_clientId_Applications_client_id_fk" FOREIGN KEY ("clientId") REFERENCES "public"."Applications"("client_id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint -ALTER TABLE "Applications" DROP COLUMN "id";--> statement-breakpoint ALTER TABLE "Applications" DROP COLUMN "vapid_key";--> statement-breakpoint ALTER TABLE "Applications" DROP COLUMN "redirect_uri";--> statement-breakpoint ALTER TABLE "Tokens" DROP COLUMN "token_type";--> statement-breakpoint From 45c3f6ae3f0d53adb3408e335d3b5003ebac323f Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 24 Oct 2025 18:45:07 +0200 Subject: [PATCH 06/11] fix(database): :bug: Cascade application ID deletion --- packages/kit/tables/migrations/0051_stiff_morbius.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/tables/migrations/0051_stiff_morbius.sql b/packages/kit/tables/migrations/0051_stiff_morbius.sql index cd9644b8..2644aa0c 100644 --- a/packages/kit/tables/migrations/0051_stiff_morbius.sql +++ b/packages/kit/tables/migrations/0051_stiff_morbius.sql @@ -18,7 +18,7 @@ ALTER TABLE "Tokens" RENAME COLUMN "applicationId" TO "clientId";--> statement-b --ALTER TABLE "Tokens" DROP CONSTRAINT "Tokens_applicationId_Applications_id_fk"; --> statement-breakpoint DROP INDEX "Applications_client_id_index";--> statement-breakpoint -ALTER TABLE "Applications" DROP COLUMN "id";--> statement-breakpoint +ALTER TABLE "Applications" DROP COLUMN "id" CASCADE;--> statement-breakpoint ALTER TABLE "Applications" ADD PRIMARY KEY ("client_id");--> statement-breakpoint ALTER TABLE "Applications" ALTER COLUMN "scopes" SET DATA TYPE text[] USING (string_to_array("scopes", ' ')::text[]);--> statement-breakpoint ALTER TABLE "Applications" ALTER COLUMN "scopes" SET DEFAULT ARRAY[]::text[];--> statement-breakpoint From 955a933fe98b95e4b5b0250e220462920b69cfe9 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 24 Oct 2025 19:12:40 +0200 Subject: [PATCH 07/11] refactor(api): :fire: Remove old forced OpenID auth code --- .github/config.workflow.toml | 8 - config/config.example.toml | 7 - .../api/routes/api/auth/login/index.test.ts | 227 ------------------ packages/api/routes/api/auth/login/index.ts | 198 --------------- packages/api/routes/api/v1/instance/index.ts | 1 - packages/api/routes/api/v2/instance/index.ts | 1 - packages/client/schemas/versia.ts | 5 - packages/config/index.ts | 1 - 8 files changed, 448 deletions(-) delete mode 100644 packages/api/routes/api/auth/login/index.test.ts delete mode 100644 packages/api/routes/api/auth/login/index.ts diff --git a/.github/config.workflow.toml b/.github/config.workflow.toml index 22a15529..42eff479 100644 --- a/.github/config.workflow.toml +++ b/.github/config.workflow.toml @@ -452,14 +452,6 @@ log_level = "info" # For console output # environment = "production" # log_level = "info" -[authentication] -# If enabled, Versia will require users to log in with an OpenID provider -forced_openid = false - -# Allow registration with OpenID providers -# If signups.registration is false, it will only be possible to register with OpenID -openid_registration = true - [authentication.keys] # Run Versia Server with those values missing to generate a new key public = "MCowBQYDK2VwAyEAfyZx8r98gVHtdH5EF1NYrBeChOXkt50mqiwKO2TX0f8=" diff --git a/config/config.example.toml b/config/config.example.toml index f7b231a7..1712b1a3 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -459,13 +459,6 @@ log_level = "info" # For console output # log_level = "info" [authentication] -# If enabled, Versia will require users to log in with an OpenID provider -forced_openid = false - -# Allow registration with OpenID providers -# If signups.registration is false, it will only be possible to register with OpenID -openid_registration = true - # Run Versia Server with this value missing to generate a new key # key = "" diff --git a/packages/api/routes/api/auth/login/index.test.ts b/packages/api/routes/api/auth/login/index.test.ts deleted file mode 100644 index dc0f08c0..00000000 --- a/packages/api/routes/api/auth/login/index.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { afterAll, describe, expect, test } from "bun:test"; -import { config } from "@versia-server/config"; -import { Client } from "@versia-server/kit/db"; -import { fakeRequest, getTestUsers } from "@versia-server/tests"; -import { randomString } from "@/math"; - -const { users, deleteUsers, passwords } = await getTestUsers(1); - -// Create application -const application = await Client.insert({ - id: randomString(32, "hex"), - name: "Test Client", - secret: "test", - redirectUris: ["https://example.com"], - scopes: ["read", "write"], -}); - -afterAll(async () => { - await deleteUsers(); - await application.delete(); -}); - -// /api/auth/login -describe("/api/auth/login", () => { - test("should get a JWT with email", async () => { - const formData = new FormData(); - - formData.append("identifier", users[0]?.data.email ?? ""); - formData.append("password", passwords[0]); - - const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - - expect(locationHeader.pathname).toBe("/oauth/consent"); - expect(locationHeader.searchParams.get("client_id")).toBe( - application.data.id, - ); - expect(locationHeader.searchParams.get("redirect_uri")).toBe( - "https://example.com", - ); - expect(locationHeader.searchParams.get("response_type")).toBe("code"); - expect(locationHeader.searchParams.get("scope")).toBe("read write"); - - expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/); - }); - - test("should get a JWT with username", async () => { - const formData = new FormData(); - - formData.append("identifier", users[0]?.data.username ?? ""); - formData.append("password", passwords[0]); - - const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - - expect(locationHeader.pathname).toBe("/oauth/consent"); - expect(locationHeader.searchParams.get("client_id")).toBe( - application.data.id, - ); - expect(locationHeader.searchParams.get("redirect_uri")).toBe( - "https://example.com", - ); - expect(locationHeader.searchParams.get("response_type")).toBe("code"); - expect(locationHeader.searchParams.get("scope")).toBe("read write"); - - expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/); - }); - - test("should have state in the URL", async () => { - const formData = new FormData(); - - formData.append("identifier", users[0]?.data.email ?? ""); - formData.append("password", passwords[0]); - - const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write&state=abc`, - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - response.headers.get("Location") ?? "", - config.http.base_url, - ); - - expect(locationHeader.pathname).toBe("/oauth/consent"); - expect(locationHeader.searchParams.get("client_id")).toBe( - application.data.id, - ); - expect(locationHeader.searchParams.get("redirect_uri")).toBe( - "https://example.com", - ); - expect(locationHeader.searchParams.get("response_type")).toBe("code"); - expect(locationHeader.searchParams.get("scope")).toBe("read write"); - expect(locationHeader.searchParams.get("state")).toBe("abc"); - - expect(response.headers.get("Set-Cookie")).toMatch(/jwt=[^;]+;/); - }); - - describe("should reject invalid credentials", () => { - // Redirects to /oauth/authorize on invalid - test("invalid email", async () => { - const formData = new FormData(); - - formData.append("identifier", "ababa@gmail.com"); - formData.append("password", "password"); - - const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - response.headers.get("Location") ?? "", - "", - ); - - expect(locationHeader.pathname).toBe("/oauth/authorize"); - expect(locationHeader.searchParams.get("error")).toBe( - "invalid_grant", - ); - expect(locationHeader.searchParams.get("error_description")).toBe( - "Invalid identifier or password", - ); - - expect(response.headers.get("Set-Cookie")).toBeNull(); - }); - - test("invalid username", async () => { - const formData = new FormData(); - - formData.append("identifier", "ababa"); - formData.append("password", "password"); - - const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - response.headers.get("Location") ?? "", - "", - ); - - expect(locationHeader.pathname).toBe("/oauth/authorize"); - expect(locationHeader.searchParams.get("error")).toBe( - "invalid_grant", - ); - expect(locationHeader.searchParams.get("error_description")).toBe( - "Invalid identifier or password", - ); - - expect(response.headers.get("Set-Cookie")).toBeNull(); - }); - - test("invalid password", async () => { - const formData = new FormData(); - - formData.append("identifier", users[0]?.data.email ?? ""); - formData.append("password", "password"); - - const response = await fakeRequest( - `/api/auth/login?client_id=${application.data.id}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - expect(response.headers.get("location")).toBeDefined(); - const locationHeader = new URL( - response.headers.get("Location") ?? "", - "", - ); - - expect(locationHeader.pathname).toBe("/oauth/authorize"); - expect(locationHeader.searchParams.get("error")).toBe( - "invalid_grant", - ); - expect(locationHeader.searchParams.get("error_description")).toBe( - "Invalid identifier or password", - ); - - expect(response.headers.get("Set-Cookie")).toBeNull(); - }); - }); -}); diff --git a/packages/api/routes/api/auth/login/index.ts b/packages/api/routes/api/auth/login/index.ts deleted file mode 100644 index f953fc0d..00000000 --- a/packages/api/routes/api/auth/login/index.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { config } from "@versia-server/config"; -import { ApiError } from "@versia-server/kit"; -import { apiRoute, handleZodError } from "@versia-server/kit/api"; -import { Client, User } from "@versia-server/kit/db"; -import { Users } from "@versia-server/kit/tables"; -import { password as bunPassword } from "bun"; -import { eq, or } from "drizzle-orm"; -import type { Context } from "hono"; -import { setCookie } from "hono/cookie"; -import { sign } from "hono/jwt"; -import { describeRoute, validator } from "hono-openapi"; -import { z } from "zod/v4"; - -const returnError = ( - context: Context, - error: string, - description: string, -): Response => { - const searchParams = new URLSearchParams(); - - // Add all data that is not undefined except email and password - for (const [key, value] of Object.entries(context.req.query())) { - if (key !== "email" && key !== "password" && value !== undefined) { - searchParams.append(key, value); - } - } - - searchParams.append("error", error); - searchParams.append("error_description", description); - - return context.redirect( - new URL( - `${config.frontend.routes.login}?${searchParams.toString()}`, - config.http.base_url, - ).toString(), - ); -}; - -export default apiRoute((app) => - app.post( - "/api/auth/login", - describeRoute({ - summary: "Login", - description: "Login to the application", - responses: { - 302: { - description: "Redirect to OAuth authorize, or error", - headers: { - "Set-Cookie": { - description: "JWT cookie", - required: false, - }, - }, - }, - }, - }), - validator( - "query", - z.object({ - scope: z.string().optional(), - redirect_uri: z.url().optional(), - response_type: z.enum([ - "code", - "token", - "none", - "id_token", - "code id_token", - "code token", - "token id_token", - "code token id_token", - ]), - client_id: z.string(), - state: z.string().optional(), - code_challenge: z.string().optional(), - code_challenge_method: z.enum(["plain", "S256"]).optional(), - prompt: z - .enum(["none", "login", "consent", "select_account"]) - .optional() - .default("none"), - max_age: z - .number() - .int() - .optional() - .default(60 * 60 * 24 * 7), - }), - handleZodError, - ), - validator( - "form", - z.object({ - identifier: z - .email() - .toLowerCase() - .or(z.string().toLowerCase()), - password: z.string().min(2).max(100), - }), - handleZodError, - ), - async (context) => { - if (config.authentication.forced_openid) { - return returnError( - context, - "invalid_request", - "Logging in with a password is disabled by the administrator. Please use a valid OpenID Connect provider.", - ); - } - - const { identifier, password } = context.req.valid("form"); - const { client_id } = context.req.valid("query"); - - // Find user - const user = await User.fromSql( - or( - eq(Users.email, identifier.toLowerCase()), - eq(Users.username, identifier.toLowerCase()), - ), - ); - - if ( - !( - user && - (await bunPassword.verify( - password, - user.data.password || "", - )) - ) - ) { - return returnError( - context, - "invalid_grant", - "Invalid identifier or password", - ); - } - - if (user.data.passwordResetToken) { - return context.redirect( - `${config.frontend.routes.password_reset}?${new URLSearchParams( - { - token: user.data.passwordResetToken ?? "", - login_reset: "true", - }, - ).toString()}`, - ); - } - - // Generate JWT - const jwt = await sign( - { - sub: user.id, - iss: config.http.base_url.origin, - aud: client_id, - exp: Math.floor(Date.now() / 1000) + 60 * 60, - iat: Math.floor(Date.now() / 1000), - nbf: Math.floor(Date.now() / 1000), - }, - config.authentication.key, - ); - - const application = await Client.fromClientId(client_id); - - if (!application) { - throw new ApiError(400, "Invalid application"); - } - - const searchParams = new URLSearchParams({ - application: application.data.name, - }); - - if (application.data.website) { - searchParams.append("website", application.data.website); - } - - // Add all data that is not undefined except email and password - for (const [key, value] of Object.entries(context.req.query())) { - if ( - key !== "email" && - key !== "password" && - value !== undefined - ) { - searchParams.append(key, String(value)); - } - } - - // Redirect to OAuth authorize with JWT - setCookie(context, "jwt", jwt, { - httpOnly: true, - secure: true, - sameSite: "Strict", - path: "/", - // 2 weeks - maxAge: 60 * 60 * 24 * 14, - }); - return context.redirect( - `${config.frontend.routes.consent}?${searchParams.toString()}`, - ); - }, - ), -); diff --git a/packages/api/routes/api/v1/instance/index.ts b/packages/api/routes/api/v1/instance/index.ts index 73b89d29..94056ae6 100644 --- a/packages/api/routes/api/v1/instance/index.ts +++ b/packages/api/routes/api/v1/instance/index.ts @@ -111,7 +111,6 @@ export default apiRoute((app) => version: "4.3.0-alpha.3+glitch", versia_version: version, sso: { - forced: config.authentication.forced_openid, providers: config.authentication.openid_providers.map( (p) => ({ name: p.name, diff --git a/packages/api/routes/api/v2/instance/index.ts b/packages/api/routes/api/v2/instance/index.ts index 18150104..b44a24e2 100644 --- a/packages/api/routes/api/v2/instance/index.ts +++ b/packages/api/routes/api/v2/instance/index.ts @@ -151,7 +151,6 @@ export default apiRoute((app) => hint: r.hint, })), sso: { - forced: config.authentication.forced_openid, providers: config.authentication.openid_providers.map( (p) => ({ name: p.name, diff --git a/packages/client/schemas/versia.ts b/packages/client/schemas/versia.ts index 120d8c32..739d1fb3 100644 --- a/packages/client/schemas/versia.ts +++ b/packages/client/schemas/versia.ts @@ -87,11 +87,6 @@ export const NoteReactionWithAccounts = NoteReaction.extend({ /* Versia Server API extension */ export const SSOConfig = z.object({ - forced: z.boolean().meta({ - description: - "If this is enabled, normal identifier/password login is disabled and login must be done through SSO.", - example: false, - }), providers: z .array( z.object({ diff --git a/packages/config/index.ts b/packages/config/index.ts index 664f1e02..161b3113 100644 --- a/packages/config/index.ts +++ b/packages/config/index.ts @@ -800,7 +800,6 @@ export const ConfigSchema = z }) .optional(), authentication: z.strictObject({ - forced_openid: z.boolean().default(false), openid_providers: z .array( z.strictObject({ From ae207c10b6d95f152b916b3ad2367155184a4d94 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 3 Nov 2025 00:17:57 +0100 Subject: [PATCH 08/11] fix: :green_heart: Update Nix hash --- nix/package.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/package.nix b/nix/package.nix index a800adb5..12e64886 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -54,7 +54,7 @@ in # Required else we get errors that our fixed-output derivation references store paths dontFixup = true; - outputHash = "sha256-aG54v3luuJTmb/eonoILv3KBKW6mulk3xOpxLA6V5L8="; + outputHash = "sha256-geahFpkyWgHXKMxLp46AJW3TVWFm6jM4QZO0Z10mBWY="; outputHashAlgo = "sha256"; outputHashMode = "recursive"; }; From a9dbd2cc4e019deeafcbf5399c95c22d876501cf Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 21 Nov 2025 06:45:12 +0100 Subject: [PATCH 09/11] fix: :fire: Remove old tests and docs related to old auth endpoints --- docs/frontend/auth.md | 56 ++----------------------------- docs/frontend/routes.md | 2 +- packages/api/routes/oauth.test.ts | 30 ++--------------- 3 files changed, 5 insertions(+), 83 deletions(-) diff --git a/docs/frontend/auth.md b/docs/frontend/auth.md index cc156eb4..a062e8c4 100644 --- a/docs/frontend/auth.md +++ b/docs/frontend/auth.md @@ -3,7 +3,7 @@ Multiple API routes are exposed for authentication, to be used by frontend developers. > [!INFO] -> +> > These are different from the Client API routes, which are used by clients to interact with the Mastodon API. A frontend is a web application that is designed to be the primary user interface for an instance. It is used also used by clients to perform authentication. @@ -48,58 +48,6 @@ Frontend configuration. } ``` -## Sign In - -```http -POST /api/auth/login -``` - -Allows users to sign in to the instance. This is the first step in the authentication process. - -- **Returns**: `302 Found` with a `Location` header to redirect the user to the next step, as well as a `Set-Cookie` header with the session JWT. -- **Authentication**: Not required -- **Permissions**: None -- **Version History**: - - `0.7.0`: First documented. - -### Request - -- `identifier` (string, required): The username or email of the user. Case-insensitive. -- `password` (string, required): The password of the user. - -#### Query Parameters - -- `client_id` (string, required): Client ID of the [application](https://docs.joinmastodon.org/entities/Application/) that is making the request. -- `redirect_uri` (string, required): Redirect URI of the [application](https://docs.joinmastodon.org/entities/Application/) that is making the request. Must match the saved value. -- `response_type` (string, required): Must be `code`. -- `scope` (string, required): OAuth2 scopes. Must match the value indicated in the [application](https://docs.joinmastodon.org/entities/Application/). - -#### Example - -```http -POST /api/auth/login?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=read%20write -Content-Type: application/json - -{ - "identifier": "bobjones@gmail.com", - "password": "hunter2" -} -``` - -### Response - -#### `302 Found` - -Redirects the user to the consent page with some query parameters. The frontend should redirect the user to this URL. - -This response also has a `Set-Cookie` header with a [JSON Web Token](https://jwt.io/) that contains the user's session information. This JWT is signed with the instance's secret key, and must be included in all subsequent authentication requests. - -```http -HTTP/2.0 302 Found -Location: /oauth/consent?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=read%20write -Set-Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=3600 -``` - ## SSO Sign In ```http @@ -136,4 +84,4 @@ Redirects the user to the OpenID Connect provider's login page. ```http HTTP/2.0 302 Found Location: https://accounts.google.com/o/oauth2/auth?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=openid%20email&state=123 -``` \ No newline at end of file +``` diff --git a/docs/frontend/routes.md b/docs/frontend/routes.md index e40ad545..3fabb6f5 100644 --- a/docs/frontend/routes.md +++ b/docs/frontend/routes.md @@ -12,7 +12,7 @@ GET /oauth/authorize This route should display a login form for the user to enter their username and password, as well as a list of OpenID providers to use if available. -The form should submit to [`POST /api/auth/login`](./auth.md#sign-in), or to the OpenID Connect flow. +The form should submit to the OpenID Connect flow. Configurable in the Versia Server configuration at `frontend.routes.login`. diff --git a/packages/api/routes/oauth.test.ts b/packages/api/routes/oauth.test.ts index 66f79e4e..1d3dbfbe 100644 --- a/packages/api/routes/oauth.test.ts +++ b/packages/api/routes/oauth.test.ts @@ -1,12 +1,7 @@ import { afterAll, describe, expect, test } from "bun:test"; -import { - fakeRequest, - generateClient, - getTestUsers, -} from "@versia-server/tests"; +import { generateClient, getTestUsers } from "@versia-server/tests"; -let clientId: string; -const { users, passwords, deleteUsers } = await getTestUsers(1); +const { users, deleteUsers } = await getTestUsers(1); afterAll(async () => { await deleteUsers(); @@ -33,27 +28,6 @@ describe("Login flow", () => { redirect_uris: ["https://example.com"], scopes: ["read", "write"], }); - - clientId = data.client_id; - }); - - test("should get a JWT", async () => { - const formData = new FormData(); - - formData.append("identifier", users[0]?.data.email ?? ""); - formData.append("password", passwords[0]); - - const response = await fakeRequest( - `/api/auth/login?client_id=${clientId}&redirect_uri=https://example.com&response_type=code&scope=read+write`, - { - method: "POST", - body: formData, - }, - ); - - expect(response.status).toBe(302); - - //jwt = response.headers.get("Set-Cookie")?.match(/jwt=([^;]+);/)?.[1] ?? ""; }); // TODO: Test full flow including OpenID part From c63b2b320b03198e6e8b44a59ea8a34f285e83a4 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 21 Nov 2025 06:50:41 +0100 Subject: [PATCH 10/11] fix(config): :green_heart: Fix incorrect CI config --- .github/config.workflow.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/config.workflow.toml b/.github/config.workflow.toml index 42eff479..647a332c 100644 --- a/.github/config.workflow.toml +++ b/.github/config.workflow.toml @@ -452,10 +452,9 @@ log_level = "info" # For console output # environment = "production" # log_level = "info" -[authentication.keys] -# Run Versia Server with those values missing to generate a new key -public = "MCowBQYDK2VwAyEAfyZx8r98gVHtdH5EF1NYrBeChOXkt50mqiwKO2TX0f8=" -private = "MC4CAQAwBQYDK2VwBCIEILDi1g7+bwNjBBvL4CRWHZpCFBR2m2OPCot62Wr+TCbq" +[authentication] +# Run Versia Server with this value missing to generate a new key +key = "ZWcwanRaQAqY3ChUro/Jey9XGQjzsxEed5iqTp4yFr8W6vEnXdz91F/Pu/uf7HBMbNeIK7V6aHsM0lq9onrO8Q==" # The provider MUST support OpenID Connect with .well-known discovery # Most notably, GitHub does not support this From 82bb92768c716060dec73f9b2a433c0353fdf083 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Fri, 21 Nov 2025 06:53:50 +0100 Subject: [PATCH 11/11] fix: :bug: Set test pattern in DeepSource code analysis config --- .deepsource.toml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.deepsource.toml b/.deepsource.toml index 73816a8d..29b099b4 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -1,16 +1,18 @@ version = 1 +test_patterns = ["**/*.test.ts"] + [[analyzers]] name = "shell" [[analyzers]] name = "javascript" - [analyzers.meta] - environment = ["nodejs"] +[analyzers.meta] +environment = ["nodejs"] [[analyzers]] name = "docker" - [analyzers.meta] - dockerfile_paths = ["Dockerfile"] \ No newline at end of file +[analyzers.meta] +dockerfile_paths = ["Dockerfile"]