added better error messages if config secret permission is missing

This commit is contained in:
dswbx
2025-01-18 13:31:33 +01:00
parent db10188945
commit fb2dff956b
9 changed files with 72 additions and 25 deletions

View File

@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.6.0-rc.12", "version": "0.6.0-rc.13",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {

View File

@@ -118,8 +118,6 @@ export class App {
this.trigger_first_boot = false; this.trigger_first_boot = false;
await this.emgr.emit(new AppFirstBoot({ app: this })); await this.emgr.emit(new AppFirstBoot({ app: this }));
} }
console.log("[APP] built");
} }
mutateConfig<Module extends keyof Modules>(module: Module) { mutateConfig<Module extends keyof Modules>(module: Module) {

View File

@@ -111,15 +111,18 @@ export class EntityManager<TBD extends object = DefaultDB> {
// caused issues because this.entity() was using a reference (for when initial config was given) // caused issues because this.entity() was using a reference (for when initial config was given)
} }
entity(e: Entity | keyof TBD | string, silent?: boolean): Entity { entity<Silent extends true | false = false>(
e: Entity | keyof TBD | string,
silent?: Silent
): Silent extends true ? Entity | undefined : Entity {
// make sure to always retrieve by name // make sure to always retrieve by name
const entity = this.entities.find((entity) => const entity = this.entities.find((entity) =>
e instanceof Entity ? entity.name === e.name : entity.name === e e instanceof Entity ? entity.name === e.name : entity.name === e
); );
if (!entity && !silent) { if (!entity) {
// @ts-ignore if (silent === true) return undefined as any;
throw new EntityNotDefinedException(e instanceof Entity ? e.name : e); throw new EntityNotDefinedException(e instanceof Entity ? e.name : (e as string));
} }
return entity; return entity;

View File

@@ -1,7 +1,10 @@
import { IconAlertHexagon } from "@tabler/icons-react";
import type { ModuleConfigs, ModuleSchemas } from "modules"; import type { ModuleConfigs, ModuleSchemas } from "modules";
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager"; import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react"; import { createContext, startTransition, useContext, useEffect, useRef, useState } from "react";
import { useApi } from "ui/client"; import { useApi } from "ui/client";
import { Button } from "ui/components/buttons/Button";
import { Alert } from "ui/components/display/Alert";
import { type TSchemaActions, getSchemaActions } from "./schema/actions"; import { type TSchemaActions, getSchemaActions } from "./schema/actions";
import { AppReduced } from "./utils/AppReduced"; import { AppReduced } from "./utils/AppReduced";
@@ -10,6 +13,7 @@ type BkndContext = {
schema: ModuleSchemas; schema: ModuleSchemas;
config: ModuleConfigs; config: ModuleConfigs;
permissions: string[]; permissions: string[];
hasSecrets: boolean;
requireSecrets: () => Promise<void>; requireSecrets: () => Promise<void>;
actions: ReturnType<typeof getSchemaActions>; actions: ReturnType<typeof getSchemaActions>;
app: AppReduced; app: AppReduced;
@@ -32,6 +36,7 @@ export function BkndProvider({
const [schema, setSchema] = const [schema, setSchema] =
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>(); useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions">>();
const [fetched, setFetched] = useState(false); const [fetched, setFetched] = useState(false);
const [error, setError] = useState<boolean>();
const errorShown = useRef<boolean>(); const errorShown = useRef<boolean>();
const [local_version, set_local_version] = useState(0); const [local_version, set_local_version] = useState(0);
const api = useApi(); const api = useApi();
@@ -50,15 +55,11 @@ export function BkndProvider({
if (!res.ok) { if (!res.ok) {
if (errorShown.current) return; if (errorShown.current) return;
errorShown.current = true; errorShown.current = true;
/*notifications.show({
title: "Failed to fetch schema", setError(true);
// @ts-ignore return;
message: body.error, } else if (error) {
color: "red", setError(false);
position: "top-right",
autoClose: false,
withCloseButton: true
});*/
} }
const schema = res.ok const schema = res.ok
@@ -98,12 +99,24 @@ export function BkndProvider({
if (!fetched || !schema) return fallback; if (!fetched || !schema) return fallback;
const app = new AppReduced(schema?.config as any); const app = new AppReduced(schema?.config as any);
const actions = getSchemaActions({ api, setSchema, reloadSchema }); const actions = getSchemaActions({ api, setSchema, reloadSchema });
const hasSecrets = withSecrets && !error;
return ( return (
<BkndContext.Provider <BkndContext.Provider
value={{ ...schema, actions, requireSecrets, app, adminOverride }} value={{ ...schema, actions, requireSecrets, app, adminOverride, hasSecrets }}
key={local_version} key={local_version}
> >
{error && (
<Alert.Exception className="gap-2">
<IconAlertHexagon />
You attempted to load system configuration with secrets without having proper
permission.
<a href={schema.config.server.admin.basepath || "/"}>
<Button variant="red">Reload</Button>
</a>
</Alert.Exception>
)}
{children} {children}
</BkndContext.Provider> </BkndContext.Provider>
); );

View File

@@ -6,16 +6,27 @@ export type AlertProps = ComponentPropsWithoutRef<"div"> & {
visible?: boolean; visible?: boolean;
title?: string; title?: string;
message?: ReactNode | string; message?: ReactNode | string;
children?: ReactNode;
}; };
const Base: React.FC<AlertProps> = ({ visible = true, title, message, className, ...props }) => const Base: React.FC<AlertProps> = ({
visible = true,
title,
message,
className,
children,
...props
}) =>
visible ? ( visible ? (
<div <div
{...props} {...props}
className={twMerge("flex flex-row dark:bg-amber-300/20 bg-amber-200 p-4", className)} className={twMerge(
"flex flex-row items-center dark:bg-amber-300/20 bg-amber-200 p-4",
className
)}
> >
{title && <b className="mr-2">{title}:</b>} {title && <b className="mr-2">{title}:</b>}
{message} {message || children}
</div> </div>
) : null; ) : null;

View File

@@ -1,9 +1,24 @@
import { IconLockAccessOff } from "@tabler/icons-react";
import { Empty, type EmptyProps } from "./Empty"; import { Empty, type EmptyProps } from "./Empty";
const NotFound = (props: Partial<EmptyProps>) => <Empty title="Not Found" {...props} />; const NotFound = (props: Partial<EmptyProps>) => <Empty title="Not Found" {...props} />;
const NotAllowed = (props: Partial<EmptyProps>) => <Empty title="Not Allowed" {...props} />; const NotAllowed = (props: Partial<EmptyProps>) => <Empty title="Not Allowed" {...props} />;
const MissingPermission = ({
what,
...props
}: Partial<EmptyProps> & {
what?: string;
}) => (
<Empty
Icon={IconLockAccessOff}
title="Missing Permission"
description={`You're not allowed to access ${what ?? "this"}.`}
{...props}
/>
);
export const Message = { export const Message = {
NotFound, NotFound,
NotAllowed NotAllowed,
MissingPermission
}; };

View File

@@ -1,10 +1,10 @@
import { notifications } from "@mantine/notifications";
import { useRef } from "react"; import { useRef } from "react";
import { TbDots } from "react-icons/tb"; import { TbDots } from "react-icons/tb";
import { useBknd } from "ui/client/bknd"; import { useBknd } from "ui/client/bknd";
import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth"; import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth";
import { Button } from "ui/components/buttons/Button"; import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton"; import { IconButton } from "ui/components/buttons/IconButton";
import { Message } from "ui/components/display/Message";
import { Dropdown } from "ui/components/overlay/Dropdown"; import { Dropdown } from "ui/components/overlay/Dropdown";
import * as AppShell from "ui/layouts/AppShell/AppShell"; import * as AppShell from "ui/layouts/AppShell/AppShell";
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2"; import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
@@ -12,7 +12,11 @@ import { routes, useNavigate } from "ui/lib/routes";
import { AuthRoleForm, type AuthRoleFormRef } from "ui/routes/auth/forms/role.form"; import { AuthRoleForm, type AuthRoleFormRef } from "ui/routes/auth/forms/role.form";
export function AuthRolesEdit(props) { export function AuthRolesEdit(props) {
useBknd({ withSecrets: true }); const { hasSecrets } = useBknd({ withSecrets: true });
if (!hasSecrets) {
return <Message.MissingPermission what="Roles & Permissions" />;
}
return <AuthRolesEditInternal {...props} />; return <AuthRolesEditInternal {...props} />;
} }

View File

@@ -168,7 +168,7 @@ const EntityContextMenu = ({
items={[ items={[
href && { href && {
icon: IconExternalLink, icon: IconExternalLink,
label: "Open in new tab", label: "Open in tab",
onClick: () => navigate(href, { target: "_blank" }) onClick: () => navigate(href, { target: "_blank" })
}, },
separator, separator,

View File

@@ -2,6 +2,7 @@ import { IconSettings } from "@tabler/icons-react";
import { ucFirst } from "core/utils"; import { ucFirst } from "core/utils";
import { useBknd } from "ui/client/bknd"; import { useBknd } from "ui/client/bknd";
import { Empty } from "ui/components/display/Empty"; import { Empty } from "ui/components/display/Empty";
import { Message } from "ui/components/display/Message";
import { Link } from "ui/components/wouter/Link"; import { Link } from "ui/components/wouter/Link";
import { useBrowserTitle } from "ui/hooks/use-browser-title"; import { useBrowserTitle } from "ui/hooks/use-browser-title";
import * as AppShell from "ui/layouts/AppShell/AppShell"; import * as AppShell from "ui/layouts/AppShell/AppShell";
@@ -44,7 +45,9 @@ function SettingsSidebar() {
} }
export default function SettingsRoutes() { export default function SettingsRoutes() {
useBknd({ withSecrets: true }); const b = useBknd({ withSecrets: true });
if (!b.hasSecrets) return <Message.MissingPermission what="the settings" />;
return ( return (
<> <>
<SettingsSidebar /> <SettingsSidebar />