@nocobase/portal-template-default 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +77 -0
- package/package.json +23 -17
- package/registry/nocobase-acl/demo/prompt-generator.tsx +6 -39
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +6 -42
- package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +1 -1
- package/registry/nocobase-ai/demo/prompt-card.tsx +7 -28
- package/registry/nocobase-ai/demo/prompt-generator.tsx +9 -30
- package/registry/nocobase-ai/demo/shortcut.tsx +1 -1
- package/registry/nocobase-ai/demo/tool-cards.tsx +10 -30
- package/registry/nocobase-auth-cas/README.md +6 -0
- package/registry/nocobase-auth-cas/cas-sign-in-button.tsx +41 -0
- package/registry/nocobase-auth-cas/demo.tsx +20 -0
- package/registry/nocobase-auth-cas/extension.tsx +46 -0
- package/registry/nocobase-auth-cas/index.ts +1 -0
- package/registry/nocobase-auth-cas/use-cas-sign-in.ts +29 -0
- package/registry/nocobase-auth-dingtalk/README.md +7 -0
- package/registry/nocobase-auth-dingtalk/auto-login-provider.tsx +154 -0
- package/registry/nocobase-auth-dingtalk/demo.tsx +23 -0
- package/registry/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +45 -0
- package/registry/nocobase-auth-dingtalk/extension.tsx +55 -0
- package/registry/nocobase-auth-dingtalk/index.ts +4 -0
- package/registry/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +64 -0
- package/registry/nocobase-auth-ldap/README.md +6 -0
- package/registry/nocobase-auth-ldap/demo.tsx +21 -0
- package/registry/nocobase-auth-ldap/extension.tsx +46 -0
- package/registry/nocobase-auth-ldap/index.ts +1 -0
- package/registry/nocobase-auth-ldap/ldap-sign-in-form.tsx +69 -0
- package/registry/nocobase-auth-ldap/use-ldap-sign-in.ts +5 -0
- package/registry/nocobase-auth-oidc/README.md +11 -0
- package/registry/nocobase-auth-oidc/auto-redirect-provider.tsx +13 -0
- package/registry/nocobase-auth-oidc/demo.tsx +20 -0
- package/registry/nocobase-auth-oidc/extension.tsx +51 -0
- package/registry/nocobase-auth-oidc/index.ts +1 -0
- package/registry/nocobase-auth-oidc/oidc-sign-in-button.tsx +44 -0
- package/registry/nocobase-auth-oidc/use-oidc-sign-in.ts +58 -0
- package/registry/nocobase-auth-saml/README.md +6 -0
- package/registry/nocobase-auth-saml/auto-redirect-provider.tsx +13 -0
- package/registry/nocobase-auth-saml/demo.tsx +20 -0
- package/registry/nocobase-auth-saml/extension.tsx +51 -0
- package/registry/nocobase-auth-saml/index.ts +1 -0
- package/registry/nocobase-auth-saml/saml-sign-in-button.tsx +43 -0
- package/registry/nocobase-auth-saml/use-saml-sign-in.ts +51 -0
- package/registry/nocobase-auth-sms/README.md +7 -0
- package/registry/nocobase-auth-sms/demo.tsx +25 -0
- package/registry/nocobase-auth-sms/extension.tsx +46 -0
- package/registry/nocobase-auth-sms/index.ts +1 -0
- package/registry/nocobase-auth-sms/sms-sign-in-form.tsx +107 -0
- package/registry/nocobase-auth-sms/use-sms-sign-in.ts +88 -0
- package/registry/nocobase-auth-wecom/README.md +6 -0
- package/registry/nocobase-auth-wecom/auto-login-provider.tsx +21 -0
- package/registry/nocobase-auth-wecom/demo.tsx +23 -0
- package/registry/nocobase-auth-wecom/extension.tsx +49 -0
- package/registry/nocobase-auth-wecom/index.ts +1 -0
- package/registry/nocobase-auth-wecom/use-wecom-sign-in.ts +62 -0
- package/registry/nocobase-auth-wecom/wecom-sign-in-button.tsx +70 -0
- package/registry/nocobase-i18n/demo/prompt-generator.tsx +30 -54
- package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +18 -21
- package/registry.config.json +112 -0
- package/scripts/build-html.mjs +106 -0
- package/src/App.tsx +34 -8
- package/src/app/extension.ts +4 -0
- package/src/app/extensions.tsx +24 -1
- package/src/components/auth/authenticator-adapters.ts +20 -0
- package/src/components/auth/auto-redirect-provider.tsx +84 -0
- package/src/components/auth/basic-sign-in-form.tsx +101 -0
- package/src/components/auth/default-sign-in-page.tsx +20 -0
- package/src/components/auth/demo/auth-demo-page.tsx +236 -0
- package/src/components/auth/demo/auth-demo-prompt-generator.tsx +93 -0
- package/src/components/auth/demo/auth-demo-route.tsx +11 -0
- package/src/components/auth/demo/auth-method-demo.tsx +48 -0
- package/src/components/auth/demo/index.ts +4 -0
- package/src/components/auth/dynamic-sign-in.tsx +191 -0
- package/src/components/auth/forgot-password-form.tsx +13 -5
- package/src/components/auth/index.ts +13 -0
- package/src/components/auth/sign-in-form.tsx +1 -111
- package/src/components/auth/sign-up-form.tsx +14 -37
- package/src/components/auth/types.ts +38 -0
- package/src/components/auth/use-authenticator-sign-in.ts +14 -0
- package/src/components/auth/use-public-authenticators.ts +26 -0
- package/src/components/data-table/data-table-filter.tsx +107 -153
- package/src/components/demo/prompt-output.tsx +71 -0
- package/src/components/ui/alert-dialog.tsx +2 -0
- package/src/components/ui/attachment.tsx +207 -0
- package/src/components/ui/bubble.tsx +128 -0
- package/src/components/ui/button-group.tsx +87 -0
- package/src/components/ui/button.tsx +6 -6
- package/src/components/ui/calendar.tsx +0 -2
- package/src/components/ui/card.tsx +1 -1
- package/src/components/ui/carousel.tsx +2 -0
- package/src/components/ui/combobox.tsx +297 -0
- package/src/components/ui/command.tsx +43 -41
- package/src/components/ui/dialog.tsx +0 -2
- package/src/components/ui/direction.tsx +4 -0
- package/src/components/ui/drawer.tsx +2 -0
- package/src/components/ui/dropdown-menu.tsx +0 -2
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +238 -0
- package/src/components/ui/hover-card.tsx +2 -0
- package/src/components/ui/input-group.tsx +0 -2
- package/src/components/ui/input-otp.tsx +0 -2
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/item.tsx +201 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +2 -0
- package/src/components/ui/marker.tsx +71 -0
- package/src/components/ui/message-scroller.tsx +131 -0
- package/src/components/ui/message.tsx +92 -0
- package/src/components/ui/native-select.tsx +61 -0
- package/src/components/ui/popover.tsx +0 -2
- package/src/components/ui/progress.tsx +2 -0
- package/src/components/ui/radio-group.tsx +0 -2
- package/src/components/ui/resizable.tsx +2 -0
- package/src/components/ui/scroll-area.tsx +0 -2
- package/src/components/ui/select.tsx +6 -4
- package/src/components/ui/separator.tsx +0 -2
- package/src/components/ui/sheet.tsx +21 -22
- package/src/components/ui/sidebar.tsx +1 -3
- package/src/components/ui/sonner.tsx +2 -0
- package/src/components/ui/spinner.tsx +10 -0
- package/src/components/ui/switch.tsx +0 -2
- package/src/components/ui/table.tsx +4 -2
- package/src/components/ui/tabs.tsx +0 -2
- package/src/components/ui/textarea.tsx +1 -1
- package/src/components/ui/toggle-group.tsx +2 -0
- package/src/components/ui/toggle.tsx +2 -0
- package/src/components/ui/tooltip.tsx +0 -2
- package/src/index.tsx +3 -0
- package/src/lib/nocobase/client.ts +32 -2
- package/src/lib/utils.ts +2 -1
- package/src/locales/en-US.ts +4 -2
- package/src/locales/zh-CN.ts +4 -2
- package/src/pages/login/index.tsx +2 -2
- package/src/pages/users/list.tsx +144 -16
- package/src/providers/auth-callback.ts +21 -0
- package/src/providers/auth.ts +108 -22
- package/src/providers/constants.ts +4 -1
- package/src/providers/runtime-config.ts +19 -0
- package/src/vite-env.d.ts +5 -0
- package/vite.config.ts +55 -2
package/src/app/extensions.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Suspense, type PropsWithChildren, type ReactNode } from "react";
|
|
2
2
|
import type { AppExtension } from "./extension";
|
|
3
|
+
import { LoadingState } from "@/components/app-shell/loading-state";
|
|
3
4
|
|
|
4
5
|
const extensionModules = import.meta.glob<{ default: AppExtension }>(
|
|
5
6
|
"../extensions/*/extension.tsx",
|
|
@@ -25,6 +26,10 @@ export const extensionUserMenuItems = appExtensions
|
|
|
25
26
|
Component: extension.UserMenuItems!,
|
|
26
27
|
}));
|
|
27
28
|
|
|
29
|
+
export const extensionAuthAdapters = appExtensions.flatMap(
|
|
30
|
+
(extension) => extension.authAdapters ?? []
|
|
31
|
+
);
|
|
32
|
+
|
|
28
33
|
export function AppExtensionProviders({ children }: PropsWithChildren) {
|
|
29
34
|
return appExtensions.reduceRight<ReactNode>((content, extension) => {
|
|
30
35
|
const Provider = extension.Provider;
|
|
@@ -32,4 +37,22 @@ export function AppExtensionProviders({ children }: PropsWithChildren) {
|
|
|
32
37
|
}, children);
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
export function AppAuthRuntimeProviders({ children }: PropsWithChildren) {
|
|
41
|
+
return [...appExtensions]
|
|
42
|
+
.filter((extension) => extension.AuthRuntimeProvider)
|
|
43
|
+
.sort(
|
|
44
|
+
(left, right) =>
|
|
45
|
+
(left.authRuntimePriority ?? 100) -
|
|
46
|
+
(right.authRuntimePriority ?? 100) || left.id.localeCompare(right.id)
|
|
47
|
+
)
|
|
48
|
+
.reduceRight<ReactNode>((content, extension) => {
|
|
49
|
+
const Provider = extension.AuthRuntimeProvider!;
|
|
50
|
+
return (
|
|
51
|
+
<Suspense fallback={<LoadingState className="min-h-svh" />}>
|
|
52
|
+
<Provider>{content}</Provider>
|
|
53
|
+
</Suspense>
|
|
54
|
+
);
|
|
55
|
+
}, children);
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
export type { AppExtension } from "./extension";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { extensionAuthAdapters } from "@/app/extensions";
|
|
2
|
+
import { BasicSignInForm } from "@/components/auth/basic-sign-in-form";
|
|
3
|
+
import type { AuthenticatorAdapter } from "@/components/auth/types";
|
|
4
|
+
|
|
5
|
+
const coreAuthAdapters: AuthenticatorAdapter[] = [
|
|
6
|
+
{
|
|
7
|
+
authType: "Email/Password",
|
|
8
|
+
placement: "form",
|
|
9
|
+
Component: BasicSignInForm,
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const authenticatorAdapters = [
|
|
14
|
+
...coreAuthAdapters,
|
|
15
|
+
...extensionAuthAdapters,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export const authenticatorAdapterMap = new Map(
|
|
19
|
+
authenticatorAdapters.map((adapter) => [adapter.authType, adapter])
|
|
20
|
+
);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useLocation } from "react-router";
|
|
3
|
+
|
|
4
|
+
import { LoadingState } from "@/components/app-shell/loading-state";
|
|
5
|
+
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
6
|
+
|
|
7
|
+
type AutoRedirectResponse = {
|
|
8
|
+
success?: boolean;
|
|
9
|
+
authUrl?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type AuthAutoRedirectProviderProps = React.PropsWithChildren<{
|
|
13
|
+
resource: string;
|
|
14
|
+
action: string;
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
query?: Record<string, string | number | boolean | null | undefined>;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
const publicAuthPaths = new Set([
|
|
20
|
+
"/login",
|
|
21
|
+
"/signin",
|
|
22
|
+
"/register",
|
|
23
|
+
"/forgot-password",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export function AuthAutoRedirectProvider({
|
|
27
|
+
resource,
|
|
28
|
+
action,
|
|
29
|
+
enabled = true,
|
|
30
|
+
query,
|
|
31
|
+
children,
|
|
32
|
+
}: AuthAutoRedirectProviderProps) {
|
|
33
|
+
const { pathname, search } = useLocation();
|
|
34
|
+
const attemptedRef = useRef(false);
|
|
35
|
+
const redirect = `${pathname}${search}`;
|
|
36
|
+
const shouldCheck =
|
|
37
|
+
enabled &&
|
|
38
|
+
!nocobaseClient.getToken() &&
|
|
39
|
+
!publicAuthPaths.has(pathname);
|
|
40
|
+
const [isChecking, setIsChecking] = useState(shouldCheck);
|
|
41
|
+
const stableQuery = useMemo(() => query, [JSON.stringify(query)]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!shouldCheck || attemptedRef.current) {
|
|
45
|
+
setIsChecking(false);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
attemptedRef.current = true;
|
|
50
|
+
setIsChecking(true);
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
|
|
53
|
+
void nocobaseClient
|
|
54
|
+
.action<AutoRedirectResponse>(resource, action, {
|
|
55
|
+
method: "GET",
|
|
56
|
+
query: { redirect, ...stableQuery },
|
|
57
|
+
signal: controller.signal,
|
|
58
|
+
authenticator: null,
|
|
59
|
+
includeRole: false,
|
|
60
|
+
withAclMeta: false,
|
|
61
|
+
})
|
|
62
|
+
.then((result) => {
|
|
63
|
+
if (result?.success === false && result.authUrl) {
|
|
64
|
+
window.location.replace(nocobaseClient.resolveUrl(result.authUrl));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
setIsChecking(false);
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setIsChecking(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return () => controller.abort();
|
|
77
|
+
}, [action, redirect, resource, shouldCheck, stableQuery]);
|
|
78
|
+
|
|
79
|
+
if (isChecking) {
|
|
80
|
+
return <LoadingState className="min-h-svh" />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return children;
|
|
84
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useLink, useLogin } from "@refinedev/core";
|
|
5
|
+
|
|
6
|
+
import { InputPassword } from "@/components/auth/input-password";
|
|
7
|
+
import type { AuthenticatorComponentProps } from "@/components/auth/types";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { Input } from "@/components/ui/input";
|
|
10
|
+
import { Label } from "@/components/ui/label";
|
|
11
|
+
|
|
12
|
+
type LoginVariables = {
|
|
13
|
+
account: string;
|
|
14
|
+
password: string;
|
|
15
|
+
authenticator: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function BasicSignInForm({
|
|
19
|
+
authenticator,
|
|
20
|
+
}: AuthenticatorComponentProps) {
|
|
21
|
+
const [account, setAccount] = useState("");
|
|
22
|
+
const [password, setPassword] = useState("");
|
|
23
|
+
const Link = useLink();
|
|
24
|
+
const { mutate: login, isPending } = useLogin<LoginVariables>();
|
|
25
|
+
const allowSignUp = authenticator.options?.allowSignUp === true;
|
|
26
|
+
const enableResetPassword =
|
|
27
|
+
authenticator.options?.enableResetPassword === true;
|
|
28
|
+
|
|
29
|
+
const handleSignIn = (event: React.FormEvent<HTMLFormElement>) => {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
login({ account, password, authenticator: authenticator.name });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<form onSubmit={handleSignIn} className="space-y-5">
|
|
36
|
+
<div className="space-y-2">
|
|
37
|
+
<Label htmlFor={`${authenticator.name}-account`}>
|
|
38
|
+
Username or email
|
|
39
|
+
</Label>
|
|
40
|
+
<Input
|
|
41
|
+
id={`${authenticator.name}-account`}
|
|
42
|
+
type="text"
|
|
43
|
+
value={account}
|
|
44
|
+
onChange={(event) => setAccount(event.target.value)}
|
|
45
|
+
autoComplete="username"
|
|
46
|
+
autoFocus
|
|
47
|
+
required
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="space-y-2">
|
|
52
|
+
<Label htmlFor={`${authenticator.name}-password`}>Password</Label>
|
|
53
|
+
<InputPassword
|
|
54
|
+
id={`${authenticator.name}-password`}
|
|
55
|
+
value={password}
|
|
56
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
57
|
+
autoComplete="current-password"
|
|
58
|
+
required
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<Button
|
|
63
|
+
type="submit"
|
|
64
|
+
className="w-full"
|
|
65
|
+
disabled={isPending}
|
|
66
|
+
>
|
|
67
|
+
{isPending ? "Signing in…" : "Sign in"}
|
|
68
|
+
</Button>
|
|
69
|
+
|
|
70
|
+
{(allowSignUp || enableResetPassword) && (
|
|
71
|
+
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 text-sm text-muted-foreground">
|
|
72
|
+
{enableResetPassword ? (
|
|
73
|
+
<Link
|
|
74
|
+
to={`/forgot-password?name=${encodeURIComponent(
|
|
75
|
+
authenticator.name
|
|
76
|
+
)}`}
|
|
77
|
+
className="transition-colors hover:text-foreground hover:underline hover:underline-offset-4"
|
|
78
|
+
>
|
|
79
|
+
Forgot password?
|
|
80
|
+
</Link>
|
|
81
|
+
) : (
|
|
82
|
+
<span />
|
|
83
|
+
)}
|
|
84
|
+
{allowSignUp && (
|
|
85
|
+
<span>
|
|
86
|
+
No account?{" "}
|
|
87
|
+
<Link
|
|
88
|
+
to={`/register?name=${encodeURIComponent(authenticator.name)}`}
|
|
89
|
+
className="font-semibold text-foreground underline underline-offset-4"
|
|
90
|
+
>
|
|
91
|
+
Sign up
|
|
92
|
+
</Link>
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</form>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
BasicSignInForm.displayName = "BasicSignInForm";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AuthLayout } from "@/components/auth/auth-layout";
|
|
2
|
+
import { DynamicSignIn } from "@/components/auth/dynamic-sign-in";
|
|
3
|
+
import type { RenderAuthenticator } from "@/components/auth/types";
|
|
4
|
+
|
|
5
|
+
type DefaultSignInPageProps = {
|
|
6
|
+
renderAuthenticator?: RenderAuthenticator;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function DefaultSignInPage({
|
|
10
|
+
renderAuthenticator,
|
|
11
|
+
}: DefaultSignInPageProps) {
|
|
12
|
+
return (
|
|
13
|
+
<AuthLayout
|
|
14
|
+
title="Welcome back"
|
|
15
|
+
description="Choose a sign-in method configured in NocoBase."
|
|
16
|
+
>
|
|
17
|
+
<DynamicSignIn renderAuthenticator={renderAuthenticator} />
|
|
18
|
+
</AuthLayout>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Building2, MessageSquare, ShieldCheck } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { BrandLogo } from "@/components/app-shell/brand";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from "@/components/ui/card";
|
|
14
|
+
import { Input } from "@/components/ui/input";
|
|
15
|
+
import { Label } from "@/components/ui/label";
|
|
16
|
+
import { Separator } from "@/components/ui/separator";
|
|
17
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
18
|
+
import {
|
|
19
|
+
AuthDemoPromptGenerator,
|
|
20
|
+
type AuthIntegrationPattern,
|
|
21
|
+
} from "./auth-demo-prompt-generator";
|
|
22
|
+
|
|
23
|
+
const scenarioCopy: Record<
|
|
24
|
+
AuthIntegrationPattern,
|
|
25
|
+
{ title: string; description: string }
|
|
26
|
+
> = {
|
|
27
|
+
dynamic: {
|
|
28
|
+
title: "Default dynamic login",
|
|
29
|
+
description:
|
|
30
|
+
"Enabled authenticators are discovered from NocoBase and arranged exactly as forms and buttons on the login page.",
|
|
31
|
+
},
|
|
32
|
+
method: {
|
|
33
|
+
title: "Replace one method",
|
|
34
|
+
description:
|
|
35
|
+
"The dynamic page remains intact while one authenticator receives application-owned branding and interaction.",
|
|
36
|
+
},
|
|
37
|
+
page: {
|
|
38
|
+
title: "Replace the complete page",
|
|
39
|
+
description:
|
|
40
|
+
"The application owns the whole visual composition while the Starter retains the authentication runtime.",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function AuthDemoPage() {
|
|
45
|
+
const [pattern, setPattern] =
|
|
46
|
+
useState<AuthIntegrationPattern>("dynamic");
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="space-y-8 pb-12">
|
|
50
|
+
<header className="space-y-2">
|
|
51
|
+
<Badge variant="secondary">Authentication</Badge>
|
|
52
|
+
<h1 className="font-heading text-3xl font-semibold tracking-tight">
|
|
53
|
+
Login composition
|
|
54
|
+
</h1>
|
|
55
|
+
<p className="max-w-3xl text-muted-foreground">
|
|
56
|
+
Compare the backend-driven default with the two application-owned
|
|
57
|
+
customization boundaries.
|
|
58
|
+
</p>
|
|
59
|
+
</header>
|
|
60
|
+
|
|
61
|
+
<Card>
|
|
62
|
+
<CardHeader>
|
|
63
|
+
<CardTitle>{scenarioCopy[pattern].title}</CardTitle>
|
|
64
|
+
<CardDescription>{scenarioCopy[pattern].description}</CardDescription>
|
|
65
|
+
</CardHeader>
|
|
66
|
+
<CardContent>
|
|
67
|
+
<Tabs
|
|
68
|
+
value={pattern}
|
|
69
|
+
onValueChange={(next) =>
|
|
70
|
+
setPattern(next as AuthIntegrationPattern)
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<TabsList className="w-full">
|
|
74
|
+
<TabsTrigger value="dynamic">Dynamic</TabsTrigger>
|
|
75
|
+
<TabsTrigger value="method">Replace method</TabsTrigger>
|
|
76
|
+
<TabsTrigger value="page">Replace page</TabsTrigger>
|
|
77
|
+
</TabsList>
|
|
78
|
+
<TabsContent value="dynamic">
|
|
79
|
+
<DynamicLoginPreview />
|
|
80
|
+
</TabsContent>
|
|
81
|
+
<TabsContent value="method">
|
|
82
|
+
<DynamicLoginPreview replaceOidc />
|
|
83
|
+
</TabsContent>
|
|
84
|
+
<TabsContent value="page">
|
|
85
|
+
<CustomLoginPagePreview />
|
|
86
|
+
</TabsContent>
|
|
87
|
+
</Tabs>
|
|
88
|
+
</CardContent>
|
|
89
|
+
</Card>
|
|
90
|
+
|
|
91
|
+
{pattern !== "dynamic" ? (
|
|
92
|
+
<AuthDemoPromptGenerator
|
|
93
|
+
value={pattern}
|
|
94
|
+
onValueChange={setPattern}
|
|
95
|
+
patterns={["method", "page"]}
|
|
96
|
+
/>
|
|
97
|
+
) : null}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function DynamicLoginPreview({ replaceOidc = false }: { replaceOidc?: boolean }) {
|
|
103
|
+
return (
|
|
104
|
+
<div className="mx-auto max-w-sm space-y-5 pt-5">
|
|
105
|
+
<div className="space-y-1 text-center">
|
|
106
|
+
<h2 className="font-heading text-xl font-semibold">Welcome back</h2>
|
|
107
|
+
<p className="text-sm text-muted-foreground">
|
|
108
|
+
Choose a sign-in method configured in NocoBase.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<Tabs defaultValue="password">
|
|
113
|
+
<TabsList className="w-full">
|
|
114
|
+
<TabsTrigger value="password">Password</TabsTrigger>
|
|
115
|
+
<TabsTrigger value="sms">SMS</TabsTrigger>
|
|
116
|
+
</TabsList>
|
|
117
|
+
<TabsContent value="password">
|
|
118
|
+
<PasswordFields />
|
|
119
|
+
</TabsContent>
|
|
120
|
+
<TabsContent value="sms">
|
|
121
|
+
<SmsFields />
|
|
122
|
+
</TabsContent>
|
|
123
|
+
</Tabs>
|
|
124
|
+
|
|
125
|
+
<div className="flex items-center gap-4 py-1">
|
|
126
|
+
<Separator className="flex-1" />
|
|
127
|
+
<span className="text-xs text-muted-foreground">Or continue with</span>
|
|
128
|
+
<Separator className="flex-1" />
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div className="grid gap-3">
|
|
132
|
+
{replaceOidc ? (
|
|
133
|
+
<div className="space-y-2 rounded-xl border border-primary/25 bg-primary/5 p-3">
|
|
134
|
+
<Button className="w-full">
|
|
135
|
+
<ShieldCheck />
|
|
136
|
+
Continue with NocoBase SSO
|
|
137
|
+
</Button>
|
|
138
|
+
<p className="text-center text-xs text-muted-foreground">
|
|
139
|
+
Custom UI for the company-oidc authenticator only
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
) : (
|
|
143
|
+
<Button variant="outline" className="w-full">
|
|
144
|
+
<Building2 />
|
|
145
|
+
Company OIDC
|
|
146
|
+
</Button>
|
|
147
|
+
)}
|
|
148
|
+
<Button variant="outline" className="w-full">
|
|
149
|
+
<MessageSquare />
|
|
150
|
+
DingTalk
|
|
151
|
+
</Button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function CustomLoginPagePreview() {
|
|
158
|
+
return (
|
|
159
|
+
<div className="mx-auto mt-5 grid max-w-4xl overflow-hidden rounded-xl border md:grid-cols-[1.1fr_1fr]">
|
|
160
|
+
<div className="hidden min-h-[460px] flex-col justify-between bg-foreground p-8 text-background md:flex">
|
|
161
|
+
<div className="flex items-center gap-3">
|
|
162
|
+
<div className="flex size-10 items-center justify-center rounded-lg bg-background text-foreground">
|
|
163
|
+
<BrandLogo className="size-7" />
|
|
164
|
+
</div>
|
|
165
|
+
<span className="font-heading text-lg font-semibold">
|
|
166
|
+
NocoBase
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="space-y-3">
|
|
170
|
+
<Badge variant="secondary">Application-owned page</Badge>
|
|
171
|
+
<p className="font-heading text-3xl font-semibold leading-tight">
|
|
172
|
+
Build freely. Keep the foundation reliable.
|
|
173
|
+
</p>
|
|
174
|
+
<p className="text-sm text-background/65">
|
|
175
|
+
This entire composition can change without replacing the Starter's
|
|
176
|
+
authentication runtime.
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="space-y-6 bg-card p-6 sm:p-8">
|
|
182
|
+
<div className="space-y-1">
|
|
183
|
+
<h2 className="font-heading text-2xl font-semibold">Sign in</h2>
|
|
184
|
+
<p className="text-sm text-muted-foreground">
|
|
185
|
+
Continue to your NocoBase application.
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
<PasswordFields />
|
|
189
|
+
<div className="flex items-center gap-4">
|
|
190
|
+
<Separator className="flex-1" />
|
|
191
|
+
<span className="text-xs text-muted-foreground">or</span>
|
|
192
|
+
<Separator className="flex-1" />
|
|
193
|
+
</div>
|
|
194
|
+
<Button variant="outline" className="w-full">
|
|
195
|
+
<ShieldCheck />
|
|
196
|
+
NocoBase SSO
|
|
197
|
+
</Button>
|
|
198
|
+
<p className="text-center text-xs text-muted-foreground">
|
|
199
|
+
By continuing, you agree to your organization's access policy.
|
|
200
|
+
</p>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function PasswordFields() {
|
|
207
|
+
return (
|
|
208
|
+
<div className="space-y-4 pt-3">
|
|
209
|
+
<div className="space-y-2">
|
|
210
|
+
<Label>Username or email</Label>
|
|
211
|
+
<Input placeholder="you@example.com" />
|
|
212
|
+
</div>
|
|
213
|
+
<div className="space-y-2">
|
|
214
|
+
<Label>Password</Label>
|
|
215
|
+
<Input type="password" value="password" readOnly />
|
|
216
|
+
</div>
|
|
217
|
+
<Button className="w-full">Sign in</Button>
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function SmsFields() {
|
|
223
|
+
return (
|
|
224
|
+
<div className="space-y-4 pt-3">
|
|
225
|
+
<div className="space-y-2">
|
|
226
|
+
<Label>Phone</Label>
|
|
227
|
+
<Input placeholder="Phone number" />
|
|
228
|
+
</div>
|
|
229
|
+
<div className="grid grid-cols-[1fr_auto] gap-2">
|
|
230
|
+
<Input placeholder="Verification code" />
|
|
231
|
+
<Button variant="outline">Send code</Button>
|
|
232
|
+
</div>
|
|
233
|
+
<Button className="w-full">Sign in</Button>
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { PromptOutput } from "@/components/demo/prompt-output";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
} from "@/components/ui/card";
|
|
11
|
+
import { Label } from "@/components/ui/label";
|
|
12
|
+
import {
|
|
13
|
+
Select,
|
|
14
|
+
SelectContent,
|
|
15
|
+
SelectItem,
|
|
16
|
+
SelectTrigger,
|
|
17
|
+
SelectValue,
|
|
18
|
+
} from "@/components/ui/select";
|
|
19
|
+
|
|
20
|
+
export type AuthIntegrationPattern = "dynamic" | "method" | "page";
|
|
21
|
+
|
|
22
|
+
const patternLabels: Record<AuthIntegrationPattern, string> = {
|
|
23
|
+
dynamic: "Use the default dynamic login",
|
|
24
|
+
method: "Replace one authentication method",
|
|
25
|
+
page: "Replace the complete login page",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function AuthDemoPromptGenerator({
|
|
29
|
+
value,
|
|
30
|
+
onValueChange,
|
|
31
|
+
patterns,
|
|
32
|
+
}: {
|
|
33
|
+
value?: AuthIntegrationPattern;
|
|
34
|
+
onValueChange?: (value: AuthIntegrationPattern) => void;
|
|
35
|
+
patterns?: AuthIntegrationPattern[];
|
|
36
|
+
}) {
|
|
37
|
+
const [localPattern, setLocalPattern] = useState<AuthIntegrationPattern>(
|
|
38
|
+
value ?? patterns?.[0] ?? "method"
|
|
39
|
+
);
|
|
40
|
+
const pattern = value ?? localPattern;
|
|
41
|
+
const availablePatterns =
|
|
42
|
+
patterns ?? (Object.keys(patternLabels) as AuthIntegrationPattern[]);
|
|
43
|
+
const setPattern = (next: AuthIntegrationPattern) => {
|
|
44
|
+
setLocalPattern(next);
|
|
45
|
+
onValueChange?.(next);
|
|
46
|
+
};
|
|
47
|
+
const prompt = useMemo(() => {
|
|
48
|
+
if (pattern === "method") {
|
|
49
|
+
return "Customize the Starter login page by replacing only one configured authenticator. Keep the default dynamic authenticator discovery and default UI for every other method. Reuse that Registry's headless sign-in hook so token callbacks, the X-Authenticator header, logout, and redirect behavior remain unchanged.";
|
|
50
|
+
}
|
|
51
|
+
if (pattern === "page") {
|
|
52
|
+
return "Create a fully custom login page for this Starter. Preserve the built-in authentication runtime, callback token capture, current authenticator storage, X-Authenticator request header, role reset, and SSO logout redirect. Keep installed authentication hooks available to the custom page.";
|
|
53
|
+
}
|
|
54
|
+
return "Keep the Starter's default dynamic login page unchanged.";
|
|
55
|
+
}, [pattern]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Card>
|
|
59
|
+
<CardHeader>
|
|
60
|
+
<CardTitle>Prompt generator</CardTitle>
|
|
61
|
+
<CardDescription>
|
|
62
|
+
Generate an implementation prompt for an application-owned login UI.
|
|
63
|
+
</CardDescription>
|
|
64
|
+
</CardHeader>
|
|
65
|
+
<CardContent className="grid items-start gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
|
|
66
|
+
<div className="space-y-2">
|
|
67
|
+
<Label>Integration pattern</Label>
|
|
68
|
+
<Select
|
|
69
|
+
value={pattern}
|
|
70
|
+
onValueChange={(next) => setPattern(next as AuthIntegrationPattern)}
|
|
71
|
+
>
|
|
72
|
+
<SelectTrigger className="w-full">
|
|
73
|
+
<SelectValue>{patternLabels[pattern]}</SelectValue>
|
|
74
|
+
</SelectTrigger>
|
|
75
|
+
<SelectContent>
|
|
76
|
+
{availablePatterns.map((key) => (
|
|
77
|
+
<SelectItem key={key} value={key}>
|
|
78
|
+
{patternLabels[key]}
|
|
79
|
+
</SelectItem>
|
|
80
|
+
))}
|
|
81
|
+
</SelectContent>
|
|
82
|
+
</Select>
|
|
83
|
+
</div>
|
|
84
|
+
<PromptOutput
|
|
85
|
+
title="Generated implementation prompt"
|
|
86
|
+
description="Updates when the customization boundary changes."
|
|
87
|
+
prompt={prompt}
|
|
88
|
+
promptClassName="min-h-36"
|
|
89
|
+
/>
|
|
90
|
+
</CardContent>
|
|
91
|
+
</Card>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
|
|
3
|
+
import { LoadingState } from "@/components/app-shell/loading-state";
|
|
4
|
+
|
|
5
|
+
export function AuthDemoRoute({ children }: { children: React.ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<Suspense fallback={<LoadingState className="min-h-80" />}>
|
|
8
|
+
{children}
|
|
9
|
+
</Suspense>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Badge } from "@/components/ui/badge";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardContent,
|
|
5
|
+
CardDescription,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "@/components/ui/card";
|
|
9
|
+
|
|
10
|
+
export function AuthMethodDemo({
|
|
11
|
+
authType,
|
|
12
|
+
children,
|
|
13
|
+
description,
|
|
14
|
+
methodName,
|
|
15
|
+
}: {
|
|
16
|
+
authType: string;
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
description: string;
|
|
19
|
+
methodName: string;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="space-y-8 pb-12">
|
|
23
|
+
<header className="space-y-2">
|
|
24
|
+
<div className="flex items-center gap-2">
|
|
25
|
+
<Badge variant="secondary">Authentication</Badge>
|
|
26
|
+
<Badge variant="outline">{authType}</Badge>
|
|
27
|
+
</div>
|
|
28
|
+
<h1 className="font-heading text-3xl font-semibold tracking-tight">
|
|
29
|
+
{methodName}
|
|
30
|
+
</h1>
|
|
31
|
+
<p className="max-w-3xl text-muted-foreground">{description}</p>
|
|
32
|
+
</header>
|
|
33
|
+
|
|
34
|
+
<Card className="max-w-2xl">
|
|
35
|
+
<CardHeader>
|
|
36
|
+
<CardTitle>Default component</CardTitle>
|
|
37
|
+
<CardDescription>
|
|
38
|
+
Installed Registries add this UI to the dynamic login page
|
|
39
|
+
automatically.
|
|
40
|
+
</CardDescription>
|
|
41
|
+
</CardHeader>
|
|
42
|
+
<CardContent>
|
|
43
|
+
<div className="max-w-sm">{children}</div>
|
|
44
|
+
</CardContent>
|
|
45
|
+
</Card>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|