@seedgrid/fe-security 2026.7.0 → 2026.7.2

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 (125) hide show
  1. package/README.md +113 -0
  2. package/dist/client/index.d.ts +4 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +5 -0
  5. package/dist/client/permission-provider.d.ts +18 -0
  6. package/dist/client/permission-provider.d.ts.map +1 -0
  7. package/dist/client/permission-provider.js +57 -0
  8. package/dist/client/pick-list-field.d.ts +15 -0
  9. package/dist/client/pick-list-field.d.ts.map +1 -0
  10. package/dist/client/pick-list-field.js +30 -0
  11. package/dist/client/status-badge.d.ts +6 -0
  12. package/dist/client/status-badge.d.ts.map +1 -0
  13. package/dist/client/status-badge.js +6 -0
  14. package/dist/config.d.ts +34 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +4 -0
  17. package/dist/dtos.d.ts +88 -0
  18. package/dist/dtos.d.ts.map +1 -0
  19. package/dist/dtos.js +3 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +10 -0
  23. package/dist/menu.d.ts +19 -0
  24. package/dist/menu.d.ts.map +1 -0
  25. package/dist/menu.js +41 -0
  26. package/dist/messages.d.ts +1858 -0
  27. package/dist/messages.d.ts.map +1 -0
  28. package/dist/messages.js +990 -0
  29. package/dist/module.d.ts +3 -0
  30. package/dist/module.d.ts.map +1 -0
  31. package/dist/module.js +37 -0
  32. package/dist/module.json +21 -0
  33. package/dist/paths.d.ts +1 -0
  34. package/dist/paths.d.ts.map +1 -1
  35. package/dist/paths.js +3 -0
  36. package/dist/permissions.d.ts +14 -0
  37. package/dist/permissions.d.ts.map +1 -0
  38. package/dist/permissions.js +39 -0
  39. package/dist/server/action.d.ts +3 -0
  40. package/dist/server/action.d.ts.map +1 -0
  41. package/dist/server/action.js +21 -0
  42. package/dist/server/create-security-server.d.ts +4 -0
  43. package/dist/server/create-security-server.d.ts.map +1 -0
  44. package/dist/server/create-security-server.js +166 -0
  45. package/dist/server/errors.d.ts +13 -0
  46. package/dist/server/errors.d.ts.map +1 -0
  47. package/dist/server/errors.js +26 -0
  48. package/dist/server/index.d.ts +8 -0
  49. package/dist/server/index.d.ts.map +1 -0
  50. package/dist/server/index.js +8 -0
  51. package/dist/server/problem-response.d.ts +3 -0
  52. package/dist/server/problem-response.d.ts.map +1 -0
  53. package/dist/server/problem-response.js +28 -0
  54. package/dist/server/route.d.ts +3 -0
  55. package/dist/server/route.d.ts.map +1 -0
  56. package/dist/server/route.js +22 -0
  57. package/dist/server/to-problem.d.ts +3 -0
  58. package/dist/server/to-problem.d.ts.map +1 -0
  59. package/dist/server/to-problem.js +22 -0
  60. package/dist/server-contract.d.ts +23 -0
  61. package/dist/server-contract.d.ts.map +1 -0
  62. package/dist/server-contract.js +4 -0
  63. package/dist/session-contract.d.ts +36 -0
  64. package/dist/session-contract.d.ts.map +1 -0
  65. package/dist/session-contract.js +8 -0
  66. package/dist/session-result.d.ts +22 -0
  67. package/dist/session-result.d.ts.map +1 -0
  68. package/dist/session-result.js +14 -0
  69. package/package.json +38 -3
  70. package/scaffold/README.md +97 -0
  71. package/scaffold/src/app/api/security/session/2fa/resend/route.ts +8 -0
  72. package/scaffold/src/app/api/security/session/2fa/verify/route.ts +8 -0
  73. package/scaffold/src/app/api/security/session/login/route.ts +7 -0
  74. package/scaffold/src/app/api/security/session/logout/route.ts +5 -0
  75. package/scaffold/src/app/api/security/session/refresh/route.ts +7 -0
  76. package/scaffold/src/app/api/security/users/[publicId]/avatar/route.ts +15 -0
  77. package/scaffold/src/app/auth/2fa/verify/page.tsx +286 -0
  78. package/scaffold/src/app/auth/forgot-password/confirm/page.tsx +96 -0
  79. package/scaffold/src/app/auth/forgot-password/page.tsx +165 -0
  80. package/scaffold/src/app/auth/forgot-password/success/page.tsx +31 -0
  81. package/scaffold/src/app/auth/login/page.tsx +386 -0
  82. package/scaffold/src/app/auth/welcome/page.tsx +90 -0
  83. package/scaffold/src/app/login/page.tsx +7 -0
  84. package/scaffold/src/app/page.tsx +12 -0
  85. package/scaffold/src/app/security/2fa/page.tsx +370 -0
  86. package/scaffold/src/app/security/companies/(create)/new/page.tsx +12 -0
  87. package/scaffold/src/app/security/companies/(edit)/[publicId]/page.tsx +18 -0
  88. package/scaffold/src/app/security/companies/(list)/page.tsx +298 -0
  89. package/scaffold/src/app/security/companies/editors/CompanyEditor.tsx +754 -0
  90. package/scaffold/src/app/security/profile/page.tsx +492 -0
  91. package/scaffold/src/app/security/roles/(create)/new/page.tsx +11 -0
  92. package/scaffold/src/app/security/roles/(edit)/[publicId]/page.tsx +17 -0
  93. package/scaffold/src/app/security/roles/(list)/page.tsx +269 -0
  94. package/scaffold/src/app/security/roles/editors/RoleEditor.tsx +328 -0
  95. package/scaffold/src/app/security/users/(create)/new/page.tsx +10 -0
  96. package/scaffold/src/app/security/users/(edit)/[publicId]/page.tsx +16 -0
  97. package/scaffold/src/app/security/users/(list)/page.tsx +289 -0
  98. package/scaffold/src/app/security/users/editors/UserEditor.tsx +478 -0
  99. package/scaffold/src/app/users/change-password/page.tsx +172 -0
  100. package/scaffold/src/modules/security/AuthProvider.tsx +405 -0
  101. package/scaffold/src/modules/security/components/AppShell.tsx.hbs +889 -0
  102. package/scaffold/src/modules/security/components/CompanyManagementGuard.tsx +59 -0
  103. package/scaffold/src/modules/security/components/PageFrame.tsx +105 -0
  104. package/scaffold/src/modules/security/components/RouteGuard.tsx +114 -0
  105. package/scaffold/src/modules/security/components/SecurityDashboard.tsx.hbs +315 -0
  106. package/scaffold/src/modules/security/config.ts +55 -0
  107. package/scaffold/src/modules/security/hooks/useAuth.ts +7 -0
  108. package/scaffold/src/modules/security/hooks/useDebouncedSearch.ts +13 -0
  109. package/scaffold/src/modules/security/hooks/usePermission.ts +35 -0
  110. package/scaffold/src/modules/security/host-access.ts +59 -0
  111. package/scaffold/src/modules/security/index.ts +44 -0
  112. package/scaffold/src/modules/security/navigation.ts +41 -0
  113. package/scaffold/src/modules/security/server/avatar-routes.ts +86 -0
  114. package/scaffold/src/modules/security/server/public-api-base-url.ts +26 -0
  115. package/scaffold/src/modules/security/server/session-routes.ts.hbs +322 -0
  116. package/scaffold/src/modules/security/server/session-server.ts.hbs +30 -0
  117. package/scaffold/src/modules/security/services/auth-service.ts +332 -0
  118. package/scaffold/src/modules/security/services/security-api.test.ts +178 -0
  119. package/scaffold/src/modules/security/services/security-api.ts +281 -0
  120. package/scaffold/src/modules/security/services/security-service.test.ts +71 -0
  121. package/scaffold/src/modules/security/services/security-service.ts +727 -0
  122. package/scaffold/src/modules/security/services/session-storage.ts +80 -0
  123. package/scaffold/src/modules/security/types.ts +295 -0
  124. package/scaffold/src/modules/security/useSecurityAppShellSections.tsx.hbs +180 -0
  125. package/scaffold/src/modules/security/utils.ts +194 -0
@@ -0,0 +1,492 @@
1
+ "use client";
2
+
3
+ import type { ChangeEvent } from "react";
4
+ import { useEffect, useRef, useState } from "react";
5
+
6
+ import {
7
+ SgAvatar,
8
+ SgButton,
9
+ SgGrid,
10
+ SgInputEmail,
11
+ SgInputPhone,
12
+ SgPanel,
13
+ SgStack,
14
+ SgInputText,
15
+ sgWhistle,
16
+ } from "@seedgrid/fe-components";
17
+ import { useRouter } from "next/navigation";
18
+
19
+ import { useI18n } from "@/i18n";
20
+ import {
21
+ InlineNotice,
22
+ PageFrame,
23
+ RouteGuard,
24
+ extractApiMessage,
25
+ securityConfig,
26
+ securityService,
27
+ useAuth,
28
+ usePermission,
29
+ type UserSummary,
30
+ } from "@/modules/security";
31
+
32
+ type ProfileFormValues = {
33
+ email: string;
34
+ name: string;
35
+ whatsApp: string;
36
+ };
37
+
38
+ const EMPTY_FORM: ProfileFormValues = {
39
+ email: "",
40
+ name: "",
41
+ whatsApp: "",
42
+ };
43
+
44
+ function hasAtLeastTwoWords(value: string) {
45
+ return value.trim().split(/\s+/).filter(Boolean).length >= 2;
46
+ }
47
+
48
+ function readFileAsDataUrl(file: File) {
49
+ return new Promise<string>((resolve, reject) => {
50
+ const reader = new FileReader();
51
+
52
+ reader.onload = () => {
53
+ const result = typeof reader.result === "string" ? reader.result : "";
54
+
55
+ if (!result) {
56
+ reject(new Error("empty-file-result"));
57
+ return;
58
+ }
59
+
60
+ resolve(result);
61
+ };
62
+
63
+ reader.onerror = () => reject(reader.error ?? new Error("file-read-error"));
64
+ reader.readAsDataURL(file);
65
+ });
66
+ }
67
+
68
+ function extractBase64Content(dataUrl: string) {
69
+ const [, base64 = ""] = dataUrl.split(",", 2);
70
+ return base64.trim();
71
+ }
72
+
73
+ function createAvatarVersion(storageKey: string) {
74
+ return `${storageKey || "avatar"}-${Date.now()}`;
75
+ }
76
+
77
+ export default function ProfilePage() {
78
+ return (
79
+ <RouteGuard>
80
+ <ProfileScreen />
81
+ </RouteGuard>
82
+ );
83
+ }
84
+
85
+ function ProfileScreen() {
86
+ const router = useRouter();
87
+ const { t } = useI18n();
88
+ const auth = useAuth();
89
+ const canUpdateUsers = usePermission("USER_UPDATE");
90
+ const [userRecord, setUserRecord] = useState<UserSummary | null>(null);
91
+ const [formValues, setFormValues] = useState<ProfileFormValues>(EMPTY_FORM);
92
+ const [loading, setLoading] = useState(true);
93
+ const [saving, setSaving] = useState(false);
94
+ const [uploadingAvatar, setUploadingAvatar] = useState(false);
95
+ const [loadError, setLoadError] = useState<string | null>(null);
96
+ const [isTwoFactorEnabled, setIsTwoFactorEnabled] = useState(false);
97
+ const [avatarVersion, setAvatarVersion] = useState("");
98
+ const [avatarObjectUrl, setAvatarObjectUrl] = useState("");
99
+ const avatarInputRef = useRef<HTMLInputElement | null>(null);
100
+ const currentLogin = auth.session?.login?.trim() ?? "";
101
+ const currentUserPublicId = auth.session?.publicId?.trim() ?? "";
102
+ const currentRoles = auth.roles.length
103
+ ? auth.roles.join(", ")
104
+ : t("security.dashboard.user.subtitle");
105
+ const loginInputProps = {
106
+ value: currentLogin,
107
+ };
108
+ const rolesInputProps = {
109
+ value: currentRoles,
110
+ };
111
+ const emailInputProps = {
112
+ value: formValues.email,
113
+ };
114
+ const nameInputProps = {
115
+ value: formValues.name,
116
+ };
117
+ const whatsAppInputProps = {
118
+ value: formValues.whatsApp,
119
+ };
120
+ const avatarUrl =
121
+ avatarObjectUrl ||
122
+ (userRecord?.publicId
123
+ ? securityService.buildUserAvatarUrl(
124
+ userRecord.publicId,
125
+ avatarVersion || userRecord.avatarStorageKey
126
+ )
127
+ : undefined);
128
+
129
+ useEffect(() => {
130
+ setAvatarObjectUrl((current) => {
131
+ if (current.startsWith("blob:")) {
132
+ URL.revokeObjectURL(current);
133
+ }
134
+
135
+ return "";
136
+ });
137
+ }, [avatarVersion, userRecord?.avatarStorageKey, userRecord?.publicId]);
138
+
139
+ useEffect(() => {
140
+ return () => {
141
+ if (avatarObjectUrl.startsWith("blob:")) {
142
+ URL.revokeObjectURL(avatarObjectUrl);
143
+ }
144
+ };
145
+ }, [avatarObjectUrl]);
146
+
147
+ useEffect(() => {
148
+ let active = true;
149
+
150
+ void (async () => {
151
+ try {
152
+ const status = await securityService.getTwoFactorStatus();
153
+
154
+ if (active) {
155
+ setIsTwoFactorEnabled(Boolean(status.enabled));
156
+ }
157
+ } catch {
158
+ if (active) {
159
+ setIsTwoFactorEnabled(false);
160
+ }
161
+ }
162
+ })();
163
+
164
+ return () => {
165
+ active = false;
166
+ };
167
+ }, []);
168
+
169
+ useEffect(() => {
170
+ let active = true;
171
+
172
+ void (async () => {
173
+ try {
174
+ if (!currentUserPublicId) {
175
+ setUserRecord(null);
176
+ setFormValues(EMPTY_FORM);
177
+ return;
178
+ }
179
+
180
+ const user = await securityService.getUser(currentUserPublicId);
181
+
182
+ if (!active) {
183
+ return;
184
+ }
185
+
186
+ setUserRecord(user);
187
+ setAvatarVersion(user?.avatarStorageKey?.trim() ?? "");
188
+ setFormValues({
189
+ email: user?.email ?? currentLogin,
190
+ name: user?.name ?? "",
191
+ whatsApp: user?.whatsApp ?? "",
192
+ });
193
+ } catch (error) {
194
+ if (!active) {
195
+ return;
196
+ }
197
+
198
+ setLoadError(
199
+ extractApiMessage(error) ?? t("security.profile.load_error")
200
+ );
201
+ } finally {
202
+ if (active) {
203
+ setLoading(false);
204
+ }
205
+ }
206
+ })();
207
+
208
+ return () => {
209
+ active = false;
210
+ };
211
+ }, [currentLogin, currentUserPublicId, t]);
212
+
213
+ async function handleSave() {
214
+ if (!userRecord?.publicId || !canUpdateUsers) {
215
+ return;
216
+ }
217
+
218
+ if (!hasAtLeastTwoWords(formValues.name)) {
219
+ sgWhistle.error({ message: t("security.profile.name_min_words") });
220
+ return;
221
+ }
222
+
223
+ setSaving(true);
224
+
225
+ try {
226
+ await securityService.updateUser(userRecord.publicId, {
227
+ email: formValues.email.trim(),
228
+ name: formValues.name.trim() || undefined,
229
+ whatsApp: formValues.whatsApp.trim() || undefined,
230
+ });
231
+ sgWhistle.success({ message: t("security.profile.save_success") });
232
+ } catch (error) {
233
+ sgWhistle.error({
234
+ message: extractApiMessage(error) ?? t("security.profile.save_error"),
235
+ });
236
+ } finally {
237
+ setSaving(false);
238
+ }
239
+ }
240
+
241
+ async function handleAvatarSelection(event: ChangeEvent<HTMLInputElement>) {
242
+ const file = event.target.files?.[0];
243
+ event.target.value = "";
244
+
245
+ if (!file || !userRecord?.publicId || !canUpdateUsers) {
246
+ return;
247
+ }
248
+
249
+ if (!file.type.startsWith("image/")) {
250
+ sgWhistle.error({ message: t("security.profile.avatar_invalid_file") });
251
+ return;
252
+ }
253
+
254
+ setUploadingAvatar(true);
255
+
256
+ try {
257
+ const dataUrl = await readFileAsDataUrl(file);
258
+ const base64 = extractBase64Content(dataUrl);
259
+
260
+ const updatedUser = await securityService.uploadUserAvatar(userRecord.publicId, {
261
+ filename: file.name,
262
+ contentType: file.type,
263
+ base64,
264
+ });
265
+
266
+ const nextStorageKey =
267
+ updatedUser?.avatarStorageKey?.trim() ||
268
+ userRecord.avatarStorageKey?.trim() ||
269
+ "avatar";
270
+ const nextAvatarVersion = createAvatarVersion(nextStorageKey);
271
+ setUserRecord((current) =>
272
+ current
273
+ ? {
274
+ ...current,
275
+ avatarStorageKey: nextStorageKey,
276
+ }
277
+ : current
278
+ );
279
+ setAvatarVersion(nextAvatarVersion);
280
+ window.dispatchEvent(
281
+ new CustomEvent("seedgrid:profile-updated", {
282
+ detail: {
283
+ publicId: userRecord.publicId,
284
+ avatarStorageKey: nextStorageKey,
285
+ avatarVersion: nextAvatarVersion,
286
+ name: formValues.name.trim(),
287
+ },
288
+ })
289
+ );
290
+ sgWhistle.success({ message: t("security.profile.avatar_upload_success") });
291
+ } catch (error) {
292
+ sgWhistle.error({
293
+ message:
294
+ extractApiMessage(error) ?? t("security.profile.avatar_upload_error"),
295
+ });
296
+ } finally {
297
+ setUploadingAvatar(false);
298
+ }
299
+ }
300
+
301
+ async function handleAvatarImageError() {
302
+ if (!userRecord?.publicId || avatarObjectUrl) {
303
+ return;
304
+ }
305
+
306
+ const fallbackObjectUrl = await securityService.fetchUserAvatarObjectUrl(
307
+ userRecord.publicId,
308
+ avatarVersion || userRecord.avatarStorageKey
309
+ );
310
+
311
+ if (!fallbackObjectUrl) {
312
+ return;
313
+ }
314
+
315
+ setAvatarObjectUrl((current) => {
316
+ if (current.startsWith("blob:")) {
317
+ URL.revokeObjectURL(current);
318
+ }
319
+
320
+ return fallbackObjectUrl;
321
+ });
322
+ }
323
+
324
+ return (
325
+ <PageFrame
326
+ title={t("security.profile.title")}
327
+ description={t("security.profile.description")}
328
+ actions={
329
+ <SgStack direction="row" wrap gap={8}>
330
+ <SgButton
331
+ appearance="outline"
332
+ severity="secondary"
333
+ shape="rounded"
334
+ label={t("security.profile.change_password")}
335
+ onClick={() => {
336
+ router.push(securityConfig.routes.changePassword);
337
+ }}
338
+ />
339
+ <SgButton
340
+ appearance="solid"
341
+ severity="primary"
342
+ shape="rounded"
343
+ label={t(
344
+ isTwoFactorEnabled
345
+ ? "security.profile.disable_two_factor"
346
+ : "security.profile.enable_two_factor"
347
+ )}
348
+ onClick={() => {
349
+ router.push(securityConfig.routes.twoFactor);
350
+ }}
351
+ />
352
+ </SgStack>
353
+ }
354
+ >
355
+ {loadError ? <InlineNotice tone="danger">{loadError}</InlineNotice> : null}
356
+
357
+ {loading ? (
358
+ <InlineNotice>{t("security.common.loading")}</InlineNotice>
359
+ ) : (
360
+ <SgStack gap={24}>
361
+ <SgPanel className="rounded-3xl border border-slate-200 bg-slate-50 p-5">
362
+ {userRecord && canUpdateUsers ? (
363
+ <>
364
+ <input
365
+ ref={avatarInputRef}
366
+ type="file"
367
+ accept="image/*"
368
+ className="hidden"
369
+ onChange={handleAvatarSelection}
370
+ />
371
+ <SgStack direction="row" wrap justify="between" align="center" gap={16}>
372
+ <SgButton
373
+ appearance="ghost"
374
+ severity="secondary"
375
+ iconOnly
376
+ onClick={() => avatarInputRef.current?.click()}
377
+ aria-label={t("security.profile.avatar_upload")}
378
+ className="rounded-full p-0 hover:scale-[1.02]"
379
+ >
380
+ <SgAvatar
381
+ src={avatarUrl}
382
+ alt={formValues.name.trim() || currentLogin || t("security.profile.title")}
383
+ label={(formValues.name.trim() || currentLogin || "SG").slice(0, 2).toUpperCase()}
384
+ size="xl"
385
+ onImageError={handleAvatarImageError}
386
+ />
387
+ </SgButton>
388
+
389
+ <SgButton
390
+ appearance="outline"
391
+ severity="secondary"
392
+ shape="rounded"
393
+ label={t("security.profile.avatar_upload")}
394
+ onClick={() => avatarInputRef.current?.click()}
395
+ loading={uploadingAvatar}
396
+ />
397
+ </SgStack>
398
+ </>
399
+ ) : (
400
+ <SgAvatar
401
+ src={avatarUrl}
402
+ alt={formValues.name.trim() || currentLogin || t("security.profile.title")}
403
+ label={(formValues.name.trim() || currentLogin || "SG").slice(0, 2).toUpperCase()}
404
+ size="xl"
405
+ onImageError={handleAvatarImageError}
406
+ />
407
+ )}
408
+ </SgPanel>
409
+
410
+ <SgGrid minItemWidth={280} gap={16}>
411
+ <SgInputText
412
+ id="security-profile-login"
413
+ label={t("security.profile.login")}
414
+ readOnly
415
+ inputProps={loginInputProps}
416
+ />
417
+ <SgInputText
418
+ id="security-profile-roles"
419
+ label={t("security.profile.roles")}
420
+ readOnly
421
+ inputProps={rolesInputProps}
422
+ />
423
+ </SgGrid>
424
+
425
+ {userRecord ? (
426
+ <SgGrid minItemWidth={280} gap={16}>
427
+ <SgInputEmail
428
+ id="security-profile-email"
429
+ label={t("security.profile.email")}
430
+ readOnly={!canUpdateUsers}
431
+ inputProps={emailInputProps}
432
+ onChange={(value) =>
433
+ setFormValues((current) => ({
434
+ ...current,
435
+ email: value,
436
+ }))
437
+ }
438
+ />
439
+ <SgInputText
440
+ id="security-profile-name"
441
+ label={t("security.profile.name")}
442
+ minNumberOfWords={2}
443
+ readOnly={!canUpdateUsers}
444
+ inputProps={nameInputProps}
445
+ onChange={(value) =>
446
+ setFormValues((current) => ({
447
+ ...current,
448
+ name: value,
449
+ }))
450
+ }
451
+ />
452
+ <SgInputPhone
453
+ id="security-profile-whatsapp"
454
+ label={t("security.profile.whatsapp")}
455
+ readOnly={!canUpdateUsers}
456
+ inputProps={whatsAppInputProps}
457
+ onChange={(value) =>
458
+ setFormValues((current) => ({
459
+ ...current,
460
+ whatsApp: value,
461
+ }))
462
+ }
463
+ />
464
+ </SgGrid>
465
+ ) : (
466
+ <InlineNotice tone="warning">
467
+ {t("security.profile.no_user_record")}
468
+ </InlineNotice>
469
+ )}
470
+
471
+ {userRecord && canUpdateUsers ? (
472
+ <SgStack direction="row" justify="end">
473
+ <SgButton
474
+ appearance="solid"
475
+ severity="primary"
476
+ shape="rounded"
477
+ label={t("security.common.save")}
478
+ onClick={handleSave}
479
+ loading={saving}
480
+ disabled={
481
+ saving ||
482
+ !formValues.email.trim() ||
483
+ !hasAtLeastTwoWords(formValues.name)
484
+ }
485
+ />
486
+ </SgStack>
487
+ ) : null}
488
+ </SgStack>
489
+ )}
490
+ </PageFrame>
491
+ );
492
+ }
@@ -0,0 +1,11 @@
1
+ import { RouteGuard } from "@/modules/security";
2
+
3
+ import { RoleEditor } from "../../editors/RoleEditor";
4
+
5
+ export default function CreateRolePage() {
6
+ return (
7
+ <RouteGuard permissions={["ROLE_CREATE"]}>
8
+ <RoleEditor />
9
+ </RouteGuard>
10
+ );
11
+ }
@@ -0,0 +1,17 @@
1
+ import { RouteGuard } from "@/modules/security";
2
+
3
+ import { RoleEditor } from "../../editors/RoleEditor";
4
+
5
+ export default async function EditRolePage({
6
+ params,
7
+ }: {
8
+ params: Promise<{ publicId: string }>;
9
+ }) {
10
+ const { publicId } = await params;
11
+
12
+ return (
13
+ <RouteGuard permissions={["ROLE_READ"]}>
14
+ <RoleEditor publicId={publicId} />
15
+ </RouteGuard>
16
+ );
17
+ }