@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.
Files changed (139) hide show
  1. package/README.MD +77 -0
  2. package/package.json +23 -17
  3. package/registry/nocobase-acl/demo/prompt-generator.tsx +6 -39
  4. package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +6 -42
  5. package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +1 -1
  6. package/registry/nocobase-ai/demo/prompt-card.tsx +7 -28
  7. package/registry/nocobase-ai/demo/prompt-generator.tsx +9 -30
  8. package/registry/nocobase-ai/demo/shortcut.tsx +1 -1
  9. package/registry/nocobase-ai/demo/tool-cards.tsx +10 -30
  10. package/registry/nocobase-auth-cas/README.md +6 -0
  11. package/registry/nocobase-auth-cas/cas-sign-in-button.tsx +41 -0
  12. package/registry/nocobase-auth-cas/demo.tsx +20 -0
  13. package/registry/nocobase-auth-cas/extension.tsx +46 -0
  14. package/registry/nocobase-auth-cas/index.ts +1 -0
  15. package/registry/nocobase-auth-cas/use-cas-sign-in.ts +29 -0
  16. package/registry/nocobase-auth-dingtalk/README.md +7 -0
  17. package/registry/nocobase-auth-dingtalk/auto-login-provider.tsx +154 -0
  18. package/registry/nocobase-auth-dingtalk/demo.tsx +23 -0
  19. package/registry/nocobase-auth-dingtalk/dingtalk-sign-in-button.tsx +45 -0
  20. package/registry/nocobase-auth-dingtalk/extension.tsx +55 -0
  21. package/registry/nocobase-auth-dingtalk/index.ts +4 -0
  22. package/registry/nocobase-auth-dingtalk/use-dingtalk-sign-in.ts +64 -0
  23. package/registry/nocobase-auth-ldap/README.md +6 -0
  24. package/registry/nocobase-auth-ldap/demo.tsx +21 -0
  25. package/registry/nocobase-auth-ldap/extension.tsx +46 -0
  26. package/registry/nocobase-auth-ldap/index.ts +1 -0
  27. package/registry/nocobase-auth-ldap/ldap-sign-in-form.tsx +69 -0
  28. package/registry/nocobase-auth-ldap/use-ldap-sign-in.ts +5 -0
  29. package/registry/nocobase-auth-oidc/README.md +11 -0
  30. package/registry/nocobase-auth-oidc/auto-redirect-provider.tsx +13 -0
  31. package/registry/nocobase-auth-oidc/demo.tsx +20 -0
  32. package/registry/nocobase-auth-oidc/extension.tsx +51 -0
  33. package/registry/nocobase-auth-oidc/index.ts +1 -0
  34. package/registry/nocobase-auth-oidc/oidc-sign-in-button.tsx +44 -0
  35. package/registry/nocobase-auth-oidc/use-oidc-sign-in.ts +58 -0
  36. package/registry/nocobase-auth-saml/README.md +6 -0
  37. package/registry/nocobase-auth-saml/auto-redirect-provider.tsx +13 -0
  38. package/registry/nocobase-auth-saml/demo.tsx +20 -0
  39. package/registry/nocobase-auth-saml/extension.tsx +51 -0
  40. package/registry/nocobase-auth-saml/index.ts +1 -0
  41. package/registry/nocobase-auth-saml/saml-sign-in-button.tsx +43 -0
  42. package/registry/nocobase-auth-saml/use-saml-sign-in.ts +51 -0
  43. package/registry/nocobase-auth-sms/README.md +7 -0
  44. package/registry/nocobase-auth-sms/demo.tsx +25 -0
  45. package/registry/nocobase-auth-sms/extension.tsx +46 -0
  46. package/registry/nocobase-auth-sms/index.ts +1 -0
  47. package/registry/nocobase-auth-sms/sms-sign-in-form.tsx +107 -0
  48. package/registry/nocobase-auth-sms/use-sms-sign-in.ts +88 -0
  49. package/registry/nocobase-auth-wecom/README.md +6 -0
  50. package/registry/nocobase-auth-wecom/auto-login-provider.tsx +21 -0
  51. package/registry/nocobase-auth-wecom/demo.tsx +23 -0
  52. package/registry/nocobase-auth-wecom/extension.tsx +49 -0
  53. package/registry/nocobase-auth-wecom/index.ts +1 -0
  54. package/registry/nocobase-auth-wecom/use-wecom-sign-in.ts +62 -0
  55. package/registry/nocobase-auth-wecom/wecom-sign-in-button.tsx +70 -0
  56. package/registry/nocobase-i18n/demo/prompt-generator.tsx +30 -54
  57. package/registry/nocobase-route-surfaces/demo/prompt-generator.tsx +18 -21
  58. package/registry.config.json +112 -0
  59. package/scripts/build-html.mjs +106 -0
  60. package/src/App.tsx +34 -8
  61. package/src/app/extension.ts +4 -0
  62. package/src/app/extensions.tsx +24 -1
  63. package/src/components/auth/authenticator-adapters.ts +20 -0
  64. package/src/components/auth/auto-redirect-provider.tsx +84 -0
  65. package/src/components/auth/basic-sign-in-form.tsx +101 -0
  66. package/src/components/auth/default-sign-in-page.tsx +20 -0
  67. package/src/components/auth/demo/auth-demo-page.tsx +236 -0
  68. package/src/components/auth/demo/auth-demo-prompt-generator.tsx +93 -0
  69. package/src/components/auth/demo/auth-demo-route.tsx +11 -0
  70. package/src/components/auth/demo/auth-method-demo.tsx +48 -0
  71. package/src/components/auth/demo/index.ts +4 -0
  72. package/src/components/auth/dynamic-sign-in.tsx +191 -0
  73. package/src/components/auth/forgot-password-form.tsx +13 -5
  74. package/src/components/auth/index.ts +13 -0
  75. package/src/components/auth/sign-in-form.tsx +1 -111
  76. package/src/components/auth/sign-up-form.tsx +14 -37
  77. package/src/components/auth/types.ts +38 -0
  78. package/src/components/auth/use-authenticator-sign-in.ts +14 -0
  79. package/src/components/auth/use-public-authenticators.ts +26 -0
  80. package/src/components/data-table/data-table-filter.tsx +107 -153
  81. package/src/components/demo/prompt-output.tsx +71 -0
  82. package/src/components/ui/alert-dialog.tsx +2 -0
  83. package/src/components/ui/attachment.tsx +207 -0
  84. package/src/components/ui/bubble.tsx +128 -0
  85. package/src/components/ui/button-group.tsx +87 -0
  86. package/src/components/ui/button.tsx +6 -6
  87. package/src/components/ui/calendar.tsx +0 -2
  88. package/src/components/ui/card.tsx +1 -1
  89. package/src/components/ui/carousel.tsx +2 -0
  90. package/src/components/ui/combobox.tsx +297 -0
  91. package/src/components/ui/command.tsx +43 -41
  92. package/src/components/ui/dialog.tsx +0 -2
  93. package/src/components/ui/direction.tsx +4 -0
  94. package/src/components/ui/drawer.tsx +2 -0
  95. package/src/components/ui/dropdown-menu.tsx +0 -2
  96. package/src/components/ui/empty.tsx +104 -0
  97. package/src/components/ui/field.tsx +238 -0
  98. package/src/components/ui/hover-card.tsx +2 -0
  99. package/src/components/ui/input-group.tsx +0 -2
  100. package/src/components/ui/input-otp.tsx +0 -2
  101. package/src/components/ui/input.tsx +1 -1
  102. package/src/components/ui/item.tsx +201 -0
  103. package/src/components/ui/kbd.tsx +26 -0
  104. package/src/components/ui/label.tsx +2 -0
  105. package/src/components/ui/marker.tsx +71 -0
  106. package/src/components/ui/message-scroller.tsx +131 -0
  107. package/src/components/ui/message.tsx +92 -0
  108. package/src/components/ui/native-select.tsx +61 -0
  109. package/src/components/ui/popover.tsx +0 -2
  110. package/src/components/ui/progress.tsx +2 -0
  111. package/src/components/ui/radio-group.tsx +0 -2
  112. package/src/components/ui/resizable.tsx +2 -0
  113. package/src/components/ui/scroll-area.tsx +0 -2
  114. package/src/components/ui/select.tsx +6 -4
  115. package/src/components/ui/separator.tsx +0 -2
  116. package/src/components/ui/sheet.tsx +21 -22
  117. package/src/components/ui/sidebar.tsx +1 -3
  118. package/src/components/ui/sonner.tsx +2 -0
  119. package/src/components/ui/spinner.tsx +10 -0
  120. package/src/components/ui/switch.tsx +0 -2
  121. package/src/components/ui/table.tsx +4 -2
  122. package/src/components/ui/tabs.tsx +0 -2
  123. package/src/components/ui/textarea.tsx +1 -1
  124. package/src/components/ui/toggle-group.tsx +2 -0
  125. package/src/components/ui/toggle.tsx +2 -0
  126. package/src/components/ui/tooltip.tsx +0 -2
  127. package/src/index.tsx +3 -0
  128. package/src/lib/nocobase/client.ts +32 -2
  129. package/src/lib/utils.ts +2 -1
  130. package/src/locales/en-US.ts +4 -2
  131. package/src/locales/zh-CN.ts +4 -2
  132. package/src/pages/login/index.tsx +2 -2
  133. package/src/pages/users/list.tsx +144 -16
  134. package/src/providers/auth-callback.ts +21 -0
  135. package/src/providers/auth.ts +108 -22
  136. package/src/providers/constants.ts +4 -1
  137. package/src/providers/runtime-config.ts +19 -0
  138. package/src/vite-env.d.ts +5 -0
  139. package/vite.config.ts +55 -2
@@ -1,5 +1,6 @@
1
- import type { PropsWithChildren, ReactNode } from "react";
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
+ }
@@ -0,0 +1,4 @@
1
+ export { AuthDemoPage } from "./auth-demo-page";
2
+ export { AuthDemoPromptGenerator } from "./auth-demo-prompt-generator";
3
+ export { AuthDemoRoute } from "./auth-demo-route";
4
+ export { AuthMethodDemo } from "./auth-method-demo";