mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
added cookie to config + fixed config set endpoint
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
/*import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { decodeJwt, jwtVerify } from "jose";
|
import { decodeJwt, jwtVerify } from "jose";
|
||||||
import { Authenticator, type User, type UserPool } from "../authenticate/Authenticator";
|
import { Authenticator, type User, type UserPool } from "../../src/auth";
|
||||||
import { PasswordStrategy } from "../authenticate/strategies/PasswordStrategy";
|
import { cookieConfig } from "../../src/auth/authenticate/Authenticator";
|
||||||
import * as hash from "../utils/hash";*/
|
import { PasswordStrategy } from "../../src/auth/authenticate/strategies/PasswordStrategy";
|
||||||
|
import * as hash from "../../src/auth/utils/hash";
|
||||||
|
import { Default, parse } from "../../src/core/utils";
|
||||||
|
|
||||||
/*class MemoryUserPool implements UserPool {
|
/*class MemoryUserPool implements UserPool {
|
||||||
constructor(private users: User[] = []) {}
|
constructor(private users: User[] = []) {}
|
||||||
@@ -17,10 +19,14 @@ import * as hash from "../utils/hash";*/
|
|||||||
this.users.push(newUser);
|
this.users.push(newUser);
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
describe("Authenticator", async () => {
|
describe("Authenticator", async () => {
|
||||||
const userpool = new MemoryUserPool([
|
test("cookie options", async () => {
|
||||||
|
console.log("parsed", parse(cookieConfig, undefined));
|
||||||
|
console.log(Default(cookieConfig, {}));
|
||||||
|
});
|
||||||
|
/*const userpool = new MemoryUserPool([
|
||||||
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
{ id: 1, email: "d", username: "test", password: await hash.sha256("test") },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -37,5 +43,5 @@ describe("Authenticator", async () => {
|
|||||||
const { iat, ...decoded } = decodeJwt<any>(token);
|
const { iat, ...decoded } = decodeJwt<any>(token);
|
||||||
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
|
expect(decoded).toEqual({ id: 1, email: "d", username: "test" });
|
||||||
expect(await auth.verify(token)).toBe(true);
|
expect(await auth.verify(token)).toBe(true);
|
||||||
});
|
});*/
|
||||||
});*/
|
});
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
"@codemirror/lang-liquid": "^6.2.1",
|
"@codemirror/lang-liquid": "^6.2.1",
|
||||||
"@dagrejs/dagre": "^1.1.4",
|
"@dagrejs/dagre": "^1.1.4",
|
||||||
"@hello-pangea/dnd": "^17.0.0",
|
"@hello-pangea/dnd": "^17.0.0",
|
||||||
"@hono/typebox-validator": "^0.2.4",
|
"@hono/typebox-validator": "^0.2.6",
|
||||||
"@hono/zod-validator": "^0.2.2",
|
"@hono/zod-validator": "^0.4.1",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.9.1",
|
||||||
"@libsql/client": "^0.14.0",
|
"@libsql/client": "^0.14.0",
|
||||||
"@libsql/kysely-libsql": "^0.4.1",
|
"@libsql/kysely-libsql": "^0.4.1",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"codemirror-lang-liquid": "^1.0.0",
|
"codemirror-lang-liquid": "^1.0.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"fast-xml-parser": "^4.4.0",
|
"fast-xml-parser": "^4.4.0",
|
||||||
"hono": "^4.4.12",
|
"hono": "^4.6.12",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
"jotai": "^2.10.1",
|
"jotai": "^2.10.1",
|
||||||
"kysely": "^0.27.4",
|
"kysely": "^0.27.4",
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.613.0",
|
"@aws-sdk/client-s3": "^3.613.0",
|
||||||
"@hono/node-server": "^1.13.3",
|
"@hono/node-server": "^1.13.7",
|
||||||
"@hono/vite-dev-server": "^0.16.0",
|
"@hono/vite-dev-server": "^0.17.0",
|
||||||
"@tanstack/react-query-devtools": "^5.59.16",
|
"@tanstack/react-query-devtools": "^5.59.16",
|
||||||
"@types/diff": "^5.2.3",
|
"@types/diff": "^5.2.3",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._authenticator = new Authenticator(strategies, this.resolveUser.bind(this), {
|
this._authenticator = new Authenticator(strategies, this.resolveUser.bind(this), {
|
||||||
jwt: this.config.jwt
|
jwt: this.config.jwt,
|
||||||
|
cookie: this.config.cookie
|
||||||
});
|
});
|
||||||
|
|
||||||
this.registerEntities();
|
this.registerEntities();
|
||||||
@@ -115,6 +116,9 @@ export class AppAuth extends Module<typeof authConfigSchema> {
|
|||||||
identifier,
|
identifier,
|
||||||
profile
|
profile
|
||||||
});
|
});
|
||||||
|
if (!this.config.allow_register && action === "register") {
|
||||||
|
throw new Exception("Registration is not allowed", 403);
|
||||||
|
}
|
||||||
|
|
||||||
const fields = this.getUsersEntity()
|
const fields = this.getUsersEntity()
|
||||||
.getFillableFields("create")
|
.getFillableFields("create")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { jwtConfig } from "auth/authenticate/Authenticator";
|
import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator";
|
||||||
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
|
import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies";
|
||||||
import { type Static, StringRecord, Type, objectTransform } from "core/utils";
|
import { type Static, StringRecord, Type, objectTransform } from "core/utils";
|
||||||
|
|
||||||
@@ -51,7 +51,9 @@ export const authConfigSchema = Type.Object(
|
|||||||
enabled: Type.Boolean({ default: false }),
|
enabled: Type.Boolean({ default: false }),
|
||||||
basepath: Type.String({ default: "/api/auth" }),
|
basepath: Type.String({ default: "/api/auth" }),
|
||||||
entity_name: Type.String({ default: "users" }),
|
entity_name: Type.String({ default: "users" }),
|
||||||
|
allow_register: Type.Optional(Type.Boolean({ default: true })),
|
||||||
jwt: jwtConfig,
|
jwt: jwtConfig,
|
||||||
|
cookie: cookieConfig,
|
||||||
strategies: Type.Optional(
|
strategies: Type.Optional(
|
||||||
StringRecord(strategiesSchema, {
|
StringRecord(strategiesSchema, {
|
||||||
title: "Strategies",
|
title: "Strategies",
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { Exception } from "core";
|
import { Exception } from "core";
|
||||||
import { addFlashMessage } from "core/server/flash";
|
import { addFlashMessage } from "core/server/flash";
|
||||||
import { type Static, type TSchema, Type, parse, randomString, transformObject } from "core/utils";
|
import {
|
||||||
|
type Static,
|
||||||
|
StringEnum,
|
||||||
|
type TSchema,
|
||||||
|
Type,
|
||||||
|
parse,
|
||||||
|
randomString,
|
||||||
|
transformObject
|
||||||
|
} from "core/utils";
|
||||||
import type { Context, Hono } from "hono";
|
import type { Context, Hono } from "hono";
|
||||||
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
import { deleteCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
|
||||||
|
import type { CookieOptions } from "hono/utils/cookie";
|
||||||
import { type JWTVerifyOptions, SignJWT, jwtVerify } from "jose";
|
import { type JWTVerifyOptions, SignJWT, jwtVerify } from "jose";
|
||||||
|
|
||||||
type Input = any; // workaround
|
type Input = any; // workaround
|
||||||
@@ -41,6 +50,18 @@ export interface UserPool<Fields = "id" | "email" | "username"> {
|
|||||||
create: (user: CreateUser) => Promise<User | undefined>;
|
create: (user: CreateUser) => Promise<User | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const cookieConfig = Type.Partial(
|
||||||
|
Type.Object({
|
||||||
|
renew: Type.Boolean({ default: true }),
|
||||||
|
path: Type.String({ default: "/" }),
|
||||||
|
sameSite: StringEnum(["strict", "lax", "none"], { default: "lax" }),
|
||||||
|
secure: Type.Boolean({ default: true }),
|
||||||
|
httpOnly: Type.Boolean({ default: true }),
|
||||||
|
expires: Type.Number({ default: 168 })
|
||||||
|
}),
|
||||||
|
{ default: {}, additionalProperties: false }
|
||||||
|
);
|
||||||
|
|
||||||
export const jwtConfig = Type.Object(
|
export const jwtConfig = Type.Object(
|
||||||
{
|
{
|
||||||
// @todo: autogenerate a secret if not present. But it must be persisted from AppAuth
|
// @todo: autogenerate a secret if not present. But it must be persisted from AppAuth
|
||||||
@@ -56,7 +77,8 @@ export const jwtConfig = Type.Object(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
export const authenticatorConfig = Type.Object({
|
export const authenticatorConfig = Type.Object({
|
||||||
jwt: jwtConfig
|
jwt: jwtConfig,
|
||||||
|
cookie: cookieConfig
|
||||||
});
|
});
|
||||||
|
|
||||||
type AuthConfig = Static<typeof authenticatorConfig>;
|
type AuthConfig = Static<typeof authenticatorConfig>;
|
||||||
@@ -179,12 +201,12 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @todo: CookieOptions not exported from hono
|
private get cookieOptions(): CookieOptions {
|
||||||
private get cookieOptions(): any {
|
const { expires = 168, renew, ...cookieConfig } = this.config.cookie;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: "/",
|
...cookieConfig,
|
||||||
sameSite: "lax",
|
expires: new Date(Date.now() + expires * 60 * 60 * 1000)
|
||||||
httpOnly: true
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +222,16 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async requestCookieRefresh(c: Context) {
|
||||||
|
if (this.config.cookie.renew) {
|
||||||
|
console.log("renewing cookie", c.req.url);
|
||||||
|
const token = await this.getAuthCookie(c);
|
||||||
|
if (token) {
|
||||||
|
await this.setAuthCookie(c, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async setAuthCookie(c: Context, token: string) {
|
private async setAuthCookie(c: Context, 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);
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class DataController implements ClassController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hono.use("*", async (c, next) => {
|
hono.use("*", async (c, next) => {
|
||||||
this.ctx.guard.throwUnlessGranted(SystemPermissions.api);
|
this.ctx.guard.throwUnlessGranted(SystemPermissions.accessApi);
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ export class SystemApi extends ModuleApi<any> {
|
|||||||
value: ModuleConfigs[Module],
|
value: ModuleConfigs[Module],
|
||||||
force?: boolean
|
force?: boolean
|
||||||
) {
|
) {
|
||||||
return await this.post<any>(["config", "set", module, `?force=${force ? 1 : 0}`], value);
|
return await this.post<any>(
|
||||||
|
["config", "set", module].join("/") + `?force=${force ? 1 : 0}`,
|
||||||
|
value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
async addConfig<Module extends ModuleKey>(module: Module, path: string, value: any) {
|
||||||
|
|||||||
@@ -78,6 +78,13 @@ export const migrations: Migration[] = [
|
|||||||
up: async (config, { db }) => {
|
up: async (config, { db }) => {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 7,
|
||||||
|
up: async (config, { db }) => {
|
||||||
|
// automatically adds auth.cookie options
|
||||||
|
return config;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Permission } from "core";
|
import { Permission } from "core";
|
||||||
|
|
||||||
export const admin = new Permission("system.admin");
|
export const accessAdmin = new Permission("system.access.admin");
|
||||||
export const api = new Permission("system.api");
|
export const accessApi = new Permission("system.access.api");
|
||||||
export const configRead = new Permission("system.config.read");
|
export const configRead = new Permission("system.config.read");
|
||||||
export const configReadSecrets = new Permission("system.config.read.secrets");
|
export const configReadSecrets = new Permission("system.config.read.secrets");
|
||||||
export const configWrite = new Permission("system.config.write");
|
export const configWrite = new Permission("system.config.write");
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export class AdminController implements ClassController {
|
|||||||
getController(): Hono<any> {
|
getController(): Hono<any> {
|
||||||
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
|
||||||
const auth_enabled = configs.auth.enabled;
|
const auth_enabled = configs.auth.enabled;
|
||||||
const basepath = (String(configs.server.admin.basepath) + "/").replace(/\/+$/, "/");
|
const basepath = (String(configs.server.admin.basepath) + "/").replace(/\/+$/, "/");
|
||||||
const hono = new Hono<{
|
const hono = new Hono<{
|
||||||
@@ -50,7 +51,7 @@ export class AdminController implements ClassController {
|
|||||||
}>().basePath(basepath);
|
}>().basePath(basepath);
|
||||||
|
|
||||||
hono.use("*", async (c, next) => {
|
hono.use("*", async (c, next) => {
|
||||||
const obj = { user: auth.authenticator.getUser() };
|
const obj = { user: auth.authenticator?.getUser() };
|
||||||
const html = await this.getHtml(obj);
|
const html = await this.getHtml(obj);
|
||||||
if (!html) {
|
if (!html) {
|
||||||
console.warn("Couldn't generate HTML for admin UI");
|
console.warn("Couldn't generate HTML for admin UI");
|
||||||
@@ -58,13 +59,17 @@ export class AdminController implements ClassController {
|
|||||||
return c.notFound() as unknown as void;
|
return c.notFound() as unknown as void;
|
||||||
}
|
}
|
||||||
c.set("html", html);
|
c.set("html", html);
|
||||||
|
|
||||||
|
// refresh cookie if needed
|
||||||
|
await auth.authenticator?.requestCookieRefresh(c);
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (auth_enabled) {
|
||||||
hono.get(authRoutes.login, async (c) => {
|
hono.get(authRoutes.login, async (c) => {
|
||||||
if (
|
if (
|
||||||
this.app.module.auth.authenticator.isUserLoggedIn() &&
|
this.app.module.auth.authenticator?.isUserLoggedIn() &&
|
||||||
this.ctx.guard.granted(SystemPermissions.admin)
|
this.ctx.guard.granted(SystemPermissions.accessAdmin)
|
||||||
) {
|
) {
|
||||||
return c.redirect(authRoutes.root);
|
return c.redirect(authRoutes.root);
|
||||||
}
|
}
|
||||||
@@ -74,13 +79,14 @@ export class AdminController implements ClassController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
hono.get(authRoutes.logout, async (c) => {
|
hono.get(authRoutes.logout, async (c) => {
|
||||||
await auth.authenticator.logout(c);
|
await auth.authenticator?.logout(c);
|
||||||
return c.redirect(authRoutes.login);
|
return c.redirect(authRoutes.login);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
hono.get("*", async (c) => {
|
hono.get("*", async (c) => {
|
||||||
console.log("admin", c.req.url);
|
console.log("admin", c.req.url);
|
||||||
if (!this.ctx.guard.granted(SystemPermissions.admin)) {
|
if (!this.ctx.guard.granted(SystemPermissions.accessAdmin)) {
|
||||||
await addFlashMessage(c, "You are not authorized to access the Admin UI", "error");
|
await addFlashMessage(c, "You are not authorized to access the Admin UI", "error");
|
||||||
return c.redirect(authRoutes.login);
|
return c.redirect(authRoutes.login);
|
||||||
}
|
}
|
||||||
@@ -128,6 +134,7 @@ export class AdminController implements ClassController {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
{/* dnd complains otherwise */}
|
||||||
{html`<!doctype html>`}
|
{html`<!doctype html>`}
|
||||||
<html lang="en" class={configs.server.admin.color_scheme ?? "light"}>
|
<html lang="en" class={configs.server.admin.color_scheme ?? "light"}>
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
@@ -30,8 +30,12 @@ export function BkndProvider({
|
|||||||
const errorShown = useRef<boolean>();
|
const errorShown = useRef<boolean>();
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
|
||||||
async function fetchSchema(_includeSecrets: boolean = false) {
|
async function reloadSchema() {
|
||||||
if (withSecrets) return;
|
await fetchSchema(includeSecrets, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) {
|
||||||
|
if (withSecrets && !force) return;
|
||||||
const { body, res } = await client.api.system.readSchema({
|
const { body, res } = await client.api.system.readSchema({
|
||||||
config: true,
|
config: true,
|
||||||
secrets: _includeSecrets
|
secrets: _includeSecrets
|
||||||
@@ -80,7 +84,7 @@ export function BkndProvider({
|
|||||||
if (!fetched || !schema) return null;
|
if (!fetched || !schema) return null;
|
||||||
const app = new AppReduced(schema?.config as any);
|
const app = new AppReduced(schema?.config as any);
|
||||||
|
|
||||||
const actions = getSchemaActions({ client, setSchema });
|
const actions = getSchemaActions({ client, setSchema, reloadSchema });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app }}>
|
<BkndContext.Provider value={{ ...schema, actions, requireSecrets, app }}>
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import type { AppQueryClient } from "../utils/AppQueryClient";
|
|||||||
export type SchemaActionsProps = {
|
export type SchemaActionsProps = {
|
||||||
client: AppQueryClient;
|
client: AppQueryClient;
|
||||||
setSchema: React.Dispatch<React.SetStateAction<any>>;
|
setSchema: React.Dispatch<React.SetStateAction<any>>;
|
||||||
|
reloadSchema: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
||||||
|
|
||||||
export function getSchemaActions({ client, setSchema }: SchemaActionsProps) {
|
export function getSchemaActions({ client, setSchema, reloadSchema }: SchemaActionsProps) {
|
||||||
const api = client.api;
|
const api = client.api;
|
||||||
|
|
||||||
async function handleConfigUpdate(
|
async function handleConfigUpdate(
|
||||||
@@ -61,6 +62,7 @@ export function getSchemaActions({ client, setSchema }: SchemaActionsProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
reload: reloadSchema,
|
||||||
set: async <Module extends keyof ModuleConfigs>(
|
set: async <Module extends keyof ModuleConfigs>(
|
||||||
module: keyof ModuleConfigs,
|
module: keyof ModuleConfigs,
|
||||||
value: ModuleConfigs[Module],
|
value: ModuleConfigs[Module],
|
||||||
|
|||||||
@@ -155,8 +155,8 @@ export function Setting<Schema extends TObject = any>({
|
|||||||
if (success) {
|
if (success) {
|
||||||
if (options?.reloadOnSave) {
|
if (options?.reloadOnSave) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
//await actions.reload();
|
||||||
}
|
}
|
||||||
//window.location.reload();
|
|
||||||
} else {
|
} else {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user