@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
@@ -0,0 +1,62 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import { useSearchParams } from "react-router";
3
+
4
+ import type { Authenticator } from "@/components/auth/types";
5
+ import { nocobaseClient } from "@/lib/nocobase/client";
6
+
7
+ type WecomAuthUrlResponse = {
8
+ url?: string;
9
+ };
10
+
11
+ export function isWecomBrowser() {
12
+ return (
13
+ typeof navigator !== "undefined" &&
14
+ navigator.userAgent.toLowerCase().includes("wxwork")
15
+ );
16
+ }
17
+
18
+ export function useWecomSignIn(authenticator: Authenticator) {
19
+ const [searchParams] = useSearchParams();
20
+ const [isPending, setIsPending] = useState(false);
21
+ const [error, setError] = useState<Error>();
22
+ const callbackError = useMemo(() => {
23
+ if (searchParams.get("authenticator") !== authenticator.name) {
24
+ return undefined;
25
+ }
26
+ const message = searchParams.get("errorMsg");
27
+ return message ? new Error(message) : undefined;
28
+ }, [authenticator.name, searchParams]);
29
+
30
+ const signIn = useCallback(async () => {
31
+ setError(undefined);
32
+ setIsPending(true);
33
+ try {
34
+ const redirect =
35
+ searchParams.get("to") || searchParams.get("redirect") || "/";
36
+ const result = await nocobaseClient.action<WecomAuthUrlResponse>(
37
+ "wecom",
38
+ "getAuthUrl",
39
+ {
40
+ method: "POST",
41
+ authenticator: authenticator.name,
42
+ includeRole: false,
43
+ withAclMeta: false,
44
+ body: { redirect, isWeComBrowser: isWecomBrowser() },
45
+ }
46
+ );
47
+ if (!result?.url) {
48
+ throw new Error("NocoBase did not return a WeCom authorization URL.");
49
+ }
50
+ window.location.replace(nocobaseClient.resolveUrl(result.url));
51
+ } catch (cause) {
52
+ setError(
53
+ cause instanceof Error
54
+ ? cause
55
+ : new Error("Unable to start WeCom sign-in.")
56
+ );
57
+ setIsPending(false);
58
+ }
59
+ }, [authenticator.name, searchParams]);
60
+
61
+ return { signIn, isPending, error: error ?? callbackError };
62
+ }
@@ -0,0 +1,70 @@
1
+ import { Building2 } from "lucide-react";
2
+
3
+ import type { AuthenticatorComponentProps } from "@/components/auth/types";
4
+ import {
5
+ Alert,
6
+ AlertDescription,
7
+ AlertTitle,
8
+ } from "@/components/ui/alert";
9
+ import { Button } from "@/components/ui/button";
10
+ import {
11
+ Tooltip,
12
+ TooltipContent,
13
+ TooltipTrigger,
14
+ } from "@/components/ui/tooltip";
15
+ import { resolveTranslatableText } from "@/lib/i18n";
16
+
17
+ import { useWecomSignIn } from "./use-wecom-sign-in";
18
+
19
+ function plainText(value: unknown) {
20
+ return typeof value === "string"
21
+ ? value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
22
+ : "";
23
+ }
24
+
25
+ export default function WecomSignInButton({
26
+ authenticator,
27
+ onSignIn,
28
+ }: AuthenticatorComponentProps & { onSignIn?: () => void }) {
29
+ const { signIn, isPending, error } = useWecomSignIn(authenticator);
30
+ const tooltip = plainText(authenticator.options?.btnTooltip);
31
+ const button = (
32
+ <Button
33
+ type="button"
34
+ variant="outline"
35
+ className="w-full"
36
+ disabled={!onSignIn && isPending}
37
+ onClick={onSignIn ?? signIn}
38
+ >
39
+ <Building2 />
40
+ {!onSignIn && isPending
41
+ ? "Redirecting…"
42
+ : resolveTranslatableText(
43
+ authenticator.title ||
44
+ authenticator.authTypeTitle ||
45
+ "Sign in via WeCom"
46
+ )}
47
+ </Button>
48
+ );
49
+
50
+ return (
51
+ <div className="space-y-3">
52
+ {error && (
53
+ <Alert variant="destructive">
54
+ <AlertTitle>WeCom sign-in failed</AlertTitle>
55
+ <AlertDescription>{error.message}</AlertDescription>
56
+ </Alert>
57
+ )}
58
+ {tooltip ? (
59
+ <Tooltip>
60
+ <TooltipTrigger render={button} />
61
+ <TooltipContent side="right" className="max-w-xs">
62
+ {tooltip}
63
+ </TooltipContent>
64
+ </Tooltip>
65
+ ) : (
66
+ button
67
+ )}
68
+ </div>
69
+ );
70
+ }
@@ -1,8 +1,8 @@
1
1
  import { useTranslate } from "@refinedev/core";
2
- import { Check, ChevronDown, Copy, Sparkles } from "lucide-react";
2
+ import { ChevronDown, Sparkles } from "lucide-react";
3
3
  import { useMemo, useState } from "react";
4
4
 
5
- import { Button } from "@/components/ui/button";
5
+ import { PromptOutput } from "@/components/demo/prompt-output";
6
6
  import {
7
7
  Card,
8
8
  CardContent,
@@ -18,8 +18,6 @@ export function I18nPromptGenerator() {
18
18
  const t = useTranslate();
19
19
  const [scene, setScene] = useState("A customer support workspace");
20
20
  const [languages, setLanguages] = useState("English and Simplified Chinese");
21
- const [copied, setCopied] = useState(false);
22
- const [copyError, setCopyError] = useState(false);
23
21
 
24
22
  const prompt = useMemo(
25
23
  () => `Build a complete multilingual scene in the NocoBase Admin Starter.
@@ -100,56 +98,34 @@ Implementation contract
100
98
  </div>
101
99
  </CardContent>
102
100
  </Card>
103
- <Card className="min-w-0 gap-0 overflow-hidden py-0">
104
- <div className="flex items-center justify-between gap-3 border-b px-4 py-3">
105
- <div>
106
- <div className="text-sm font-medium">
107
- {t(
108
- "demo.prompt.output",
109
- { ns: "nocobase-i18n" },
110
- "Generated prompt"
111
- )}
112
- </div>
113
- <div className="text-xs text-muted-foreground">
114
- {t(
115
- "demo.prompt.outputDescription",
116
- { ns: "nocobase-i18n" },
117
- "Updates as you change the scene and language requirements."
118
- )}
119
- </div>
120
- </div>
121
- <Button
122
- size="sm"
123
- onClick={async () => {
124
- try {
125
- await navigator.clipboard.writeText(prompt);
126
- setCopyError(false);
127
- setCopied(true);
128
- window.setTimeout(() => setCopied(false), 1500);
129
- } catch {
130
- setCopyError(true);
131
- }
132
- }}
133
- >
134
- {copied ? <Check /> : <Copy />}
135
- {copied
136
- ? t("demo.prompt.copied", { ns: "nocobase-i18n" }, "Copied")
137
- : t("demo.prompt.copy", { ns: "nocobase-i18n" }, "Copy prompt")}
138
- </Button>
139
- </div>
140
- {copyError ? (
141
- <p className="border-b px-4 py-2 text-xs text-destructive">
142
- {t(
143
- "demo.prompt.copyError",
144
- { ns: "nocobase-i18n" },
145
- "Clipboard access failed. Select the prompt and copy it manually."
146
- )}
147
- </p>
148
- ) : null}
149
- <pre className="max-h-[520px] overflow-auto whitespace-pre-wrap bg-muted/25 p-5 font-mono text-xs leading-5 text-muted-foreground">
150
- {prompt}
151
- </pre>
152
- </Card>
101
+ <PromptOutput
102
+ title={t(
103
+ "demo.prompt.output",
104
+ { ns: "nocobase-i18n" },
105
+ "Generated prompt"
106
+ )}
107
+ description={t(
108
+ "demo.prompt.outputDescription",
109
+ { ns: "nocobase-i18n" },
110
+ "Updates as you change the scene and language requirements."
111
+ )}
112
+ prompt={prompt}
113
+ copyLabel={t(
114
+ "demo.prompt.copy",
115
+ { ns: "nocobase-i18n" },
116
+ "Copy prompt"
117
+ )}
118
+ copiedLabel={t(
119
+ "demo.prompt.copied",
120
+ { ns: "nocobase-i18n" },
121
+ "Copied"
122
+ )}
123
+ copyErrorLabel={t(
124
+ "demo.prompt.copyError",
125
+ { ns: "nocobase-i18n" },
126
+ "Clipboard access failed. Select the prompt and copy it manually."
127
+ )}
128
+ />
153
129
  </div>
154
130
  </details>
155
131
  );
@@ -1,8 +1,13 @@
1
- import { Check, Copy } from "lucide-react";
2
1
  import { useMemo, useState } from "react";
3
2
 
4
- import { Button } from "@/components/ui/button";
5
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
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";
6
11
  import { Input } from "@/components/ui/input";
7
12
  import { Label } from "@/components/ui/label";
8
13
  import {
@@ -12,7 +17,6 @@ import {
12
17
  SelectTrigger,
13
18
  SelectValue,
14
19
  } from "@/components/ui/select";
15
- import { Textarea } from "@/components/ui/textarea";
16
20
  import {
17
21
  getRouteSurfacePrompt,
18
22
  routeSurfaceScenarios,
@@ -23,7 +27,6 @@ export function RouteSurfacePromptGenerator() {
23
27
  const [scenarioId, setScenarioId] =
24
28
  useState<RouteSurfaceScenarioId>("drawer");
25
29
  const [target, setTarget] = useState("a customer detail workflow");
26
- const [copied, setCopied] = useState(false);
27
30
  const scenario =
28
31
  routeSurfaceScenarios.find((item) => item.id === scenarioId) ??
29
32
  routeSurfaceScenarios[0];
@@ -32,22 +35,17 @@ export function RouteSurfacePromptGenerator() {
32
35
  [scenario, target]
33
36
  );
34
37
 
35
- const copyPrompt = async () => {
36
- await navigator.clipboard.writeText(prompt);
37
- setCopied(true);
38
- window.setTimeout(() => setCopied(false), 1500);
39
- };
40
-
41
38
  return (
42
39
  <Card>
43
40
  <CardHeader>
44
41
  <CardTitle>Prompt generator</CardTitle>
45
42
  <CardDescription>
46
- Generate a complete routing scenario, not just an isolated overlay component.
43
+ Generate a complete routing scenario, not just an isolated overlay
44
+ component.
47
45
  </CardDescription>
48
46
  </CardHeader>
49
- <CardContent className="space-y-4">
50
- <div className="grid gap-4 md:grid-cols-2">
47
+ <CardContent className="grid items-start gap-5 xl:grid-cols-[360px_minmax(0,1fr)]">
48
+ <div className="space-y-4">
51
49
  <div className="space-y-2">
52
50
  <Label htmlFor="route-surface-scenario">Scenario</Label>
53
51
  <Select
@@ -77,13 +75,12 @@ export function RouteSurfacePromptGenerator() {
77
75
  />
78
76
  </div>
79
77
  </div>
80
- <Textarea value={prompt} readOnly className="min-h-80 font-mono text-xs" />
81
- <div className="flex justify-end">
82
- <Button type="button" onClick={() => void copyPrompt()}>
83
- {copied ? <Check /> : <Copy />}
84
- {copied ? "Copied" : "Copy prompt"}
85
- </Button>
86
- </div>
78
+ <PromptOutput
79
+ title="Generated routing prompt"
80
+ description="Updates as you change the scenario and business target."
81
+ prompt={prompt}
82
+ promptClassName="min-h-80"
83
+ />
87
84
  </CardContent>
88
85
  </Card>
89
86
  );
@@ -58,6 +58,118 @@
58
58
  "include": ["."]
59
59
  }
60
60
  },
61
+ {
62
+ "name": "auth-oidc",
63
+ "type": "registry:block",
64
+ "title": "NocoBase OIDC Authentication",
65
+ "description": "Dynamic OIDC sign-in adapter, headless sign-in hook, and integrated component demo.",
66
+ "dependencies": [
67
+ "lucide-react@^0.487.0",
68
+ "react-router@^7.0.2"
69
+ ],
70
+ "docs": "Requires the Starter authentication runtime and an enabled OIDC authenticator in NocoBase. The default UI is discovered automatically and can be replaced per authentication type or authenticator instance from the application login page.",
71
+ "source": {
72
+ "root": "registry/nocobase-auth-oidc",
73
+ "target": "src/extensions/nocobase-auth-oidc",
74
+ "install": false,
75
+ "include": ["."]
76
+ }
77
+ },
78
+ {
79
+ "name": "auth-saml",
80
+ "type": "registry:block",
81
+ "title": "NocoBase SAML Authentication",
82
+ "description": "Dynamic SAML sign-in adapter, automatic redirect provider, headless hook, and integrated demo.",
83
+ "dependencies": [
84
+ "lucide-react@^0.487.0",
85
+ "react-router@^7.0.2"
86
+ ],
87
+ "docs": "Requires the Starter authentication runtime and an enabled SAML authenticator in NocoBase.",
88
+ "source": {
89
+ "root": "registry/nocobase-auth-saml",
90
+ "target": "src/extensions/nocobase-auth-saml",
91
+ "install": false,
92
+ "include": ["."]
93
+ }
94
+ },
95
+ {
96
+ "name": "auth-cas",
97
+ "type": "registry:block",
98
+ "title": "NocoBase CAS Authentication",
99
+ "description": "Dynamic CAS sign-in adapter, headless sign-in hook, and integrated component demo.",
100
+ "dependencies": [
101
+ "lucide-react@^0.487.0",
102
+ "react-router@^7.0.2"
103
+ ],
104
+ "docs": "Requires the Starter authentication runtime and an enabled CAS authenticator in NocoBase.",
105
+ "source": {
106
+ "root": "registry/nocobase-auth-cas",
107
+ "target": "src/extensions/nocobase-auth-cas",
108
+ "install": false,
109
+ "include": ["."]
110
+ }
111
+ },
112
+ {
113
+ "name": "auth-ldap",
114
+ "type": "registry:block",
115
+ "title": "NocoBase LDAP Authentication",
116
+ "description": "Dynamic LDAP account/password form, headless sign-in hook, and integrated component demo.",
117
+ "docs": "Requires the Starter authentication runtime and an enabled LDAP authenticator in NocoBase.",
118
+ "source": {
119
+ "root": "registry/nocobase-auth-ldap",
120
+ "target": "src/extensions/nocobase-auth-ldap",
121
+ "install": false,
122
+ "include": ["."]
123
+ }
124
+ },
125
+ {
126
+ "name": "auth-sms",
127
+ "type": "registry:block",
128
+ "title": "NocoBase SMS Authentication",
129
+ "description": "Dynamic SMS verification-code form, delivery countdown, headless hook, and integrated demo.",
130
+ "docs": "Requires the Starter authentication runtime, an enabled SMS authenticator, and a configured public SMS verifier in NocoBase.",
131
+ "source": {
132
+ "root": "registry/nocobase-auth-sms",
133
+ "target": "src/extensions/nocobase-auth-sms",
134
+ "install": false,
135
+ "include": ["."]
136
+ }
137
+ },
138
+ {
139
+ "name": "auth-wecom",
140
+ "type": "registry:block",
141
+ "title": "NocoBase WeCom Authentication",
142
+ "description": "Dynamic WeCom sign-in adapter, browser automatic login, headless hook, and integrated demo.",
143
+ "dependencies": [
144
+ "lucide-react@^0.487.0",
145
+ "react-router@^7.0.2"
146
+ ],
147
+ "docs": "Requires the Starter authentication runtime and an enabled WeCom authenticator in NocoBase.",
148
+ "source": {
149
+ "root": "registry/nocobase-auth-wecom",
150
+ "target": "src/extensions/nocobase-auth-wecom",
151
+ "install": false,
152
+ "include": ["."]
153
+ }
154
+ },
155
+ {
156
+ "name": "auth-dingtalk",
157
+ "type": "registry:block",
158
+ "title": "NocoBase DingTalk Authentication",
159
+ "description": "Dynamic DingTalk sign-in adapter, automatic login, headless hook, and integrated demo.",
160
+ "dependencies": [
161
+ "dingtalk-jsapi@3.1.1",
162
+ "lucide-react@^0.487.0",
163
+ "react-router@^7.0.2"
164
+ ],
165
+ "docs": "Requires the Starter authentication runtime and an enabled DingTalk authenticator in NocoBase. DingTalk JSAPI is loaded lazily for internal micro-app login.",
166
+ "source": {
167
+ "root": "registry/nocobase-auth-dingtalk",
168
+ "target": "src/extensions/nocobase-auth-dingtalk",
169
+ "install": false,
170
+ "include": ["."]
171
+ }
172
+ },
61
173
  {
62
174
  "name": "ai-runtime",
63
175
  "type": "registry:lib",
@@ -0,0 +1,106 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const rootDir = path.resolve(__dirname, "..");
7
+ const distDir = path.join(rootDir, "dist");
8
+ const rawIndexPath = path.join(distDir, "index.raw.html");
9
+ const indexPath = path.join(distDir, "index.html");
10
+
11
+ const startMarker = "<!-- nocobase-runtime-config:start -->";
12
+ const endMarker = "<!-- nocobase-runtime-config:end -->";
13
+
14
+ const normalizePortalBase = (base) => {
15
+ const normalized = String(base || "/").trim();
16
+ if (!normalized || normalized === "/") return "/";
17
+ return `/${normalized.replace(/^\/+|\/+$/g, "")}/`;
18
+ };
19
+
20
+ const getBasePrefix = (base) => base.replace(/\/$/, "");
21
+
22
+ const getRawPortalBase = (html) => {
23
+ const moduleScriptPattern =
24
+ /<script\s+[^>]*type=["']module["'][^>]*\bsrc=(["'])([^"']+)\1[^>]*>/i;
25
+ const match = html.match(moduleScriptPattern);
26
+ const src = match?.[2];
27
+
28
+ if (!src?.startsWith("/")) return "/";
29
+
30
+ const assetsIndex = src.indexOf("/assets/");
31
+ if (assetsIndex === -1) return "/";
32
+
33
+ return normalizePortalBase(src.slice(0, assetsIndex) || "/");
34
+ };
35
+
36
+ const rewriteHtmlFilePaths = (html, portalBase, rawPortalBase) => {
37
+ const portalPrefix = getBasePrefix(portalBase);
38
+ const rawPrefix = getBasePrefix(rawPortalBase);
39
+ const attributePattern = /\b(src|href|content)=(["'])\/(?!\/)([^"']*)\2/g;
40
+
41
+ if (!portalPrefix && !rawPrefix) return html;
42
+
43
+ return html.replace(attributePattern, (match, attribute, quote, path) => {
44
+ let value = `/${path}`;
45
+
46
+ if (rawPrefix && value.startsWith(`${rawPrefix}/`)) {
47
+ value = value.slice(rawPrefix.length) || "/";
48
+ }
49
+
50
+ if (!portalPrefix || value.startsWith(`${portalPrefix}/`)) {
51
+ return `${attribute}=${quote}${value}${quote}`;
52
+ }
53
+
54
+ return `${attribute}=${quote}${portalPrefix}${value}${quote}`;
55
+ });
56
+ };
57
+
58
+ const stripExistingRuntimeConfig = (html) => {
59
+ const pattern = new RegExp(
60
+ `${startMarker}[\\s\\S]*?${endMarker}\\s*`,
61
+ "g"
62
+ );
63
+ return html.replace(pattern, "");
64
+ };
65
+
66
+ const portalBase = normalizePortalBase(process.env.NOCOBASE_PORTAL_BASE);
67
+ const apiUrl = String(process.env.NOCOBASE_API_URL || "/api").trim() || "/api";
68
+
69
+ if (!fs.existsSync(rawIndexPath)) {
70
+ throw new Error(
71
+ `Missing ${path.relative(rootDir, rawIndexPath)}. Run pnpm build first.`
72
+ );
73
+ }
74
+
75
+ const runtimeConfig = `${startMarker}
76
+ <script>
77
+ window.NOCOBASE_PORTAL_BASE = ${JSON.stringify(portalBase)};
78
+ window.NOCOBASE_API_URL = ${JSON.stringify(apiUrl)};
79
+ </script>
80
+ ${endMarker}
81
+ `;
82
+
83
+ const rawHtml = fs.readFileSync(rawIndexPath, "utf8");
84
+ const html = rewriteHtmlFilePaths(
85
+ stripExistingRuntimeConfig(rawHtml),
86
+ portalBase,
87
+ getRawPortalBase(rawHtml)
88
+ );
89
+
90
+ const moduleScriptPattern = /<script\s+[^>]*type=["']module["'][^>]*>/i;
91
+ const moduleScriptMatch = html.match(moduleScriptPattern);
92
+
93
+ if (moduleScriptMatch?.index === undefined) {
94
+ throw new Error("Could not find the module script in dist/index.raw.html.");
95
+ }
96
+
97
+ const outputHtml = `${html.slice(0, moduleScriptMatch.index)}${runtimeConfig}${html.slice(moduleScriptMatch.index)}`;
98
+
99
+ fs.writeFileSync(indexPath, outputHtml);
100
+
101
+ console.log(
102
+ `Generated ${path.relative(rootDir, indexPath)} from ${path.relative(
103
+ rootDir,
104
+ rawIndexPath
105
+ )}`
106
+ );
package/src/App.tsx CHANGED
@@ -27,6 +27,7 @@ import { TooltipProvider } from "./components/ui/tooltip";
27
27
  import { BrandLogo } from "./components/app-shell/brand";
28
28
  import {
29
29
  AppExtensionProviders,
30
+ AppAuthRuntimeProviders,
30
31
  extensionResources,
31
32
  extensionRouteElements,
32
33
  } from "./app/extensions";
@@ -36,9 +37,11 @@ import { accessControlProvider } from "./providers/access-control";
36
37
  import { AclBootstrap } from "./components/access-control/acl-bootstrap";
37
38
  import { ResourceAccessGuard } from "./components/access-control/resource-access-guard";
38
39
  import { NavigateToAccessibleResource } from "./components/access-control/navigate-to-accessible-resource";
39
- import { UsersRound } from "lucide-react";
40
+ import { KeyRound, PanelsTopLeft, UsersRound } from "lucide-react";
40
41
  import { i18nProvider } from "./providers/i18n";
41
42
  import { SystemSettingsProvider } from "./providers/system-settings";
43
+ import { AuthDemoPage } from "./components/auth/demo";
44
+ import { getPortalBase } from "./providers/runtime-config";
42
45
 
43
46
  const coreResources: ResourceProps[] = [
44
47
  {
@@ -64,16 +67,36 @@ const coreResources: ResourceProps[] = [
64
67
  },
65
68
  },
66
69
  },
70
+ {
71
+ name: "auth-components",
72
+ meta: {
73
+ label: "Authentication",
74
+ icon: <KeyRound />,
75
+ description: "NocoBase authentication UI and integration patterns.",
76
+ acl: { type: "authenticated" },
77
+ },
78
+ },
79
+ {
80
+ name: "auth-patterns",
81
+ list: "/auth",
82
+ meta: {
83
+ parent: "auth-components",
84
+ label: "Login composition",
85
+ icon: <PanelsTopLeft />,
86
+ acl: { type: "authenticated" },
87
+ },
88
+ },
67
89
  ];
68
90
 
69
- const basename = import.meta.env.BASE_URL.replace(/\/+$/, "");
91
+ const basename = getPortalBase().replace(/\/+$/, "");
70
92
 
71
93
  function App() {
72
94
  return (
73
95
  <BrowserRouter basename={basename || undefined}>
74
- <ThemeProvider>
75
- <TooltipProvider>
76
- <SystemSettingsProvider>
96
+ <AppAuthRuntimeProviders>
97
+ <ThemeProvider>
98
+ <TooltipProvider>
99
+ <SystemSettingsProvider>
77
100
  <Refine
78
101
  dataProvider={dataProvider}
79
102
  notificationProvider={useNotificationProvider()}
@@ -110,6 +133,7 @@ function App() {
110
133
  }
111
134
  >
112
135
  <Route index element={<NavigateToAccessibleResource />} />
136
+ <Route path="/auth" element={<AuthDemoPage />} />
113
137
  <Route
114
138
  path="/users"
115
139
  element={<UserResourceLayout />}
@@ -180,6 +204,7 @@ function App() {
180
204
  }
181
205
  >
182
206
  <Route path="/login" element={<Login />} />
207
+ <Route path="/signin" element={<Login />} />
183
208
  <Route path="/register" element={<Register />} />
184
209
  <Route path="/forgot-password" element={<ForgotPassword />} />
185
210
  </Route>
@@ -189,9 +214,10 @@ function App() {
189
214
  <UnsavedChangesNotifier />
190
215
  <DocumentTitleHandler appName="NocoBase" />
191
216
  </Refine>
192
- </SystemSettingsProvider>
193
- </TooltipProvider>
194
- </ThemeProvider>
217
+ </SystemSettingsProvider>
218
+ </TooltipProvider>
219
+ </ThemeProvider>
220
+ </AppAuthRuntimeProviders>
195
221
  </BrowserRouter>
196
222
  );
197
223
  }
@@ -1,10 +1,14 @@
1
1
  import type { ResourceProps } from "@refinedev/core";
2
2
  import type { ComponentType, PropsWithChildren, ReactElement } from "react";
3
+ import type { AuthenticatorAdapter } from "@/components/auth/types";
3
4
 
4
5
  export type AppExtension = {
5
6
  id: string;
6
7
  resources?: ResourceProps[];
7
8
  routes?: ReactElement;
8
9
  Provider?: ComponentType<PropsWithChildren>;
10
+ AuthRuntimeProvider?: ComponentType<PropsWithChildren>;
11
+ authRuntimePriority?: number;
9
12
  UserMenuItems?: ComponentType;
13
+ authAdapters?: AuthenticatorAdapter[];
10
14
  };