@nocobase/portal-template-default 1.0.15 → 1.0.17
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/AGENTS.md +8 -0
- package/README.MD +4 -0
- package/dist/assets/{LineDraw-iG-ADes-.js → LineDraw-BBB5hwuJ.js} +1 -1
- package/dist/assets/{SymbolDraw-DXoSw1tM.js → SymbolDraw-CKS3uUb_.js} +1 -1
- package/dist/assets/{VisualMapping-C_ee4Eht.js → VisualMapping-BCwaW3TD.js} +1 -1
- package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
- package/dist/assets/{auto-login-provider-5Uxza9Ms.js → auto-login-provider-DO5Il6Dp.js} +2 -2
- package/dist/assets/auto-login-provider-TQ08B6-i.js +1 -0
- package/dist/assets/auto-redirect-provider-ButXp0A3.js +1 -0
- package/dist/assets/auto-redirect-provider-DnGGP2Vn.js +1 -0
- package/dist/assets/auto-redirect-provider-ja-vFPaF.js +1 -0
- package/dist/assets/calendar-C7xnRgOT.js +1 -0
- package/dist/assets/{cas-sign-in-button-CyqG1QME.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
- package/dist/assets/{components-D-ptYjX_.js → components-_g-GjQeV.js} +3 -3
- package/dist/assets/{configuration-gate-C08mkkiX.js → configuration-gate-B4XVz7tS.js} +1 -1
- package/dist/assets/{createClipPathFromCoordSys-wj34-b1B.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
- package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
- package/dist/assets/{customGraphicKeyframeAnimation-DWrhrHL_.js → customGraphicKeyframeAnimation-DMU5crAu.js} +1 -1
- package/dist/assets/demo-BPlae2bp.js +1 -0
- package/dist/assets/demo-Bj_Y0RrW.js +1 -0
- package/dist/assets/demo-CKPAQOib.js +1 -0
- package/dist/assets/demo-CXRTPvIx.js +1 -0
- package/dist/assets/demo-DJuVDu1n.js +1 -0
- package/dist/assets/demo-DUmWU7ik.js +1 -0
- package/dist/assets/demo-NhIzFRE1.js +1 -0
- package/dist/assets/{dingtalk-sign-in-button-DSWsSruv.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
- package/dist/assets/{echarts-preview-DxHNfq1C.js → echarts-preview-R96Pbqmz.js} +1 -1
- package/dist/assets/{echarts-runtime-D8kXn6ME.js → echarts-runtime-Ce_z2eX_.js} +3 -3
- package/dist/assets/{echarts-runtime-advanced-QY8v95qK.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
- package/dist/assets/{echarts-runtime-common-D-yqAK-r.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
- package/dist/assets/{echarts-runtime-components-P4-i8jPU.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
- package/dist/assets/{echarts-runtime-hierarchy-ByQZ-DbP.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
- package/dist/assets/{floating-BFVEyPEp.js → floating-O9WmiDXk.js} +2 -2
- package/dist/assets/index-BBKV6O5d.css +1 -0
- package/dist/assets/{index-Btk35Nx-.js → index-CPzehPGe.js} +3 -3
- package/dist/assets/index-Cc6awvdg.js +1064 -0
- package/dist/assets/index-ChA0z9ke.js +38 -0
- package/dist/assets/{index-um5hdKdZ.js → index-DgB7uTMs.js} +2 -2
- package/dist/assets/{index-CwNxd66J.js → index-DroUAK80.js} +1 -1
- package/dist/assets/index-WKvygTBp.js +59 -0
- package/dist/assets/{labelHelper-DQqRy3a_.js → labelHelper-Dk5jkiD-.js} +1 -1
- package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
- package/dist/assets/{oidc-sign-in-button-CQdRcR_p.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
- package/dist/assets/{page-context-CnQ4GYM7.js → page-context-DAiW_dId.js} +3 -3
- package/dist/assets/prompt-card-rGZqpqkF.js +1 -0
- package/dist/assets/prompt-output-B4h3uLWf.js +1 -0
- package/dist/assets/saml-sign-in-button-B4ugaLIb.js +1 -0
- package/dist/assets/{sectorHelper-DN_Vv6OL.js → sectorHelper-BuxrrJrA.js} +1 -1
- package/dist/assets/shortcut-BhKXxJLs.js +47 -0
- package/dist/assets/{sms-sign-in-form-DTX9GV2I.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
- package/dist/assets/{static.browser-jI1psK0d.js → static.browser-kzzpH0Xt.js} +8 -8
- package/dist/assets/switch-BdVxPIVU.js +1 -0
- package/dist/assets/{tool-cards-DFttLAfo.js → tool-cards-CpczIke-.js} +3 -3
- package/dist/assets/{use-authenticator-sign-in-CRGjzXju.js → use-authenticator-sign-in-CIj210ry.js} +1 -1
- package/dist/assets/use-dingtalk-sign-in-BW4kkXD3.js +1 -0
- package/dist/assets/{use-wecom-sign-in-C9j8wfCD.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
- package/dist/assets/wecom-sign-in-button-Dn_2zDab.js +1 -0
- package/dist/index.html +2 -2
- package/dist/index.raw.html +2 -2
- package/package.json +9 -1
- package/src/App.tsx +5 -2
- package/src/app/extension.ts +2 -0
- package/src/app/extensions.tsx +18 -6
- package/src/app/route-access-guard.tsx +22 -0
- package/src/app/route-runtime.ts +173 -0
- package/src/app/routes.tsx +2 -2
- package/src/components/app-shell/header.tsx +56 -1
- package/src/components/app-shell/sidebar.tsx +14 -2
- package/src/components/auth/sign-up-form.tsx +287 -59
- package/src/components/auth/types.ts +23 -1
- package/src/extensions/nocobase-acl/README.md +28 -14
- package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
- package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
- package/src/extensions/nocobase-mail/README.md +74 -0
- package/src/extensions/nocobase-mail/components/index.ts +79 -0
- package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
- package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
- package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
- package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
- package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
- package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
- package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
- package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
- package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
- package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
- package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
- package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
- package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
- package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
- package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
- package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
- package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
- package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
- package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
- package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
- package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
- package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
- package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
- package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
- package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
- package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
- package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
- package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
- package/src/extensions/nocobase-mail/components/types.ts +297 -0
- package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
- package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
- package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
- package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
- package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
- package/src/extensions/nocobase-mail/extension.tsx +105 -0
- package/src/extensions/nocobase-mail/index.ts +1 -0
- package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
- package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
- package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
- package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
- package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
- package/src/extensions/nocobase-users-example/extension.tsx +2 -100
- package/src/extensions/nocobase-users-example/index.ts +1 -0
- package/src/lib/nocobase/acl/menu.ts +17 -3
- package/src/lib/nocobase/acl/types.ts +4 -0
- package/src/locales/en-US.ts +2 -1
- package/src/locales/zh-CN.ts +2 -1
- package/src/providers/runtime-config.ts +18 -0
- package/src/routes.tsx +10 -0
- package/src/vite-env.d.ts +3 -0
- package/vite.config.ts +8 -0
- package/dist/assets/auth-method-demo-DaOCuvsq.js +0 -1
- package/dist/assets/auto-login-provider-DyaPGK2D.js +0 -1
- package/dist/assets/auto-redirect-provider-CfwRvYOI.js +0 -1
- package/dist/assets/auto-redirect-provider-Cr3jmYts.js +0 -1
- package/dist/assets/auto-redirect-provider-QkJQRBbG.js +0 -1
- package/dist/assets/calendar-DjDRfD5t.js +0 -1
- package/dist/assets/card-DuQBOS6A.js +0 -1
- package/dist/assets/createSeriesDataSimply-D0A6SAVQ.js +0 -1
- package/dist/assets/demo-BYWHeKUe.js +0 -1
- package/dist/assets/demo-CPWoNtx8.js +0 -1
- package/dist/assets/demo-CyvJk5zV.js +0 -1
- package/dist/assets/demo-D4OiKplc.js +0 -1
- package/dist/assets/demo-DBueJg0m.js +0 -1
- package/dist/assets/demo-DkIhyiRK.js +0 -1
- package/dist/assets/demo-tpTjO0sC.js +0 -1
- package/dist/assets/index-BgCbGDMc.js +0 -43
- package/dist/assets/index-MB-jfQld.js +0 -686
- package/dist/assets/index-NH3aqqQz.css +0 -1
- package/dist/assets/index-gFtCXH3o.js +0 -58
- package/dist/assets/ldap-sign-in-form--6Gt3yGq.js +0 -1
- package/dist/assets/prompt-card-B2RS4nt2.js +0 -1
- package/dist/assets/prompt-output-DZHRM2ft.js +0 -1
- package/dist/assets/saml-sign-in-button-BHL5a0JV.js +0 -1
- package/dist/assets/shortcut-C5x5LQWU.js +0 -47
- package/dist/assets/switch-CRL8PmRI.js +0 -1
- package/dist/assets/use-dingtalk-sign-in-8WYzJ2jf.js +0 -1
- package/dist/assets/wecom-sign-in-button-mIS1zGSU.js +0 -1
|
@@ -1,27 +1,201 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { AlertCircle, KeyRound } from "lucide-react";
|
|
3
4
|
import { useState } from "react";
|
|
4
5
|
import { useLink, useNotification, useRegister } from "@refinedev/core";
|
|
5
6
|
import { useSearchParams } from "react-router";
|
|
6
7
|
|
|
7
8
|
import { AuthLayout } from "@/components/auth/auth-layout";
|
|
8
9
|
import { InputPassword } from "@/components/auth/input-password";
|
|
10
|
+
import type {
|
|
11
|
+
Authenticator,
|
|
12
|
+
AuthenticatorSignUpField,
|
|
13
|
+
} from "@/components/auth/types";
|
|
14
|
+
import { usePublicAuthenticators } from "@/components/auth/use-public-authenticators";
|
|
15
|
+
import {
|
|
16
|
+
Alert,
|
|
17
|
+
AlertDescription,
|
|
18
|
+
AlertTitle,
|
|
19
|
+
} from "@/components/ui/alert";
|
|
9
20
|
import { Button } from "@/components/ui/button";
|
|
21
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
22
|
+
import {
|
|
23
|
+
Empty,
|
|
24
|
+
EmptyDescription,
|
|
25
|
+
EmptyHeader,
|
|
26
|
+
EmptyMedia,
|
|
27
|
+
EmptyTitle,
|
|
28
|
+
} from "@/components/ui/empty";
|
|
10
29
|
import { Input } from "@/components/ui/input";
|
|
11
30
|
import { Label } from "@/components/ui/label";
|
|
31
|
+
import {
|
|
32
|
+
Select,
|
|
33
|
+
SelectContent,
|
|
34
|
+
SelectItem,
|
|
35
|
+
SelectTrigger,
|
|
36
|
+
SelectValue,
|
|
37
|
+
} from "@/components/ui/select";
|
|
38
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
39
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
40
|
+
import { resolveTranslatableText } from "@/lib/i18n";
|
|
12
41
|
|
|
13
|
-
|
|
14
|
-
|
|
42
|
+
type SignUpFieldValue = string | number | boolean;
|
|
43
|
+
type SignUpValues = Record<string, SignUpFieldValue>;
|
|
44
|
+
|
|
45
|
+
type SignUpVariables = SignUpValues & {
|
|
46
|
+
authenticator: string;
|
|
47
|
+
password: string;
|
|
48
|
+
confirm_password: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function getFieldLabel(field: AuthenticatorSignUpField) {
|
|
52
|
+
return (
|
|
53
|
+
resolveTranslatableText(field.uiSchema?.title, {
|
|
54
|
+
ns: "lm-collections",
|
|
55
|
+
}) || field.field
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getFieldId(authenticatorName: string, fieldName: string) {
|
|
60
|
+
return `${authenticatorName}-${fieldName}`.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function DynamicSignUpField({
|
|
64
|
+
authenticatorName,
|
|
65
|
+
field,
|
|
66
|
+
value,
|
|
67
|
+
onChange,
|
|
68
|
+
}: {
|
|
69
|
+
authenticatorName: string;
|
|
70
|
+
field: AuthenticatorSignUpField;
|
|
71
|
+
value: SignUpFieldValue | undefined;
|
|
72
|
+
onChange: (value: SignUpFieldValue) => void;
|
|
73
|
+
}) {
|
|
74
|
+
const id = getFieldId(authenticatorName, field.field);
|
|
75
|
+
const label = getFieldLabel(field);
|
|
76
|
+
const component = field.uiSchema?.["x-component"];
|
|
77
|
+
const options = field.uiSchema?.enum;
|
|
78
|
+
|
|
79
|
+
if (options?.length) {
|
|
80
|
+
const selectedValue =
|
|
81
|
+
typeof value === "undefined" ? undefined : String(value);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="space-y-2">
|
|
85
|
+
<Label htmlFor={id}>{label}</Label>
|
|
86
|
+
<Select
|
|
87
|
+
value={selectedValue}
|
|
88
|
+
onValueChange={(nextValue) => {
|
|
89
|
+
if (nextValue === null) return;
|
|
90
|
+
const option = options.find(
|
|
91
|
+
(item) => String(item.value) === nextValue
|
|
92
|
+
);
|
|
93
|
+
onChange(option?.value ?? nextValue);
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<SelectTrigger id={id} className="w-full" aria-required={field.required}>
|
|
97
|
+
<SelectValue />
|
|
98
|
+
</SelectTrigger>
|
|
99
|
+
<SelectContent>
|
|
100
|
+
{options.map((option) => (
|
|
101
|
+
<SelectItem
|
|
102
|
+
key={`${typeof option.value}:${String(option.value)}`}
|
|
103
|
+
value={String(option.value)}
|
|
104
|
+
>
|
|
105
|
+
{resolveTranslatableText(option.label) || String(option.value)}
|
|
106
|
+
</SelectItem>
|
|
107
|
+
))}
|
|
108
|
+
</SelectContent>
|
|
109
|
+
</Select>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (component === "Checkbox") {
|
|
115
|
+
return (
|
|
116
|
+
<div className="flex items-center gap-2">
|
|
117
|
+
<Checkbox
|
|
118
|
+
id={id}
|
|
119
|
+
name={field.field}
|
|
120
|
+
checked={value === true}
|
|
121
|
+
onCheckedChange={(checked) => onChange(checked)}
|
|
122
|
+
aria-required={field.required}
|
|
123
|
+
/>
|
|
124
|
+
<Label htmlFor={id}>{label}</Label>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (component === "Input.TextArea" || component === "TextArea") {
|
|
130
|
+
return (
|
|
131
|
+
<div className="space-y-2">
|
|
132
|
+
<Label htmlFor={id}>{label}</Label>
|
|
133
|
+
<Textarea
|
|
134
|
+
id={id}
|
|
135
|
+
name={field.field}
|
|
136
|
+
value={typeof value === "string" ? value : ""}
|
|
137
|
+
onChange={(event) => onChange(event.target.value)}
|
|
138
|
+
required={field.required}
|
|
139
|
+
rows={4}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div className="space-y-2">
|
|
147
|
+
<Label htmlFor={id}>{label}</Label>
|
|
148
|
+
<Input
|
|
149
|
+
id={id}
|
|
150
|
+
name={field.field}
|
|
151
|
+
type={field.field === "email" ? "email" : "text"}
|
|
152
|
+
value={typeof value === "string" ? value : ""}
|
|
153
|
+
onChange={(event) => onChange(event.target.value)}
|
|
154
|
+
autoComplete={
|
|
155
|
+
field.field === "email"
|
|
156
|
+
? "email"
|
|
157
|
+
: field.field === "username"
|
|
158
|
+
? "username"
|
|
159
|
+
: undefined
|
|
160
|
+
}
|
|
161
|
+
required={field.required}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function BasicSignUpForm({ authenticator }: { authenticator: Authenticator }) {
|
|
168
|
+
const [values, setValues] = useState<SignUpValues>({});
|
|
15
169
|
const [password, setPassword] = useState("");
|
|
16
170
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
17
171
|
const { open } = useNotification();
|
|
18
172
|
const Link = useLink();
|
|
19
|
-
const
|
|
20
|
-
const
|
|
173
|
+
const { mutate: register, isPending } = useRegister<SignUpVariables>();
|
|
174
|
+
const fields = (authenticator.options?.signupForm ?? []).filter(
|
|
175
|
+
(field) =>
|
|
176
|
+
field?.show &&
|
|
177
|
+
field.field !== "password" &&
|
|
178
|
+
field.field !== "confirm_password"
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const updateValue = (field: string, value: SignUpFieldValue) => {
|
|
182
|
+
setValues((current) => ({ ...current, [field]: value }));
|
|
183
|
+
};
|
|
21
184
|
|
|
22
185
|
const handleSignUp = (event: React.FormEvent<HTMLFormElement>) => {
|
|
23
186
|
event.preventDefault();
|
|
24
187
|
|
|
188
|
+
const missingField = fields.find(
|
|
189
|
+
(field) => field.required && !values[field.field]
|
|
190
|
+
);
|
|
191
|
+
if (missingField) {
|
|
192
|
+
open?.({
|
|
193
|
+
type: "error",
|
|
194
|
+
message: `Please enter ${getFieldLabel(missingField)}`,
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
25
199
|
if (password !== confirmPassword) {
|
|
26
200
|
open?.({
|
|
27
201
|
type: "error",
|
|
@@ -32,71 +206,125 @@ export const SignUpForm = () => {
|
|
|
32
206
|
}
|
|
33
207
|
|
|
34
208
|
register({
|
|
35
|
-
|
|
209
|
+
...values,
|
|
36
210
|
password,
|
|
37
|
-
|
|
211
|
+
confirm_password: confirmPassword,
|
|
212
|
+
authenticator: authenticator.name,
|
|
38
213
|
});
|
|
39
214
|
};
|
|
40
215
|
|
|
216
|
+
return (
|
|
217
|
+
<form onSubmit={handleSignUp} className="space-y-5">
|
|
218
|
+
{fields.map((field) => (
|
|
219
|
+
<DynamicSignUpField
|
|
220
|
+
key={field.field}
|
|
221
|
+
authenticatorName={authenticator.name}
|
|
222
|
+
field={field}
|
|
223
|
+
value={values[field.field]}
|
|
224
|
+
onChange={(value) => updateValue(field.field, value)}
|
|
225
|
+
/>
|
|
226
|
+
))}
|
|
227
|
+
|
|
228
|
+
<div className="space-y-2">
|
|
229
|
+
<Label htmlFor={`${authenticator.name}-password`}>Password</Label>
|
|
230
|
+
<InputPassword
|
|
231
|
+
id={`${authenticator.name}-password`}
|
|
232
|
+
value={password}
|
|
233
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
234
|
+
autoComplete="new-password"
|
|
235
|
+
required
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="space-y-2">
|
|
240
|
+
<Label htmlFor={`${authenticator.name}-confirm-password`}>
|
|
241
|
+
Confirm password
|
|
242
|
+
</Label>
|
|
243
|
+
<InputPassword
|
|
244
|
+
id={`${authenticator.name}-confirm-password`}
|
|
245
|
+
value={confirmPassword}
|
|
246
|
+
onChange={(event) => setConfirmPassword(event.target.value)}
|
|
247
|
+
autoComplete="new-password"
|
|
248
|
+
required
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<Button type="submit" className="w-full" disabled={isPending}>
|
|
253
|
+
{isPending ? "Creating account…" : "Sign up"}
|
|
254
|
+
</Button>
|
|
255
|
+
|
|
256
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
257
|
+
Have an account?{" "}
|
|
258
|
+
<Link
|
|
259
|
+
to="/login"
|
|
260
|
+
className="font-semibold text-foreground underline underline-offset-4"
|
|
261
|
+
>
|
|
262
|
+
Sign in
|
|
263
|
+
</Link>
|
|
264
|
+
</p>
|
|
265
|
+
</form>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export const SignUpForm = () => {
|
|
270
|
+
const [searchParams] = useSearchParams();
|
|
271
|
+
const { data: authenticators = [], error, isPending } =
|
|
272
|
+
usePublicAuthenticators();
|
|
273
|
+
const authenticatorName = searchParams.get("name");
|
|
274
|
+
const authenticator = authenticators.find(
|
|
275
|
+
(item) => item.name === authenticatorName
|
|
276
|
+
);
|
|
277
|
+
|
|
41
278
|
return (
|
|
42
279
|
<AuthLayout
|
|
43
280
|
title="Create your account"
|
|
44
281
|
description="Create your NocoBase account."
|
|
45
282
|
>
|
|
46
|
-
|
|
47
|
-
<div className="
|
|
48
|
-
<
|
|
49
|
-
<Input
|
|
50
|
-
id="email"
|
|
51
|
-
type="email"
|
|
52
|
-
value={email}
|
|
53
|
-
onChange={(event) => setEmail(event.target.value)}
|
|
54
|
-
autoComplete="email"
|
|
55
|
-
autoFocus
|
|
56
|
-
required
|
|
57
|
-
/>
|
|
283
|
+
{isPending ? (
|
|
284
|
+
<div className="flex min-h-64 items-center justify-center">
|
|
285
|
+
<Spinner className="size-6 text-muted-foreground" />
|
|
58
286
|
</div>
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
287
|
+
) : error ? (
|
|
288
|
+
<Alert variant="destructive">
|
|
289
|
+
<AlertCircle />
|
|
290
|
+
<AlertTitle>Unable to load the sign-up method</AlertTitle>
|
|
291
|
+
<AlertDescription>
|
|
292
|
+
{import.meta.env.DEV && error instanceof Error
|
|
293
|
+
? error.message
|
|
294
|
+
: "Please try again or contact your administrator."}
|
|
295
|
+
</AlertDescription>
|
|
296
|
+
</Alert>
|
|
297
|
+
) : !authenticator || authenticator.authType !== "Email/Password" ? (
|
|
298
|
+
<Empty className="min-h-64 border">
|
|
299
|
+
<EmptyHeader>
|
|
300
|
+
<EmptyMedia variant="icon">
|
|
301
|
+
<KeyRound />
|
|
302
|
+
</EmptyMedia>
|
|
303
|
+
<EmptyTitle>No sign-up method available</EmptyTitle>
|
|
304
|
+
<EmptyDescription>
|
|
305
|
+
This sign-up link is invalid or the authentication method does
|
|
306
|
+
not support account registration.
|
|
307
|
+
</EmptyDescription>
|
|
308
|
+
</EmptyHeader>
|
|
309
|
+
</Empty>
|
|
310
|
+
) : authenticator.options?.allowSignUp !== true ? (
|
|
311
|
+
<Empty className="min-h-64 border">
|
|
312
|
+
<EmptyHeader>
|
|
313
|
+
<EmptyMedia variant="icon">
|
|
314
|
+
<KeyRound />
|
|
315
|
+
</EmptyMedia>
|
|
316
|
+
<EmptyTitle>Account registration is disabled</EmptyTitle>
|
|
317
|
+
<EmptyDescription>
|
|
318
|
+
Contact your administrator if you need an account.
|
|
319
|
+
</EmptyDescription>
|
|
320
|
+
</EmptyHeader>
|
|
321
|
+
</Empty>
|
|
322
|
+
) : (
|
|
323
|
+
<BasicSignUpForm
|
|
324
|
+
key={authenticator.name}
|
|
325
|
+
authenticator={authenticator}
|
|
326
|
+
/>
|
|
327
|
+
)}
|
|
100
328
|
</AuthLayout>
|
|
101
329
|
);
|
|
102
330
|
};
|
|
@@ -4,12 +4,34 @@ import type {
|
|
|
4
4
|
ReactNode,
|
|
5
5
|
} from "react";
|
|
6
6
|
|
|
7
|
+
export type AuthenticatorSignUpFieldOption = {
|
|
8
|
+
label?: unknown;
|
|
9
|
+
value: string | number | boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AuthenticatorSignUpField = {
|
|
13
|
+
field: string;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
show?: boolean;
|
|
16
|
+
uiSchema?: {
|
|
17
|
+
enum?: AuthenticatorSignUpFieldOption[];
|
|
18
|
+
title?: unknown;
|
|
19
|
+
"x-component"?: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type AuthenticatorPublicOptions = Record<string, unknown> & {
|
|
24
|
+
allowSignUp?: boolean;
|
|
25
|
+
enableResetPassword?: boolean;
|
|
26
|
+
signupForm?: AuthenticatorSignUpField[];
|
|
27
|
+
};
|
|
28
|
+
|
|
7
29
|
export type Authenticator = {
|
|
8
30
|
name: string;
|
|
9
31
|
authType: string;
|
|
10
32
|
authTypeTitle?: string;
|
|
11
33
|
title?: string;
|
|
12
|
-
options?:
|
|
34
|
+
options?: AuthenticatorPublicOptions;
|
|
13
35
|
sort?: number;
|
|
14
36
|
};
|
|
15
37
|
|
|
@@ -12,11 +12,17 @@ After installation, import the components from `@/extensions/nocobase-acl`.
|
|
|
12
12
|
normal roles, Anonymous when enabled, and Full permissions through the
|
|
13
13
|
`__union__` role when the system role mode allows it.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
`allOf`, and `noneOf`.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
Application route definitions and `AclPage` accept a `roles` constraint with
|
|
16
|
+
`anyOf`, `allOf`, and `noneOf`. Put route-level constraints in `access.roles`
|
|
17
|
+
through `defineAppRoutes`; the Starter applies the same constraint to menu
|
|
18
|
+
visibility and direct URL access. Role constraints use the roles in the current
|
|
19
|
+
effective ACL context. In union mode this is the set of roles participating in
|
|
20
|
+
the union; in single-role mode it is only the selected role. Regions, fields,
|
|
21
|
+
and actions continue to use resource permissions.
|
|
22
|
+
|
|
23
|
+
Nested routes inherit their parents' route access. The generated navigation
|
|
24
|
+
item evaluates every constraint in that route chain, matching the nested route
|
|
25
|
+
guards used for direct URL access.
|
|
20
26
|
|
|
21
27
|
Use the Starter's `useGetRoles()` hook when application UI needs the same
|
|
22
28
|
effective role names. Its result follows the familiar Refine query shape with
|
|
@@ -35,15 +41,23 @@ function CurrentAccessContext() {
|
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
```tsx
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
import { defineAppRoutes } from "@/app/route-runtime";
|
|
45
|
+
|
|
46
|
+
export const appRoutes = defineAppRoutes([
|
|
47
|
+
{
|
|
48
|
+
name: "administration",
|
|
49
|
+
path: "/administration",
|
|
50
|
+
element: <AdministrationPage />,
|
|
51
|
+
resource: {
|
|
52
|
+
meta: {
|
|
53
|
+
label: "Administration",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
access: {
|
|
43
57
|
roles: { anyOf: ["admin"] },
|
|
44
58
|
},
|
|
45
59
|
},
|
|
46
|
-
|
|
60
|
+
]);
|
|
47
61
|
|
|
48
62
|
function AuditLogsRoute() {
|
|
49
63
|
return (
|
|
@@ -56,9 +70,9 @@ function AuditLogsRoute() {
|
|
|
56
70
|
}
|
|
57
71
|
```
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
the `collection`, `snippet`, and `route` ACL metadata variants when
|
|
61
|
-
should participate in NocoBase permission checks.
|
|
73
|
+
The route runtime marks generated non-collection resources as authenticated.
|
|
74
|
+
Use the `collection`, `snippet`, and `route` ACL metadata variants when a
|
|
75
|
+
resource should also participate in NocoBase permission checks.
|
|
62
76
|
|
|
63
77
|
The underlying `accessControlProvider`, `roles:check` cache, record permissions,
|
|
64
78
|
and Refine integration are provided by the compatible Starter.
|
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
UserRoundCog,
|
|
10
10
|
UsersRound,
|
|
11
11
|
} from "lucide-react";
|
|
12
|
+
import type { TreeMenuItem } from "@refinedev/core";
|
|
12
13
|
|
|
14
|
+
import {
|
|
15
|
+
buildRouteResources,
|
|
16
|
+
defineAppRoutes,
|
|
17
|
+
RouteAccessGuard,
|
|
18
|
+
} from "@/app/route-runtime";
|
|
13
19
|
import { Badge } from "@/components/ui/badge";
|
|
14
20
|
import { Button } from "@/components/ui/button";
|
|
15
21
|
import { CanAccess } from "@/components/access-control/can-access";
|
|
@@ -21,7 +27,11 @@ import {
|
|
|
21
27
|
CardTitle,
|
|
22
28
|
} from "@/components/ui/card";
|
|
23
29
|
import { Input } from "@/components/ui/input";
|
|
24
|
-
import
|
|
30
|
+
import {
|
|
31
|
+
filterMenuItemsByAcl,
|
|
32
|
+
type RoleConstraint,
|
|
33
|
+
useAclState,
|
|
34
|
+
} from "@/lib/nocobase/acl";
|
|
25
35
|
import { AclField, AclPage, AclRegion } from "../components/acl-boundary";
|
|
26
36
|
import { AclPreviewProvider } from "./acl-preview-provider";
|
|
27
37
|
import { AclBoundaryApi } from "./boundary-api";
|
|
@@ -58,6 +68,53 @@ const usersRouteRoles = {
|
|
|
58
68
|
anyOf: ["admin"],
|
|
59
69
|
} satisfies RoleConstraint;
|
|
60
70
|
|
|
71
|
+
const navigationDemoRoutes = defineAppRoutes([
|
|
72
|
+
{
|
|
73
|
+
name: "dashboard",
|
|
74
|
+
path: "/dashboard",
|
|
75
|
+
resource: {
|
|
76
|
+
meta: {
|
|
77
|
+
label: "Dashboard",
|
|
78
|
+
icon: <LayoutPanelTop />,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "users",
|
|
84
|
+
path: "/users",
|
|
85
|
+
resource: {
|
|
86
|
+
meta: {
|
|
87
|
+
label: "Users",
|
|
88
|
+
icon: <UsersRound />,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
access: {
|
|
92
|
+
roles: usersRouteRoles,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "roles",
|
|
97
|
+
path: "/roles",
|
|
98
|
+
resource: {
|
|
99
|
+
meta: {
|
|
100
|
+
label: "Roles",
|
|
101
|
+
icon: <UserRoundCog />,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const navigationDemoResources = buildRouteResources(navigationDemoRoutes);
|
|
108
|
+
const navigationDemoMenuItems: TreeMenuItem[] = navigationDemoResources.map(
|
|
109
|
+
(resource) => ({
|
|
110
|
+
key: resource.name,
|
|
111
|
+
name: resource.name,
|
|
112
|
+
route: typeof resource.list === "string" ? resource.list : undefined,
|
|
113
|
+
meta: resource.meta,
|
|
114
|
+
children: [],
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
61
118
|
const allowedRegionPermissions = {
|
|
62
119
|
"users:list": {},
|
|
63
120
|
"departments:list": {},
|
|
@@ -127,12 +184,12 @@ export function AclPatternsPage() {
|
|
|
127
184
|
defaultScene: "User and role administration",
|
|
128
185
|
defaultTarget:
|
|
129
186
|
"Allow administrators to open Users, but remove it from navigation and block its direct URL for other roles.",
|
|
130
|
-
requirements: `-
|
|
131
|
-
-
|
|
132
|
-
- Let the
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
187
|
+
requirements: `- Define application-owned navigation once through defineAppRoutes in src/routes.tsx.
|
|
188
|
+
- Put role constraints in the route access.roles field using anyOf, allOf, or noneOf.
|
|
189
|
+
- Let the route runtime generate the Refine resource, filter the sidebar item, and wrap the direct route with CanAccess.
|
|
190
|
+
- Do not repeat the same role list in Resource meta or a manually written route guard.
|
|
191
|
+
- Keep resource/action, record, region, and field permissions in their dedicated ACL boundaries.
|
|
192
|
+
- Let the Starter choose the first accessible route after inaccessible leaves and empty groups are removed.`,
|
|
136
193
|
}}
|
|
137
194
|
>
|
|
138
195
|
<OutcomeComparison
|
|
@@ -343,21 +400,36 @@ function OutcomeComparison({
|
|
|
343
400
|
}
|
|
344
401
|
|
|
345
402
|
function NavigationPreview() {
|
|
403
|
+
const acl = useAclState();
|
|
404
|
+
const menuItems =
|
|
405
|
+
acl.status === "ready"
|
|
406
|
+
? filterMenuItemsByAcl(navigationDemoMenuItems, acl.permissions)
|
|
407
|
+
: [];
|
|
408
|
+
const usersVisible = menuItems.some((item) => item.name === "users");
|
|
409
|
+
const usersRoute = navigationDemoRoutes.find(
|
|
410
|
+
(route) => route.name === "users"
|
|
411
|
+
);
|
|
412
|
+
|
|
346
413
|
return (
|
|
347
414
|
<div className="grid min-h-48 grid-cols-[150px_minmax(0,1fr)] overflow-hidden rounded-xl border bg-background">
|
|
348
415
|
<aside className="border-r bg-muted/35 p-3">
|
|
349
416
|
<div className="mb-3 text-xs font-semibold text-muted-foreground">
|
|
350
417
|
Workspace
|
|
351
418
|
</div>
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
419
|
+
{menuItems.map((item) => (
|
|
420
|
+
<NavItem
|
|
421
|
+
key={item.name}
|
|
422
|
+
icon={item.meta?.icon}
|
|
423
|
+
label={String(item.meta?.label ?? item.name)}
|
|
424
|
+
active={
|
|
425
|
+
usersVisible ? item.name === "users" : item.name === "dashboard"
|
|
426
|
+
}
|
|
427
|
+
/>
|
|
428
|
+
))}
|
|
357
429
|
</aside>
|
|
358
430
|
<div className="flex items-center justify-center p-5 text-center">
|
|
359
|
-
<
|
|
360
|
-
|
|
431
|
+
<RouteAccessGuard
|
|
432
|
+
access={usersRoute?.access}
|
|
361
433
|
fallback={
|
|
362
434
|
<div>
|
|
363
435
|
<LayoutPanelTop className="mx-auto size-7 text-muted-foreground" />
|
|
@@ -375,7 +447,7 @@ function NavigationPreview() {
|
|
|
375
447
|
Route is available
|
|
376
448
|
</div>
|
|
377
449
|
</div>
|
|
378
|
-
</
|
|
450
|
+
</RouteAccessGuard>
|
|
379
451
|
</div>
|
|
380
452
|
</div>
|
|
381
453
|
);
|
|
@@ -45,6 +45,7 @@ Implementation contract
|
|
|
45
45
|
- Use the Starter's built-in NocoBase accessControlProvider and ACL store.
|
|
46
46
|
- NocoBase roles:check is the source of truth. Do not create a second permission store. Use configured role names only when the requirement explicitly includes a role constraint.
|
|
47
47
|
- Import the Starter CanAccess from @/components/access-control/can-access. Use it with AclPage, AclRegion, and AclField for permission presentation. Do not copy their checks into local booleans or role comparisons.
|
|
48
|
+
- For route-level role access, define the route once with defineAppRoutes in src/routes.tsx and put the constraint in access.roles. Let the route runtime control both its menu item and direct URL.
|
|
48
49
|
- Use useGetRoles only when UI needs to display or pass through the current effective ACL roles; it is not a replacement permission evaluator.
|
|
49
50
|
- Keep backend ACL enforcement in place; frontend checks only control presentation and navigation.
|
|
50
51
|
- Import reusable components from the installed local entry point at @/extensions/nocobase-acl when page composition is needed.
|