refactor(api): 🚚 Refactor authentication middleware and implement some OpenAPI routes

This commit is contained in:
Jesse Wierzbinski 2024-08-27 17:20:36 +02:00
parent edf5edca9f
commit 1ab1c68d36
No known key found for this signature in database
66 changed files with 383 additions and 279 deletions

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig } from "@/api";
import { redirect } from "@/response"; import { redirect } from "@/response";
import { zValidator } from "@hono/zod-validator"; 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";
import { z } from "zod"; import { z } from "zod";
@ -59,6 +59,34 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "post",
path: "/api/auth/login",
summary: "Login",
description: "Login to the application",
request: {
body: {
content: {
"multipart/form-data": {
schema: schemas.form,
},
},
},
query: schemas.query,
},
responses: {
302: {
description: "Redirect to OAuth authorize, or error",
headers: {
"Set-Cookie": {
description: "JWT cookie",
required: false,
},
},
},
},
});
const returnError = (query: object, error: string, description: string) => { const returnError = (query: object, error: string, description: string) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
@ -81,12 +109,7 @@ const returnError = (query: object, error: string, description: string) => {
}; };
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
zValidator("form", schemas.form, handleZodError),
zValidator("query", schemas.query, handleZodError),
async (context) => {
if (config.oidc.forced) { if (config.oidc.forced) {
return returnError( return returnError(
context.req.query(), context.req.query(),
@ -109,10 +132,7 @@ export default apiRoute((app) =>
if ( if (
!( !(
user && user &&
(await Bun.password.verify( (await Bun.password.verify(password, user.data.password || ""))
password,
user.data.password || "",
))
) )
) { ) {
return returnError( return returnError(
@ -124,12 +144,12 @@ export default apiRoute((app) =>
if (user.data.passwordResetToken) { if (user.data.passwordResetToken) {
return redirect( return redirect(
`${ `${config.frontend.routes.password_reset}?${new URLSearchParams(
config.frontend.routes.password_reset {
}?${new URLSearchParams({
token: user.data.passwordResetToken ?? "", token: user.data.passwordResetToken ?? "",
login_reset: "true", login_reset: "true",
}).toString()}`, },
).toString()}`,
); );
} }
@ -172,11 +192,7 @@ export default apiRoute((app) =>
// 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(context.req.query())) { for (const [key, value] of Object.entries(context.req.query())) {
if ( if (key !== "email" && key !== "password" && value !== undefined) {
key !== "email" &&
key !== "password" &&
value !== undefined
) {
searchParams.append(key, String(value)); searchParams.append(key, String(value));
} }
} }
@ -191,6 +207,5 @@ export default apiRoute((app) =>
}`, }`,
}, },
); );
}, }),
),
); );

View file

@ -1,5 +1,5 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig } from "@/api";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
@ -26,17 +26,29 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "get",
path: "/api/auth/redirect",
summary: "OAuth Code flow",
description:
"Redirects to the application, or back to login if the code is invalid",
responses: {
302: {
description:
"Redirects to the application, or back to login if the code is invalid",
},
},
request: {
query: schemas.query,
},
});
/** /**
* OAuth Code flow * OAuth Code flow
*/ */
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods, const { redirect_uri, client_id, code } = context.req.valid("query");
meta.route,
zValidator("query", schemas.query, handleZodError),
async (context) => {
const { redirect_uri, client_id, code } =
context.req.valid("query");
const redirectToLogin = (error: string) => const redirectToLogin = (error: string) =>
Response.redirect( Response.redirect(
@ -50,10 +62,7 @@ export default apiRoute((app) =>
const foundToken = await db const foundToken = await db
.select() .select()
.from(Tokens) .from(Tokens)
.leftJoin( .leftJoin(Applications, eq(Tokens.applicationId, Applications.id))
Applications,
eq(Tokens.applicationId, Applications.id),
)
.where( .where(
and( and(
eq(Tokens.code, code), eq(Tokens.code, code),
@ -68,6 +77,5 @@ export default apiRoute((app) =>
// Redirect back to application // Redirect back to application
return Response.redirect(`${redirect_uri}?code=${code}`, 302); return Response.redirect(`${redirect_uri}?code=${code}`, 302);
}, }),
),
); );

View file

@ -1,6 +1,6 @@
import { apiRoute, applyConfig, handleZodError } from "@/api"; import { apiRoute, applyConfig } from "@/api";
import { response } from "@/response"; import { response } from "@/response";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { Users } from "~/drizzle/schema"; import { Users } from "~/drizzle/schema";
@ -26,6 +26,30 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "post",
path: "/api/auth/reset",
summary: "Reset password",
description: "Reset password",
responses: {
302: {
description: "Redirect to the password reset page with a message",
},
},
request: {
body: {
content: {
"application/x-www-form-urlencoded": {
schema: schemas.form,
},
"multipart/form-data": {
schema: schemas.form,
},
},
},
},
});
const returnError = (token: string, error: string, description: string) => { const returnError = (token: string, error: string, description: string) => {
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
@ -44,16 +68,10 @@ const returnError = (token: string, error: string, description: string) => {
}; };
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
zValidator("form", schemas.form, handleZodError),
async (context) => {
const { token, password } = context.req.valid("form"); const { token, password } = context.req.valid("form");
const user = await User.fromSql( const user = await User.fromSql(eq(Users.passwordResetToken, token));
eq(Users.passwordResetToken, token),
);
if (!user) { if (!user) {
return returnError(token, "invalid_token", "Invalid token"); return returnError(token, "invalid_token", "Invalid token");
@ -67,6 +85,5 @@ export default apiRoute((app) =>
return response(null, 302, { return response(null, 302, {
Location: `${config.frontend.routes.password_reset}?success=true`, Location: `${config.frontend.routes.password_reset}?success=true`,
}); });
}, }),
),
); );

View file

@ -1,9 +1,10 @@
import { apiRoute, applyConfig, auth, handleZodError } from "@/api"; import { apiRoute, applyConfig, auth } from "@/api";
import { zValidator } from "@hono/zod-validator"; import { createRoute } from "@hono/zod-openapi";
import { z } from "zod"; import { z } from "zod";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
import { Relationship } from "~/packages/database-interface/relationship"; import { Relationship } from "~/packages/database-interface/relationship";
import { User } from "~/packages/database-interface/user"; import { User } from "~/packages/database-interface/user";
import { ErrorSchema } from "~/types/api";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -30,15 +31,47 @@ export const schemas = {
}), }),
}; };
const route = createRoute({
method: "post",
path: "/api/v1/accounts/{id}/block",
summary: "Block user",
description: "Block a user",
middleware: [auth(meta.auth, meta.permissions)],
responses: {
200: {
description: "User blocked",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
request: {
params: schemas.param,
},
});
export default apiRoute((app) => export default apiRoute((app) =>
app.on( app.openapi(route, async (context) => {
meta.allowedMethods,
meta.route,
zValidator("param", schemas.param, handleZodError),
auth(meta.auth, meta.permissions),
async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);
@ -61,7 +94,6 @@ export default apiRoute((app) =>
}); });
} }
return context.json(foundRelationship.toApi()); return context.json(foundRelationship.toApi(), 200);
}, }),
),
); );

View file

@ -50,7 +50,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const { reblogs, notify, languages } = context.req.valid("json"); const { reblogs, notify, languages } = context.req.valid("json");
if (!user) { if (!user) {

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const foundUser = await User.fromId(id); const foundUser = await User.fromId(id);

View file

@ -48,7 +48,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
// TODO: Add duration support // TODO: Add duration support
const { notifications } = context.req.valid("json"); const { notifications } = context.req.valid("json");

View file

@ -42,7 +42,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const { comment } = context.req.valid("json"); const { comment } = context.req.valid("json");
if (!user) { if (!user) {

View file

@ -38,7 +38,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -38,7 +38,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -69,7 +69,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const otherUser = await User.fromId(id); const otherUser = await User.fromId(id);

View file

@ -38,7 +38,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -38,7 +38,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -38,7 +38,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -38,7 +38,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -36,7 +36,7 @@ export default apiRoute((app) =>
zValidator("query", schemas.query, handleZodError), zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
const { id: ids } = context.req.valid("query"); const { id: ids } = context.req.valid("query");
if (!self) { if (!self) {

View file

@ -46,7 +46,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { acct } = context.req.valid("query"); const { acct } = context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!acct) { if (!acct) {
return context.json({ error: "Invalid acct parameter" }, 400); return context.json({ error: "Invalid acct parameter" }, 400);

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
zValidator("query", schemas.query, handleZodError), zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
const { id } = context.req.valid("query"); const { id } = context.req.valid("query");
const ids = Array.isArray(id) ? id : [id]; const ids = Array.isArray(id) ? id : [id];

View file

@ -76,7 +76,7 @@ export default apiRoute((app) =>
async (context) => { async (context) => {
const { q, limit, offset, resolve, following } = const { q, limit, offset, resolve, following } =
context.req.valid("query"); context.req.valid("query");
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
if (!self && following) { if (!self && following) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -133,7 +133,7 @@ export default apiRoute((app) =>
zValidator("json", schemas.json, handleZodError), zValidator("json", schemas.json, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const { const {
display_name, display_name,
username, username,

View file

@ -20,7 +20,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
(context) => { (context) => {
// TODO: Add checks for disabled/unverified accounts // TODO: Add checks for disabled/unverified accounts
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -23,7 +23,7 @@ export default apiRoute((app) =>
meta.route, meta.route,
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user, token } = context.req.valid("header"); const { user, token } = context.get("auth");
if (!token) { if (!token) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -46,7 +46,7 @@ export default apiRoute((app) =>
const { max_id, since_id, min_id, limit } = const { max_id, since_id, min_id, limit } =
context.req.valid("query"); context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -24,7 +24,7 @@ export default apiRoute((app) =>
meta.route, meta.route,
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const emojis = await Emoji.manyFromSql( const emojis = await Emoji.manyFromSql(
and( and(

View file

@ -77,7 +77,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -69,7 +69,7 @@ export default apiRoute((app) =>
async (context) => { async (context) => {
const { shortcode, element, alt, global, category } = const { shortcode, element, alt, global, category } =
context.req.valid("json"); context.req.valid("json");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -45,7 +45,7 @@ export default apiRoute((app) =>
const { max_id, since_id, min_id, limit } = const { max_id, since_id, min_id, limit } =
context.req.valid("query"); context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
zValidator("param", schemas.param, handleZodError), zValidator("param", schemas.param, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
zValidator("param", schemas.param, handleZodError), zValidator("param", schemas.param, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -45,7 +45,7 @@ export default apiRoute((app) =>
const { max_id, since_id, min_id, limit } = const { max_id, since_id, min_id, limit } =
context.req.valid("query"); context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -48,7 +48,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { "timeline[]": timelines } = context.req.valid("query"); const { "timeline[]": timelines } = context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const timeline = Array.isArray(timelines) ? timelines : []; const timeline = Array.isArray(timelines) ? timelines : [];

View file

@ -45,7 +45,7 @@ export default apiRoute((app) =>
async (context) => { async (context) => {
const { max_id, since_id, limit, min_id } = const { max_id, since_id, limit, min_id } =
context.req.valid("query"); context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -36,7 +36,7 @@ export default apiRoute((app) =>
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);
} }

View file

@ -35,7 +35,7 @@ export default apiRoute((app) =>
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);
} }

View file

@ -25,7 +25,7 @@ export default apiRoute((app) =>
meta.route, meta.route,
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);
} }

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
zValidator("query", schemas.query, handleZodError), zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -102,7 +102,7 @@ export default apiRoute((app) =>
zValidator("query", schemas.query, handleZodError), zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);
} }

View file

@ -22,7 +22,7 @@ export default apiRoute((app) =>
meta.route, meta.route,
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -22,7 +22,7 @@ export default apiRoute((app) =>
meta.route, meta.route,
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
if (!self) { if (!self) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -36,7 +36,7 @@ export default apiRoute((app) =>
zValidator("param", schemas.param, handleZodError), zValidator("param", schemas.param, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
if (!user) { if (!user) {

View file

@ -19,7 +19,7 @@ export default apiRoute((app) =>
meta.route, meta.route,
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -41,7 +41,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id: issuerId } = context.req.valid("param"); const { id: issuerId } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -55,7 +55,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const form = context.req.valid("json"); const form = context.req.valid("json");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const foundStatus = await Note.fromId(id, user?.id); const foundStatus = await Note.fromId(id, user?.id);

View file

@ -36,7 +36,7 @@ export default apiRoute((app) =>
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -51,7 +51,7 @@ export default apiRoute((app) =>
context.req.valid("query"); context.req.valid("query");
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -105,7 +105,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
// TODO: Polls // TODO: Polls
const { const {

View file

@ -40,7 +40,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -41,7 +41,7 @@ export default apiRoute((app) =>
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { visibility } = context.req.valid("json"); const { visibility } = context.req.valid("json");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -44,7 +44,7 @@ export default apiRoute((app) =>
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { max_id, min_id, since_id, limit } = const { max_id, min_id, since_id, limit } =
context.req.valid("query"); context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -33,7 +33,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -34,7 +34,7 @@ export default apiRoute((app) =>
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -108,7 +108,7 @@ export default apiRoute((app) =>
zValidator("json", schemas.json, handleZodError), zValidator("json", schemas.json, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user, application } = context.req.valid("header"); const { user, application } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -50,7 +50,7 @@ export default apiRoute((app) =>
const { max_id, since_id, min_id, limit } = const { max_id, since_id, min_id, limit } =
context.req.valid("query"); context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -68,7 +68,7 @@ export default apiRoute((app) =>
only_media, only_media,
} = context.req.valid("query"); } = context.req.valid("query");
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const { objects, link } = await Timeline.getNoteTimeline( const { objects, link } = await Timeline.getNoteTimeline(
and( and(

View file

@ -77,7 +77,7 @@ export default apiRoute((app) =>
zValidator("json", schemas.json, handleZodError), zValidator("json", schemas.json, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
const { id } = context.req.valid("param"); const { id } = context.req.valid("param");
if (!user) { if (!user) {

View file

@ -65,7 +65,7 @@ export default apiRoute((app) =>
zValidator("json", schemas.json, handleZodError), zValidator("json", schemas.json, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user } = context.req.valid("header"); const { user } = context.get("auth");
if (!user) { if (!user) {
return context.json({ error: "Unauthorized" }, 401); return context.json({ error: "Unauthorized" }, 401);

View file

@ -57,7 +57,7 @@ export default apiRoute((app) =>
zValidator("query", schemas.query, handleZodError), zValidator("query", schemas.query, handleZodError),
auth(meta.auth, meta.permissions), auth(meta.auth, meta.permissions),
async (context) => { async (context) => {
const { user: self } = context.req.valid("header"); const { user: self } = context.get("auth");
const { q, type, resolve, following, account_id, limit, offset } = const { q, type, resolve, following, account_id, limit, offset } =
context.req.valid("query"); context.req.valid("query");

4
app.ts
View file

@ -12,12 +12,12 @@ import { boundaryCheck } from "./middlewares/boundary-check";
import { ipBans } from "./middlewares/ip-bans"; import { ipBans } from "./middlewares/ip-bans";
import { logger } from "./middlewares/logger"; import { logger } from "./middlewares/logger";
import { routes } from "./routes"; import { routes } from "./routes";
import type { ApiRouteExports } from "./types/api"; import type { ApiRouteExports, HonoEnv } from "./types/api";
export const appFactory = async () => { export const appFactory = async () => {
const serverLogger = getLogger("server"); const serverLogger = getLogger("server");
const app = new OpenAPIHono({ const app = new OpenAPIHono<HonoEnv>({
strict: false, strict: false,
}); });

View file

@ -8,6 +8,7 @@ import {
eq, eq,
inArray, inArray,
} from "drizzle-orm"; } from "drizzle-orm";
import { z } from "zod";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { Relationships } from "~/drizzle/schema"; import { Relationships } from "~/drizzle/schema";
import { BaseInterface } from "./base"; import { BaseInterface } from "./base";
@ -25,6 +26,23 @@ export class Relationship extends BaseInterface<
typeof Relationships, typeof Relationships,
RelationshipWithOpposite RelationshipWithOpposite
> { > {
static schema = z.object({
id: z.string(),
blocked_by: z.boolean(),
blocking: z.boolean(),
domain_blocking: z.boolean(),
endorsed: z.boolean(),
followed_by: z.boolean(),
following: z.boolean(),
muting_notifications: z.boolean(),
muting: z.boolean(),
note: z.string().nullable(),
notifying: z.boolean(),
requested_by: z.boolean(),
requested: z.boolean(),
showing_reblogs: z.boolean(),
});
async reload(): Promise<void> { async reload(): Promise<void> {
const reloaded = await Relationship.fromId(this.data.id); const reloaded = await Relationship.fromId(this.data.id);

View file

@ -11,8 +11,10 @@ import type {
Unfollow, Unfollow,
User, User,
} from "@versia/federation/types"; } from "@versia/federation/types";
import type { z } from "zod"; import { z } from "zod";
import type { Application } from "~/classes/functions/application";
import type { RolePermissions } from "~/drizzle/schema"; import type { RolePermissions } from "~/drizzle/schema";
import type { User as DatabaseUser } from "~/packages/database-interface/user";
export type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS"; export type HttpVerb = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";
export interface ApiRouteMetadata { export interface ApiRouteMetadata {
@ -43,13 +45,27 @@ export interface ApiRouteMetadata {
}; };
} }
export const ErrorSchema = z.object({
error: z.string(),
});
export type HonoEnv = {
Variables: {
auth: {
user: DatabaseUser | null;
token: string | null;
application: Application | null;
};
};
};
export interface ApiRouteExports { export interface ApiRouteExports {
meta: ApiRouteMetadata; meta: ApiRouteMetadata;
schemas?: { schemas?: {
query?: z.AnyZodObject; query?: z.AnyZodObject;
body?: z.AnyZodObject; body?: z.AnyZodObject;
}; };
default: (app: OpenAPIHono) => RouterRoute; default: (app: OpenAPIHono<HonoEnv>) => RouterRoute;
} }
export type KnownEntity = export type KnownEntity =

View file

@ -1,6 +1,5 @@
import type { Context } from "@hono/hono"; import type { Context } from "@hono/hono";
import { createMiddleware } from "@hono/hono/factory"; import { createMiddleware } from "@hono/hono/factory";
import { validator } from "@hono/hono/validator";
import type { OpenAPIHono } from "@hono/zod-openapi"; import type { OpenAPIHono } from "@hono/zod-openapi";
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
import { extractParams, verifySolution } from "altcha-lib"; import { extractParams, verifySolution } from "altcha-lib";
@ -29,7 +28,7 @@ import { db } from "~/drizzle/db";
import { Challenges } from "~/drizzle/schema"; import { Challenges } from "~/drizzle/schema";
import { config } from "~/packages/config-manager/index"; import { config } from "~/packages/config-manager/index";
import type { User } from "~/packages/database-interface/user"; import type { User } from "~/packages/database-interface/user";
import type { ApiRouteMetadata, HttpVerb } from "~/types/api"; import type { ApiRouteMetadata, HonoEnv, HttpVerb } from "~/types/api";
export const applyConfig = (routeMeta: ApiRouteMetadata) => { export const applyConfig = (routeMeta: ApiRouteMetadata) => {
const newMeta = routeMeta; const newMeta = routeMeta;
@ -45,13 +44,7 @@ export const applyConfig = (routeMeta: ApiRouteMetadata) => {
return newMeta; return newMeta;
}; };
export const apiRoute = ( export const apiRoute = (fn: (app: OpenAPIHono<HonoEnv>) => void) => fn;
fn: (
app: OpenAPIHono /* <{
Bindings: {};
}> */,
) => void,
) => fn;
export const idValidator = createRegExp( export const idValidator = createRegExp(
anyOf(digit, charIn("ABCDEF")).times(8), anyOf(digit, charIn("ABCDEF")).times(8),
@ -151,12 +144,6 @@ export const handleZodError = (
} }
}; };
const getAuth = async (value: Record<string, string>) => {
return value.authorization
? await getFromHeader(value.authorization)
: null;
};
const checkPermissions = ( const checkPermissions = (
auth: AuthData | null, auth: AuthData | null,
permissionData: ApiRouteMetadata["permissions"], permissionData: ApiRouteMetadata["permissions"],
@ -300,8 +287,10 @@ export const auth = (
permissionData?: ApiRouteMetadata["permissions"], permissionData?: ApiRouteMetadata["permissions"],
challengeData?: ApiRouteMetadata["challenge"], challengeData?: ApiRouteMetadata["challenge"],
) => ) =>
validator("header", async (value, context) => { createMiddleware<HonoEnv>(async (context, next) => {
const auth = await getAuth(value); const header = context.req.header("Authorization");
const auth = header ? await getFromHeader(header) : null;
// Only exists for type casting, as otherwise weird errors happen with Hono // Only exists for type casting, as otherwise weird errors happen with Hono
const fakeResponse = context.json({}); const fakeResponse = context.json({});
@ -328,13 +317,21 @@ export const auth = (
} }
} }
return checkRouteNeedsAuth(auth, authData, context) as const authCheck = checkRouteNeedsAuth(auth, authData, context) as
| typeof fakeResponse | typeof fakeResponse
| { | {
user: User | null; user: User | null;
token: string | null; token: string | null;
application: Application | null; application: Application | null;
}; };
if (authCheck instanceof Response) {
return authCheck;
}
context.set("auth", authCheck);
await next();
}); });
// Helper function to parse form data // Helper function to parse form data

View file

@ -1,7 +1,8 @@
import type { OpenAPIHono } from "@hono/zod-openapi"; import type { OpenAPIHono } from "@hono/zod-openapi";
import type { Config } from "~/packages/config-manager/config.type"; import type { Config } from "~/packages/config-manager/config.type";
import type { HonoEnv } from "~/types/api";
export const createServer = (config: Config, app: OpenAPIHono) => export const createServer = (config: Config, app: OpenAPIHono<HonoEnv>) =>
Bun.serve({ Bun.serve({
port: config.http.bind_port, port: config.http.bind_port,
reusePort: true, reusePort: true,