@arch-cadre/panel 1.0.6 → 1.0.9
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/dist/actions/activity-log/index.cjs +1 -1
- package/dist/actions/activity-log/index.mjs +1 -1
- package/dist/actions/index.cjs +2 -2
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.mjs +2 -2
- package/dist/actions/profile.d.ts +1 -1
- package/dist/index.cjs +7 -7
- package/dist/index.mjs +7 -7
- package/dist/routes.cjs +10 -10
- package/dist/routes.mjs +10 -10
- package/dist/schema.cjs +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.mjs +1 -1
- package/dist/ui/activity-log/components/ActivityStatsWidget.cjs +1 -1
- package/dist/ui/activity-log/components/ActivityStatsWidget.mjs +1 -1
- package/dist/ui/activity-log/components/RecentLogsWidget.cjs +1 -1
- package/dist/ui/activity-log/components/RecentLogsWidget.mjs +1 -1
- package/dist/ui/activity-log/pages/log-list.cjs +2 -2
- package/dist/ui/activity-log/pages/log-list.mjs +1 -1
- package/dist/ui/components/app-content.cjs +1 -1
- package/dist/ui/components/app-content.mjs +1 -1
- package/dist/ui/components/app-sidebar.cjs +3 -3
- package/dist/ui/components/app-sidebar.mjs +2 -2
- package/dist/ui/components/app-user.cjs +5 -5
- package/dist/ui/components/app-user.mjs +4 -4
- package/dist/ui/components/breadcrumb-slot.cjs +2 -2
- package/dist/ui/components/breadcrumb-slot.mjs +1 -1
- package/dist/ui/components/manager/module-card.cjs +3 -3
- package/dist/ui/components/manager/module-card.mjs +2 -2
- package/dist/ui/components/manager/module-list.cjs +3 -3
- package/dist/ui/components/manager/module-list.mjs +2 -2
- package/dist/ui/components/manager/module-upload.cjs +3 -3
- package/dist/ui/components/manager/module-upload.mjs +2 -2
- package/dist/ui/components/profile/components.cjs +7 -7
- package/dist/ui/components/profile/components.d.ts +1 -1
- package/dist/ui/components/profile/components.mjs +3 -3
- package/dist/ui/components/profile/link.cjs +2 -2
- package/dist/ui/components/profile/link.mjs +1 -1
- package/dist/ui/components/profile/page.cjs +2 -6
- package/dist/ui/components/profile/page.mjs +3 -10
- package/dist/ui/components/sidebar-slot.cjs +1 -1
- package/dist/ui/components/sidebar-slot.mjs +1 -1
- package/dist/ui/dashboard/page.cjs +3 -3
- package/dist/ui/dashboard/page.mjs +1 -1
- package/dist/ui/dashboard/widgets/WelcomeBackUserWidget.cjs +1 -0
- package/dist/ui/dashboard/widgets/WelcomeBackUserWidget.mjs +5 -4
- package/dist/ui/error.cjs +2 -2
- package/dist/ui/error.mjs +1 -1
- package/dist/ui/layout.cjs +3 -3
- package/dist/ui/layout.mjs +3 -3
- package/dist/ui/modules/docs/page.cjs +1 -1
- package/dist/ui/modules/docs/page.mjs +1 -1
- package/dist/ui/modules/page.cjs +3 -3
- package/dist/ui/modules/page.mjs +3 -3
- package/dist/ui/rbac/pages/rbac-admin.cjs +16 -16
- package/dist/ui/rbac/pages/rbac-admin.mjs +2 -2
- package/dist/ui/router.cjs +1 -1
- package/dist/ui/router.mjs +1 -1
- package/dist/ui/session-manager/components/sessions-list.cjs +7 -7
- package/dist/ui/session-manager/components/sessions-list.mjs +3 -3
- package/dist/ui/session-manager/pages/sessions-page.cjs +4 -4
- package/dist/ui/session-manager/pages/sessions-page.mjs +3 -3
- package/dist/ui/settings-page.cjs +3 -3
- package/dist/ui/settings-page.mjs +2 -2
- package/package.json +7 -6
- package/src/actions/actions.ts +17 -0
- package/src/actions/activity-log/index.ts +17 -0
- package/src/actions/index.ts +2 -0
- package/src/actions/manager.ts +168 -0
- package/src/actions/profile.ts +173 -0
- package/src/actions/rbac/index.ts +131 -0
- package/src/actions/session-manager/index.ts +87 -0
- package/src/actions/settings.ts +34 -0
- package/src/index.ts +135 -0
- package/src/intl.d.ts +9 -0
- package/src/navigation.ts +57 -0
- package/src/routes.ts +107 -0
- package/src/schema/activity-log.ts +16 -0
- package/src/schema.ts +1 -0
- package/src/types.ts +18 -0
- package/src/ui/activity-log/components/ActivityStatsWidget.tsx +37 -0
- package/src/ui/activity-log/components/RecentLogsWidget.tsx +74 -0
- package/src/ui/activity-log/pages/log-list.tsx +91 -0
- package/src/ui/components/app-content.tsx +51 -0
- package/src/ui/components/app-header.tsx +65 -0
- package/src/ui/components/app-sidebar.tsx +249 -0
- package/src/ui/components/app-user.tsx +126 -0
- package/src/ui/components/breadcrumb-slot.tsx +52 -0
- package/src/ui/components/manager/module-card.tsx +327 -0
- package/src/ui/components/manager/module-list.tsx +59 -0
- package/src/ui/components/manager/module-upload.tsx +84 -0
- package/src/ui/components/profile/components.tsx +311 -0
- package/src/ui/components/profile/link.tsx +36 -0
- package/src/ui/components/profile/page.tsx +45 -0
- package/src/ui/components/sidebar-slot.tsx +47 -0
- package/src/ui/dashboard/page.tsx +17 -0
- package/src/ui/dashboard/widgets/WelcomeBackUserWidget.tsx +47 -0
- package/src/ui/error.tsx +82 -0
- package/src/ui/layout.tsx +54 -0
- package/src/ui/modules/docs/page.tsx +105 -0
- package/src/ui/modules/page.tsx +30 -0
- package/src/ui/page.tsx +15 -0
- package/src/ui/rbac/pages/rbac-admin.tsx +551 -0
- package/src/ui/router.tsx +69 -0
- package/src/ui/session-manager/components/sessions-list.tsx +303 -0
- package/src/ui/session-manager/pages/sessions-page.tsx +22 -0
- package/src/ui/settings/page.tsx +73 -0
- package/src/ui/settings-page.tsx +97 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FullUser } from "@arch-cadre/core";
|
|
4
|
+
// import { cn, getInitials } from "@arch-cadre/core";
|
|
5
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
6
|
+
import { cn, Icon, toast } from "@arch-cadre/ui";
|
|
7
|
+
import {
|
|
8
|
+
Avatar,
|
|
9
|
+
AvatarFallback,
|
|
10
|
+
AvatarImage,
|
|
11
|
+
} from "@arch-cadre/ui/components/avatar";
|
|
12
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
13
|
+
import { Input } from "@arch-cadre/ui/components/input";
|
|
14
|
+
import { Label } from "@arch-cadre/ui/components/label";
|
|
15
|
+
import * as React from "react";
|
|
16
|
+
import { useActionState, useRef, useState, useTransition } from "react";
|
|
17
|
+
// import { mutate } from "swr";
|
|
18
|
+
import {
|
|
19
|
+
updateEmailAction,
|
|
20
|
+
updatePasswordAction,
|
|
21
|
+
updateProfileAction,
|
|
22
|
+
} from "../../../actions/profile.js";
|
|
23
|
+
|
|
24
|
+
// Update Profile Form
|
|
25
|
+
export function UpdateProfileForm({ user }: { user: FullUser }) {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const [name, setName] = useState(user.name);
|
|
28
|
+
const [isPending, startTransition] = useTransition();
|
|
29
|
+
const [image, setImage] = useState<string | null>(user.image);
|
|
30
|
+
const [avatar, setAvatar] = useState<File | undefined>(undefined);
|
|
31
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
32
|
+
|
|
33
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
34
|
+
const file = e.target.files?.[0];
|
|
35
|
+
if (file) {
|
|
36
|
+
// if (file.size > 1048576) {
|
|
37
|
+
// alert("File size exceeds 1MB limit");
|
|
38
|
+
// return;
|
|
39
|
+
// }
|
|
40
|
+
setAvatar(file);
|
|
41
|
+
|
|
42
|
+
const reader = new FileReader();
|
|
43
|
+
reader.onload = (event) => {
|
|
44
|
+
setImage(event.target?.result as string);
|
|
45
|
+
};
|
|
46
|
+
reader.readAsDataURL(file);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const triggerFileInput = () => {
|
|
51
|
+
fileInputRef.current?.click();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
if (!name.trim()) {
|
|
57
|
+
toast.error(t("Name is required"));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
startTransition(async () => {
|
|
62
|
+
const result = await updateProfileAction(name.trim(), avatar);
|
|
63
|
+
if (result.error) {
|
|
64
|
+
toast.error(result.error);
|
|
65
|
+
} else {
|
|
66
|
+
// mutate("user");
|
|
67
|
+
// toast.success("Profile updated successfully");
|
|
68
|
+
toast.success(result.message);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
75
|
+
<div className="grid grid-cols-12">
|
|
76
|
+
<div className="col-span-3 flex items-center flex-col">
|
|
77
|
+
<div className="relative mb-2">
|
|
78
|
+
<Avatar className="h-24 w-24 border-2 border-muted">
|
|
79
|
+
<AvatarImage src={image || undefined} alt={name} />
|
|
80
|
+
<AvatarFallback className="text-3xl">
|
|
81
|
+
{name.charAt(0)}
|
|
82
|
+
</AvatarFallback>
|
|
83
|
+
</Avatar>
|
|
84
|
+
<Button
|
|
85
|
+
type="button"
|
|
86
|
+
variant="ghost"
|
|
87
|
+
size="icon"
|
|
88
|
+
className="absolute -top-0.5 -right-0.5 bg-accent rounded-full border-[3px] border-background h-8 w-8 hover:bg-accent"
|
|
89
|
+
onClick={() => {
|
|
90
|
+
if (image) {
|
|
91
|
+
setImage(null);
|
|
92
|
+
if (fileInputRef.current) {
|
|
93
|
+
fileInputRef.current.value = "";
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
triggerFileInput();
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{image ? (
|
|
101
|
+
<Icon
|
|
102
|
+
icon="solar:trash-bin-trash-broken"
|
|
103
|
+
className="h-4 w-4 text-muted-foreground"
|
|
104
|
+
/>
|
|
105
|
+
) : (
|
|
106
|
+
<Icon
|
|
107
|
+
icon="solar:add-circle-broken"
|
|
108
|
+
className="h-3 w-3 text-muted-foreground"
|
|
109
|
+
/>
|
|
110
|
+
)}
|
|
111
|
+
<span className="sr-only">
|
|
112
|
+
{image ? t("Remove image") : t("Upload image")}
|
|
113
|
+
</span>
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<p className="text-center font-medium">{t("Upload Image")}</p>
|
|
118
|
+
<p className="text-center text-sm text-muted-foreground">
|
|
119
|
+
{t("Max file size: 1MB")}
|
|
120
|
+
</p>
|
|
121
|
+
<input
|
|
122
|
+
type="file"
|
|
123
|
+
ref={fileInputRef}
|
|
124
|
+
onChange={handleFileChange}
|
|
125
|
+
accept="image/*"
|
|
126
|
+
className="hidden"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="col-span-9 space-y-4">
|
|
130
|
+
<div className="space-y-2">
|
|
131
|
+
<Label htmlFor="name">{t("Display Name")}</Label>
|
|
132
|
+
<Input
|
|
133
|
+
id="name"
|
|
134
|
+
value={name}
|
|
135
|
+
onChange={(e) => setName(e.target.value)}
|
|
136
|
+
placeholder={t("Your name")}
|
|
137
|
+
disabled={isPending}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<Button type="submit" disabled={isPending}>
|
|
144
|
+
{isPending ? (
|
|
145
|
+
<>
|
|
146
|
+
<Icon
|
|
147
|
+
icon="solar:refresh-broken"
|
|
148
|
+
className="h-4 w-4 mr-2 animate-spin"
|
|
149
|
+
/>
|
|
150
|
+
{t("Saving...")}
|
|
151
|
+
</>
|
|
152
|
+
) : (
|
|
153
|
+
<>
|
|
154
|
+
<Icon icon="solar:diskette-broken" className="h-4 w-4 mr-2" />
|
|
155
|
+
{t("Save Changes")}
|
|
156
|
+
</>
|
|
157
|
+
)}
|
|
158
|
+
</Button>
|
|
159
|
+
</form>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Update Password Form
|
|
164
|
+
const initialUpdatePasswordState = {
|
|
165
|
+
message: "",
|
|
166
|
+
error: false,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export function UpdatePasswordForm() {
|
|
170
|
+
const [state, action, isPending] = useActionState(
|
|
171
|
+
updatePasswordAction,
|
|
172
|
+
initialUpdatePasswordState,
|
|
173
|
+
);
|
|
174
|
+
const { t } = useTranslation();
|
|
175
|
+
const [currentPassword, setCurrentPassword] = useState("");
|
|
176
|
+
const [newPassword, setNewPassword] = useState("");
|
|
177
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<form action={action} className="space-y-4">
|
|
181
|
+
<div className="space-y-2">
|
|
182
|
+
<Label htmlFor="current-password">{t("Current Password")}</Label>
|
|
183
|
+
<Input
|
|
184
|
+
type="password"
|
|
185
|
+
id="current-password"
|
|
186
|
+
name="password"
|
|
187
|
+
value={currentPassword}
|
|
188
|
+
onChange={(e) => setCurrentPassword(e.target.value)}
|
|
189
|
+
autoComplete="current-password"
|
|
190
|
+
placeholder={t("Enter current password")}
|
|
191
|
+
required
|
|
192
|
+
disabled={isPending}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="space-y-2">
|
|
196
|
+
<Label htmlFor="new-password">{t("New Password")}</Label>
|
|
197
|
+
<Input
|
|
198
|
+
type="password"
|
|
199
|
+
id="new-password"
|
|
200
|
+
name="new_password"
|
|
201
|
+
value={newPassword}
|
|
202
|
+
onChange={(e) => setNewPassword(e.target.value)}
|
|
203
|
+
autoComplete="new-password"
|
|
204
|
+
placeholder={t("Enter new password")}
|
|
205
|
+
required
|
|
206
|
+
disabled={isPending}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
<div className="space-y-2">
|
|
210
|
+
<Label htmlFor="confirm-password">{t("Confirm New Password")}</Label>
|
|
211
|
+
<Input
|
|
212
|
+
type="password"
|
|
213
|
+
id="confirm-password"
|
|
214
|
+
value={confirmPassword}
|
|
215
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
216
|
+
autoComplete="new-password"
|
|
217
|
+
placeholder={t("Confirm new password")}
|
|
218
|
+
required
|
|
219
|
+
disabled={isPending}
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
{state.message && (
|
|
223
|
+
<p
|
|
224
|
+
className={cn(
|
|
225
|
+
"text-sm",
|
|
226
|
+
state.error ? "text-destructive" : "text-green-600",
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{state.message}
|
|
230
|
+
</p>
|
|
231
|
+
)}
|
|
232
|
+
<Button type="submit" disabled={isPending}>
|
|
233
|
+
{isPending ? (
|
|
234
|
+
<>
|
|
235
|
+
<Icon
|
|
236
|
+
icon="solar:refresh-broken"
|
|
237
|
+
className="h-4 w-4 mr-2 animate-spin"
|
|
238
|
+
/>
|
|
239
|
+
{t("Updating...")}
|
|
240
|
+
</>
|
|
241
|
+
) : (
|
|
242
|
+
<>
|
|
243
|
+
<Icon icon="solar:key-broken" className="h-4 w-4 mr-2" />
|
|
244
|
+
{t("Update Password")}
|
|
245
|
+
</>
|
|
246
|
+
)}
|
|
247
|
+
</Button>
|
|
248
|
+
</form>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Update Email Form
|
|
253
|
+
const initialUpdateEmailState = {
|
|
254
|
+
message: "",
|
|
255
|
+
error: false,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export function UpdateEmailForm({ user }: { user: FullUser }) {
|
|
259
|
+
const [state, action, isPending] = useActionState(
|
|
260
|
+
updateEmailAction,
|
|
261
|
+
initialUpdateEmailState,
|
|
262
|
+
);
|
|
263
|
+
const { t } = useTranslation();
|
|
264
|
+
const [email, setEmail] = useState(user.email);
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<form action={action} className="space-y-4">
|
|
268
|
+
<div className="space-y-2">
|
|
269
|
+
<Label htmlFor="new-email">{t("New Email Address")}</Label>
|
|
270
|
+
<Input
|
|
271
|
+
type="email"
|
|
272
|
+
id="new-email"
|
|
273
|
+
name="email"
|
|
274
|
+
value={email}
|
|
275
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
276
|
+
placeholder={t("Enter new email address")}
|
|
277
|
+
required
|
|
278
|
+
disabled={isPending}
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
{state.message && (
|
|
282
|
+
<p
|
|
283
|
+
className={cn(
|
|
284
|
+
"text-sm",
|
|
285
|
+
state.error ? "text-destructive" : "text-green-600",
|
|
286
|
+
)}
|
|
287
|
+
>
|
|
288
|
+
{state.message}
|
|
289
|
+
</p>
|
|
290
|
+
)}
|
|
291
|
+
<Button type="submit" disabled={isPending}>
|
|
292
|
+
{isPending ? (
|
|
293
|
+
<>
|
|
294
|
+
<Icon
|
|
295
|
+
icon="solar:refresh-broken"
|
|
296
|
+
className="h-4 w-4 mr-2 animate-spin"
|
|
297
|
+
/>
|
|
298
|
+
{t("Updating...")}
|
|
299
|
+
</>
|
|
300
|
+
) : (
|
|
301
|
+
<>
|
|
302
|
+
<Icon icon="solar:letter-broken" className="h-4 w-4 mr-2" />
|
|
303
|
+
{t("Update Email")}
|
|
304
|
+
</>
|
|
305
|
+
)}
|
|
306
|
+
</Button>
|
|
307
|
+
</form>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Recovery Code Section removed - moved to module
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
5
|
+
import { Icon } from "@arch-cadre/ui";
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenuGroup,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuSeparator,
|
|
10
|
+
} from "@arch-cadre/ui/components/dropdown-menu";
|
|
11
|
+
import Link from "next/link";
|
|
12
|
+
|
|
13
|
+
export function UserDropdownLink() {
|
|
14
|
+
const root = "/kryo";
|
|
15
|
+
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
{/*<DropdownMenuSeparator />*/}
|
|
20
|
+
<DropdownMenuGroup>
|
|
21
|
+
<Link href={`${root}/profile`}>
|
|
22
|
+
<DropdownMenuItem className="cursor-pointer ">
|
|
23
|
+
<Icon icon="solar:user-circle-broken" />
|
|
24
|
+
{t("Profile Settings")}
|
|
25
|
+
</DropdownMenuItem>
|
|
26
|
+
</Link>
|
|
27
|
+
<Link href={`${root}/sessions`}>
|
|
28
|
+
<DropdownMenuItem className="cursor-pointer ">
|
|
29
|
+
<Icon icon="solar:monitor-broken" />
|
|
30
|
+
{t("Manage Device Sessions")}
|
|
31
|
+
</DropdownMenuItem>
|
|
32
|
+
</Link>
|
|
33
|
+
</DropdownMenuGroup>
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { checkSecurity, getCurrentSession } from "@arch-cadre/core/server";
|
|
2
|
+
import { getTranslation } from "@arch-cadre/intl/server";
|
|
3
|
+
// import { ExtensionPointClient } from "@arch-cadre/modules";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
import {
|
|
7
|
+
UpdateEmailForm,
|
|
8
|
+
UpdatePasswordForm,
|
|
9
|
+
UpdateProfileForm,
|
|
10
|
+
} from "./components.js";
|
|
11
|
+
|
|
12
|
+
export default async function ProfileSettingsPage() {
|
|
13
|
+
const { session, user } = await getCurrentSession();
|
|
14
|
+
const { t } = await getTranslation();
|
|
15
|
+
|
|
16
|
+
if (session === null || user === null) {
|
|
17
|
+
return redirect("/signin");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const security = await checkSecurity(session, user);
|
|
21
|
+
|
|
22
|
+
if (!security.satisfied && security.redirect) {
|
|
23
|
+
return redirect(security.redirect);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="space-y-6 max-w-2xl">
|
|
28
|
+
<div>
|
|
29
|
+
<h2 className="text-2xl font-bold">{t("Profile Settings")}</h2>
|
|
30
|
+
<p className="text-muted-foreground">
|
|
31
|
+
{t("Manage your account settings and profile information.")}
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<UpdateProfileForm user={user} />
|
|
36
|
+
<UpdateEmailForm user={user} />
|
|
37
|
+
<UpdatePasswordForm />
|
|
38
|
+
|
|
39
|
+
{/* <ExtensionPointClient
|
|
40
|
+
module="user-profile"
|
|
41
|
+
point="settings:extra-sections"
|
|
42
|
+
/> */}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SidebarGroupType } from "@arch-cadre/modules";
|
|
2
|
+
import { getKryoModuleNavigationGrouped } from "@arch-cadre/modules/server";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { AppSidebar } from "./app-sidebar.js";
|
|
5
|
+
|
|
6
|
+
const staticData: SidebarGroupType[] = [];
|
|
7
|
+
|
|
8
|
+
export default async function SidebarSlot() {
|
|
9
|
+
const moduleGroups = await getKryoModuleNavigationGrouped("admin");
|
|
10
|
+
|
|
11
|
+
// 1. Inicjalizujemy mapę grup danymi statycznymi
|
|
12
|
+
const groupsMap = new Map<string, SidebarGroupType>();
|
|
13
|
+
|
|
14
|
+
for (const group of staticData) {
|
|
15
|
+
groupsMap.set(group.title, { ...group });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 2. Scalamy grupy dynamiczne z modułów
|
|
19
|
+
for (const [title, items] of Object.entries(moduleGroups)) {
|
|
20
|
+
if (groupsMap.has(title)) {
|
|
21
|
+
const existing = groupsMap.get(title)!;
|
|
22
|
+
for (const item of items as any[]) {
|
|
23
|
+
if (!existing.items.some((i) => i.url === item.url)) {
|
|
24
|
+
existing.items.push(item);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
groupsMap.set(title, {
|
|
29
|
+
title,
|
|
30
|
+
items: items as any,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Przygotowujemy grupy do wyświetlenia i sortujemy
|
|
36
|
+
const finalData = Array.from(groupsMap.values());
|
|
37
|
+
|
|
38
|
+
finalData.sort((a, b) => a.title.localeCompare(b.title));
|
|
39
|
+
|
|
40
|
+
// 4. Budujemy strukturę finalną dla AppSidebar
|
|
41
|
+
const sidebarData: SidebarGroupType[] = finalData.map(({ title, items }) => ({
|
|
42
|
+
title,
|
|
43
|
+
items,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
return <AppSidebar isMain={true} data={sidebarData} />;
|
|
47
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { WidgetArea } from "@arch-cadre/modules";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
export default async function Page() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex flex-col gap-6">
|
|
7
|
+
<WidgetArea
|
|
8
|
+
area="dashboard-stats"
|
|
9
|
+
className="grid grid-cols-1 md:grid-cols-3 gap-6"
|
|
10
|
+
/>
|
|
11
|
+
<WidgetArea
|
|
12
|
+
area="dashboard-main"
|
|
13
|
+
className="grid grid-cols-1 md:grid-cols-2 gap-6"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { getCurrentSession } from "@arch-cadre/core/server";
|
|
4
|
+
import { getTranslation } from "@arch-cadre/intl/server";
|
|
5
|
+
import {
|
|
6
|
+
Avatar,
|
|
7
|
+
AvatarFallback,
|
|
8
|
+
AvatarImage,
|
|
9
|
+
Card,
|
|
10
|
+
CardContent,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from "@arch-cadre/ui";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
|
|
16
|
+
export default async function WelcomeBackUserWidget() {
|
|
17
|
+
const { user } = await getCurrentSession();
|
|
18
|
+
const { t } = await getTranslation();
|
|
19
|
+
|
|
20
|
+
if (!user) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card className="col-span-1 md:col-span-2 lg:col-span-1">
|
|
24
|
+
<CardHeader className="flex flex-row items-center gap-4 pb-2">
|
|
25
|
+
<Avatar className="h-12 w-12">
|
|
26
|
+
<AvatarImage src={user.image || ""} alt={user.name || "User"} />
|
|
27
|
+
<AvatarFallback>
|
|
28
|
+
{user.name?.substring(0, 2).toUpperCase() || "U"}
|
|
29
|
+
</AvatarFallback>
|
|
30
|
+
</Avatar>
|
|
31
|
+
<div className="flex flex-col">
|
|
32
|
+
<CardTitle className="text-lg font-bold">
|
|
33
|
+
{t("Welcome back, {name}!", { name: user.name })}
|
|
34
|
+
</CardTitle>
|
|
35
|
+
<p className="text-sm text-muted-foreground">
|
|
36
|
+
{t("It's good to see you again.")}
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
</CardHeader>
|
|
40
|
+
<CardContent>
|
|
41
|
+
<div className="text-sm text-muted-foreground">
|
|
42
|
+
{t("You are logged in as {email}", { email: user.email })}
|
|
43
|
+
</div>
|
|
44
|
+
</CardContent>
|
|
45
|
+
</Card>
|
|
46
|
+
);
|
|
47
|
+
}
|
package/src/ui/error.tsx
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslation } from "@arch-cadre/intl";
|
|
4
|
+
import { Button } from "@arch-cadre/ui/components/button";
|
|
5
|
+
import {
|
|
6
|
+
Empty,
|
|
7
|
+
EmptyContent,
|
|
8
|
+
EmptyDescription,
|
|
9
|
+
EmptyHeader,
|
|
10
|
+
EmptyTitle,
|
|
11
|
+
} from "@arch-cadre/ui/components/empty";
|
|
12
|
+
import { Icon } from "@iconify/react";
|
|
13
|
+
import * as React from "react";
|
|
14
|
+
import { useEffect } from "react";
|
|
15
|
+
|
|
16
|
+
export default function ErrorPage({
|
|
17
|
+
error,
|
|
18
|
+
reset,
|
|
19
|
+
}: {
|
|
20
|
+
error: Error & { digest?: string };
|
|
21
|
+
reset: () => void;
|
|
22
|
+
}) {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
console.error(error);
|
|
26
|
+
}, [error]);
|
|
27
|
+
|
|
28
|
+
const dbError = error.message.startsWith("DB_ERROR");
|
|
29
|
+
const unauthorizedError = error.message.startsWith("UNAUTHORIZED");
|
|
30
|
+
const otherError = !dbError && !unauthorizedError;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex-1 flex w-full items-center justify-center">
|
|
34
|
+
<div className="flex w-full justify-center h-[calc(100svh-56px-var(--header-height))] items-center">
|
|
35
|
+
<div>
|
|
36
|
+
<Empty>
|
|
37
|
+
<EmptyHeader className="max-w-xl">
|
|
38
|
+
<EmptyTitle className="font-black font-mono text-5xl mb-5">
|
|
39
|
+
{t("Error")}
|
|
40
|
+
</EmptyTitle>
|
|
41
|
+
<EmptyDescription className="text-nowrap">
|
|
42
|
+
{t("An error occurred while loading the page.")}
|
|
43
|
+
{dbError && (
|
|
44
|
+
<>
|
|
45
|
+
<p>{t("errors.db_error")}</p>
|
|
46
|
+
<ul className="list-inside list-disc text-sm">
|
|
47
|
+
<li>{t("Check database connection")}</li>
|
|
48
|
+
<li>{t("Check core settings")}</li>
|
|
49
|
+
<li>{t("Check module dependencies")}</li>
|
|
50
|
+
</ul>
|
|
51
|
+
</>
|
|
52
|
+
)}
|
|
53
|
+
{unauthorizedError && (
|
|
54
|
+
<>
|
|
55
|
+
<p>{t("Unauthorized")}</p>
|
|
56
|
+
<ul className="list-inside list-disc text-sm">
|
|
57
|
+
<li>
|
|
58
|
+
{t("Unauthorized access to the requested resource")}
|
|
59
|
+
</li>
|
|
60
|
+
<li>{t("Check permissions for the current user")}</li>
|
|
61
|
+
</ul>
|
|
62
|
+
</>
|
|
63
|
+
)}
|
|
64
|
+
{otherError && <p>{error.message ?? t("Unknown error")}</p>}
|
|
65
|
+
</EmptyDescription>
|
|
66
|
+
</EmptyHeader>
|
|
67
|
+
<EmptyContent>
|
|
68
|
+
<Button
|
|
69
|
+
variant="destructive"
|
|
70
|
+
className="mt-4"
|
|
71
|
+
onClick={() => reset()}
|
|
72
|
+
>
|
|
73
|
+
<Icon icon="solar:refresh-circle-broken" />
|
|
74
|
+
{t("Try Again")}
|
|
75
|
+
</Button>
|
|
76
|
+
</EmptyContent>
|
|
77
|
+
</Empty>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { checkSecurity, getCurrentSession } from "@arch-cadre/core/server";
|
|
4
|
+
import { SidebarProvider } from "@arch-cadre/ui/components/sidebar";
|
|
5
|
+
import { AccessDenied } from "@arch-cadre/ui/shared/access-denied";
|
|
6
|
+
import { redirect } from "next/navigation";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { AppContent } from "./components/app-content.js";
|
|
9
|
+
import BreadcrumbSlot from "./components/breadcrumb-slot.js";
|
|
10
|
+
import SidebarSlot from "./components/sidebar-slot.js";
|
|
11
|
+
|
|
12
|
+
export default async function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: Readonly<{
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}>) {
|
|
17
|
+
const { user, session } = await getCurrentSession();
|
|
18
|
+
|
|
19
|
+
if (session === null || user === null) {
|
|
20
|
+
return redirect("/signin");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Security requirements check (EXTENSIBLE - handles email, 2FA, etc.)
|
|
24
|
+
const security = await checkSecurity(session, user, ["admin", "user"]);
|
|
25
|
+
|
|
26
|
+
if (!security.satisfied) {
|
|
27
|
+
if (security.redirect && security.redirect !== "/kryo") {
|
|
28
|
+
return redirect(security.redirect);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex items-center justify-center min-h-screen bg-background w-full">
|
|
33
|
+
<AccessDenied />
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="bg-sidebar [--header-height:calc(--spacing(14))]">
|
|
40
|
+
<SidebarProvider
|
|
41
|
+
style={
|
|
42
|
+
{
|
|
43
|
+
"--sidebar": "transparent",
|
|
44
|
+
"--sidebar-width": "280px",
|
|
45
|
+
} as React.CSSProperties
|
|
46
|
+
}
|
|
47
|
+
>
|
|
48
|
+
<SidebarSlot />
|
|
49
|
+
|
|
50
|
+
<AppContent breadcrumbs={<BreadcrumbSlot />}>{children}</AppContent>
|
|
51
|
+
</SidebarProvider>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|