mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
feat(federation): ✨ Add UI to view BullMQ metadata
This commit is contained in:
parent
8a920218ea
commit
ecc7d1eee7
2
app.ts
2
app.ts
|
|
@ -1,5 +1,6 @@
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { handleZodError } from "@/api";
|
import { handleZodError } from "@/api";
|
||||||
|
import { applyToHono } from "@/bull-board.ts";
|
||||||
import { configureLoggers } from "@/loggers";
|
import { configureLoggers } from "@/loggers";
|
||||||
import { sentry } from "@/sentry";
|
import { sentry } from "@/sentry";
|
||||||
import { cors } from "@hono/hono/cors";
|
import { cors } from "@hono/hono/cors";
|
||||||
|
|
@ -160,6 +161,7 @@ export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
|
app.get("/docs", swaggerUI({ url: "/openapi.json" }));
|
||||||
|
applyToHono(app);
|
||||||
|
|
||||||
app.options("*", (context) => {
|
app.options("*", (context) => {
|
||||||
return context.text("", 204);
|
return context.text("", 204);
|
||||||
|
|
|
||||||
4
build.ts
4
build.ts
|
|
@ -35,6 +35,10 @@ await Bun.build({
|
||||||
|
|
||||||
buildSpinner.text = "Transforming";
|
buildSpinner.text = "Transforming";
|
||||||
|
|
||||||
|
// Fix Bun incorrectly transforming aliased imports
|
||||||
|
await $`sed -i 's/var serveStatic = (options) => {/var serveStaticBase = (options) => {/g' dist/index.js`;
|
||||||
|
await $`sed -i 's/ return serveStatic({/ return serveStaticBase({/g' dist/index.js`;
|
||||||
|
|
||||||
// Copy Drizzle migrations to dist
|
// Copy Drizzle migrations to dist
|
||||||
await $`cp -r drizzle dist/drizzle`;
|
await $`cp -r drizzle dist/drizzle`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client",
|
"@bradenmacdonald/s3-lite-client": "npm:@jsr/bradenmacdonald__s3-lite-client",
|
||||||
|
"@bull-board/api": "^6.5.3",
|
||||||
|
"@bull-board/hono": "^6.5.3",
|
||||||
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
"@hackmd/markdown-it-task-lists": "^2.1.4",
|
||||||
"@hono/hono": "npm:@jsr/hono__hono@4.6.11",
|
"@hono/hono": "npm:@jsr/hono__hono@4.6.11",
|
||||||
"@hono/prometheus": "^1.0.1",
|
"@hono/prometheus": "^1.0.1",
|
||||||
|
|
@ -164,5 +166,8 @@
|
||||||
"xss": "^1.0.15",
|
"xss": "^1.0.15",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-validation-error": "^3.4.0"
|
"zod-validation-error": "^3.4.0"
|
||||||
|
},
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@bull-board/api@6.5.3": "patches/@bull-board%2Fapi@6.5.3.patch"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
patches/@bull-board%2Fapi@6.5.3.patch
Normal file
13
patches/@bull-board%2Fapi@6.5.3.patch
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
diff --git a/dist/src/index.js b/dist/src/index.js
|
||||||
|
index b7fa76b4861aefc96e27b3167b1511c3723ad318..c56d37721672b9ede3c85c84cb40c91c4ed2cc83 100644
|
||||||
|
--- a/dist/src/index.js
|
||||||
|
+++ b/dist/src/index.js
|
||||||
|
@@ -10,7 +10,7 @@ const queuesApi_1 = require("./queuesApi");
|
||||||
|
const routes_1 = require("./routes");
|
||||||
|
function createBullBoard({ queues, serverAdapter, options = { uiConfig: {} }, }) {
|
||||||
|
const { bullBoardQueues, setQueues, replaceQueues, addQueue, removeQueue } = (0, queuesApi_1.getQueuesApi)(queues);
|
||||||
|
- const uiBasePath = options.uiBasePath || path_1.default.dirname(eval(`require.resolve('@bull-board/ui/package.json')`));
|
||||||
|
+ const uiBasePath = options.uiBasePath || path_1.default.dirname(import.meta.require.resolve('@bull-board/ui/package.json'));
|
||||||
|
serverAdapter
|
||||||
|
.setQueues(bullBoardQueues)
|
||||||
|
.setViewsPath(path_1.default.join(uiBasePath, 'dist'))
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
|
import { getCookie } from "@hono/hono/cookie";
|
||||||
import { Hooks, Plugin } from "@versia/kit";
|
import { Hooks, Plugin } from "@versia/kit";
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
import { jwtVerify } from "jose";
|
||||||
|
import { JOSEError, JWTExpired } from "jose/errors";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { RolePermissions } from "~/drizzle/schema.ts";
|
||||||
import authorizeRoute from "./routes/authorize.ts";
|
import authorizeRoute from "./routes/authorize.ts";
|
||||||
import jwksRoute from "./routes/jwks.ts";
|
import jwksRoute from "./routes/jwks.ts";
|
||||||
import ssoLoginCallbackRoute from "./routes/oauth/callback.ts";
|
import ssoLoginCallbackRoute from "./routes/oauth/callback.ts";
|
||||||
|
|
@ -11,8 +15,7 @@ import tokenRoute from "./routes/oauth/token.ts";
|
||||||
import ssoIdRoute from "./routes/sso/:id/index.ts";
|
import ssoIdRoute from "./routes/sso/:id/index.ts";
|
||||||
import ssoRoute from "./routes/sso/index.ts";
|
import ssoRoute from "./routes/sso/index.ts";
|
||||||
|
|
||||||
const plugin = new Plugin(
|
const configSchema = z.object({
|
||||||
z.object({
|
|
||||||
forced: z.boolean().default(false),
|
forced: z.boolean().default(false),
|
||||||
allow_registration: z.boolean().default(true),
|
allow_registration: z.boolean().default(true),
|
||||||
providers: z
|
providers: z
|
||||||
|
|
@ -69,8 +72,7 @@ const plugin = new Plugin(
|
||||||
.optional()
|
.optional()
|
||||||
.transform(async (v, ctx) => {
|
.transform(async (v, ctx) => {
|
||||||
if (!(v?.private && v?.public)) {
|
if (!(v?.private && v?.public)) {
|
||||||
const { public_key, private_key } =
|
const { public_key, private_key } = await User.generateKeys();
|
||||||
await User.generateKeys();
|
|
||||||
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
|
|
@ -81,8 +83,9 @@ const plugin = new Plugin(
|
||||||
|
|
||||||
return v as Exclude<typeof v, undefined>;
|
return v as Exclude<typeof v, undefined>;
|
||||||
}),
|
}),
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
const plugin = new Plugin(configSchema);
|
||||||
|
|
||||||
// Test hook for screenshots
|
// Test hook for screenshots
|
||||||
plugin.registerHandler(Hooks.Response, (req) => {
|
plugin.registerHandler(Hooks.Response, (req) => {
|
||||||
|
|
@ -99,5 +102,84 @@ jwksRoute(plugin);
|
||||||
ssoLoginRoute(plugin);
|
ssoLoginRoute(plugin);
|
||||||
ssoLoginCallbackRoute(plugin);
|
ssoLoginCallbackRoute(plugin);
|
||||||
|
|
||||||
|
plugin.registerRoute("/admin/*", (app) => {
|
||||||
|
// Check for JWT when accessing the admin panel
|
||||||
|
app.use("/admin/*", async (context, next) => {
|
||||||
|
const jwtCookie = getCookie(context, "jwt");
|
||||||
|
|
||||||
|
if (!jwtCookie) {
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "Unauthorized",
|
||||||
|
message: "No JWT cookie provided",
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { keys } = context.get("pluginConfig");
|
||||||
|
|
||||||
|
const result = await jwtVerify(jwtCookie, keys.public, {
|
||||||
|
algorithms: ["EdDSA"],
|
||||||
|
issuer: new URL(context.get("config").http.base_url).origin,
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error instanceof JOSEError) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result instanceof JOSEError) {
|
||||||
|
if (result instanceof JWTExpired) {
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "Unauthorized",
|
||||||
|
message: "JWT has expired. Please log in again.",
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "Unauthorized",
|
||||||
|
message: "Invalid JWT",
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
payload: { sub },
|
||||||
|
} = result;
|
||||||
|
|
||||||
|
if (!sub) {
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "Unauthorized",
|
||||||
|
message: "Invalid JWT (no sub)",
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.fromId(sub);
|
||||||
|
|
||||||
|
if (!user?.hasPermission(RolePermissions.ManageInstanceFederation)) {
|
||||||
|
return context.json(
|
||||||
|
{
|
||||||
|
error: "Unauthorized",
|
||||||
|
message:
|
||||||
|
"You do not have permission to access this resource",
|
||||||
|
},
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export type PluginType = typeof plugin;
|
export type PluginType = typeof plugin;
|
||||||
export default plugin;
|
export default plugin;
|
||||||
|
|
|
||||||
38
utils/bull-board.ts
Normal file
38
utils/bull-board.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { createBullBoard } from "@bull-board/api";
|
||||||
|
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
|
||||||
|
import { HonoAdapter } from "@bull-board/hono";
|
||||||
|
import { serveStatic } from "@hono/hono/bun";
|
||||||
|
import type { OpenAPIHono } from "@hono/zod-openapi";
|
||||||
|
import { config } from "~/packages/config-manager";
|
||||||
|
import type { HonoEnv } from "~/types/api";
|
||||||
|
import { deliveryQueue, inboxQueue } from "~/worker";
|
||||||
|
|
||||||
|
export const applyToHono = (app: OpenAPIHono<HonoEnv>): void => {
|
||||||
|
const serverAdapter = new HonoAdapter(serveStatic);
|
||||||
|
|
||||||
|
createBullBoard({
|
||||||
|
queues: [
|
||||||
|
new BullMQAdapter(inboxQueue),
|
||||||
|
new BullMQAdapter(deliveryQueue),
|
||||||
|
],
|
||||||
|
serverAdapter,
|
||||||
|
options: {
|
||||||
|
uiConfig: {
|
||||||
|
boardTitle: "Server Queues",
|
||||||
|
favIcon: {
|
||||||
|
default: "/favicon.png",
|
||||||
|
alternative: "/favicon.ico",
|
||||||
|
},
|
||||||
|
boardLogo: {
|
||||||
|
path:
|
||||||
|
config.instance.logo ??
|
||||||
|
"https://cdn.versia.pub/branding/icon.svg",
|
||||||
|
height: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
serverAdapter.setBasePath("/admin/queues");
|
||||||
|
app.route("/admin/queues", serverAdapter.registerPlugin());
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue