mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 12:56:05 +00:00
reworked admin auth to use form and cookie + adjusted oauth to support API and cookie-based auth
This commit is contained in:
@@ -1,15 +1,19 @@
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import React from "react";
|
||||
import { FlashMessage } from "ui/modules/server/FlashMessage";
|
||||
import { BkndProvider, ClientProvider, useBknd } from "./client";
|
||||
import { createMantineTheme } from "./lib/mantine/theme";
|
||||
import { BkndModalsProvider } from "./modals";
|
||||
import { Routes } from "./routes";
|
||||
|
||||
export default function Admin({
|
||||
baseUrl: baseUrlOverride,
|
||||
withProvider = false
|
||||
}: { baseUrl?: string; withProvider?: boolean }) {
|
||||
export type BkndAdminProps = {
|
||||
baseUrl?: string;
|
||||
withProvider?: boolean;
|
||||
// @todo: add admin config override
|
||||
};
|
||||
|
||||
export default function Admin({ baseUrl: baseUrlOverride, withProvider = false }: BkndAdminProps) {
|
||||
const Component = (
|
||||
<BkndProvider>
|
||||
<AdminInternal />
|
||||
@@ -25,9 +29,11 @@ export default function Admin({
|
||||
function AdminInternal() {
|
||||
const b = useBknd();
|
||||
const theme = b.app.getAdminConfig().color_scheme;
|
||||
|
||||
return (
|
||||
<MantineProvider {...createMantineTheme(theme ?? "light")}>
|
||||
<Notifications />
|
||||
<FlashMessage />
|
||||
<BkndModalsProvider>
|
||||
<Routes />
|
||||
</BkndModalsProvider>
|
||||
|
||||
@@ -40,7 +40,7 @@ export function BkndProvider({
|
||||
if (!res.ok) {
|
||||
if (errorShown.current) return;
|
||||
errorShown.current = true;
|
||||
notifications.show({
|
||||
/*notifications.show({
|
||||
title: "Failed to fetch schema",
|
||||
// @ts-ignore
|
||||
message: body.error,
|
||||
@@ -48,7 +48,7 @@ export function BkndProvider({
|
||||
position: "top-right",
|
||||
autoClose: false,
|
||||
withCloseButton: true
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
const schema = res.ok
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type NotificationData, notifications } from "@mantine/notifications";
|
||||
import type { ModuleConfigs } from "../../../modules";
|
||||
import { ucFirst } from "core/utils";
|
||||
import type { ApiResponse, ModuleConfigs } from "../../../modules";
|
||||
import type { AppQueryClient } from "../utils/AppQueryClient";
|
||||
|
||||
export type SchemaActionsProps = {
|
||||
@@ -10,25 +11,53 @@ export type SchemaActionsProps = {
|
||||
export type TSchemaActions = ReturnType<typeof getSchemaActions>;
|
||||
|
||||
export function getSchemaActions({ client, setSchema }: SchemaActionsProps) {
|
||||
const baseUrl = client.baseUrl;
|
||||
const token = client.auth().state()?.token;
|
||||
const api = client.api;
|
||||
|
||||
async function displayError(action: string, module: string, res: Response, path?: string) {
|
||||
const notification_data: NotificationData = {
|
||||
id: "schema-error-" + [action, module, path].join("-"),
|
||||
title: `Config update failed${path ? ": " + path : ""}`,
|
||||
message: "Failed to complete config update",
|
||||
color: "red",
|
||||
async function handleConfigUpdate(
|
||||
action: string,
|
||||
module: string,
|
||||
res: ApiResponse,
|
||||
path?: string
|
||||
): Promise<boolean> {
|
||||
const base: Partial<NotificationData> = {
|
||||
id: "schema-" + [action, module, path].join("-"),
|
||||
position: "top-right",
|
||||
withCloseButton: true,
|
||||
autoClose: false
|
||||
autoClose: 3000
|
||||
};
|
||||
try {
|
||||
const { error } = (await res.json()) as any;
|
||||
notifications.show({ ...notification_data, message: error });
|
||||
} catch (e) {
|
||||
notifications.show(notification_data);
|
||||
|
||||
if (res.res.ok && res.body.success) {
|
||||
console.log("update config", action, module, path, res.body);
|
||||
if (res.body.success) {
|
||||
setSchema((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
[module]: res.body.config
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
notifications.show({
|
||||
...base,
|
||||
title: `Config updated: ${ucFirst(module)}`,
|
||||
color: "blue",
|
||||
message: `Operation ${action.toUpperCase()} at ${module}${path ? "." + path : ""}`
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
notifications.show({
|
||||
...base,
|
||||
title: `Config Update failed: ${ucFirst(module)}${path ? "." + path : ""}`,
|
||||
color: "red",
|
||||
withCloseButton: true,
|
||||
autoClose: false,
|
||||
message: res.body.error ?? "Failed to complete config update"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -37,183 +66,39 @@ export function getSchemaActions({ client, setSchema }: SchemaActionsProps) {
|
||||
value: ModuleConfigs[Module],
|
||||
force?: boolean
|
||||
) => {
|
||||
const res = await fetch(
|
||||
`${baseUrl}/api/system/config/set/${module}?force=${force ? 1 : 0}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(value)
|
||||
}
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as any;
|
||||
console.log("update config set", module, data);
|
||||
if (data.success) {
|
||||
setSchema((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
[module]: data.config
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return data.success;
|
||||
} else {
|
||||
await displayError("set", module, res);
|
||||
}
|
||||
|
||||
return false;
|
||||
const res = await api.system.setConfig(module, value, force);
|
||||
return await handleConfigUpdate("set", module, res);
|
||||
},
|
||||
patch: async <Module extends keyof ModuleConfigs>(
|
||||
module: keyof ModuleConfigs,
|
||||
path: string,
|
||||
value: any
|
||||
): Promise<boolean> => {
|
||||
const res = await fetch(`${baseUrl}/api/system/config/patch/${module}/${path}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(value)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as any;
|
||||
console.log("update config patch", module, path, data);
|
||||
if (data.success) {
|
||||
setSchema((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
[module]: data.config
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return data.success;
|
||||
} else {
|
||||
await displayError("patch", module, res, path);
|
||||
}
|
||||
|
||||
return false;
|
||||
const res = await api.system.patchConfig(module, path, value);
|
||||
return await handleConfigUpdate("patch", module, res, path);
|
||||
},
|
||||
overwrite: async <Module extends keyof ModuleConfigs>(
|
||||
module: keyof ModuleConfigs,
|
||||
path: string,
|
||||
value: any
|
||||
) => {
|
||||
const res = await fetch(`${baseUrl}/api/system/config/overwrite/${module}/${path}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(value)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as any;
|
||||
console.log("update config overwrite", module, path, data);
|
||||
if (data.success) {
|
||||
setSchema((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
[module]: data.config
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return data.success;
|
||||
} else {
|
||||
await displayError("overwrite", module, res, path);
|
||||
}
|
||||
|
||||
return false;
|
||||
const res = await api.system.overwriteConfig(module, path, value);
|
||||
return await handleConfigUpdate("overwrite", module, res, path);
|
||||
},
|
||||
add: async <Module extends keyof ModuleConfigs>(
|
||||
module: keyof ModuleConfigs,
|
||||
path: string,
|
||||
value: any
|
||||
) => {
|
||||
const res = await fetch(`${baseUrl}/api/system/config/add/${module}/${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(value)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as any;
|
||||
console.log("update config add", module, data);
|
||||
|
||||
if (data.success) {
|
||||
setSchema((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
[module]: data.config
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return data.success;
|
||||
} else {
|
||||
await displayError("add", module, res, path);
|
||||
}
|
||||
|
||||
return false;
|
||||
const res = await api.system.addConfig(module, path, value);
|
||||
return await handleConfigUpdate("add", module, res, path);
|
||||
},
|
||||
remove: async <Module extends keyof ModuleConfigs>(
|
||||
module: keyof ModuleConfigs,
|
||||
path: string
|
||||
) => {
|
||||
const res = await fetch(`${baseUrl}/api/system/config/remove/${module}/${path}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as any;
|
||||
console.log("update config remove", module, data);
|
||||
|
||||
if (data.success) {
|
||||
setSchema((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
config: {
|
||||
...prev.config,
|
||||
[module]: data.config
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return data.success;
|
||||
} else {
|
||||
await displayError("remove", module, res, path);
|
||||
}
|
||||
|
||||
return false;
|
||||
const res = await api.system.removeConfig(module, path);
|
||||
return await handleConfigUpdate("remove", module, res, path);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,14 +89,13 @@ export const useAuthStrategies = (options?: { baseUrl?: string }): Partial<AuthS
|
||||
const [data, setData] = useState<AuthStrategyData>();
|
||||
const ctxBaseUrl = useBaseUrl();
|
||||
const api = new Api({
|
||||
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl,
|
||||
tokenStorage: "localStorage"
|
||||
host: options?.baseUrl ? options?.baseUrl : ctxBaseUrl
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await api.auth.strategies();
|
||||
console.log("res", res);
|
||||
//console.log("res", res);
|
||||
if (res.res.ok) {
|
||||
setData(res.body);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export function useBkndAuth() {
|
||||
if (window.confirm(`Are you sure you want to delete the role "${name}"?`)) {
|
||||
return await bkndActions.remove("auth", `roles.${name}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,8 +15,7 @@ export class AppQueryClient {
|
||||
api: Api;
|
||||
constructor(public baseUrl: string) {
|
||||
this.api = new Api({
|
||||
host: baseUrl,
|
||||
tokenStorage: "localStorage"
|
||||
host: baseUrl
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,30 @@ const Base: React.FC<AlertProps> = ({ visible = true, title, message, className,
|
||||
{...props}
|
||||
className={twMerge("flex flex-row dark:bg-amber-300/20 bg-amber-200 p-4", className)}
|
||||
>
|
||||
<div>
|
||||
{title && <b className="mr-2">{title}:</b>}
|
||||
{message}
|
||||
</div>
|
||||
{title && <b className="mr-2">{title}:</b>}
|
||||
{message}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const Warning: React.FC<AlertProps> = (props) => (
|
||||
<Base {...props} className="dark:bg-amber-300/20 bg-amber-200" />
|
||||
const Warning: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||
<Base {...props} className={twMerge("dark:bg-amber-300/20 bg-amber-200", className)} />
|
||||
);
|
||||
|
||||
const Exception: React.FC<AlertProps> = (props) => (
|
||||
<Base {...props} className="dark:bg-red-950 bg-red-100" />
|
||||
const Exception: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||
<Base {...props} className={twMerge("dark:bg-red-950 bg-red-100", className)} />
|
||||
);
|
||||
|
||||
const Success: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||
<Base {...props} className={twMerge("dark:bg-green-950 bg-green-100", className)} />
|
||||
);
|
||||
|
||||
const Info: React.FC<AlertProps> = ({ className, ...props }) => (
|
||||
<Base {...props} className={twMerge("dark:bg-blue-950 bg-blue-100", className)} />
|
||||
);
|
||||
|
||||
export const Alert = {
|
||||
Warning,
|
||||
Exception
|
||||
Exception,
|
||||
Success,
|
||||
Info
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ export function DataTable<Data extends Record<string, any> = Record<string, any>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-muted border rounded-md shadow-sm w-full max-w-full overflow-x-scroll overflow-y-hidden">
|
||||
<table className="w-full">
|
||||
<table className="w-full text-md">
|
||||
{select.length > 0 ? (
|
||||
<thead className="sticky top-0 bg-muted/10">
|
||||
<tr>
|
||||
|
||||
@@ -150,7 +150,8 @@ function UserMenu() {
|
||||
|
||||
async function handleLogout() {
|
||||
await auth.logout();
|
||||
navigate("/auth/login", { replace: true });
|
||||
// @todo: grab from somewhere constant
|
||||
window.location.href = "/auth/logout";
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
|
||||
@@ -1,117 +1,55 @@
|
||||
import { type FieldApi, useForm } from "@tanstack/react-form";
|
||||
import { Type, type TypeInvalidError, parse } from "core/utils";
|
||||
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { typeboxResolver } from "@hookform/resolvers/typebox";
|
||||
import { Type } from "core/utils";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "ui";
|
||||
import * as Formy from "ui/components/form/Formy";
|
||||
|
||||
type LoginFormProps = {
|
||||
onSubmitted?: (value: { email: string; password: string }) => Promise<void>;
|
||||
export type LoginFormProps = Omit<ComponentPropsWithoutRef<"form">, "onSubmit"> & {
|
||||
className?: string;
|
||||
formData?: any;
|
||||
};
|
||||
|
||||
export function LoginForm({ onSubmitted }: LoginFormProps) {
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: ""
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
onSubmitted?.(value);
|
||||
},
|
||||
defaultState: {
|
||||
canSubmit: false,
|
||||
isValid: false
|
||||
},
|
||||
validatorAdapter: () => {
|
||||
function validate(
|
||||
{ value, fieldApi }: { value: any; fieldApi: FieldApi<any, any> },
|
||||
fn: any
|
||||
): any {
|
||||
if (fieldApi.form.state.submissionAttempts === 0) return;
|
||||
const schema = Type.Object({
|
||||
email: Type.String({
|
||||
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
|
||||
}),
|
||||
password: Type.String({
|
||||
minLength: 8 // @todo: this should be configurable
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
parse(fn, value);
|
||||
} catch (e) {
|
||||
return (e as TypeInvalidError).errors
|
||||
.map((error) => error.schema.error ?? error.message)
|
||||
.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
return { validate, validateAsync: validate };
|
||||
}
|
||||
export function LoginForm({ formData, className, method = "POST", ...props }: LoginFormProps) {
|
||||
const {
|
||||
register,
|
||||
formState: { isValid, errors }
|
||||
} = useForm({
|
||||
mode: "onChange",
|
||||
defaultValues: formData,
|
||||
resolver: typeboxResolver(schema)
|
||||
});
|
||||
|
||||
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
void form.handleSubmit();
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3 w-full" noValidate>
|
||||
<form.Field
|
||||
name="email"
|
||||
validators={{
|
||||
onChange: Type.String({
|
||||
pattern: "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"
|
||||
})
|
||||
}}
|
||||
children={(field) => (
|
||||
<Formy.Group error={field.state.meta.errors.length > 0}>
|
||||
<Formy.Label htmlFor={field.name}>Email address</Formy.Label>
|
||||
<Formy.Input
|
||||
type="email"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
/>
|
||||
</Formy.Group>
|
||||
)}
|
||||
/>
|
||||
<form.Field
|
||||
name="password"
|
||||
validators={{
|
||||
onChange: Type.String({
|
||||
minLength: 8
|
||||
})
|
||||
}}
|
||||
children={(field) => (
|
||||
<Formy.Group error={field.state.meta.errors.length > 0}>
|
||||
<Formy.Label htmlFor={field.name}>Password</Formy.Label>
|
||||
<Formy.Input
|
||||
type="password"
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
/>
|
||||
</Formy.Group>
|
||||
)}
|
||||
/>
|
||||
<form.Subscribe
|
||||
selector={(state) => {
|
||||
//console.log("state", state, Object.values(state.fieldMeta));
|
||||
const fieldMeta = Object.values(state.fieldMeta).map((f) => f.isDirty);
|
||||
const allDirty = fieldMeta.length > 0 ? fieldMeta.reduce((a, b) => a && b) : false;
|
||||
return [allDirty, state.isSubmitting];
|
||||
}}
|
||||
children={([allDirty, isSubmitting]) => {
|
||||
return (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="w-full mt-2 justify-center"
|
||||
disabled={!allDirty || isSubmitting}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form {...props} method={method} className={twMerge("flex flex-col gap-3 w-full", className)}>
|
||||
<Formy.Group>
|
||||
<Formy.Label htmlFor="email">Email address</Formy.Label>
|
||||
<Formy.Input type="email" {...register("email")} />
|
||||
</Formy.Group>
|
||||
<Formy.Group>
|
||||
<Formy.Label htmlFor="password">Password</Formy.Label>
|
||||
<Formy.Input type="password" {...register("password")} />
|
||||
</Formy.Group>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="large"
|
||||
className="w-full mt-2 justify-center"
|
||||
disabled={!isValid}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
39
app/src/ui/modules/server/FlashMessage.tsx
Normal file
39
app/src/ui/modules/server/FlashMessage.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getFlashMessage } from "core/server/flash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "ui/components/display/Alert";
|
||||
|
||||
/**
|
||||
* Handles flash message from server
|
||||
* @constructor
|
||||
*/
|
||||
export function FlashMessage() {
|
||||
const [flash, setFlash] = useState<any>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!flash) {
|
||||
const content = getFlashMessage();
|
||||
if (content) {
|
||||
setFlash(content);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (flash) {
|
||||
let Component = Alert.Info;
|
||||
switch (flash.type) {
|
||||
case "error":
|
||||
Component = Alert.Exception;
|
||||
break;
|
||||
case "success":
|
||||
Component = Alert.Success;
|
||||
break;
|
||||
case "warning":
|
||||
Component = Alert.Warning;
|
||||
break;
|
||||
}
|
||||
|
||||
return <Component message={flash.message} className="justify-center" />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,58 +1,17 @@
|
||||
import type { AppAuthOAuthStrategy } from "auth/auth-schema";
|
||||
import { Type, ucFirst, ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
|
||||
import { ucFirstAllSnakeToPascalWithSpaces } from "core/utils";
|
||||
import { transform } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAuth } from "ui/client";
|
||||
import { useAuthStrategies } from "ui/client/schema/auth/use-auth";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { Logo } from "ui/components/display/Logo";
|
||||
import { Link } from "ui/components/wouter/Link";
|
||||
import { useBrowserTitle } from "ui/hooks/use-browser-title";
|
||||
import { useSearch } from "ui/hooks/use-search";
|
||||
import { LoginForm } from "ui/modules/auth/LoginForm";
|
||||
import { useLocation } from "wouter";
|
||||
import * as AppShell from "../../layouts/AppShell/AppShell";
|
||||
|
||||
const schema = Type.Object({
|
||||
token: Type.String()
|
||||
});
|
||||
|
||||
export function AuthLogin() {
|
||||
useBrowserTitle(["Login"]);
|
||||
const [, navigate] = useLocation();
|
||||
const search = useSearch(schema);
|
||||
const token = search.value.token;
|
||||
//console.log("search", token, "/api/auth/google?redirect=" + window.location.href);
|
||||
|
||||
const auth = useAuth();
|
||||
const { strategies, basepath, loading } = useAuthStrategies();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
auth.setToken(token);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
async function handleSubmit(value: { email: string; password: string }) {
|
||||
console.log("submit", value);
|
||||
const { res, data } = await auth.login(value);
|
||||
if (!res.ok) {
|
||||
if (data && "error" in data) {
|
||||
setError(data.error.message);
|
||||
} else {
|
||||
setError("An error occurred");
|
||||
}
|
||||
} else if (error) {
|
||||
setError(null);
|
||||
}
|
||||
console.log("res:login", { res, data });
|
||||
}
|
||||
|
||||
if (auth.user) {
|
||||
console.log("user set", auth.user);
|
||||
navigate("/", { replace: true });
|
||||
}
|
||||
|
||||
const oauth = transform(
|
||||
strategies ?? {},
|
||||
@@ -63,7 +22,7 @@ export function AuthLogin() {
|
||||
},
|
||||
{}
|
||||
) as Record<string, AppAuthOAuthStrategy>;
|
||||
console.log("oauth", oauth, strategies);
|
||||
//console.log("oauth", oauth, strategies);
|
||||
|
||||
return (
|
||||
<AppShell.Root>
|
||||
@@ -77,26 +36,26 @@ export function AuthLogin() {
|
||||
<h1 className="text-xl font-bold">Sign in to your admin panel</h1>
|
||||
<p className="text-primary/50">Enter your credentials below to get access.</p>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="bg-red-500/40 p-3 w-full rounded font-bold mb-1">
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{Object.keys(oauth).length > 0 && (
|
||||
<>
|
||||
{Object.entries(oauth)?.map(([name, oauth], key) => (
|
||||
<Button
|
||||
<form
|
||||
method="POST"
|
||||
action={`${basepath}/${name}/login`}
|
||||
key={key}
|
||||
size="large"
|
||||
variant="outline"
|
||||
className="justify-center"
|
||||
onClick={() => {
|
||||
window.location.href = `${basepath}/${name}/login?redirect=${window.location.href}`;
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
Continue with {ucFirstAllSnakeToPascalWithSpaces(oauth.name)}
|
||||
</Button>
|
||||
<Button
|
||||
key={key}
|
||||
type="submit"
|
||||
size="large"
|
||||
variant="outline"
|
||||
className="justify-center w-full"
|
||||
>
|
||||
Continue with {ucFirstAllSnakeToPascalWithSpaces(oauth.name)}
|
||||
</Button>
|
||||
</form>
|
||||
))}
|
||||
|
||||
<div className="w-full flex flex-row items-center">
|
||||
@@ -111,7 +70,8 @@ export function AuthLogin() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<LoginForm onSubmitted={handleSubmit} />
|
||||
<LoginForm action="/api/auth/password/login" />
|
||||
{/*<a href="/auth/logout">Logout</a>*/}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -29,13 +29,13 @@ function AuthRolesEditInternal({ params }) {
|
||||
const data = formRef.current?.getData();
|
||||
const success = await actions.roles.patch(roleName, data);
|
||||
|
||||
notifications.show({
|
||||
/*notifications.show({
|
||||
id: `role-${roleName}-update`,
|
||||
position: "top-right",
|
||||
title: success ? "Update success" : "Update failed",
|
||||
message: success ? "Role updated successfully" : "Failed to update role",
|
||||
color: !success ? "red" : undefined
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
|
||||
@@ -90,14 +90,18 @@ const renderValue = ({ value, property }) => {
|
||||
}
|
||||
|
||||
if (property === "permissions") {
|
||||
return [...(value || [])].map((p, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-block px-2 py-1.5 text-sm bg-primary/5 rounded font-mono leading-none"
|
||||
>
|
||||
{p}
|
||||
</span>
|
||||
));
|
||||
return (
|
||||
<div className="flex flex-row gap-1">
|
||||
{[...(value || [])].map((p, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-block px-2 py-1.5 text-sm bg-primary/5 rounded font-mono leading-none"
|
||||
>
|
||||
{p}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <CellValue value={value} property={property} />;
|
||||
|
||||
@@ -6,10 +6,9 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
import { isDebug } from "core";
|
||||
import type { Entity } from "data";
|
||||
import { cloneDeep, omit } from "lodash-es";
|
||||
import { forwardRef, useImperativeHandle, useRef, useState } from "react";
|
||||
import { TbArrowLeft, TbDots } from "react-icons/tb";
|
||||
import { useBknd } from "ui/client";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useRef, useState } from "react";
|
||||
import { TbDots } from "react-icons/tb";
|
||||
import { useBkndData } from "ui/client/schema/data/use-bknd-data";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "ui/components/buttons/IconButton";
|
||||
@@ -20,9 +19,8 @@ import {
|
||||
} from "ui/components/form/json-schema/JsonSchemaForm";
|
||||
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||
import { SectionHeaderAccordionItem } from "ui/layouts/AppShell/AppShell";
|
||||
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
|
||||
import { routes, useGoBack, useNavigate } from "ui/lib/routes";
|
||||
import { routes, useNavigate } from "ui/lib/routes";
|
||||
import { extractSchema } from "../settings/utils/schema";
|
||||
import { EntityFieldsForm, type EntityFieldsFormRef } from "./forms/entity.fields.form";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Suspense, lazy } from "react";
|
||||
import { useBknd } from "ui/client";
|
||||
import { Route, Router, Switch } from "wouter";
|
||||
import { useBknd } from "../client/BkndProvider";
|
||||
import { AuthLogin } from "./auth/auth.login";
|
||||
import { Root, RootEmpty } from "./root";
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ html.fixed, html.fixed body {
|
||||
@mixin dark {
|
||||
--mantine-color-body: rgb(9 9 11);
|
||||
}
|
||||
|
||||
table {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
|
||||
Reference in New Issue
Block a user