@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.
Files changed (108) hide show
  1. package/dist/actions/activity-log/index.cjs +1 -1
  2. package/dist/actions/activity-log/index.mjs +1 -1
  3. package/dist/actions/index.cjs +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/index.mjs +2 -2
  6. package/dist/actions/profile.d.ts +1 -1
  7. package/dist/index.cjs +7 -7
  8. package/dist/index.mjs +7 -7
  9. package/dist/routes.cjs +10 -10
  10. package/dist/routes.mjs +10 -10
  11. package/dist/schema.cjs +1 -1
  12. package/dist/schema.d.ts +1 -1
  13. package/dist/schema.mjs +1 -1
  14. package/dist/ui/activity-log/components/ActivityStatsWidget.cjs +1 -1
  15. package/dist/ui/activity-log/components/ActivityStatsWidget.mjs +1 -1
  16. package/dist/ui/activity-log/components/RecentLogsWidget.cjs +1 -1
  17. package/dist/ui/activity-log/components/RecentLogsWidget.mjs +1 -1
  18. package/dist/ui/activity-log/pages/log-list.cjs +2 -2
  19. package/dist/ui/activity-log/pages/log-list.mjs +1 -1
  20. package/dist/ui/components/app-content.cjs +1 -1
  21. package/dist/ui/components/app-content.mjs +1 -1
  22. package/dist/ui/components/app-sidebar.cjs +3 -3
  23. package/dist/ui/components/app-sidebar.mjs +2 -2
  24. package/dist/ui/components/app-user.cjs +5 -5
  25. package/dist/ui/components/app-user.mjs +4 -4
  26. package/dist/ui/components/breadcrumb-slot.cjs +2 -2
  27. package/dist/ui/components/breadcrumb-slot.mjs +1 -1
  28. package/dist/ui/components/manager/module-card.cjs +3 -3
  29. package/dist/ui/components/manager/module-card.mjs +2 -2
  30. package/dist/ui/components/manager/module-list.cjs +3 -3
  31. package/dist/ui/components/manager/module-list.mjs +2 -2
  32. package/dist/ui/components/manager/module-upload.cjs +3 -3
  33. package/dist/ui/components/manager/module-upload.mjs +2 -2
  34. package/dist/ui/components/profile/components.cjs +7 -7
  35. package/dist/ui/components/profile/components.d.ts +1 -1
  36. package/dist/ui/components/profile/components.mjs +3 -3
  37. package/dist/ui/components/profile/link.cjs +2 -2
  38. package/dist/ui/components/profile/link.mjs +1 -1
  39. package/dist/ui/components/profile/page.cjs +2 -6
  40. package/dist/ui/components/profile/page.mjs +3 -10
  41. package/dist/ui/components/sidebar-slot.cjs +1 -1
  42. package/dist/ui/components/sidebar-slot.mjs +1 -1
  43. package/dist/ui/dashboard/page.cjs +3 -3
  44. package/dist/ui/dashboard/page.mjs +1 -1
  45. package/dist/ui/dashboard/widgets/WelcomeBackUserWidget.cjs +1 -0
  46. package/dist/ui/dashboard/widgets/WelcomeBackUserWidget.mjs +5 -4
  47. package/dist/ui/error.cjs +2 -2
  48. package/dist/ui/error.mjs +1 -1
  49. package/dist/ui/layout.cjs +3 -3
  50. package/dist/ui/layout.mjs +3 -3
  51. package/dist/ui/modules/docs/page.cjs +1 -1
  52. package/dist/ui/modules/docs/page.mjs +1 -1
  53. package/dist/ui/modules/page.cjs +3 -3
  54. package/dist/ui/modules/page.mjs +3 -3
  55. package/dist/ui/rbac/pages/rbac-admin.cjs +16 -16
  56. package/dist/ui/rbac/pages/rbac-admin.mjs +2 -2
  57. package/dist/ui/router.cjs +1 -1
  58. package/dist/ui/router.mjs +1 -1
  59. package/dist/ui/session-manager/components/sessions-list.cjs +7 -7
  60. package/dist/ui/session-manager/components/sessions-list.mjs +3 -3
  61. package/dist/ui/session-manager/pages/sessions-page.cjs +4 -4
  62. package/dist/ui/session-manager/pages/sessions-page.mjs +3 -3
  63. package/dist/ui/settings-page.cjs +3 -3
  64. package/dist/ui/settings-page.mjs +2 -2
  65. package/package.json +7 -6
  66. package/src/actions/actions.ts +17 -0
  67. package/src/actions/activity-log/index.ts +17 -0
  68. package/src/actions/index.ts +2 -0
  69. package/src/actions/manager.ts +168 -0
  70. package/src/actions/profile.ts +173 -0
  71. package/src/actions/rbac/index.ts +131 -0
  72. package/src/actions/session-manager/index.ts +87 -0
  73. package/src/actions/settings.ts +34 -0
  74. package/src/index.ts +135 -0
  75. package/src/intl.d.ts +9 -0
  76. package/src/navigation.ts +57 -0
  77. package/src/routes.ts +107 -0
  78. package/src/schema/activity-log.ts +16 -0
  79. package/src/schema.ts +1 -0
  80. package/src/types.ts +18 -0
  81. package/src/ui/activity-log/components/ActivityStatsWidget.tsx +37 -0
  82. package/src/ui/activity-log/components/RecentLogsWidget.tsx +74 -0
  83. package/src/ui/activity-log/pages/log-list.tsx +91 -0
  84. package/src/ui/components/app-content.tsx +51 -0
  85. package/src/ui/components/app-header.tsx +65 -0
  86. package/src/ui/components/app-sidebar.tsx +249 -0
  87. package/src/ui/components/app-user.tsx +126 -0
  88. package/src/ui/components/breadcrumb-slot.tsx +52 -0
  89. package/src/ui/components/manager/module-card.tsx +327 -0
  90. package/src/ui/components/manager/module-list.tsx +59 -0
  91. package/src/ui/components/manager/module-upload.tsx +84 -0
  92. package/src/ui/components/profile/components.tsx +311 -0
  93. package/src/ui/components/profile/link.tsx +36 -0
  94. package/src/ui/components/profile/page.tsx +45 -0
  95. package/src/ui/components/sidebar-slot.tsx +47 -0
  96. package/src/ui/dashboard/page.tsx +17 -0
  97. package/src/ui/dashboard/widgets/WelcomeBackUserWidget.tsx +47 -0
  98. package/src/ui/error.tsx +82 -0
  99. package/src/ui/layout.tsx +54 -0
  100. package/src/ui/modules/docs/page.tsx +105 -0
  101. package/src/ui/modules/page.tsx +30 -0
  102. package/src/ui/page.tsx +15 -0
  103. package/src/ui/rbac/pages/rbac-admin.tsx +551 -0
  104. package/src/ui/router.tsx +69 -0
  105. package/src/ui/session-manager/components/sessions-list.tsx +303 -0
  106. package/src/ui/session-manager/pages/sessions-page.tsx +22 -0
  107. package/src/ui/settings/page.tsx +73 -0
  108. 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
+ }
@@ -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
+ }