mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
reworked authentication and permission handling
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||

|

|
||||||
|
|
||||||
bknd simplifies app development by providing fully functional backend for data management,
|
bknd simplifies app development by providing fully functional backend for data management,
|
||||||
authentication, workflows and media. Since it's lightweight and built on Web Standards, it can
|
authentication, workflows and media. Since it's lightweight and built on Web Standards, it can
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ const result = await esbuild.build({
|
|||||||
const manifest_file = "dist/static/manifest.json";
|
const manifest_file = "dist/static/manifest.json";
|
||||||
await Bun.write(manifest_file, JSON.stringify(manifest, null, 2));
|
await Bun.write(manifest_file, JSON.stringify(manifest, null, 2));
|
||||||
console.log(`Manifest written to ${manifest_file}`, manifest);
|
console.log(`Manifest written to ${manifest_file}`, manifest);
|
||||||
|
|
||||||
|
// copy assets to static
|
||||||
|
await $`cp -r src/ui/assets/* dist/static/assets`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"bin": "./dist/cli/index.js",
|
"bin": "./dist/cli/index.js",
|
||||||
"version": "0.5.0-rc6",
|
"version": "0.5.0-rc13",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
"build:all": "NODE_ENV=production bun run build.ts --minify --types --clean && bun run build:cli",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"updater": "bun x npm-check-updates -ui",
|
"updater": "bun x npm-check-updates -ui",
|
||||||
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
"build:cli": "bun build src/cli/index.ts --target node --outdir dist/cli --minify",
|
||||||
"cli": "LOCAL=1 bun src/cli/index.ts",
|
"cli": "LOCAL=1 bun src/cli/index.ts",
|
||||||
"prepublishOnly": "bun run build:all"
|
"prepublishOnly": "bun run test && bun run build:all"
|
||||||
},
|
},
|
||||||
"license": "FSL-1.1-MIT",
|
"license": "FSL-1.1-MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { CreateUserPayload } from "auth/AppAuth";
|
import type { CreateUserPayload } from "auth/AppAuth";
|
||||||
|
import { auth } from "auth/middlewares";
|
||||||
|
import { config } from "core";
|
||||||
import { Event } from "core/events";
|
import { Event } from "core/events";
|
||||||
|
import { patternMatch } from "core/utils";
|
||||||
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
import { Connection, type LibSqlCredentials, LibsqlConnection } from "data";
|
||||||
import {
|
import {
|
||||||
type InitialModuleConfigs,
|
type InitialModuleConfigs,
|
||||||
@@ -71,6 +74,9 @@ export class App {
|
|||||||
this.trigger_first_boot = true;
|
this.trigger_first_boot = true;
|
||||||
},
|
},
|
||||||
onServerInit: async (server) => {
|
onServerInit: async (server) => {
|
||||||
|
server.get("/favicon.ico", (c) =>
|
||||||
|
c.redirect(config.server.assets_path + "/favicon.ico")
|
||||||
|
);
|
||||||
server.use(async (c, next) => {
|
server.use(async (c, next) => {
|
||||||
c.set("app", this);
|
c.set("app", this);
|
||||||
await next();
|
await next();
|
||||||
@@ -159,7 +165,7 @@ export class App {
|
|||||||
registerAdminController(config?: AdminControllerOptions) {
|
registerAdminController(config?: AdminControllerOptions) {
|
||||||
// register admin
|
// register admin
|
||||||
this.adminController = new AdminController(this, config);
|
this.adminController = new AdminController(this, config);
|
||||||
this.modules.server.route("/", this.adminController.getController());
|
this.modules.server.route(config?.basepath ?? "/", this.adminController.getController());
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,10 +91,6 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
return this._controller;
|
return this._controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
override onServerInit(hono: Hono<any>) {
|
|
||||||
hono.use(auth);
|
|
||||||
}
|
|
||||||
|
|
||||||
getSchema() {
|
getSchema() {
|
||||||
return authConfigSchema;
|
return authConfigSchema;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export class AuthController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getController() {
|
override getController() {
|
||||||
|
const { auth } = this.middlewares;
|
||||||
const hono = this.create();
|
const hono = this.create();
|
||||||
const strategies = this.auth.authenticator.getStrategies();
|
const strategies = this.auth.authenticator.getStrategies();
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ export class AuthController extends Controller {
|
|||||||
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
hono.route(`/${name}`, strategy.getController(this.auth.authenticator));
|
||||||
}
|
}
|
||||||
|
|
||||||
hono.get("/me", async (c) => {
|
hono.get("/me", auth(), async (c) => {
|
||||||
if (this.auth.authenticator.isUserLoggedIn()) {
|
if (this.auth.authenticator.isUserLoggedIn()) {
|
||||||
return c.json({ user: await this.auth.authenticator.getUser() });
|
return c.json({ user: await this.auth.authenticator.getUser() });
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,7 @@ export class AuthController extends Controller {
|
|||||||
return c.json({ user: null }, 403);
|
return c.json({ user: null }, 403);
|
||||||
});
|
});
|
||||||
|
|
||||||
hono.get("/logout", async (c) => {
|
hono.get("/logout", auth(), async (c) => {
|
||||||
await this.auth.authenticator.logout(c);
|
await this.auth.authenticator.logout(c);
|
||||||
if (this.auth.authenticator.isJsonRequest(c)) {
|
if (this.auth.authenticator.isJsonRequest(c)) {
|
||||||
return c.json({ ok: true });
|
return c.json({ ok: true });
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { Context, Hono } from "hono";
|
|||||||
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
||||||
import { sign, verify } from "hono/jwt";
|
import { sign, verify } from "hono/jwt";
|
||||||
import type { CookieOptions } from "hono/utils/cookie";
|
import type { CookieOptions } from "hono/utils/cookie";
|
||||||
import { omit } from "lodash-es";
|
import type { ServerEnv } from "modules/Module";
|
||||||
|
|
||||||
type Input = any; // workaround
|
type Input = any; // workaround
|
||||||
export type JWTPayload = Parameters<typeof sign>[0];
|
export type JWTPayload = Parameters<typeof sign>[0];
|
||||||
@@ -101,7 +101,13 @@ export type AuthUserResolver = (
|
|||||||
export class Authenticator<Strategies extends Record<string, Strategy> = Record<string, Strategy>> {
|
export class Authenticator<Strategies extends Record<string, Strategy> = Record<string, Strategy>> {
|
||||||
private readonly strategies: Strategies;
|
private readonly strategies: Strategies;
|
||||||
private readonly config: AuthConfig;
|
private readonly config: AuthConfig;
|
||||||
private _user: SafeUser | undefined;
|
private _claims:
|
||||||
|
| undefined
|
||||||
|
| (SafeUser & {
|
||||||
|
iat: number;
|
||||||
|
iss?: string;
|
||||||
|
exp?: number;
|
||||||
|
});
|
||||||
private readonly userResolver: AuthUserResolver;
|
private readonly userResolver: AuthUserResolver;
|
||||||
|
|
||||||
constructor(strategies: Strategies, userResolver?: AuthUserResolver, config?: AuthConfig) {
|
constructor(strategies: Strategies, userResolver?: AuthUserResolver, config?: AuthConfig) {
|
||||||
@@ -134,16 +140,18 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
|
|
||||||
isUserLoggedIn(): boolean {
|
isUserLoggedIn(): boolean {
|
||||||
return this._user !== undefined;
|
return this._claims !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser() {
|
getUser(): SafeUser | undefined {
|
||||||
return this._user;
|
if (!this._claims) return;
|
||||||
|
|
||||||
|
const { iat, exp, iss, ...user } = this._claims;
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo: determine what to do exactly
|
|
||||||
resetUser() {
|
resetUser() {
|
||||||
this._user = undefined;
|
this._claims = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
strategy<
|
strategy<
|
||||||
@@ -157,6 +165,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @todo: add jwt tests
|
||||||
async jwt(user: Omit<User, "password">): Promise<string> {
|
async jwt(user: Omit<User, "password">): Promise<string> {
|
||||||
const prohibited = ["password"];
|
const prohibited = ["password"];
|
||||||
for (const prop of prohibited) {
|
for (const prop of prohibited) {
|
||||||
@@ -203,7 +212,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._user = omit(payload, ["iat", "exp", "iss"]) as SafeUser;
|
this._claims = payload as any;
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.resetUser();
|
this.resetUser();
|
||||||
@@ -249,7 +258,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setAuthCookie(c: Context, token: string) {
|
private async setAuthCookie(c: Context<ServerEnv>, token: string) {
|
||||||
const secret = this.config.jwt.secret;
|
const secret = this.config.jwt.secret;
|
||||||
await setSignedCookie(c, "auth", token, secret, this.cookieOptions);
|
await setSignedCookie(c, "auth", token, secret, this.cookieOptions);
|
||||||
}
|
}
|
||||||
@@ -281,10 +290,12 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
const successPath = this.config.cookie.pathSuccess ?? "/";
|
const successPath = this.config.cookie.pathSuccess ?? "/";
|
||||||
const successUrl = new URL(c.req.url).origin + successPath.replace(/\/+$/, "/");
|
const successUrl = new URL(c.req.url).origin + successPath.replace(/\/+$/, "/");
|
||||||
const referer = new URL(redirect ?? c.req.header("Referer") ?? successUrl);
|
const referer = new URL(redirect ?? c.req.header("Referer") ?? successUrl);
|
||||||
|
console.log("auth respond", { redirect, successUrl, successPath });
|
||||||
|
|
||||||
if ("token" in data) {
|
if ("token" in data) {
|
||||||
await this.setAuthCookie(c, data.token);
|
await this.setAuthCookie(c, data.token);
|
||||||
// can't navigate to "/" – doesn't work on nextjs
|
// can't navigate to "/" – doesn't work on nextjs
|
||||||
|
console.log("auth success, redirecting to", successUrl);
|
||||||
return c.redirect(successUrl);
|
return c.redirect(successUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,6 +305,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
}
|
}
|
||||||
|
|
||||||
await addFlashMessage(c, message, "error");
|
await addFlashMessage(c, message, "error");
|
||||||
|
console.log("auth failed, redirecting to", referer);
|
||||||
return c.redirect(referer);
|
return c.redirect(referer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +321,7 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
await this.verify(token);
|
await this.verify(token);
|
||||||
return this._user;
|
return this.getUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -98,12 +98,16 @@ export class Guard {
|
|||||||
if (this.user && typeof this.user.role === "string") {
|
if (this.user && typeof this.user.role === "string") {
|
||||||
const role = this.roles?.find((role) => role.name === this.user?.role);
|
const role = this.roles?.find((role) => role.name === this.user?.role);
|
||||||
if (role) {
|
if (role) {
|
||||||
debug && console.log("guard: role found", this.user.role);
|
debug && console.log("guard: role found", [this.user.role]);
|
||||||
return role;
|
return role;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug && console.log("guard: role not found", this.user, this.user?.role);
|
debug &&
|
||||||
|
console.log("guard: role not found", {
|
||||||
|
user: this.user,
|
||||||
|
role: this.user?.role
|
||||||
|
});
|
||||||
return this.getDefaultRole();
|
return this.getDefaultRole();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { type Permission, config } from "core";
|
import type { Permission } from "core";
|
||||||
|
import { patternMatch } from "core/utils";
|
||||||
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";
|
||||||
@@ -8,51 +9,71 @@ function getPath(reqOrCtx: Request | Context) {
|
|||||||
return new URL(req.url).pathname;
|
return new URL(req.url).pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldSkipAuth(req: Request) {
|
export function shouldSkip(c: Context<ServerEnv>, skip?: (string | RegExp)[]) {
|
||||||
const skip = getPath(req).startsWith(config.server.assets_path);
|
if (c.get("auth_skip")) return true;
|
||||||
if (skip) {
|
|
||||||
//console.log("skip auth for", req.url);
|
const req = c.req.raw;
|
||||||
}
|
if (!skip) return false;
|
||||||
return skip;
|
|
||||||
|
const path = getPath(req);
|
||||||
|
const result = skip.some((s) => patternMatch(path, s));
|
||||||
|
|
||||||
|
c.set("auth_skip", result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const auth = createMiddleware<ServerEnv>(async (c, next) => {
|
export const auth = (options?: {
|
||||||
// make sure to only register once
|
skip?: (string | RegExp)[];
|
||||||
if (c.get("auth_registered")) {
|
}) =>
|
||||||
throw new Error(`auth middleware already registered for ${getPath(c)}`);
|
createMiddleware<ServerEnv>(async (c, next) => {
|
||||||
}
|
// make sure to only register once
|
||||||
c.set("auth_registered", true);
|
if (c.get("auth_registered")) {
|
||||||
|
throw new Error(`auth middleware already registered for ${getPath(c)}`);
|
||||||
|
}
|
||||||
|
c.set("auth_registered", true);
|
||||||
|
|
||||||
const skipped = shouldSkipAuth(c.req.raw);
|
const app = c.get("app");
|
||||||
const app = c.get("app");
|
const skipped = shouldSkip(c, options?.skip) || !app.module.auth.enabled;
|
||||||
const guard = app.modules.ctx().guard;
|
const guard = app.modules.ctx().guard;
|
||||||
const authenticator = app.module.auth.authenticator;
|
const authenticator = app.module.auth.authenticator;
|
||||||
|
|
||||||
if (!skipped) {
|
if (!skipped) {
|
||||||
const resolved = c.get("auth_resolved");
|
const resolved = c.get("auth_resolved");
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
if (!app.module.auth.enabled) {
|
if (!app.module.auth.enabled) {
|
||||||
guard.setUserContext(undefined);
|
guard.setUserContext(undefined);
|
||||||
} else {
|
} else {
|
||||||
guard.setUserContext(await authenticator.resolveAuthFromRequest(c));
|
guard.setUserContext(await authenticator.resolveAuthFromRequest(c));
|
||||||
|
c.set("auth_resolved", true);
|
||||||
// renew cookie if applicable
|
}
|
||||||
authenticator.requestCookieRefresh(c);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
|
||||||
|
if (!skipped) {
|
||||||
|
// renew cookie if applicable
|
||||||
|
authenticator.requestCookieRefresh(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// release
|
||||||
|
guard.setUserContext(undefined);
|
||||||
|
authenticator?.resetUser();
|
||||||
|
c.set("auth_resolved", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const permission = (
|
||||||
|
permission: Permission | Permission[],
|
||||||
|
options?: {
|
||||||
|
onGranted?: (c: Context<ServerEnv>) => Promise<Response | void | undefined>;
|
||||||
|
onDenied?: (c: Context<ServerEnv>) => Promise<Response | void | undefined>;
|
||||||
}
|
}
|
||||||
|
) =>
|
||||||
await next();
|
// @ts-ignore
|
||||||
|
|
||||||
// release
|
|
||||||
guard.setUserContext(undefined);
|
|
||||||
authenticator.resetUser();
|
|
||||||
c.set("auth_resolved", false);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const permission = (...permissions: Permission[]) =>
|
|
||||||
createMiddleware<ServerEnv>(async (c, next) => {
|
createMiddleware<ServerEnv>(async (c, next) => {
|
||||||
const app = c.get("app");
|
const app = c.get("app");
|
||||||
|
//console.log("skip?", c.get("auth_skip"));
|
||||||
|
|
||||||
// in tests, app is not defined
|
// in tests, app is not defined
|
||||||
if (!c.get("auth_registered")) {
|
if (!c.get("auth_registered")) {
|
||||||
const msg = `auth middleware not registered, cannot check permissions for ${getPath(c)}`;
|
const msg = `auth middleware not registered, cannot check permissions for ${getPath(c)}`;
|
||||||
@@ -61,11 +82,22 @@ export const permission = (...permissions: Permission[]) =>
|
|||||||
} else {
|
} else {
|
||||||
console.warn(msg);
|
console.warn(msg);
|
||||||
}
|
}
|
||||||
} else if (!shouldSkipAuth(c.req.raw)) {
|
} else if (!c.get("auth_skip")) {
|
||||||
const p = Array.isArray(permissions) ? permissions : [permissions];
|
|
||||||
const guard = app.modules.ctx().guard;
|
const guard = app.modules.ctx().guard;
|
||||||
for (const permission of p) {
|
const permissions = Array.isArray(permission) ? permission : [permission];
|
||||||
guard.throwUnlessGranted(permission);
|
|
||||||
|
if (options?.onGranted || options?.onDenied) {
|
||||||
|
let returned: undefined | void | Response;
|
||||||
|
if (permissions.every((p) => guard.granted(p))) {
|
||||||
|
returned = await options?.onGranted?.(c);
|
||||||
|
} else {
|
||||||
|
returned = await options?.onDenied?.(c);
|
||||||
|
}
|
||||||
|
if (returned instanceof Response) {
|
||||||
|
return returned;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
permissions.some((p) => guard.throwUnlessGranted(p));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { Config } from "@libsql/client/node";
|
import type { Config } from "@libsql/client/node";
|
||||||
|
import { config } from "core";
|
||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
import open from "open";
|
import open from "open";
|
||||||
import { fileExists, getRelativeDistPath } from "../../utils/sys";
|
import { fileExists, getRelativeDistPath } from "../../utils/sys";
|
||||||
@@ -26,7 +27,7 @@ export async function serveStatic(server: Platform): Promise<MiddlewareHandler>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function attachServeStatic(app: any, platform: Platform) {
|
export async function attachServeStatic(app: any, platform: Platform) {
|
||||||
app.module.server.client.get("/*", await serveStatic(platform));
|
app.module.server.client.get(config.server.assets_path + "*", await serveStatic(platform));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startServer(server: Platform, app: any, options: { port: number }) {
|
export async function startServer(server: Platform, app: any, options: { port: number }) {
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import { setCookie } from "hono/cookie";
|
|||||||
const flash_key = "__bknd_flash";
|
const flash_key = "__bknd_flash";
|
||||||
export type FlashMessageType = "error" | "warning" | "success" | "info";
|
export type FlashMessageType = "error" | "warning" | "success" | "info";
|
||||||
|
|
||||||
export async function addFlashMessage(
|
export function addFlashMessage(c: Context, message: string, type: FlashMessageType = "info") {
|
||||||
c: Context,
|
if (c.req.header("Accept")?.includes("text/html")) {
|
||||||
message: string,
|
setCookie(c, flash_key, JSON.stringify({ type, message }), {
|
||||||
type: FlashMessageType = "info"
|
path: "/"
|
||||||
) {
|
});
|
||||||
setCookie(c, flash_key, JSON.stringify({ type, message }), {
|
}
|
||||||
path: "/"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCookieValue(name) {
|
function getCookieValue(name) {
|
||||||
|
|||||||
@@ -104,3 +104,14 @@ export function replaceSimplePlaceholders(str: string, vars: Record<string, any>
|
|||||||
return key in vars ? vars[key] : match;
|
return key in vars ? vars[key] : match;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function patternMatch(target: string, pattern: RegExp | string): boolean {
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
return pattern.test(target);
|
||||||
|
} else if (typeof pattern === "string" && pattern.startsWith("/")) {
|
||||||
|
return new RegExp(pattern).test(target);
|
||||||
|
} else if (typeof pattern === "string") {
|
||||||
|
return target.startsWith(pattern);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,8 +69,9 @@ export class DataController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getController() {
|
override getController() {
|
||||||
const hono = this.create();
|
const { permission, auth } = this.middlewares;
|
||||||
const { permission } = this.middlewares;
|
const hono = this.create().use(auth());
|
||||||
|
|
||||||
const definedEntities = this.em.entities.map((e) => e.name);
|
const definedEntities = this.em.entities.map((e) => e.name);
|
||||||
const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" }))
|
const tbNumber = Type.Transform(Type.String({ pattern: "^[1-9][0-9]{0,}$" }))
|
||||||
.Decode(Number.parseInt)
|
.Decode(Number.parseInt)
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export class MediaController extends Controller {
|
|||||||
override getController() {
|
override getController() {
|
||||||
// @todo: multiple providers?
|
// @todo: multiple providers?
|
||||||
// @todo: implement range requests
|
// @todo: implement range requests
|
||||||
|
const { auth } = this.middlewares;
|
||||||
const hono = this.create();
|
const hono = this.create().use(auth());
|
||||||
|
|
||||||
// get files list (temporary)
|
// get files list (temporary)
|
||||||
hono.get("/files", async (c) => {
|
hono.get("/files", async (c) => {
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import { auth, permission } from "auth/middlewares";
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import type { ServerEnv } from "modules/Module";
|
import type { ServerEnv } from "modules/Module";
|
||||||
|
|
||||||
|
const middlewares = {
|
||||||
|
auth,
|
||||||
|
permission
|
||||||
|
} as const;
|
||||||
|
|
||||||
export class Controller {
|
export class Controller {
|
||||||
protected middlewares = {
|
protected middlewares = middlewares;
|
||||||
auth,
|
|
||||||
permission
|
|
||||||
};
|
|
||||||
|
|
||||||
protected create(): Hono<ServerEnv> {
|
protected create(): Hono<ServerEnv> {
|
||||||
return Controller.createServer();
|
return Controller.createServer();
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export type ServerEnv = {
|
|||||||
auth_resolved: boolean;
|
auth_resolved: boolean;
|
||||||
// to only register once
|
// to only register once
|
||||||
auth_registered: boolean;
|
auth_registered: boolean;
|
||||||
|
// whether or not to bypass auth
|
||||||
|
auth_skip: boolean;
|
||||||
html?: string;
|
html?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,13 @@ export class AdminController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getController() {
|
override getController() {
|
||||||
const hono = this.create().basePath(this.withBasePath());
|
const { auth: authMiddleware, permission } = this.middlewares;
|
||||||
|
const hono = this.create().use(
|
||||||
|
authMiddleware({
|
||||||
|
skip: [/favicon\.ico$/]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const auth = this.app.module.auth;
|
const auth = this.app.module.auth;
|
||||||
const configs = this.app.modules.configs();
|
const configs = this.app.modules.configs();
|
||||||
// if auth is not enabled, authenticator is undefined
|
// if auth is not enabled, authenticator is undefined
|
||||||
@@ -78,16 +84,17 @@ export class AdminController extends Controller {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (auth_enabled) {
|
if (auth_enabled) {
|
||||||
hono.get(authRoutes.login, async (c) => {
|
hono.get(
|
||||||
if (
|
authRoutes.login,
|
||||||
this.app.module.auth.authenticator?.isUserLoggedIn() &&
|
permission([SystemPermissions.accessAdmin, SystemPermissions.schemaRead], {
|
||||||
this.ctx.guard.granted(SystemPermissions.accessAdmin)
|
onGranted: async (c) => {
|
||||||
) {
|
return c.redirect(authRoutes.success);
|
||||||
return c.redirect(authRoutes.success);
|
}
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
return c.html(c.get("html")!);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return c.html(c.get("html")!);
|
|
||||||
});
|
|
||||||
|
|
||||||
hono.get(authRoutes.logout, async (c) => {
|
hono.get(authRoutes.logout, async (c) => {
|
||||||
await auth.authenticator?.logout(c);
|
await auth.authenticator?.logout(c);
|
||||||
@@ -95,14 +102,25 @@ export class AdminController extends Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hono.get("*", async (c) => {
|
hono.get(
|
||||||
if (!this.ctx.guard.granted(SystemPermissions.accessAdmin)) {
|
"/*",
|
||||||
await addFlashMessage(c, "You are not authorized to access the Admin UI", "error");
|
permission(SystemPermissions.accessAdmin, {
|
||||||
return c.redirect(authRoutes.login);
|
onDenied: async (c) => {
|
||||||
}
|
addFlashMessage(c, "You are not authorized to access the Admin UI", "error");
|
||||||
|
|
||||||
return c.html(c.get("html")!);
|
console.log("redirecting");
|
||||||
});
|
return c.redirect(authRoutes.login);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
permission(SystemPermissions.schemaRead, {
|
||||||
|
onDenied: async (c) => {
|
||||||
|
addFlashMessage(c, "You not allowed to read the schema", "warning");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
return c.html(c.get("html")!);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return hono;
|
return hono;
|
||||||
}
|
}
|
||||||
@@ -150,6 +168,7 @@ export class AdminController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const theme = configs.server.admin.color_scheme ?? "light";
|
const theme = configs.server.admin.color_scheme ?? "light";
|
||||||
|
const favicon = isProd ? this.options.assets_path + "favicon.ico" : "/favicon.ico";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -162,6 +181,7 @@ export class AdminController extends Controller {
|
|||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||||
/>
|
/>
|
||||||
|
<link rel="icon" href={favicon} type="image/x-icon" />
|
||||||
<title>BKND</title>
|
<title>BKND</title>
|
||||||
{isProd ? (
|
{isProd ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ export class SystemController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerConfigController(client: Hono<any>): void {
|
private registerConfigController(client: Hono<any>): void {
|
||||||
const hono = this.create();
|
|
||||||
const { permission } = this.middlewares;
|
const { permission } = this.middlewares;
|
||||||
|
const hono = this.create();
|
||||||
|
|
||||||
hono.use(permission(SystemPermissions.configRead));
|
hono.use(permission(SystemPermissions.configRead));
|
||||||
|
|
||||||
@@ -202,8 +202,8 @@ export class SystemController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override getController() {
|
override getController() {
|
||||||
const hono = this.create();
|
const { permission, auth } = this.middlewares;
|
||||||
const { permission } = this.middlewares;
|
const hono = this.create().use(auth());
|
||||||
|
|
||||||
this.registerConfigController(hono);
|
this.registerConfigController(hono);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default defineConfig({
|
|||||||
__isDev: "1"
|
__isDev: "1"
|
||||||
},
|
},
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
publicDir: "./src/admin/assets",
|
publicDir: "./src/ui/assets",
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
port: 28623,
|
port: 28623,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
Reference in New Issue
Block a user