Refactor asset handling and authentication logic (for node)

Updated asset path configuration and server-side logic to standardize asset serving. Introduced `shouldSkipAuth` to bypass authentication for asset requests. Added test coverage for the new asset path handling logic.
This commit is contained in:
dswbx
2025-01-10 20:58:03 +01:00
parent 87e07570d4
commit 3bf92a8c65
7 changed files with 65 additions and 27 deletions

View File

@@ -0,0 +1,9 @@
import { describe, expect, it } from "bun:test";
import { shouldSkipAuth } from "../../src/auth/middlewares";
describe("auth middleware", () => {
it("should skip auth on asset paths", () => {
expect(shouldSkipAuth({ req: new Request("http://localhost/assets/test.js") })).toBe(true);
expect(shouldSkipAuth({ req: new Request("http://localhost/") })).toBe(false);
});
});

View File

@@ -58,7 +58,7 @@ const result = await esbuild.build({
sourcemap, sourcemap,
entryPoints: ["src/ui/main.tsx"], entryPoints: ["src/ui/main.tsx"],
entryNames: "[dir]/[name]-[hash]", entryNames: "[dir]/[name]-[hash]",
outdir: "dist/static", outdir: "dist/static/assets",
platform: "browser", platform: "browser",
bundle: true, bundle: true,
splitting: true, splitting: true,
@@ -224,4 +224,4 @@ await tsup.build({
await tsup.build({ await tsup.build({
...baseConfig("astro") ...baseConfig("astro")
}); });

View File

@@ -1,5 +1,6 @@
import type { IncomingMessage } from "node:http"; import type { IncomingMessage } from "node:http";
import { App, type CreateAppConfig, registries } from "bknd"; import { App, type CreateAppConfig, registries } from "bknd";
import { config as $config } from "core";
import type { MiddlewareHandler } from "hono"; import type { MiddlewareHandler } from "hono";
import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter"; import { StorageLocalAdapter } from "media/storage/adapters/StorageLocalAdapter";
import type { AdminControllerOptions } from "modules/server/AdminController"; import type { AdminControllerOptions } from "modules/server/AdminController";
@@ -106,12 +107,10 @@ export async function createRuntimeApp<Env = any>(
App.Events.AppBuiltEvent, App.Events.AppBuiltEvent,
async () => { async () => {
if (serveStatic) { if (serveStatic) {
if (Array.isArray(serveStatic)) { const [path, handler] = Array.isArray(serveStatic)
const [path, handler] = serveStatic; ? serveStatic
app.modules.server.get(path, handler); : [$config.server.assets_path + "*", serveStatic];
} else { app.modules.server.get(path, handler);
app.modules.server.get("/*", serveStatic);
}
} }
await config.onBuilt?.(app); await config.onBuilt?.(app);

View File

@@ -1,4 +1,4 @@
import type { Permission } from "core"; import { type Permission, config } from "core";
import type { Context } from "hono"; import type { Context } from "hono";
import { createMiddleware } from "hono/factory"; import { createMiddleware } from "hono/factory";
import type { ServerEnv } from "modules/Module"; import type { ServerEnv } from "modules/Module";
@@ -21,27 +21,37 @@ async function resolveAuth(app: ServerEnv["Variables"]["app"], c: Context<Server
authenticator.requestCookieRefresh(c); authenticator.requestCookieRefresh(c);
} }
export function shouldSkipAuth(c: { req: Request }) {
return new URL(c.req.url).pathname.startsWith(config.server.assets_path);
}
export const auth = createMiddleware<ServerEnv>(async (c, next) => { export const auth = createMiddleware<ServerEnv>(async (c, next) => {
// make sure to only register once if (!shouldSkipAuth) {
if (c.get("auth_registered")) { // make sure to only register once
return; if (c.get("auth_registered")) {
return;
}
await resolveAuth(c.get("app"), c);
c.set("auth_registered", true);
} }
await resolveAuth(c.get("app"), c);
c.set("auth_registered", true);
await next(); await next();
}); });
export const permission = (...permissions: Permission[]) => export const permission = (...permissions: Permission[]) =>
createMiddleware<ServerEnv>(async (c, next) => { createMiddleware<ServerEnv>(async (c, next) => {
const app = c.get("app"); if (!shouldSkipAuth) {
if (app) { const app = c.get("app");
const p = Array.isArray(permissions) ? permissions : [permissions]; if (app) {
const guard = app.modules.ctx().guard; const p = Array.isArray(permissions) ? permissions : [permissions];
for (const permission of p) { const guard = app.modules.ctx().guard;
guard.throwUnlessGranted(permission); for (const permission of p) {
guard.throwUnlessGranted(permission);
}
} else {
console.warn("app not in context, skip permission check");
} }
} else {
console.warn("app not in context, skip permission check");
} }
await next(); await next();

View File

@@ -10,7 +10,8 @@ export interface DB {}
export const config = { export const config = {
server: { server: {
default_port: 1337 default_port: 1337,
assets_path: "/assets/"
}, },
data: { data: {
default_primary_field: "id" default_primary_field: "id"

View File

@@ -10,7 +10,9 @@ import type { Hono } from "hono";
export type ServerEnv = { export type ServerEnv = {
Variables: { Variables: {
app: App; app: App;
// to prevent resolving auth multiple times
auth_resolved: boolean; auth_resolved: boolean;
// to only register once
auth_registered: boolean; auth_registered: boolean;
html?: string; html?: string;
}; };

View File

@@ -1,7 +1,7 @@
/** @jsxImportSource hono/jsx */ /** @jsxImportSource hono/jsx */
import type { App } from "App"; import type { App } from "App";
import { isDebug } from "core"; import { config, isDebug } from "core";
import { addFlashMessage } from "core/server/flash"; import { addFlashMessage } from "core/server/flash";
import { html } from "hono/html"; import { html } from "hono/html";
import { Fragment } from "hono/jsx"; import { Fragment } from "hono/jsx";
@@ -13,6 +13,7 @@ const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
// @todo: add migration to remove admin path from config // @todo: add migration to remove admin path from config
export type AdminControllerOptions = { export type AdminControllerOptions = {
basepath?: string; basepath?: string;
assets_path?: string;
html?: string; html?: string;
forceDev?: boolean | { mainPath: string }; forceDev?: boolean | { mainPath: string };
}; };
@@ -20,7 +21,7 @@ export type AdminControllerOptions = {
export class AdminController extends Controller { export class AdminController extends Controller {
constructor( constructor(
private readonly app: App, private readonly app: App,
private options: AdminControllerOptions = {} private _options: AdminControllerOptions = {}
) { ) {
super(); super();
} }
@@ -29,6 +30,14 @@ export class AdminController extends Controller {
return this.app.modules.ctx(); return this.app.modules.ctx();
} }
get options() {
return {
...this._options,
basepath: this._options.basepath ?? "/",
assets_path: this._options.assets_path ?? config.server.assets_path
};
}
get basepath() { get basepath() {
return this.options.basepath ?? "/"; return this.options.basepath ?? "/";
} }
@@ -156,8 +165,16 @@ export class AdminController extends Controller {
<title>BKND</title> <title>BKND</title>
{isProd ? ( {isProd ? (
<Fragment> <Fragment>
<script type="module" CrossOrigin src={"/" + assets?.js} /> <script
<link rel="stylesheet" crossOrigin href={"/" + assets?.css} /> type="module"
CrossOrigin
src={this.options.assets_path + assets?.js}
/>
<link
rel="stylesheet"
crossOrigin
href={this.options.assets_path + assets?.css}
/>
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>