refactor(api): ♻️ Move from @hono/zod-openapi to hono-openapi

hono-openapi is easier to work with and generates better OpenAPI definitions
This commit is contained in:
Jesse Wierzbinski 2025-03-29 03:30:06 +01:00
parent 0576aff972
commit 58342e86e1
No known key found for this signature in database
240 changed files with 9494 additions and 9575 deletions

View file

@ -1,46 +1,28 @@
import { handleZodError } from "@/api";
import { randomString } from "@/math.ts";
import { createRoute, z } from "@hono/zod-openapi";
import { Account as AccountSchema } from "@versia/client/schemas";
import { RolePermission } from "@versia/client/schemas";
import { Media, Token, User, db } from "@versia/kit/db";
import { type SQL, and, eq, isNull } from "@versia/kit/drizzle";
import { OpenIdAccounts, Users } from "@versia/kit/tables";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { setCookie } from "hono/cookie";
import { SignJWT } from "jose";
import { z } from "zod";
import { ApiError } from "~/classes/errors/api-error.ts";
import type { PluginType } from "../../index.ts";
import { automaticOidcFlow } from "../../utils.ts";
const schemas = {
query: z.object({
client_id: z.string().optional(),
flow: z.string(),
link: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.optional(),
user_id: z.string().uuid().optional(),
}),
param: z.object({
issuer: z.string(),
}),
};
export default (plugin: PluginType): void => {
plugin.registerRoute("/oauth/sso/{issuer}/callback", (app) => {
app.openapi(
createRoute({
method: "get",
path: "/oauth/sso/{issuer}/callback",
app.get(
"/oauth/sso/:issuer/callback",
describeRoute({
summary: "SSO callback",
tags: ["OpenID"],
description:
"After the user has authenticated to an external OpenID provider, they are redirected here to complete the OAuth flow and get a code",
middleware: [plugin.middleware] as const,
request: {
query: schemas.query,
params: schemas.param,
},
responses: {
302: {
description:
@ -48,6 +30,29 @@ export default (plugin: PluginType): void => {
},
},
}),
plugin.middleware,
validator(
"param",
z.object({
issuer: z.string(),
}),
handleZodError,
),
validator(
"query",
z.object({
client_id: z.string().optional(),
flow: z.string(),
link: z
.string()
.transform((v) =>
["true", "1", "on"].includes(v.toLowerCase()),
)
.optional(),
user_id: z.string().uuid().optional(),
}),
handleZodError,
),
async (context) => {
const currentUrl = new URL(context.req.url);
const redirectUrl = new URL(context.req.url);

View file

@ -1,48 +1,25 @@
import { jsonOrForm } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { handleZodError, jsonOrForm } from "@/api";
import { Token, db } from "@versia/kit/db";
import { and, eq } from "@versia/kit/drizzle";
import { Tokens } from "@versia/kit/tables";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import type { PluginType } from "../../index.ts";
const schemas = {
json: z.object({
client_id: z.string(),
client_secret: z.string(),
token: z.string().optional(),
}),
};
export default (plugin: PluginType): void => {
plugin.registerRoute("/oauth/revoke", (app) => {
app.openapi(
createRoute({
method: "post",
path: "/oauth/revoke",
app.post(
"/oauth/revoke",
describeRoute({
summary: "Revoke token",
tags: ["OpenID"],
middleware: [jsonOrForm(), plugin.middleware],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
"application/x-www-form-urlencoded": {
schema: schemas.json,
},
"multipart/form-data": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Token deleted",
content: {
"application/json": {
schema: z.object({}),
schema: resolver(z.object({})),
},
},
},
@ -50,15 +27,28 @@ export default (plugin: PluginType): void => {
description: "Authorization error",
content: {
"application/json": {
schema: z.object({
error: z.string(),
error_description: z.string(),
}),
schema: resolver(
z.object({
error: z.string(),
error_description: z.string(),
}),
),
},
},
},
},
}),
jsonOrForm(),
plugin.middleware,
validator(
"json",
z.object({
client_id: z.string(),
client_secret: z.string(),
token: z.string().optional(),
}),
handleZodError,
),
async (context) => {
const { client_id, client_secret, token } =
context.req.valid("json");

View file

@ -1,37 +1,25 @@
import { createRoute, z } from "@hono/zod-openapi";
import { handleZodError } from "@/api.ts";
import { Application, db } from "@versia/kit/db";
import { OpenIdLoginFlows } from "@versia/kit/tables";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import {
calculatePKCECodeChallenge,
discoveryRequest,
generateRandomCodeVerifier,
processDiscoveryResponse,
} from "oauth4webapi";
import { z } from "zod";
import type { PluginType } from "../../index.ts";
import { oauthRedirectUri } from "../../utils.ts";
const schemas = {
query: z.object({
issuer: z.string(),
client_id: z.string().optional(),
redirect_uri: z.string().url().optional(),
scope: z.string().optional(),
response_type: z.enum(["code"]).optional(),
}),
};
export default (plugin: PluginType): void => {
plugin.registerRoute("/oauth/sso", (app) => {
app.openapi(
createRoute({
method: "get",
path: "/oauth/sso",
app.get(
"/oauth/sso",
describeRoute({
summary: "Initiate SSO login flow",
tags: ["OpenID"],
request: {
query: schemas.query,
},
middleware: [plugin.middleware] as const,
responses: {
302: {
description:
@ -39,6 +27,18 @@ export default (plugin: PluginType): void => {
},
},
}),
plugin.middleware,
validator(
"query",
z.object({
issuer: z.string(),
client_id: z.string().optional(),
redirect_uri: z.string().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 } =

View file

@ -1,87 +1,44 @@
import { jsonOrForm } from "@/api";
import { createRoute, z } from "@hono/zod-openapi";
import { handleZodError, jsonOrForm } from "@/api";
import { Application, Token } from "@versia/kit/db";
import { and, eq } from "@versia/kit/drizzle";
import { Tokens } from "@versia/kit/tables";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import type { PluginType } from "../../index.ts";
const schemas = {
json: z.object({
code: z.string().optional(),
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.string().url().optional(),
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(),
}),
};
export default (plugin: PluginType): void => {
plugin.registerRoute("/oauth/token", (app) => {
app.openapi(
createRoute({
method: "post",
path: "/oauth/token",
app.post(
"/oauth/token",
describeRoute({
summary: "Get token",
tags: ["OpenID"],
middleware: [jsonOrForm(), plugin.middleware],
request: {
body: {
content: {
"application/json": {
schema: schemas.json,
},
"application/x-www-form-urlencoded": {
schema: schemas.json,
},
"multipart/form-data": {
schema: schemas.json,
},
},
},
},
responses: {
200: {
description: "Token",
content: {
"application/json": {
schema: 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(
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(),
}),
),
},
},
},
@ -89,15 +46,53 @@ export default (plugin: PluginType): void => {
description: "Authorization error",
content: {
"application/json": {
schema: z.object({
error: z.string(),
error_description: z.string(),
}),
schema: resolver(
z.object({
error: z.string(),
error_description: z.string(),
}),
),
},
},
},
},
}),
jsonOrForm(),
plugin.middleware,
validator(
"json",
z.object({
code: z.string().optional(),
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.string().url().optional(),
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(),
}),
handleZodError,
),
async (context) => {
const {
grant_type,