@nocobase/portal-template-default 1.0.0
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/.env.example +10 -0
- package/Dockerfile +36 -0
- package/README.MD +50 -0
- package/components.json +25 -0
- package/eslint.config.js +28 -0
- package/index.html +39 -0
- package/package.json +84 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/favicon.ico +0 -0
- package/public/logo-dark.png +0 -0
- package/public/logo-mark-dark.png +0 -0
- package/public/logo-mark.png +0 -0
- package/public/logo.png +0 -0
- package/src/App.css +158 -0
- package/src/App.tsx +151 -0
- package/src/components/app-shell/brand.tsx +70 -0
- package/src/components/app-shell/breadcrumb.tsx +81 -0
- package/src/components/app-shell/document-title-handler.tsx +169 -0
- package/src/components/app-shell/error-component.tsx +134 -0
- package/src/components/app-shell/header.tsx +127 -0
- package/src/components/app-shell/layout.tsx +39 -0
- package/src/components/app-shell/loading-overlay.tsx +36 -0
- package/src/components/app-shell/sidebar.tsx +351 -0
- package/src/components/app-shell/user-avatar.tsx +42 -0
- package/src/components/app-shell/user-info.tsx +53 -0
- package/src/components/auth/auth-layout.tsx +82 -0
- package/src/components/auth/forgot-password-form.tsx +58 -0
- package/src/components/auth/input-password.tsx +38 -0
- package/src/components/auth/sign-in-form.tsx +111 -0
- package/src/components/auth/sign-up-form.tsx +127 -0
- package/src/components/data-table/data-table-filter.tsx +994 -0
- package/src/components/data-table/data-table-pagination.tsx +146 -0
- package/src/components/data-table/data-table-sorter.tsx +47 -0
- package/src/components/data-table/data-table.tsx +321 -0
- package/src/components/notifications/toaster.tsx +23 -0
- package/src/components/notifications/undoable-notification.tsx +84 -0
- package/src/components/notifications/use-notification-provider.tsx +62 -0
- package/src/components/resources/buttons/clone.tsx +81 -0
- package/src/components/resources/buttons/create.tsx +70 -0
- package/src/components/resources/buttons/delete.tsx +113 -0
- package/src/components/resources/buttons/edit.tsx +81 -0
- package/src/components/resources/buttons/list.tsx +70 -0
- package/src/components/resources/buttons/refresh.tsx +81 -0
- package/src/components/resources/buttons/show.tsx +81 -0
- package/src/components/resources/status-badge.tsx +37 -0
- package/src/components/resources/views/create-view.tsx +81 -0
- package/src/components/resources/views/edit-view.tsx +104 -0
- package/src/components/resources/views/list-view.tsx +86 -0
- package/src/components/resources/views/show-view.tsx +108 -0
- package/src/components/theme/theme-provider.tsx +71 -0
- package/src/components/theme/theme-select.tsx +97 -0
- package/src/components/theme/theme-toggle.tsx +66 -0
- package/src/components/ui/accordion.tsx +72 -0
- package/src/components/ui/alert-dialog.tsx +185 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/avatar.tsx +107 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/breadcrumb.tsx +125 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/calendar.tsx +221 -0
- package/src/components/ui/card.tsx +103 -0
- package/src/components/ui/carousel.tsx +240 -0
- package/src/components/ui/chart.tsx +373 -0
- package/src/components/ui/checkbox.tsx +29 -0
- package/src/components/ui/collapsible.tsx +19 -0
- package/src/components/ui/command.tsx +192 -0
- package/src/components/ui/context-menu.tsx +271 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/drawer.tsx +226 -0
- package/src/components/ui/dropdown-menu.tsx +268 -0
- package/src/components/ui/form.tsx +174 -0
- package/src/components/ui/hover-card.tsx +49 -0
- package/src/components/ui/input-group.tsx +158 -0
- package/src/components/ui/input-otp.tsx +87 -0
- package/src/components/ui/input.tsx +20 -0
- package/src/components/ui/label.tsx +18 -0
- package/src/components/ui/menubar.tsx +278 -0
- package/src/components/ui/navigation-menu.tsx +168 -0
- package/src/components/ui/pagination.tsx +130 -0
- package/src/components/ui/popover.tsx +91 -0
- package/src/components/ui/progress.tsx +81 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/resizable.tsx +48 -0
- package/src/components/ui/scroll-area.tsx +55 -0
- package/src/components/ui/select.tsx +199 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/sheet.tsx +136 -0
- package/src/components/ui/sidebar.tsx +723 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +52 -0
- package/src/components/ui/sonner.tsx +47 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/tabs.tsx +82 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/toggle-group.tsx +87 -0
- package/src/components/ui/toggle.tsx +43 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.tsx +17 -0
- package/src/lib/utils.ts +19 -0
- package/src/pages/blog-posts/create.tsx +177 -0
- package/src/pages/blog-posts/edit.tsx +197 -0
- package/src/pages/blog-posts/index.ts +4 -0
- package/src/pages/blog-posts/list.tsx +131 -0
- package/src/pages/blog-posts/show.tsx +81 -0
- package/src/pages/categories/create.tsx +73 -0
- package/src/pages/categories/edit.tsx +73 -0
- package/src/pages/categories/index.ts +4 -0
- package/src/pages/categories/list.tsx +86 -0
- package/src/pages/categories/show.tsx +37 -0
- package/src/pages/forgot-password/index.tsx +6 -0
- package/src/pages/forgotPassword/index.tsx +5 -0
- package/src/pages/login/index.tsx +5 -0
- package/src/pages/register/index.tsx +5 -0
- package/src/providers/auth.ts +204 -0
- package/src/providers/constants.ts +75 -0
- package/src/providers/data.ts +232 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +25 -0
- package/tsconfig.node.json +21 -0
- package/vite.config.ts +74 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useForm } from "@refinedev/react-hook-form";
|
|
2
|
+
import { useNavigate } from "react-router";
|
|
3
|
+
|
|
4
|
+
import { EditView } from "@/components/resources/views/edit-view";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import {
|
|
8
|
+
Form,
|
|
9
|
+
FormControl,
|
|
10
|
+
FormField,
|
|
11
|
+
FormItem,
|
|
12
|
+
FormLabel,
|
|
13
|
+
FormMessage,
|
|
14
|
+
} from "@/components/ui/form";
|
|
15
|
+
|
|
16
|
+
export const CategoryEdit = () => {
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
refineCore: { onFinish },
|
|
21
|
+
...form
|
|
22
|
+
} = useForm({
|
|
23
|
+
refineCoreProps: {},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function onSubmit(values: Record<string, string>) {
|
|
27
|
+
onFinish(values);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<EditView>
|
|
32
|
+
<Form {...form}>
|
|
33
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="resource-form">
|
|
34
|
+
<FormField
|
|
35
|
+
control={form.control}
|
|
36
|
+
name="title"
|
|
37
|
+
rules={{ required: "Title is required" }}
|
|
38
|
+
render={({ field }) => (
|
|
39
|
+
<FormItem>
|
|
40
|
+
<FormLabel>Title</FormLabel>
|
|
41
|
+
<FormControl
|
|
42
|
+
render={<Input
|
|
43
|
+
{...field}
|
|
44
|
+
value={field.value || ""}
|
|
45
|
+
placeholder="Enter category title"
|
|
46
|
+
/>}
|
|
47
|
+
/>
|
|
48
|
+
<FormMessage />
|
|
49
|
+
</FormItem>
|
|
50
|
+
)}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<div className="flex gap-2">
|
|
54
|
+
<Button
|
|
55
|
+
type="submit"
|
|
56
|
+
{...form.saveButtonProps}
|
|
57
|
+
disabled={form.formState.isSubmitting}
|
|
58
|
+
>
|
|
59
|
+
{form.formState.isSubmitting ? "Updating..." : "Update"}
|
|
60
|
+
</Button>
|
|
61
|
+
<Button
|
|
62
|
+
type="button"
|
|
63
|
+
variant="outline"
|
|
64
|
+
onClick={() => navigate(-1)}
|
|
65
|
+
>
|
|
66
|
+
Cancel
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
</form>
|
|
70
|
+
</Form>
|
|
71
|
+
</EditView>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type BaseRecord } from "@refinedev/core";
|
|
2
|
+
import { useTable } from "@refinedev/react-table";
|
|
3
|
+
import { createColumnHelper } from "@tanstack/react-table";
|
|
4
|
+
import { Eye, Pencil, Trash2 } from "lucide-react";
|
|
5
|
+
import React from "react";
|
|
6
|
+
|
|
7
|
+
import { DataTable } from "@/components/data-table/data-table";
|
|
8
|
+
import { ListView } from "@/components/resources/views/list-view";
|
|
9
|
+
import { EditButton } from "@/components/resources/buttons/edit";
|
|
10
|
+
import { ShowButton } from "@/components/resources/buttons/show";
|
|
11
|
+
import { DeleteButton } from "@/components/resources/buttons/delete";
|
|
12
|
+
|
|
13
|
+
type Category = {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const CategoryList = () => {
|
|
19
|
+
const columns = React.useMemo(() => {
|
|
20
|
+
const columnHelper = createColumnHelper<Category>();
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
columnHelper.accessor("id", {
|
|
24
|
+
id: "id",
|
|
25
|
+
header: "ID",
|
|
26
|
+
enableSorting: false,
|
|
27
|
+
}),
|
|
28
|
+
columnHelper.accessor("title", {
|
|
29
|
+
id: "title",
|
|
30
|
+
header: "Title",
|
|
31
|
+
enableSorting: true,
|
|
32
|
+
}),
|
|
33
|
+
columnHelper.display({
|
|
34
|
+
id: "actions",
|
|
35
|
+
header: "Actions",
|
|
36
|
+
cell: ({ row }) => (
|
|
37
|
+
<div className="flex items-center gap-1">
|
|
38
|
+
<EditButton
|
|
39
|
+
recordItemId={row.original.id}
|
|
40
|
+
variant="ghost"
|
|
41
|
+
size="icon"
|
|
42
|
+
aria-label="Edit category"
|
|
43
|
+
title="Edit"
|
|
44
|
+
>
|
|
45
|
+
<Pencil />
|
|
46
|
+
</EditButton>
|
|
47
|
+
<ShowButton
|
|
48
|
+
recordItemId={row.original.id}
|
|
49
|
+
variant="ghost"
|
|
50
|
+
size="icon"
|
|
51
|
+
aria-label="View category"
|
|
52
|
+
title="View"
|
|
53
|
+
>
|
|
54
|
+
<Eye />
|
|
55
|
+
</ShowButton>
|
|
56
|
+
<DeleteButton
|
|
57
|
+
recordItemId={row.original.id}
|
|
58
|
+
variant="ghost"
|
|
59
|
+
size="icon"
|
|
60
|
+
className="text-destructive hover:text-destructive"
|
|
61
|
+
aria-label="Delete category"
|
|
62
|
+
title="Delete"
|
|
63
|
+
>
|
|
64
|
+
<Trash2 />
|
|
65
|
+
</DeleteButton>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
enableSorting: false,
|
|
69
|
+
size: 144,
|
|
70
|
+
}),
|
|
71
|
+
];
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const table = useTable({
|
|
75
|
+
columns,
|
|
76
|
+
refineCoreProps: {
|
|
77
|
+
syncWithLocation: true,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<ListView>
|
|
83
|
+
<DataTable table={table} />
|
|
84
|
+
</ListView>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useShow } from "@refinedev/core";
|
|
2
|
+
import React from "react";
|
|
3
|
+
|
|
4
|
+
import { ShowView } from "@/components/resources/views/show-view";
|
|
5
|
+
import {
|
|
6
|
+
Card,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
} from "@/components/ui/card";
|
|
12
|
+
|
|
13
|
+
export const CategoryShow = () => {
|
|
14
|
+
const { result: record, query } = useShow({});
|
|
15
|
+
const { isLoading } = query;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<ShowView>
|
|
19
|
+
<Card className="resource-detail-card max-w-3xl">
|
|
20
|
+
<CardHeader>
|
|
21
|
+
<CardTitle>{record?.title}</CardTitle>
|
|
22
|
+
<CardDescription>Category ID: {record?.id}</CardDescription>
|
|
23
|
+
</CardHeader>
|
|
24
|
+
<CardContent>
|
|
25
|
+
<div className="space-y-4">
|
|
26
|
+
<div>
|
|
27
|
+
<h4 className="text-sm font-medium mb-2">Title</h4>
|
|
28
|
+
<p className="text-sm text-muted-foreground">
|
|
29
|
+
{record?.title || "-"}
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</CardContent>
|
|
34
|
+
</Card>
|
|
35
|
+
</ShowView>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { AuthProvider } from "@refinedev/core";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AUTH_API_URL,
|
|
5
|
+
getNocoBaseHeaders,
|
|
6
|
+
NOCOBASE_TOKEN_KEY,
|
|
7
|
+
} from "./constants";
|
|
8
|
+
|
|
9
|
+
type NocoBaseUser = {
|
|
10
|
+
id: number | string;
|
|
11
|
+
nickname?: string;
|
|
12
|
+
username?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
avatar?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type NocoBaseSignInResponse = {
|
|
18
|
+
token?: string;
|
|
19
|
+
user?: NocoBaseUser;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type CurrentUserCache = {
|
|
23
|
+
token: string;
|
|
24
|
+
user: NocoBaseUser;
|
|
25
|
+
expiresAt: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const CURRENT_USER_CACHE_MS = 30_000;
|
|
29
|
+
let currentUserCache: CurrentUserCache | undefined;
|
|
30
|
+
let currentUserRequest: Promise<NocoBaseUser> | undefined;
|
|
31
|
+
|
|
32
|
+
const authUrl = (action: "signIn" | "signOut" | "check") =>
|
|
33
|
+
`${AUTH_API_URL.replace(/\/$/, "")}/auth:${action}`;
|
|
34
|
+
|
|
35
|
+
const getErrorMessage = (payload: unknown, fallback: string) => {
|
|
36
|
+
if (typeof payload !== "object" || payload === null) return fallback;
|
|
37
|
+
const error = payload as { message?: string; errors?: Array<{ message?: string }> };
|
|
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);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const clearCurrentUserCache = () => {
|
|
47
|
+
currentUserCache = undefined;
|
|
48
|
+
currentUserRequest = undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getCurrentUser = async (): Promise<NocoBaseUser> => {
|
|
52
|
+
const token = localStorage.getItem(NOCOBASE_TOKEN_KEY);
|
|
53
|
+
if (!token) throw new Error("No NocoBase token");
|
|
54
|
+
|
|
55
|
+
if (currentUserCache?.token === token && currentUserCache.expiresAt > Date.now()) {
|
|
56
|
+
return currentUserCache.user;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (currentUserRequest) return currentUserRequest;
|
|
60
|
+
|
|
61
|
+
currentUserRequest = (async () => {
|
|
62
|
+
const response = await fetch(authUrl("check"), {
|
|
63
|
+
headers: getNocoBaseHeaders({ token, includeAuthenticator: true }),
|
|
64
|
+
});
|
|
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
|
+
currentUserCache = {
|
|
71
|
+
token: localStorage.getItem(NOCOBASE_TOKEN_KEY) ?? token,
|
|
72
|
+
user,
|
|
73
|
+
expiresAt: Date.now() + CURRENT_USER_CACHE_MS,
|
|
74
|
+
};
|
|
75
|
+
return user;
|
|
76
|
+
})();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
return await currentUserRequest;
|
|
80
|
+
} finally {
|
|
81
|
+
currentUserRequest = undefined;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const authProvider: AuthProvider = {
|
|
86
|
+
login: async ({ username, email, password, providerName }) => {
|
|
87
|
+
if (providerName) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: {
|
|
91
|
+
name: "UnsupportedAuthenticator",
|
|
92
|
+
message: "Configure this provider as a NocoBase authenticator before using social sign-in.",
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const account = username ?? email;
|
|
98
|
+
if (!account || !password) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: { name: "LoginError", message: "Please enter your account and password." },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(authUrl("signIn"), {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: getNocoBaseHeaders({
|
|
109
|
+
includeAuthenticator: true,
|
|
110
|
+
includeContentType: true,
|
|
111
|
+
}),
|
|
112
|
+
body: JSON.stringify({ account, password }),
|
|
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;
|
|
123
|
+
if (!result.token) {
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
error: { name: "LoginError", message: "NocoBase did not return an access token." },
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
localStorage.setItem(NOCOBASE_TOKEN_KEY, result.token);
|
|
131
|
+
clearCurrentUserCache();
|
|
132
|
+
return { success: true, redirectTo: "/" };
|
|
133
|
+
} catch {
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: {
|
|
137
|
+
name: "NetworkError",
|
|
138
|
+
message:
|
|
139
|
+
"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.",
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
logout: async () => {
|
|
146
|
+
const token = localStorage.getItem(NOCOBASE_TOKEN_KEY);
|
|
147
|
+
try {
|
|
148
|
+
if (token) {
|
|
149
|
+
await fetch(authUrl("signOut"), {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: getNocoBaseHeaders({ token, includeAuthenticator: true }),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
localStorage.removeItem(NOCOBASE_TOKEN_KEY);
|
|
156
|
+
clearCurrentUserCache();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { success: true, redirectTo: "/login" };
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
check: async () => {
|
|
163
|
+
try {
|
|
164
|
+
await getCurrentUser();
|
|
165
|
+
return { authenticated: true };
|
|
166
|
+
} catch {
|
|
167
|
+
localStorage.removeItem(NOCOBASE_TOKEN_KEY);
|
|
168
|
+
clearCurrentUserCache();
|
|
169
|
+
return { authenticated: false, redirectTo: "/login" };
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
getPermissions: async () => null,
|
|
174
|
+
|
|
175
|
+
getIdentity: async () => {
|
|
176
|
+
try {
|
|
177
|
+
const user = await getCurrentUser();
|
|
178
|
+
const fullName = user.nickname ?? user.username ?? user.email ?? "NocoBase user";
|
|
179
|
+
return {
|
|
180
|
+
id: user.id,
|
|
181
|
+
firstName: fullName,
|
|
182
|
+
lastName: "",
|
|
183
|
+
fullName,
|
|
184
|
+
email: user.email ?? "",
|
|
185
|
+
avatar: user.avatar,
|
|
186
|
+
};
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
onError: async (error) => {
|
|
193
|
+
const status = (error as { status?: number; statusCode?: number }).status ??
|
|
194
|
+
(error as { status?: number; statusCode?: number }).statusCode;
|
|
195
|
+
|
|
196
|
+
if (status === 401 || status === 403) {
|
|
197
|
+
localStorage.removeItem(NOCOBASE_TOKEN_KEY);
|
|
198
|
+
clearCurrentUserCache();
|
|
199
|
+
return { logout: true, redirectTo: "/login" };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return { error };
|
|
203
|
+
},
|
|
204
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const rawApiUrl =
|
|
2
|
+
import.meta.env.NOCOBASE_API_URL ?? "http://127.0.0.1:13000/api";
|
|
3
|
+
|
|
4
|
+
const getDefaultProxyTarget = (apiUrl?: string) => {
|
|
5
|
+
if (!apiUrl || apiUrl.startsWith("/")) return undefined;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
return new URL(apiUrl).origin;
|
|
9
|
+
} catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const proxyTarget = import.meta.env.DEV
|
|
15
|
+
? getDefaultProxyTarget(rawApiUrl)
|
|
16
|
+
: undefined;
|
|
17
|
+
|
|
18
|
+
const toProxyRelativeUrl = (url: string, target?: string) => {
|
|
19
|
+
if (!target || url.startsWith("/")) return url;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const parsedUrl = new URL(url);
|
|
23
|
+
const parsedTarget = new URL(target);
|
|
24
|
+
|
|
25
|
+
if (parsedUrl.origin === parsedTarget.origin) {
|
|
26
|
+
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return url;
|
|
33
|
+
};
|
|
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
|
+
export const API_URL = toProxyRelativeUrl(rawApiUrl, proxyTarget);
|
|
49
|
+
export const AUTH_API_URL = API_URL;
|
|
50
|
+
export const TOKEN_KEY = "refine-auth";
|
|
51
|
+
export const NOCOBASE_TOKEN_KEY = "nocobase-auth-token";
|
|
52
|
+
export const NOCOBASE_AUTHENTICATOR =
|
|
53
|
+
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
|
+
};
|