@nocobase/portal-template-default 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.MD +70 -0
- package/components.json +3 -1
- package/package.json +19 -5
- package/registry/nocobase-acl/README.md +19 -0
- package/registry/nocobase-acl/components/acl-boundary.tsx +106 -0
- package/registry/nocobase-acl/components/index.ts +3 -0
- package/registry/nocobase-acl/components/role-options.ts +29 -0
- package/registry/nocobase-acl/components/role-switcher.tsx +130 -0
- package/registry/nocobase-acl/demo/boundary-api.tsx +115 -0
- package/registry/nocobase-acl/demo/components.tsx +210 -0
- package/registry/nocobase-acl/demo/index.tsx +474 -0
- package/registry/nocobase-acl/demo/lazy-route.tsx +15 -0
- package/registry/nocobase-acl/demo/prompt-generator.tsx +115 -0
- package/registry/nocobase-acl/demo/role-switcher-prompt-generator.tsx +179 -0
- package/registry/nocobase-acl/demo/scenario-section.tsx +37 -0
- package/registry/nocobase-acl/extension.tsx +71 -0
- package/registry/nocobase-acl/index.ts +1 -0
- package/registry/nocobase-acl/tests/acl-regression.mjs +313 -0
- package/registry/nocobase-ai/README.md +70 -0
- package/registry/nocobase-ai/adapters/react-hook-form.ts +20 -0
- package/registry/nocobase-ai/assets/nocobase-ai-chat.svg +50 -0
- package/registry/nocobase-ai/components/ai-root-provider.tsx +32 -0
- package/registry/nocobase-ai/components/chat/ai-employee-avatar.tsx +31 -0
- package/registry/nocobase-ai/components/chat/chat-attachment.tsx +92 -0
- package/registry/nocobase-ai/components/chat/chat-compact.tsx +90 -0
- package/registry/nocobase-ai/components/chat/chat-composer.tsx +429 -0
- package/registry/nocobase-ai/components/chat/chat-empty-state.tsx +51 -0
- package/registry/nocobase-ai/components/chat/chat-header.tsx +88 -0
- package/registry/nocobase-ai/components/chat/chat-history-dialog.tsx +77 -0
- package/registry/nocobase-ai/components/chat/chat-message.tsx +252 -0
- package/registry/nocobase-ai/components/chat/chat-messages.tsx +174 -0
- package/registry/nocobase-ai/components/chat/chat-window.tsx +118 -0
- package/registry/nocobase-ai/components/chat/conversation-list.tsx +345 -0
- package/registry/nocobase-ai/components/chat/markdown-message.tsx +165 -0
- package/registry/nocobase-ai/components/chat/model-select-options.tsx +30 -0
- package/registry/nocobase-ai/components/chat/reasoning-panel.tsx +29 -0
- package/registry/nocobase-ai/components/chat/sub-agent-conversation.tsx +142 -0
- package/registry/nocobase-ai/components/chat/tool-call-card.tsx +316 -0
- package/registry/nocobase-ai/components/chat/user-prompt-editor.tsx +99 -0
- package/registry/nocobase-ai/components/chat/work-context-chip.tsx +114 -0
- package/registry/nocobase-ai/components/index.ts +59 -0
- package/registry/nocobase-ai/components/page-elements/ai-form.tsx +44 -0
- package/registry/nocobase-ai/components/page-elements/page-element-provider.tsx +469 -0
- package/registry/nocobase-ai/components/surfaces/chat-dialog.tsx +31 -0
- package/registry/nocobase-ai/components/surfaces/chat-inline.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-page.tsx +18 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel-layout.tsx +54 -0
- package/registry/nocobase-ai/components/surfaces/chat-side-panel.tsx +74 -0
- package/registry/nocobase-ai/components/surfaces/chat-surface-actions.tsx +54 -0
- package/registry/nocobase-ai/components/tools/builtin-tool-renderers.tsx +31 -0
- package/registry/nocobase-ai/components/tools/business-report-dialog.tsx +367 -0
- package/registry/nocobase-ai/components/tools/business-report-renderer.tsx +122 -0
- package/registry/nocobase-ai/components/tools/business-report-utils.tsx +282 -0
- package/registry/nocobase-ai/components/tools/chart-renderer.tsx +110 -0
- package/registry/nocobase-ai/components/tools/echarts-preview.tsx +62 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-advanced.ts +18 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-common.ts +20 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-components.ts +34 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime-hierarchy.ts +24 -0
- package/registry/nocobase-ai/components/tools/echarts-runtime.ts +138 -0
- package/registry/nocobase-ai/components/tools/sub-agent-renderer.tsx +67 -0
- package/registry/nocobase-ai/components/tools/suggestions-renderer.tsx +59 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-provider.tsx +55 -0
- package/registry/nocobase-ai/components/tools/tool-renderer-utils.ts +18 -0
- package/registry/nocobase-ai/components/tools/workflow-renderer.tsx +108 -0
- package/registry/nocobase-ai/components/triggers/ai-chat-floating-trigger.tsx +73 -0
- package/registry/nocobase-ai/components/triggers/ai-employee-shortcut.tsx +149 -0
- package/registry/nocobase-ai/demo/configuration-gate.tsx +52 -0
- package/registry/nocobase-ai/demo/container-showcase.tsx +210 -0
- package/registry/nocobase-ai/demo/floating.tsx +134 -0
- package/registry/nocobase-ai/demo/index.tsx +346 -0
- package/registry/nocobase-ai/demo/interaction-showcase.tsx +151 -0
- package/registry/nocobase-ai/demo/page-context-prompt-generator.tsx +355 -0
- package/registry/nocobase-ai/demo/page-context.tsx +884 -0
- package/registry/nocobase-ai/demo/page-element-showcase.tsx +197 -0
- package/registry/nocobase-ai/demo/prompt-card.tsx +41 -0
- package/registry/nocobase-ai/demo/prompt-generator.tsx +452 -0
- package/registry/nocobase-ai/demo/shortcut.tsx +1207 -0
- package/registry/nocobase-ai/demo/tool-cards.tsx +530 -0
- package/registry/nocobase-ai/extension.tsx +163 -0
- package/registry/nocobase-ai/global-ai-chat.tsx +213 -0
- package/registry/nocobase-ai/index.ts +3 -0
- package/registry/nocobase-ai/providers/ai-provider.tsx +323 -0
- package/registry/nocobase-ai/providers/avatars.ts +817 -0
- package/registry/nocobase-ai/providers/chat-context.tsx +109 -0
- package/registry/nocobase-ai/providers/chat-controller.ts +84 -0
- package/registry/nocobase-ai/providers/chat-message-utils.ts +198 -0
- package/registry/nocobase-ai/providers/chat-provider.tsx +871 -0
- package/registry/nocobase-ai/providers/chat-reducer.ts +147 -0
- package/registry/nocobase-ai/providers/chat-task-utils.ts +105 -0
- package/registry/nocobase-ai/providers/chat-transport.ts +184 -0
- package/registry/nocobase-ai/providers/form-registry.tsx +271 -0
- package/registry/nocobase-ai/providers/frontend-tool-registry.tsx +231 -0
- package/registry/nocobase-ai/providers/index.ts +61 -0
- package/registry/nocobase-ai/providers/model.ts +32 -0
- package/registry/nocobase-ai/providers/page-context.tsx +136 -0
- package/registry/nocobase-ai/providers/stream-coalescer.ts +60 -0
- package/registry/nocobase-ai/providers/stream-event-utils.ts +82 -0
- package/registry/nocobase-ai/providers/stream-parser.ts +61 -0
- package/registry/nocobase-ai/providers/sub-agent-stream.ts +316 -0
- package/registry/nocobase-ai/providers/types.ts +183 -0
- package/registry/nocobase-ai/providers/ui-message-stream.ts +463 -0
- package/registry/nocobase-ai/providers/use-automatic-tool-approval.ts +94 -0
- package/registry/nocobase-ai/providers/use-chat-attachments.ts +206 -0
- package/registry/nocobase-ai/providers/use-chat-message-actions.ts +554 -0
- package/registry/nocobase-ai/providers/use-chat-runtime.ts +210 -0
- package/registry/nocobase-ai/providers/use-chat-work-context.ts +93 -0
- package/registry/nocobase-ai/providers/use-conversation-catalog.ts +108 -0
- package/registry/nocobase-ai/providers/use-conversation-history.ts +178 -0
- package/registry/nocobase-ai/services/index.ts +7 -0
- package/registry/nocobase-ai/services/nocobase-ai-service.ts +454 -0
- package/registry/nocobase-ai/services/types.ts +82 -0
- package/registry/nocobase-ai/tests/business-report-regression.mjs +70 -0
- package/registry/nocobase-ai/tests/form-filler-regression.mjs +129 -0
- package/registry/nocobase-ai/tests/frontend-tool-regression.mjs +128 -0
- package/registry/nocobase-ai/tests/page-context-regression.mjs +48 -0
- package/registry/nocobase-ai/tests/stream-regression.mjs +207 -0
- package/registry.config.json +143 -0
- package/scripts/registry.mjs +161 -0
- package/src/App.css +95 -0
- package/src/App.tsx +127 -50
- package/src/app/extension.ts +9 -0
- package/src/app/extensions.tsx +28 -0
- package/src/components/access-control/access-denied.tsx +30 -0
- package/src/components/access-control/acl-bootstrap.tsx +9 -0
- package/src/components/access-control/acl-gate.tsx +42 -0
- package/src/components/access-control/navigate-to-accessible-resource.tsx +17 -0
- package/src/components/access-control/resource-access-guard.tsx +21 -0
- package/src/components/access-control/use-acl-runtime.ts +28 -0
- package/src/components/app-shell/header.tsx +4 -1
- package/src/components/app-shell/sidebar.tsx +34 -4
- package/src/lib/nocobase/acl/action.ts +147 -0
- package/src/lib/nocobase/acl/data-source.ts +24 -0
- package/src/lib/nocobase/acl/index.ts +6 -0
- package/src/lib/nocobase/acl/menu.ts +41 -0
- package/src/lib/nocobase/acl/record-permissions.ts +111 -0
- package/src/lib/nocobase/acl/store.ts +182 -0
- package/src/lib/nocobase/acl/types.ts +96 -0
- package/src/lib/nocobase/client.ts +262 -0
- package/src/lib/nocobase/error.ts +32 -0
- package/src/providers/access-control.ts +34 -0
- package/src/providers/auth.ts +74 -60
- package/src/providers/constants.ts +2 -37
- package/src/providers/data.ts +242 -85
- package/vite.config.ts +17 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
API_ORIGIN,
|
|
3
|
+
API_URL,
|
|
4
|
+
NOCOBASE_AUTHENTICATOR,
|
|
5
|
+
NOCOBASE_ROLE_KEY,
|
|
6
|
+
NOCOBASE_TOKEN_KEY,
|
|
7
|
+
} from "@/providers/constants";
|
|
8
|
+
import { getNocoBaseErrorMessage, NocoBaseHttpError } from "./error";
|
|
9
|
+
|
|
10
|
+
type QueryValue =
|
|
11
|
+
| string
|
|
12
|
+
| number
|
|
13
|
+
| boolean
|
|
14
|
+
| null
|
|
15
|
+
| undefined
|
|
16
|
+
| Array<string | number | boolean>;
|
|
17
|
+
|
|
18
|
+
export type NocoBaseRequestOptions = {
|
|
19
|
+
method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
20
|
+
query?: Record<string, QueryValue>;
|
|
21
|
+
body?: unknown;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
token?: string;
|
|
24
|
+
role?: string;
|
|
25
|
+
includeRole?: boolean;
|
|
26
|
+
includeAuthenticator?: boolean;
|
|
27
|
+
withAclMeta?: boolean;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
accept?: "json" | "stream";
|
|
30
|
+
unwrap?: "data" | "deep-data" | "none";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getClientLocale = () =>
|
|
34
|
+
typeof navigator === "undefined" ? undefined : navigator.language;
|
|
35
|
+
|
|
36
|
+
const getClientTimezone = () => {
|
|
37
|
+
const offsetMinutes = -new Date().getTimezoneOffset();
|
|
38
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
39
|
+
const absoluteMinutes = Math.abs(offsetMinutes);
|
|
40
|
+
const hours = String(Math.floor(absoluteMinutes / 60)).padStart(2, "0");
|
|
41
|
+
const minutes = String(absoluteMinutes % 60).padStart(2, "0");
|
|
42
|
+
return `${sign}${hours}:${minutes}`;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const unwrapPayload = (
|
|
46
|
+
payload: unknown,
|
|
47
|
+
mode: NocoBaseRequestOptions["unwrap"]
|
|
48
|
+
) => {
|
|
49
|
+
if (mode === "none") return payload;
|
|
50
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
51
|
+
const data = (payload as { data?: unknown }).data;
|
|
52
|
+
if (mode === "deep-data" && data && typeof data === "object") {
|
|
53
|
+
return (data as { data?: unknown }).data ?? data;
|
|
54
|
+
}
|
|
55
|
+
return data ?? payload;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export class NocoBaseClient {
|
|
59
|
+
private readonly apiOrigin?: string;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
private readonly apiUrl = API_URL,
|
|
63
|
+
apiOrigin = getUrlOrigin(apiUrl) ?? API_ORIGIN
|
|
64
|
+
) {
|
|
65
|
+
this.apiOrigin = apiOrigin;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getApiUrl() {
|
|
69
|
+
return this.apiUrl;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resolveUrl(value: string) {
|
|
73
|
+
if (!value || /^[a-z][a-z\d+.-]*:/i.test(value)) return value;
|
|
74
|
+
const base =
|
|
75
|
+
this.apiOrigin ??
|
|
76
|
+
(typeof window === "undefined" ? undefined : window.location.origin);
|
|
77
|
+
if (!base) return value;
|
|
78
|
+
try {
|
|
79
|
+
return new URL(value, `${base.replace(/\/$/, "")}/`).toString();
|
|
80
|
+
} catch {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getToken() {
|
|
86
|
+
return (
|
|
87
|
+
(typeof localStorage === "undefined"
|
|
88
|
+
? undefined
|
|
89
|
+
: localStorage.getItem(NOCOBASE_TOKEN_KEY) ?? undefined) ??
|
|
90
|
+
import.meta.env.NOCOBASE_API_TOKEN
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setToken(token?: string | null) {
|
|
95
|
+
if (typeof localStorage === "undefined") return;
|
|
96
|
+
if (token) localStorage.setItem(NOCOBASE_TOKEN_KEY, token);
|
|
97
|
+
else localStorage.removeItem(NOCOBASE_TOKEN_KEY);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getRole() {
|
|
101
|
+
return typeof localStorage === "undefined"
|
|
102
|
+
? undefined
|
|
103
|
+
: localStorage.getItem(NOCOBASE_ROLE_KEY) ?? undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setRole(role?: string | null) {
|
|
107
|
+
if (typeof localStorage === "undefined") return;
|
|
108
|
+
if (role) localStorage.setItem(NOCOBASE_ROLE_KEY, role);
|
|
109
|
+
else localStorage.removeItem(NOCOBASE_ROLE_KEY);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
buildUrl(endpoint: string, query?: Record<string, QueryValue>) {
|
|
113
|
+
const base = `${this.apiUrl.replace(/\/$/, "")}/${endpoint.replace(
|
|
114
|
+
/^\//,
|
|
115
|
+
""
|
|
116
|
+
)}`;
|
|
117
|
+
const url = /^https?:\/\//.test(base)
|
|
118
|
+
? new URL(base)
|
|
119
|
+
: new URL(base, window.location.origin);
|
|
120
|
+
|
|
121
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
122
|
+
if (value === undefined) continue;
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
value.forEach((item) => url.searchParams.append(key, String(item)));
|
|
125
|
+
} else if (value !== null) {
|
|
126
|
+
url.searchParams.set(key, String(value));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return url;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getHeaders({
|
|
133
|
+
token = this.getToken(),
|
|
134
|
+
role = this.getRole(),
|
|
135
|
+
includeRole = true,
|
|
136
|
+
includeAuthenticator = false,
|
|
137
|
+
withAclMeta = true,
|
|
138
|
+
headers,
|
|
139
|
+
accept = "json",
|
|
140
|
+
body,
|
|
141
|
+
}: Pick<
|
|
142
|
+
NocoBaseRequestOptions,
|
|
143
|
+
| "token"
|
|
144
|
+
| "role"
|
|
145
|
+
| "includeRole"
|
|
146
|
+
| "includeAuthenticator"
|
|
147
|
+
| "withAclMeta"
|
|
148
|
+
| "headers"
|
|
149
|
+
| "accept"
|
|
150
|
+
| "body"
|
|
151
|
+
> = {}) {
|
|
152
|
+
const locale = getClientLocale();
|
|
153
|
+
const formData =
|
|
154
|
+
typeof FormData !== "undefined" && body instanceof FormData;
|
|
155
|
+
return {
|
|
156
|
+
Accept: accept === "stream" ? "text/event-stream" : "application/json",
|
|
157
|
+
...(body !== undefined && !formData
|
|
158
|
+
? { "Content-Type": "application/json" }
|
|
159
|
+
: {}),
|
|
160
|
+
...(accept === "stream"
|
|
161
|
+
? { "Cache-Control": "no-cache", Pragma: "no-cache" }
|
|
162
|
+
: {}),
|
|
163
|
+
...(includeAuthenticator
|
|
164
|
+
? { "X-Authenticator": NOCOBASE_AUTHENTICATOR }
|
|
165
|
+
: {}),
|
|
166
|
+
...(includeRole && role ? { "X-Role": role } : {}),
|
|
167
|
+
...(withAclMeta ? { "X-With-ACL-Meta": "true" } : {}),
|
|
168
|
+
...(locale ? { "X-Locale": locale } : {}),
|
|
169
|
+
"X-Timezone": getClientTimezone(),
|
|
170
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
171
|
+
...headers,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async request<T>(
|
|
176
|
+
endpoint: string,
|
|
177
|
+
options: NocoBaseRequestOptions = {}
|
|
178
|
+
): Promise<T> {
|
|
179
|
+
const method =
|
|
180
|
+
options.method ?? (options.body === undefined ? "GET" : "POST");
|
|
181
|
+
const response = await fetch(this.buildUrl(endpoint, options.query), {
|
|
182
|
+
method,
|
|
183
|
+
headers: this.getHeaders({ ...options, body: options.body }),
|
|
184
|
+
body:
|
|
185
|
+
options.body === undefined
|
|
186
|
+
? undefined
|
|
187
|
+
: options.body instanceof FormData
|
|
188
|
+
? options.body
|
|
189
|
+
: JSON.stringify(options.body),
|
|
190
|
+
signal: options.signal,
|
|
191
|
+
});
|
|
192
|
+
this.captureRenewedToken(response);
|
|
193
|
+
const payload = await response.json().catch(() => undefined);
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
throw new NocoBaseHttpError({
|
|
196
|
+
status: response.status,
|
|
197
|
+
payload,
|
|
198
|
+
message: getNocoBaseErrorMessage(
|
|
199
|
+
payload,
|
|
200
|
+
`NocoBase request failed (${response.status})`
|
|
201
|
+
),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return unwrapPayload(payload, options.unwrap ?? "data") as T;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
action<T>(
|
|
208
|
+
resource: string,
|
|
209
|
+
action: string,
|
|
210
|
+
options: Omit<NocoBaseRequestOptions, "accept"> = {}
|
|
211
|
+
) {
|
|
212
|
+
const method =
|
|
213
|
+
options.method ?? (["get", "list"].includes(action) ? "GET" : "POST");
|
|
214
|
+
return this.request<T>(`${resource}:${action}`, { ...options, method });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async stream(
|
|
218
|
+
endpoint: string,
|
|
219
|
+
options: Omit<NocoBaseRequestOptions, "accept" | "unwrap"> = {}
|
|
220
|
+
) {
|
|
221
|
+
const response = await fetch(this.buildUrl(endpoint, options.query), {
|
|
222
|
+
method: options.method ?? "POST",
|
|
223
|
+
headers: this.getHeaders({
|
|
224
|
+
...options,
|
|
225
|
+
accept: "stream",
|
|
226
|
+
body: options.body,
|
|
227
|
+
}),
|
|
228
|
+
body:
|
|
229
|
+
options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
230
|
+
signal: options.signal,
|
|
231
|
+
});
|
|
232
|
+
this.captureRenewedToken(response);
|
|
233
|
+
if (!response.ok || !response.body) {
|
|
234
|
+
const payload = await response.json().catch(() => undefined);
|
|
235
|
+
throw new NocoBaseHttpError({
|
|
236
|
+
status: response.status,
|
|
237
|
+
payload,
|
|
238
|
+
message: getNocoBaseErrorMessage(
|
|
239
|
+
payload,
|
|
240
|
+
`NocoBase stream failed (${response.status})`
|
|
241
|
+
),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return response.body;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private captureRenewedToken(response: Response) {
|
|
248
|
+
const token = response.headers.get("x-new-token");
|
|
249
|
+
if (token) this.setToken(token);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const getUrlOrigin = (value: string) => {
|
|
254
|
+
if (!/^https?:\/\//i.test(value)) return undefined;
|
|
255
|
+
try {
|
|
256
|
+
return new URL(value).origin;
|
|
257
|
+
} catch {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const nocobaseClient = new NocoBaseClient();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class NocoBaseHttpError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly payload: unknown;
|
|
4
|
+
|
|
5
|
+
constructor({
|
|
6
|
+
message,
|
|
7
|
+
status,
|
|
8
|
+
payload,
|
|
9
|
+
}: {
|
|
10
|
+
message: string;
|
|
11
|
+
status: number;
|
|
12
|
+
payload?: unknown;
|
|
13
|
+
}) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "NocoBaseHttpError";
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.payload = payload;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const getNocoBaseErrorMessage = (payload: unknown, fallback: string) => {
|
|
22
|
+
if (!payload || typeof payload !== "object") return fallback;
|
|
23
|
+
const value = payload as {
|
|
24
|
+
message?: string;
|
|
25
|
+
error?: { message?: string };
|
|
26
|
+
errors?: Array<{ message?: string } | string>;
|
|
27
|
+
messages?: Array<{ message?: string } | string>;
|
|
28
|
+
};
|
|
29
|
+
const first = value.errors?.[0] ?? value.messages?.[0];
|
|
30
|
+
if (typeof first === "string") return first;
|
|
31
|
+
return first?.message ?? value.error?.message ?? value.message ?? fallback;
|
|
32
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AccessControlProvider } from "@refinedev/core";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
canAccessWithSnapshot,
|
|
5
|
+
getAclSnapshot,
|
|
6
|
+
loadAcl,
|
|
7
|
+
} from "@/lib/nocobase/acl";
|
|
8
|
+
|
|
9
|
+
export const accessControlProvider: AccessControlProvider = {
|
|
10
|
+
can: async ({ resource, action, params }) => {
|
|
11
|
+
const snapshot = await loadAcl();
|
|
12
|
+
const can = canAccessWithSnapshot(snapshot, { resource, action, params });
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
can,
|
|
16
|
+
reason: can
|
|
17
|
+
? undefined
|
|
18
|
+
: "You don't have permission to perform this action.",
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
options: {
|
|
22
|
+
buttons: {
|
|
23
|
+
enableAccessControl: true,
|
|
24
|
+
hideIfUnauthorized: true,
|
|
25
|
+
},
|
|
26
|
+
queryOptions: {
|
|
27
|
+
staleTime: Infinity,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const canAccessCurrentAcl = (
|
|
33
|
+
params: Parameters<typeof canAccessWithSnapshot>[1]
|
|
34
|
+
) => canAccessWithSnapshot(getAclSnapshot(), params);
|
package/src/providers/auth.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { AuthProvider } from "@refinedev/core";
|
|
2
2
|
|
|
3
|
+
import { nocobaseClient } from "@/lib/nocobase/client";
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
5
|
+
clearAcl,
|
|
6
|
+
loadAcl,
|
|
7
|
+
type Role,
|
|
8
|
+
} from "@/lib/nocobase/acl";
|
|
9
|
+
import {
|
|
10
|
+
getNocoBaseErrorMessage,
|
|
11
|
+
NocoBaseHttpError,
|
|
12
|
+
} from "@/lib/nocobase/error";
|
|
8
13
|
|
|
9
14
|
type NocoBaseUser = {
|
|
10
15
|
id: number | string;
|
|
@@ -12,6 +17,7 @@ type NocoBaseUser = {
|
|
|
12
17
|
username?: string;
|
|
13
18
|
email?: string;
|
|
14
19
|
avatar?: string;
|
|
20
|
+
roles?: Role[];
|
|
15
21
|
};
|
|
16
22
|
|
|
17
23
|
type NocoBaseSignInResponse = {
|
|
@@ -29,18 +35,9 @@ const CURRENT_USER_CACHE_MS = 30_000;
|
|
|
29
35
|
let currentUserCache: CurrentUserCache | undefined;
|
|
30
36
|
let currentUserRequest: Promise<NocoBaseUser> | undefined;
|
|
31
37
|
|
|
32
|
-
const authUrl = (action: "signIn" | "signOut" | "check") =>
|
|
33
|
-
`${AUTH_API_URL.replace(/\/$/, "")}/auth:${action}`;
|
|
34
|
-
|
|
35
38
|
const getErrorMessage = (payload: unknown, fallback: string) => {
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
return error.errors?.[0]?.message ?? error.message ?? fallback;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const saveRenewedToken = (response: Response) => {
|
|
42
|
-
const token = response.headers.get("x-new-token");
|
|
43
|
-
if (token) localStorage.setItem(NOCOBASE_TOKEN_KEY, token);
|
|
39
|
+
if (payload instanceof NocoBaseHttpError) return payload.message;
|
|
40
|
+
return getNocoBaseErrorMessage(payload, fallback);
|
|
44
41
|
};
|
|
45
42
|
|
|
46
43
|
const clearCurrentUserCache = () => {
|
|
@@ -49,26 +46,23 @@ const clearCurrentUserCache = () => {
|
|
|
49
46
|
};
|
|
50
47
|
|
|
51
48
|
const getCurrentUser = async (): Promise<NocoBaseUser> => {
|
|
52
|
-
const token =
|
|
49
|
+
const token = nocobaseClient.getToken();
|
|
53
50
|
if (!token) throw new Error("No NocoBase token");
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
const cached = currentUserCache;
|
|
53
|
+
if (cached && cached.token === token && cached.expiresAt > Date.now()) {
|
|
54
|
+
return cached.user;
|
|
57
55
|
}
|
|
58
56
|
|
|
59
57
|
if (currentUserRequest) return currentUserRequest;
|
|
60
58
|
|
|
61
59
|
currentUserRequest = (async () => {
|
|
62
|
-
const
|
|
63
|
-
|
|
60
|
+
const user = await nocobaseClient.action<NocoBaseUser>("auth", "check", {
|
|
61
|
+
token,
|
|
62
|
+
includeAuthenticator: true,
|
|
64
63
|
});
|
|
65
|
-
const payload = await response.json().catch(() => undefined);
|
|
66
|
-
if (!response.ok) throw new Error(getErrorMessage(payload, "NocoBase session is invalid"));
|
|
67
|
-
|
|
68
|
-
saveRenewedToken(response);
|
|
69
|
-
const user = (payload?.data ?? payload) as NocoBaseUser;
|
|
70
64
|
currentUserCache = {
|
|
71
|
-
token:
|
|
65
|
+
token: nocobaseClient.getToken() ?? token,
|
|
72
66
|
user,
|
|
73
67
|
expiresAt: Date.now() + CURRENT_USER_CACHE_MS,
|
|
74
68
|
};
|
|
@@ -89,7 +83,8 @@ export const authProvider: AuthProvider = {
|
|
|
89
83
|
success: false,
|
|
90
84
|
error: {
|
|
91
85
|
name: "UnsupportedAuthenticator",
|
|
92
|
-
message:
|
|
86
|
+
message:
|
|
87
|
+
"Configure this provider as a NocoBase authenticator before using social sign-in.",
|
|
93
88
|
},
|
|
94
89
|
};
|
|
95
90
|
}
|
|
@@ -98,62 +93,68 @@ export const authProvider: AuthProvider = {
|
|
|
98
93
|
if (!account || !password) {
|
|
99
94
|
return {
|
|
100
95
|
success: false,
|
|
101
|
-
error: {
|
|
96
|
+
error: {
|
|
97
|
+
name: "LoginError",
|
|
98
|
+
message: "Please enter your account and password.",
|
|
99
|
+
},
|
|
102
100
|
};
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
const result = await nocobaseClient.action<NocoBaseSignInResponse>(
|
|
105
|
+
"auth",
|
|
106
|
+
"signIn",
|
|
107
|
+
{
|
|
108
|
+
method: "POST",
|
|
109
109
|
includeAuthenticator: true,
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
const payload = await response.json().catch(() => undefined);
|
|
115
|
-
if (!response.ok) {
|
|
116
|
-
return {
|
|
117
|
-
success: false,
|
|
118
|
-
error: { name: "LoginError", message: getErrorMessage(payload, "Unable to sign in.") },
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const result = (payload?.data ?? payload) as NocoBaseSignInResponse;
|
|
110
|
+
body: { account, password },
|
|
111
|
+
}
|
|
112
|
+
);
|
|
123
113
|
if (!result.token) {
|
|
124
114
|
return {
|
|
125
115
|
success: false,
|
|
126
|
-
error: {
|
|
116
|
+
error: {
|
|
117
|
+
name: "LoginError",
|
|
118
|
+
message: "NocoBase did not return an access token.",
|
|
119
|
+
},
|
|
127
120
|
};
|
|
128
121
|
}
|
|
129
122
|
|
|
130
|
-
|
|
123
|
+
nocobaseClient.setToken(result.token);
|
|
124
|
+
nocobaseClient.setRole(null);
|
|
131
125
|
clearCurrentUserCache();
|
|
126
|
+
clearAcl();
|
|
132
127
|
return { success: true, redirectTo: "/" };
|
|
133
|
-
} catch {
|
|
128
|
+
} catch (error) {
|
|
134
129
|
return {
|
|
135
130
|
success: false,
|
|
136
131
|
error: {
|
|
137
|
-
name:
|
|
138
|
-
|
|
139
|
-
|
|
132
|
+
name:
|
|
133
|
+
error instanceof NocoBaseHttpError ? "LoginError" : "NetworkError",
|
|
134
|
+
message: getErrorMessage(
|
|
135
|
+
error,
|
|
136
|
+
"Unable to reach the NocoBase server. If this is a remote NocoBase from localhost, enable backend CORS for X-Authenticator or use the Vite proxy."
|
|
137
|
+
),
|
|
140
138
|
},
|
|
141
139
|
};
|
|
142
140
|
}
|
|
143
141
|
},
|
|
144
142
|
|
|
145
143
|
logout: async () => {
|
|
146
|
-
const token =
|
|
144
|
+
const token = nocobaseClient.getToken();
|
|
147
145
|
try {
|
|
148
146
|
if (token) {
|
|
149
|
-
await
|
|
147
|
+
await nocobaseClient.action("auth", "signOut", {
|
|
150
148
|
method: "POST",
|
|
151
|
-
|
|
149
|
+
token,
|
|
150
|
+
includeAuthenticator: true,
|
|
152
151
|
});
|
|
153
152
|
}
|
|
154
153
|
} finally {
|
|
155
|
-
|
|
154
|
+
nocobaseClient.setToken(null);
|
|
155
|
+
nocobaseClient.setRole(null);
|
|
156
156
|
clearCurrentUserCache();
|
|
157
|
+
clearAcl();
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
return { success: true, redirectTo: "/login" };
|
|
@@ -164,18 +165,27 @@ export const authProvider: AuthProvider = {
|
|
|
164
165
|
await getCurrentUser();
|
|
165
166
|
return { authenticated: true };
|
|
166
167
|
} catch {
|
|
167
|
-
|
|
168
|
+
nocobaseClient.setToken(null);
|
|
169
|
+
nocobaseClient.setRole(null);
|
|
168
170
|
clearCurrentUserCache();
|
|
171
|
+
clearAcl();
|
|
169
172
|
return { authenticated: false, redirectTo: "/login" };
|
|
170
173
|
}
|
|
171
174
|
},
|
|
172
175
|
|
|
173
|
-
getPermissions: async () =>
|
|
176
|
+
getPermissions: async () => {
|
|
177
|
+
try {
|
|
178
|
+
return await loadAcl();
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
},
|
|
174
183
|
|
|
175
184
|
getIdentity: async () => {
|
|
176
185
|
try {
|
|
177
186
|
const user = await getCurrentUser();
|
|
178
|
-
const fullName =
|
|
187
|
+
const fullName =
|
|
188
|
+
user.nickname ?? user.username ?? user.email ?? "NocoBase user";
|
|
179
189
|
return {
|
|
180
190
|
id: user.id,
|
|
181
191
|
firstName: fullName,
|
|
@@ -183,6 +193,7 @@ export const authProvider: AuthProvider = {
|
|
|
183
193
|
fullName,
|
|
184
194
|
email: user.email ?? "",
|
|
185
195
|
avatar: user.avatar,
|
|
196
|
+
roles: user.roles ?? [],
|
|
186
197
|
};
|
|
187
198
|
} catch {
|
|
188
199
|
return null;
|
|
@@ -190,12 +201,15 @@ export const authProvider: AuthProvider = {
|
|
|
190
201
|
},
|
|
191
202
|
|
|
192
203
|
onError: async (error) => {
|
|
193
|
-
const status =
|
|
204
|
+
const status =
|
|
205
|
+
(error as { status?: number; statusCode?: number }).status ??
|
|
194
206
|
(error as { status?: number; statusCode?: number }).statusCode;
|
|
195
207
|
|
|
196
|
-
if (status === 401
|
|
197
|
-
|
|
208
|
+
if (status === 401) {
|
|
209
|
+
nocobaseClient.setToken(null);
|
|
210
|
+
nocobaseClient.setRole(null);
|
|
198
211
|
clearCurrentUserCache();
|
|
212
|
+
clearAcl();
|
|
199
213
|
return { logout: true, redirectTo: "/login" };
|
|
200
214
|
}
|
|
201
215
|
|
|
@@ -32,44 +32,9 @@ const toProxyRelativeUrl = (url: string, target?: string) => {
|
|
|
32
32
|
return url;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
const getClientLocale = () =>
|
|
36
|
-
typeof navigator !== "undefined" ? navigator.language : undefined;
|
|
37
|
-
|
|
38
|
-
const getClientTimezone = () => {
|
|
39
|
-
const offsetMinutes = -new Date().getTimezoneOffset();
|
|
40
|
-
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
41
|
-
const absoluteMinutes = Math.abs(offsetMinutes);
|
|
42
|
-
const hours = String(Math.floor(absoluteMinutes / 60)).padStart(2, "0");
|
|
43
|
-
const minutes = String(absoluteMinutes % 60).padStart(2, "0");
|
|
44
|
-
|
|
45
|
-
return `${sign}${hours}:${minutes}`;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
35
|
export const API_URL = toProxyRelativeUrl(rawApiUrl, proxyTarget);
|
|
49
|
-
export const
|
|
50
|
-
export const TOKEN_KEY = "refine-auth";
|
|
36
|
+
export const API_ORIGIN = getDefaultProxyTarget(rawApiUrl);
|
|
51
37
|
export const NOCOBASE_TOKEN_KEY = "nocobase-auth-token";
|
|
38
|
+
export const NOCOBASE_ROLE_KEY = "nocobase-auth-role";
|
|
52
39
|
export const NOCOBASE_AUTHENTICATOR =
|
|
53
40
|
import.meta.env.NOCOBASE_AUTHENTICATOR ?? "basic";
|
|
54
|
-
|
|
55
|
-
export const getNocoBaseHeaders = ({
|
|
56
|
-
token,
|
|
57
|
-
includeAuthenticator = false,
|
|
58
|
-
includeContentType = false,
|
|
59
|
-
}: {
|
|
60
|
-
token?: string;
|
|
61
|
-
includeAuthenticator?: boolean;
|
|
62
|
-
includeContentType?: boolean;
|
|
63
|
-
} = {}) => {
|
|
64
|
-
const locale = getClientLocale();
|
|
65
|
-
const timezone = getClientTimezone();
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
Accept: "application/json",
|
|
69
|
-
...(includeContentType ? { "Content-Type": "application/json" } : {}),
|
|
70
|
-
...(includeAuthenticator ? { "X-Authenticator": NOCOBASE_AUTHENTICATOR } : {}),
|
|
71
|
-
...(locale ? { "X-Locale": locale } : {}),
|
|
72
|
-
...(timezone ? { "X-Timezone": timezone } : {}),
|
|
73
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
74
|
-
};
|
|
75
|
-
};
|