refactor(api): ♻️ Remove old redirect() and response() in favour of Hono's builtins

This commit is contained in:
Jesse Wierzbinski 2024-08-28 17:01:56 +02:00
parent 691716f7eb
commit 69d7d50239
No known key found for this signature in database
20 changed files with 188 additions and 174 deletions

View file

@ -1,5 +1,6 @@
import { apiRoute, applyConfig } from "@/api"; import { apiRoute, applyConfig } from "@/api";
import { redirect } from "@/response"; import type { Context } from "@hono/hono";
import { setCookie } from "@hono/hono/cookie";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { eq, or } from "drizzle-orm"; import { eq, or } from "drizzle-orm";
import { SignJWT } from "jose"; import { SignJWT } from "jose";
@ -87,11 +88,11 @@ const route = createRoute({
}, },
}); });
const returnError = (query: object, error: string, description: string) => { const returnError = (context: Context, error: string, description: string) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
// Add all data that is not undefined except email and password // Add all data that is not undefined except email and password
for (const [key, value] of Object.entries(query)) { for (const [key, value] of Object.entries(context.req.query())) {
if (key !== "email" && key !== "password" && value !== undefined) { if (key !== "email" && key !== "password" && value !== undefined) {
searchParams.append(key, value); searchParams.append(key, value);
} }
@ -100,11 +101,11 @@ const returnError = (query: object, error: string, description: string) => {
searchParams.append("error", error); searchParams.append("error", error);
searchParams.append("error_description", description); searchParams.append("error_description", description);
return redirect( return context.redirect(
new URL( new URL(
`${config.frontend.routes.login}?${searchParams.toString()}`, `${config.frontend.routes.login}?${searchParams.toString()}`,
config.http.base_url, config.http.base_url,
), ).toString(),
); );
}; };
@ -112,7 +113,7 @@ export default apiRoute((app) =>
app.openapi(route, async (context) => { app.openapi(route, async (context) => {
if (config.oidc.forced) { if (config.oidc.forced) {
return returnError( return returnError(
context.req.query(), context,
"invalid_request", "invalid_request",
"Logging in with a password is disabled by the administrator. Please use a valid OpenID Connect provider.", "Logging in with a password is disabled by the administrator. Please use a valid OpenID Connect provider.",
); );
@ -136,14 +137,14 @@ export default apiRoute((app) =>
) )
) { ) {
return returnError( return returnError(
context.req.query(), context,
"invalid_grant", "invalid_grant",
"Invalid identifier or password", "Invalid identifier or password",
); );
} }
if (user.data.passwordResetToken) { if (user.data.passwordResetToken) {
return redirect( return context.redirect(
`${config.frontend.routes.password_reset}?${new URLSearchParams( `${config.frontend.routes.password_reset}?${new URLSearchParams(
{ {
token: user.data.passwordResetToken ?? "", token: user.data.passwordResetToken ?? "",
@ -198,14 +199,15 @@ export default apiRoute((app) =>
} }
// Redirect to OAuth authorize with JWT // Redirect to OAuth authorize with JWT
return redirect( setCookie(context, "jwt", jwt, {
httpOnly: true,
secure: true,
sameSite: "Strict",
path: "/",
maxAge: 60 * 60,
});
return context.redirect(
`${config.frontend.routes.consent}?${searchParams.toString()}`, `${config.frontend.routes.consent}?${searchParams.toString()}`,
302,
{
"Set-Cookie": `jwt=${jwt}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${
60 * 60
}`,
},
); );
}), }),
); );

View file

@ -51,12 +51,11 @@ export default apiRoute((app) =>
const { redirect_uri, client_id, code } = context.req.valid("query"); const { redirect_uri, client_id, code } = context.req.valid("query");
const redirectToLogin = (error: string) => const redirectToLogin = (error: string) =>
Response.redirect( context.redirect(
`${config.frontend.routes.login}?${new URLSearchParams({ `${config.frontend.routes.login}?${new URLSearchParams({
...context.req.query, ...context.req.query,
error: encodeURIComponent(error), error: encodeURIComponent(error),
}).toString()}`, }).toString()}`,
302,
); );
const foundToken = await db const foundToken = await db

View file

@ -1,7 +1,7 @@
import { apiRoute, applyConfig } from "@/api"; import { apiRoute, applyConfig } from "@/api";
import { response } from "@/response";
import { createRoute } from "@hono/zod-openapi"; import { createRoute } from "@hono/zod-openapi";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { Context } from "hono";
import { z } from "zod"; import { z } from "zod";
import { Users } from "~/drizzle/schema"; import { Users } from "~/drizzle/schema";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
@ -50,21 +50,26 @@ const route = createRoute({
}, },
}); });
const returnError = (token: string, error: string, description: string) => { const returnError = (
context: Context,
token: string,
error: string,
description: string,
) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
searchParams.append("error", error); searchParams.append("error", error);
searchParams.append("error_description", description); searchParams.append("error_description", description);
searchParams.append("token", token); searchParams.append("token", token);
return response(null, 302, { return context.redirect(
Location: new URL( new URL(
`${ `${
config.frontend.routes.password_reset config.frontend.routes.password_reset
}?${searchParams.toString()}`, }?${searchParams.toString()}`,
config.http.base_url, config.http.base_url,
).toString(), ).toString(),
}); );
}; };
export default apiRoute((app) => export default apiRoute((app) =>
@ -74,7 +79,12 @@ export default apiRoute((app) =>
const user = await User.fromSql(eq(Users.passwordResetToken, token)); const user = await User.fromSql(eq(Users.passwordResetToken, token));
if (!user) { if (!user) {
return returnError(token, "invalid_token", "Invalid token"); return returnError(
context,
token,
"invalid_token",
"Invalid token",
);
} }
await user.update({ await user.update({
@ -82,8 +92,8 @@ export default apiRoute((app) =>
passwordResetToken: null, passwordResetToken: null,
}); });
return response(null, 302, { return context.redirect(
Location: `${config.frontend.routes.password_reset}?success=true`, `${config.frontend.routes.password_reset}?success=true`,
}); );
}), }),
); );

View file

@ -7,7 +7,6 @@ import {
jsonOrForm, jsonOrForm,
} from "@/api"; } from "@/api";
import { mimeLookup } from "@/content_types"; import { mimeLookup } from "@/content_types";
import { response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
@ -110,7 +109,7 @@ export default apiRoute((app) =>
await db.delete(Emojis).where(eq(Emojis.id, id)); await db.delete(Emojis).where(eq(Emojis.id, id));
return response(null, 204); return context.newResponse(null, 204);
} }
case "PATCH": { case "PATCH": {

View file

@ -5,7 +5,6 @@ import {
handleZodError, handleZodError,
idValidator, idValidator,
} from "@/api"; } from "@/api";
import { response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { z } from "zod"; import { z } from "zod";
import { MediaManager } from "~/classes/media/media-manager"; import { MediaManager } from "~/classes/media/media-manager";
@ -71,7 +70,7 @@ export default apiRoute((app) =>
if (attachment.data.url) { if (attachment.data.url) {
return context.json(attachment.toApi()); return context.json(attachment.toApi());
} }
return response(null, 206); return context.newResponse(null, 206);
} }
case "PUT": { case "PUT": {
const { description, thumbnail } = const { description, thumbnail } =

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; import { apiRoute, applyConfig, auth, handleZodError } from "@/api";
import { response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { z } from "zod"; import { z } from "zod";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
@ -76,7 +75,7 @@ export default apiRoute((app) =>
await role.linkUser(user.id); await role.linkUser(user.id);
return response(null, 204); return context.newResponse(null, 204);
} }
case "DELETE": { case "DELETE": {
const userHighestRole = userRoles.reduce((prev, current) => const userHighestRole = userRoles.reduce((prev, current) =>
@ -96,7 +95,7 @@ export default apiRoute((app) =>
await role.unlinkUser(user.id); await role.unlinkUser(user.id);
return response(null, 204); return context.newResponse(null, 204);
} }
} }
}, },

View file

@ -1,5 +1,5 @@
import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; import { apiRoute, applyConfig, auth, handleZodError } from "@/api";
import { proxyUrl, response } from "@/response"; import { proxyUrl } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
@ -103,7 +103,7 @@ export default apiRoute((app) =>
.delete(OpenIdAccounts) .delete(OpenIdAccounts)
.where(eq(OpenIdAccounts.id, account.id)); .where(eq(OpenIdAccounts.id, account.id));
return response(null, 204); return context.newResponse(null, 204);
} }
} }
}, },

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig, handleZodError } from "@/api";
import { response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { z } from "zod"; import { z } from "zod";
@ -54,7 +53,7 @@ export default apiRoute((app) =>
} }
// Can't directly copy file into Response because this crashes Bun for now // Can't directly copy file into Response because this crashes Bun for now
return response(buffer, 200, { return context.newResponse(buffer, 200, {
"Content-Type": file.type || "application/octet-stream", "Content-Type": file.type || "application/octet-stream",
"Content-Length": `${file.size - start}`, "Content-Length": `${file.size - start}`,
"Content-Range": `bytes ${start}-${end}/${file.size}`, "Content-Range": `bytes ${start}-${end}/${file.size}`,

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig, handleZodError } from "@/api";
import { response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import type { StatusCode } from "hono/utils/http-status";
import { z } from "zod"; import { z } from "zod";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
@ -62,7 +62,7 @@ export default apiRoute((app) =>
.get("Content-Disposition") .get("Content-Disposition")
?.match(/filename="(.+)"/)?.[1] || id.split("/").pop(); ?.match(/filename="(.+)"/)?.[1] || id.split("/").pop();
return response(media.body, media.status, { return context.newResponse(media.body, media.status as StatusCode, {
"Content-Type": "Content-Type":
media.headers.get("Content-Type") || media.headers.get("Content-Type") ||
"application/octet-stream", "application/octet-stream",

View file

@ -1,8 +1,8 @@
import { apiRoute, applyConfig, handleZodError, jsonOrForm } from "@/api"; import { apiRoute, applyConfig, handleZodError, jsonOrForm } from "@/api";
import { randomString } from "@/math"; import { randomString } from "@/math";
import { response } from "@/response";
import { sentry } from "@/sentry"; import { sentry } from "@/sentry";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import type { Context } from "hono";
import { SignJWT, jwtVerify } from "jose"; import { SignJWT, jwtVerify } from "jose";
import { z } from "zod"; import { z } from "zod";
import { TokenType } from "~/classes/functions/token"; import { TokenType } from "~/classes/functions/token";
@ -59,11 +59,16 @@ export const schemas = {
}), }),
}; };
const returnError = (query: object, error: string, description: string) => { const returnError = (
context: Context,
data: object,
error: string,
description: string,
) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
// Add all data that is not undefined except email and password // Add all data that is not undefined except email and password
for (const [key, value] of Object.entries(query)) { for (const [key, value] of Object.entries(data)) {
if (key !== "email" && key !== "password" && value !== undefined) { if (key !== "email" && key !== "password" && value !== undefined) {
searchParams.append(key, value); searchParams.append(key, value);
} }
@ -72,9 +77,9 @@ const returnError = (query: object, error: string, description: string) => {
searchParams.append("error", error); searchParams.append("error", error);
searchParams.append("error_description", description); searchParams.append("error_description", description);
return response(null, 302, { return context.redirect(
Location: `${config.frontend.routes.login}?${searchParams.toString()}`, `${config.frontend.routes.login}?${searchParams.toString()}`,
}); );
}; };
export default apiRoute((app) => export default apiRoute((app) =>
@ -94,6 +99,7 @@ export default apiRoute((app) =>
if (!cookie) { if (!cookie) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"No cookies were sent with the request", "No cookies were sent with the request",
@ -107,6 +113,7 @@ export default apiRoute((app) =>
if (!jwt) { if (!jwt) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"No jwt cookie was sent in the request", "No jwt cookie was sent in the request",
@ -142,6 +149,7 @@ export default apiRoute((app) =>
if (!result) { if (!result) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"Invalid JWT, could not verify", "Invalid JWT, could not verify",
@ -151,24 +159,45 @@ export default apiRoute((app) =>
const payload = result.payload; const payload = result.payload;
if (!payload.sub) { if (!payload.sub) {
return returnError(body, "invalid_request", "Invalid sub"); return returnError(
context,
body,
"invalid_request",
"Invalid sub",
);
} }
if (!payload.aud) { if (!payload.aud) {
return returnError(body, "invalid_request", "Invalid aud"); return returnError(
context,
body,
"invalid_request",
"Invalid aud",
);
} }
if (!payload.exp) { if (!payload.exp) {
return returnError(body, "invalid_request", "Invalid exp"); return returnError(
context,
body,
"invalid_request",
"Invalid exp",
);
} }
// Check if the user is authenticated // Check if the user is authenticated
const user = await User.fromId(payload.sub); const user = await User.fromId(payload.sub);
if (!user) { if (!user) {
return returnError(body, "invalid_request", "Invalid sub"); return returnError(
context,
body,
"invalid_request",
"Invalid sub",
);
} }
if (!user.hasPermission(RolePermissions.OAuth)) { if (!user.hasPermission(RolePermissions.OAuth)) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
`User is missing the ${RolePermissions.OAuth} permission`, `User is missing the ${RolePermissions.OAuth} permission`,
@ -183,6 +212,7 @@ export default apiRoute((app) =>
if (!(asksCode || asksToken || asksIdToken)) { if (!(asksCode || asksToken || asksIdToken)) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"Invalid response_type, must ask for code, token, or id_token", "Invalid response_type, must ask for code, token, or id_token",
@ -191,6 +221,7 @@ export default apiRoute((app) =>
if (asksCode && !redirect_uri) { if (asksCode && !redirect_uri) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"Redirect URI is required for code flow (can be urn:ietf:wg:oauth:2.0:oob)", "Redirect URI is required for code flow (can be urn:ietf:wg:oauth:2.0:oob)",
@ -216,6 +247,7 @@ export default apiRoute((app) =>
if (!application) { if (!application) {
return returnError( return returnError(
context,
body, body,
"invalid_client", "invalid_client",
"Invalid client_id or client_secret", "Invalid client_id or client_secret",
@ -224,6 +256,7 @@ export default apiRoute((app) =>
if (application.redirectUri !== redirect_uri) { if (application.redirectUri !== redirect_uri) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"Redirect URI does not match client_id", "Redirect URI does not match client_id",
@ -237,7 +270,12 @@ export default apiRoute((app) =>
scope && scope &&
!scope.split(" ").every((s) => applicationScopes.includes(s)) !scope.split(" ").every((s) => applicationScopes.includes(s))
) { ) {
return returnError(body, "invalid_scope", "Invalid scope"); return returnError(
context,
body,
"invalid_scope",
"Invalid scope",
);
} }
// Generate tokens // Generate tokens
@ -323,11 +361,7 @@ export default apiRoute((app) =>
redirectUri.search = searchParams.toString(); redirectUri.search = searchParams.toString();
return response(null, 302, { return context.redirect(redirectUri.toString());
Location: redirectUri.toString(),
"Cache-Control": "no-store",
Pragma: "no-cache",
});
}, },
), ),
); );

View file

@ -1,8 +1,9 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig, handleZodError } from "@/api";
import { randomString } from "@/math"; import { randomString } from "@/math";
import { response } from "@/response"; import { setCookie } from "@hono/hono/cookie";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import type { Context } from "hono";
import { SignJWT } from "jose"; import { SignJWT } from "jose";
import { z } from "zod"; import { z } from "zod";
import { TokenType } from "~/classes/functions/token"; import { TokenType } from "~/classes/functions/token";
@ -39,7 +40,12 @@ export const schemas = {
}), }),
}; };
const returnError = (query: object, error: string, description: string) => { const returnError = (
context: Context,
query: object,
error: string,
description: string,
) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
// Add all data that is not undefined except email and password // Add all data that is not undefined except email and password
@ -52,9 +58,9 @@ const returnError = (query: object, error: string, description: string) => {
searchParams.append("error", error); searchParams.append("error", error);
searchParams.append("error_description", description); searchParams.append("error_description", description);
return response(null, 302, { return context.redirect(
Location: `${config.frontend.routes.login}?${searchParams.toString()}`, `${config.frontend.routes.login}?${searchParams.toString()}`,
}); );
}; };
/** /**
@ -99,9 +105,8 @@ export default apiRoute((app) =>
redirectUrl, redirectUrl,
(error, message, app) => (error, message, app) =>
returnError( returnError(
{ context,
...manager.processOAuth2Error(app), manager.processOAuth2Error(app),
},
error, error,
message, message,
), ),
@ -117,7 +122,7 @@ export default apiRoute((app) =>
// If linking account // If linking account
if (link && user_id) { if (link && user_id) {
return await manager.linkUser(user_id, userInfo); return await manager.linkUser(user_id, context, userInfo);
} }
let userId = ( let userId = (
@ -191,6 +196,7 @@ export default apiRoute((app) =>
userId = user.id; userId = user.id;
} else { } else {
return returnError( return returnError(
context,
{ {
redirect_uri: flow.application?.redirectUri, redirect_uri: flow.application?.redirectUri,
client_id: flow.application?.clientId, client_id: flow.application?.clientId,
@ -207,6 +213,7 @@ export default apiRoute((app) =>
if (!user) { if (!user) {
return returnError( return returnError(
context,
{ {
redirect_uri: flow.application?.redirectUri, redirect_uri: flow.application?.redirectUri,
client_id: flow.application?.clientId, client_id: flow.application?.clientId,
@ -220,6 +227,7 @@ export default apiRoute((app) =>
if (!user.hasPermission(RolePermissions.OAuth)) { if (!user.hasPermission(RolePermissions.OAuth)) {
return returnError( return returnError(
context,
{ {
redirect_uri: flow.application?.redirectUri, redirect_uri: flow.application?.redirectUri,
client_id: flow.application?.clientId, client_id: flow.application?.clientId,
@ -268,8 +276,16 @@ export default apiRoute((app) =>
.sign(privateKey); .sign(privateKey);
// Redirect back to application // Redirect back to application
return response(null, 302, { setCookie(context, "jwt", jwt, {
Location: new URL( httpOnly: true,
secure: true,
sameSite: "strict",
path: "/",
maxAge: 60 * 60,
});
return context.redirect(
new URL(
`${config.frontend.routes.consent}?${new URLSearchParams({ `${config.frontend.routes.consent}?${new URLSearchParams({
redirect_uri: flow.application.redirectUri, redirect_uri: flow.application.redirectUri,
code, code,
@ -281,11 +297,7 @@ export default apiRoute((app) =>
}).toString()}`, }).toString()}`,
config.http.base_url, config.http.base_url,
).toString(), ).toString(),
// Set cookie with JWT );
"Set-Cookie": `jwt=${jwt}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${
60 * 60
}`,
});
}, },
), ),
); );

View file

@ -1,7 +1,7 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig, handleZodError } from "@/api";
import { oauthRedirectUri } from "@/constants"; import { oauthRedirectUri } from "@/constants";
import { redirect, response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import type { Context } from "hono";
import { import {
calculatePKCECodeChallenge, calculatePKCECodeChallenge,
discoveryRequest, discoveryRequest,
@ -35,7 +35,12 @@ export const schemas = {
}), }),
}; };
const returnError = (query: object, error: string, description: string) => { const returnError = (
context: Context,
query: object,
error: string,
description: string,
) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
// Add all data that is not undefined except email and password // Add all data that is not undefined except email and password
@ -48,9 +53,9 @@ const returnError = (query: object, error: string, description: string) => {
searchParams.append("error", error); searchParams.append("error", error);
searchParams.append("error_description", description); searchParams.append("error_description", description);
return response(null, 302, { return context.redirect(
Location: `${config.frontend.routes.login}?${searchParams.toString()}`, `${config.frontend.routes.login}?${searchParams.toString()}`,
}); );
}; };
export default apiRoute((app) => export default apiRoute((app) =>
@ -65,6 +70,7 @@ export default apiRoute((app) =>
if (!client_id || client_id === "undefined") { if (!client_id || client_id === "undefined") {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"client_id is required", "client_id is required",
@ -77,6 +83,7 @@ export default apiRoute((app) =>
if (!issuer) { if (!issuer) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"issuer is invalid", "issuer is invalid",
@ -98,6 +105,7 @@ export default apiRoute((app) =>
if (!application) { if (!application) {
return returnError( return returnError(
context,
body, body,
"invalid_request", "invalid_request",
"client_id is invalid", "client_id is invalid",
@ -119,7 +127,7 @@ export default apiRoute((app) =>
const codeChallenge = const codeChallenge =
await calculatePKCECodeChallenge(codeVerifier); await calculatePKCECodeChallenge(codeVerifier);
return redirect( return context.redirect(
`${authServer.authorization_endpoint}?${new URLSearchParams({ `${authServer.authorization_endpoint}?${new URLSearchParams({
client_id: issuer.client_id, client_id: issuer.client_id,
redirect_uri: `${oauthRedirectUri(issuerId)}?flow=${ redirect_uri: `${oauthRedirectUri(issuerId)}?flow=${
@ -131,7 +139,6 @@ export default apiRoute((app) =>
code_challenge_method: "S256", code_challenge_method: "S256",
code_challenge: codeChallenge, code_challenge: codeChallenge,
}).toString()}`, }).toString()}`,
302,
); );
}, },
), ),

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig, handleZodError } from "@/api";
import { response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { and, eq, inArray, sql } from "drizzle-orm"; import { and, eq, inArray, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
@ -83,9 +82,6 @@ export default apiRoute((app) =>
403, 403,
); );
} }
const objectString = JSON.stringify(apiObject);
// If base_url uses https and request uses http, rewrite request to use https // If base_url uses https and request uses http, rewrite request to use https
// This fixes reverse proxy errors // This fixes reverse proxy errors
const reqUrl = new URL(context.req.url); const reqUrl = new URL(context.req.url);
@ -102,10 +98,7 @@ export default apiRoute((app) =>
"GET", "GET",
); );
return response(objectString, 200, { return context.json(apiObject, 200, headers.toJSON());
"Content-Type": "application/json",
...headers.toJSON(),
});
}, },
), ),
); );

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig, debugRequest, handleZodError } from "@/api"; import { apiRoute, applyConfig, debugRequest, handleZodError } from "@/api";
import { response } from "@/response";
import { sentry } from "@/sentry"; import { sentry } from "@/sentry";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
@ -153,7 +152,7 @@ export default apiRoute((app) =>
) )
) { ) {
// Pretend to accept request // Pretend to accept request
return response(null, 201); return context.newResponse(null, 201);
} }
// Verify request signature // Verify request signature
@ -200,7 +199,7 @@ export default apiRoute((app) =>
const handler = new RequestParserHandler(body, validator); const handler = new RequestParserHandler(body, validator);
try { try {
const result = await handler.parseBody({ return await handler.parseBody<Response>({
note: async (note) => { note: async (note) => {
const account = await User.resolve(note.author); const account = await User.resolve(note.author);
@ -227,7 +226,7 @@ export default apiRoute((app) =>
); );
} }
return response("Note created", 201); return context.text("Note created", 201);
}, },
follow: async (follow) => { follow: async (follow) => {
const account = await User.resolve(follow.author); const account = await User.resolve(follow.author);
@ -246,7 +245,7 @@ export default apiRoute((app) =>
); );
if (foundRelationship.data.following) { if (foundRelationship.data.following) {
return response("Already following", 200); return context.text("Already following", 200);
} }
await foundRelationship.update({ await foundRelationship.update({
@ -269,7 +268,7 @@ export default apiRoute((app) =>
await sendFollowAccept(account, user); await sendFollowAccept(account, user);
} }
return response("Follow request sent", 200); return context.text("Follow request sent", 200);
}, },
followAccept: async (followAccept) => { followAccept: async (followAccept) => {
const account = await User.resolve(followAccept.author); const account = await User.resolve(followAccept.author);
@ -288,7 +287,7 @@ export default apiRoute((app) =>
); );
if (!foundRelationship.data.requested) { if (!foundRelationship.data.requested) {
return response( return context.text(
"There is no follow request to accept", "There is no follow request to accept",
200, 200,
); );
@ -299,7 +298,7 @@ export default apiRoute((app) =>
following: true, following: true,
}); });
return response("Follow request accepted", 200); return context.text("Follow request accepted", 200);
}, },
followReject: async (followReject) => { followReject: async (followReject) => {
const account = await User.resolve(followReject.author); const account = await User.resolve(followReject.author);
@ -318,7 +317,7 @@ export default apiRoute((app) =>
); );
if (!foundRelationship.data.requested) { if (!foundRelationship.data.requested) {
return response( return context.text(
"There is no follow request to reject", "There is no follow request to reject",
200, 200,
); );
@ -329,7 +328,7 @@ export default apiRoute((app) =>
following: false, following: false,
}); });
return response("Follow request rejected", 200); return context.text("Follow request rejected", 200);
}, },
// "delete" is a reserved keyword in JS // "delete" is a reserved keyword in JS
delete: async (delete_) => { delete: async (delete_) => {
@ -345,7 +344,7 @@ export default apiRoute((app) =>
if (note) { if (note) {
await note.delete(); await note.delete();
return response("Note deleted", 200); return context.text("Note deleted", 200);
} }
break; break;
@ -357,7 +356,10 @@ export default apiRoute((app) =>
if (otherUser.id === user.id) { if (otherUser.id === user.id) {
// Delete own account // Delete own account
await user.delete(); await user.delete();
return response("Account deleted", 200); return context.text(
"Account deleted",
200,
);
} }
return context.json( return context.json(
{ {
@ -378,6 +380,11 @@ export default apiRoute((app) =>
); );
} }
} }
return context.json(
{ error: "Object not found or not owned by user" },
404,
);
}, },
user: async (user) => { user: async (user) => {
// Refetch user to ensure we have the latest data // Refetch user to ensure we have the latest data
@ -392,18 +399,15 @@ export default apiRoute((app) =>
); );
} }
return response("User refreshed", 200); return context.text("User refreshed", 200);
}, },
}); unknown: () => {
if (result) {
return result;
}
return context.json( return context.json(
{ error: "Object has not been implemented" }, { error: "Unknown entity type" },
400, 400,
); );
},
});
} catch (e) { } catch (e) {
if (isValidationError(e)) { if (isValidationError(e)) {
return context.json( return context.json(

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig, handleZodError } from "@/api";
import { redirect } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { z } from "zod"; import { z } from "zod";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
@ -48,7 +47,7 @@ export default apiRoute((app) =>
context.req.header("user-agent")?.includes("Mozilla") && context.req.header("user-agent")?.includes("Mozilla") &&
uuid !== "actor" uuid !== "actor"
) { ) {
return redirect(user.toApi().url); return context.redirect(user.toApi().url);
} }
const userJson = user.toVersia(); const userJson = user.toVersia();

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig } from "@/api"; import { apiRoute, applyConfig } from "@/api";
import { xmlResponse } from "@/response";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const meta = applyConfig({ export const meta = applyConfig({
@ -15,8 +14,9 @@ export const meta = applyConfig({
}); });
export default apiRoute((app) => export default apiRoute((app) =>
app.on(meta.allowedMethods, meta.route, () => { app.on(meta.allowedMethods, meta.route, (context) => {
return xmlResponse( context.header("Content-Type", "application/xrd+xml");
return context.body(
`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="${new URL( `<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" template="${new URL(
"/.well-known/webfinger", "/.well-known/webfinger",
config.http.base_url, config.http.base_url,

View file

@ -1,5 +1,4 @@
import { apiRoute, applyConfig } from "@/api"; import { apiRoute, applyConfig } from "@/api";
import { redirect } from "@/response";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const meta = applyConfig({ export const meta = applyConfig({
@ -15,9 +14,12 @@ export const meta = applyConfig({
}); });
export default apiRoute((app) => export default apiRoute((app) =>
app.on(meta.allowedMethods, meta.route, () => { app.on(meta.allowedMethods, meta.route, (context) => {
return redirect( return context.redirect(
new URL("/.well-known/nodeinfo/2.0", config.http.base_url), new URL(
"/.well-known/nodeinfo/2.0",
config.http.base_url,
).toString(),
301, 301,
); );
}), }),

View file

@ -1,4 +1,3 @@
import { response } from "@/response";
import { createMiddleware } from "@hono/hono/factory"; import { createMiddleware } from "@hono/hono/factory";
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
import type { SocketAddress } from "bun"; import type { SocketAddress } from "bun";
@ -34,7 +33,7 @@ export const bait = createMiddleware(async (context, next) => {
if (requestIp?.address) { if (requestIp?.address) {
for (const ip of config.http.bait.bait_ips) { for (const ip of config.http.bait.bait_ips) {
if (matches(ip, requestIp.address)) { if (matches(ip, requestIp.address)) {
return response(file); return context.newResponse(file.stream());
} }
} }
} }
@ -44,7 +43,7 @@ export const bait = createMiddleware(async (context, next) => {
for (const agent of config.http.bait.bait_user_agents) { for (const agent of config.http.bait.bait_user_agents) {
if (new RegExp(agent).test(ua)) { if (new RegExp(agent).test(ua)) {
return response(file); return context.newResponse(file.stream());
} }
} }

View file

@ -1,5 +1,5 @@
import { response } from "@/response";
import type { InferInsertModel } from "drizzle-orm"; import type { InferInsertModel } from "drizzle-orm";
import type { Context } from "hono";
import { import {
type AuthorizationServer, type AuthorizationServer,
authorizationCodeGrantRequest, authorizationCodeGrantRequest,
@ -146,6 +146,7 @@ export class OAuthManager {
async linkUser( async linkUser(
userId: string, userId: string,
context: Context,
// Return value of automaticOidcFlow // Return value of automaticOidcFlow
oidcFlowData: Exclude< oidcFlowData: Exclude<
Awaited< Awaited<
@ -158,14 +159,14 @@ export class OAuthManager {
// Check if userId is equal to application.clientId // Check if userId is equal to application.clientId
if (!flow.application?.clientId.startsWith(userId)) { if (!flow.application?.clientId.startsWith(userId)) {
return response(null, 302, { return context.redirect(
Location: `${config.http.base_url}${ `${config.http.base_url}${
config.frontend.routes.home config.frontend.routes.home
}?${new URLSearchParams({ }?${new URLSearchParams({
oidc_account_linking_error: "Account linking error", oidc_account_linking_error: "Account linking error",
oidc_account_linking_error_message: `User ID does not match application client ID (${userId} != ${flow.application?.clientId})`, oidc_account_linking_error_message: `User ID does not match application client ID (${userId} != ${flow.application?.clientId})`,
})}`, })}`,
}); );
} }
// Check if account is already linked // Check if account is already linked
@ -178,27 +179,27 @@ export class OAuthManager {
}); });
if (account) { if (account) {
return response(null, 302, { return context.redirect(
Location: `${config.http.base_url}${ `${config.http.base_url}${
config.frontend.routes.home config.frontend.routes.home
}?${new URLSearchParams({ }?${new URLSearchParams({
oidc_account_linking_error: "Account already linked", oidc_account_linking_error: "Account already linked",
oidc_account_linking_error_message: oidc_account_linking_error_message:
"This account has already been linked to this OpenID Connect provider.", "This account has already been linked to this OpenID Connect provider.",
})}`, })}`,
}); );
} }
// Link the account // Link the account
await this.linkUserInDatabase(userId, userInfo.sub); await this.linkUserInDatabase(userId, userInfo.sub);
return response(null, 302, { return context.redirect(
Location: `${config.http.base_url}${ `${config.http.base_url}${
config.frontend.routes.home config.frontend.routes.home
}?${new URLSearchParams({ }?${new URLSearchParams({
oidc_account_linked: "true", oidc_account_linked: "true",
})}`, })}`,
}); );
} }
async automaticOidcFlow( async automaticOidcFlow(

View file

@ -1,32 +1,5 @@
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
export const response = (
data: BodyInit | null = null,
status = 200,
headers: Record<string, string> = {},
) => {
return new Response(data, {
headers: {
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
"Strict-Transport-Security": "max-age=3153600",
"X-Permitted-Cross-Domain-Policies": "none",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers":
"Authorization,Content-Type,Idempotency-Key",
"Access-Control-Allow-Methods": "POST,PUT,DELETE,GET,PATCH,OPTIONS",
"Access-Control-Allow-Origin": "*",
"Access-Control-Expose-Headers":
"Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id,Idempotency-Key",
"Content-Security-Policy":
"default-src 'none'; frame-ancestors 'none'; form-action 'none'",
...headers,
},
status,
});
};
export type Json = export type Json =
| string | string
| number | number
@ -36,23 +9,6 @@ export type Json =
| Json[] | Json[]
| { [key: string]: Json }; | { [key: string]: Json };
export const xmlResponse = (data: string, status = 200) => {
return response(data, status, {
"Content-Type": "application/xml",
});
};
export const redirect = (
url: string | URL,
status = 302,
extraHeaders: Record<string, string> = {},
) => {
return response(null, status, {
Location: url.toString(),
...extraHeaders,
});
};
export const proxyUrl = (url: string | null = null) => { export const proxyUrl = (url: string | null = null) => {
const urlAsBase64Url = Buffer.from(url || "").toString("base64url"); const urlAsBase64Url = Buffer.from(url || "").toString("base64url");
return url return url