@seedgrid/fe-security 2026.6.2 → 2026.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/client/index.d.ts +4 -0
  2. package/dist/client/index.d.ts.map +1 -0
  3. package/dist/client/index.js +5 -0
  4. package/dist/client/permission-provider.d.ts +18 -0
  5. package/dist/client/permission-provider.d.ts.map +1 -0
  6. package/dist/client/permission-provider.js +57 -0
  7. package/dist/client/pick-list-field.d.ts +15 -0
  8. package/dist/client/pick-list-field.d.ts.map +1 -0
  9. package/dist/client/pick-list-field.js +30 -0
  10. package/dist/client/status-badge.d.ts +6 -0
  11. package/dist/client/status-badge.d.ts.map +1 -0
  12. package/dist/client/status-badge.js +6 -0
  13. package/dist/config.d.ts +34 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +4 -0
  16. package/dist/dtos.d.ts +88 -0
  17. package/dist/dtos.d.ts.map +1 -0
  18. package/dist/dtos.js +3 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +10 -0
  22. package/dist/menu.d.ts +19 -0
  23. package/dist/menu.d.ts.map +1 -0
  24. package/dist/menu.js +41 -0
  25. package/dist/messages.d.ts +1858 -0
  26. package/dist/messages.d.ts.map +1 -0
  27. package/dist/messages.js +990 -0
  28. package/dist/module.d.ts +3 -0
  29. package/dist/module.d.ts.map +1 -0
  30. package/dist/module.js +37 -0
  31. package/dist/module.json +21 -0
  32. package/dist/paths.d.ts +1 -0
  33. package/dist/paths.d.ts.map +1 -1
  34. package/dist/paths.js +3 -0
  35. package/dist/permissions.d.ts +14 -0
  36. package/dist/permissions.d.ts.map +1 -0
  37. package/dist/permissions.js +39 -0
  38. package/dist/server/action.d.ts +3 -0
  39. package/dist/server/action.d.ts.map +1 -0
  40. package/dist/server/action.js +21 -0
  41. package/dist/server/create-security-server.d.ts +4 -0
  42. package/dist/server/create-security-server.d.ts.map +1 -0
  43. package/dist/server/create-security-server.js +166 -0
  44. package/dist/server/errors.d.ts +13 -0
  45. package/dist/server/errors.d.ts.map +1 -0
  46. package/dist/server/errors.js +26 -0
  47. package/dist/server/index.d.ts +8 -0
  48. package/dist/server/index.d.ts.map +1 -0
  49. package/dist/server/index.js +8 -0
  50. package/dist/server/problem-response.d.ts +3 -0
  51. package/dist/server/problem-response.d.ts.map +1 -0
  52. package/dist/server/problem-response.js +28 -0
  53. package/dist/server/route.d.ts +3 -0
  54. package/dist/server/route.d.ts.map +1 -0
  55. package/dist/server/route.js +22 -0
  56. package/dist/server/to-problem.d.ts +3 -0
  57. package/dist/server/to-problem.d.ts.map +1 -0
  58. package/dist/server/to-problem.js +22 -0
  59. package/dist/server-contract.d.ts +23 -0
  60. package/dist/server-contract.d.ts.map +1 -0
  61. package/dist/server-contract.js +4 -0
  62. package/dist/session-contract.d.ts +36 -0
  63. package/dist/session-contract.d.ts.map +1 -0
  64. package/dist/session-contract.js +8 -0
  65. package/dist/session-result.d.ts +22 -0
  66. package/dist/session-result.d.ts.map +1 -0
  67. package/dist/session-result.js +14 -0
  68. package/package.json +38 -3
  69. package/scaffold/README.md +97 -0
  70. package/scaffold/src/app/api/security/session/2fa/resend/route.ts +8 -0
  71. package/scaffold/src/app/api/security/session/2fa/verify/route.ts +8 -0
  72. package/scaffold/src/app/api/security/session/login/route.ts +7 -0
  73. package/scaffold/src/app/api/security/session/logout/route.ts +5 -0
  74. package/scaffold/src/app/api/security/session/refresh/route.ts +7 -0
  75. package/scaffold/src/app/api/security/users/[publicId]/avatar/route.ts +15 -0
  76. package/scaffold/src/app/auth/2fa/verify/page.tsx +286 -0
  77. package/scaffold/src/app/auth/forgot-password/confirm/page.tsx +96 -0
  78. package/scaffold/src/app/auth/forgot-password/page.tsx +165 -0
  79. package/scaffold/src/app/auth/forgot-password/success/page.tsx +31 -0
  80. package/scaffold/src/app/auth/login/page.tsx +386 -0
  81. package/scaffold/src/app/auth/welcome/page.tsx +90 -0
  82. package/scaffold/src/app/login/page.tsx +7 -0
  83. package/scaffold/src/app/page.tsx +12 -0
  84. package/scaffold/src/app/security/2fa/page.tsx +370 -0
  85. package/scaffold/src/app/security/companies/(create)/new/page.tsx +12 -0
  86. package/scaffold/src/app/security/companies/(edit)/[publicId]/page.tsx +18 -0
  87. package/scaffold/src/app/security/companies/(list)/page.tsx +298 -0
  88. package/scaffold/src/app/security/companies/editors/CompanyEditor.tsx +754 -0
  89. package/scaffold/src/app/security/profile/page.tsx +492 -0
  90. package/scaffold/src/app/security/roles/(create)/new/page.tsx +11 -0
  91. package/scaffold/src/app/security/roles/(edit)/[publicId]/page.tsx +17 -0
  92. package/scaffold/src/app/security/roles/(list)/page.tsx +269 -0
  93. package/scaffold/src/app/security/roles/editors/RoleEditor.tsx +328 -0
  94. package/scaffold/src/app/security/users/(create)/new/page.tsx +10 -0
  95. package/scaffold/src/app/security/users/(edit)/[publicId]/page.tsx +16 -0
  96. package/scaffold/src/app/security/users/(list)/page.tsx +289 -0
  97. package/scaffold/src/app/security/users/editors/UserEditor.tsx +478 -0
  98. package/scaffold/src/app/users/change-password/page.tsx +172 -0
  99. package/scaffold/src/modules/security/AuthProvider.tsx +405 -0
  100. package/scaffold/src/modules/security/components/AppShell.tsx.hbs +889 -0
  101. package/scaffold/src/modules/security/components/CompanyManagementGuard.tsx +59 -0
  102. package/scaffold/src/modules/security/components/PageFrame.tsx +105 -0
  103. package/scaffold/src/modules/security/components/RouteGuard.tsx +114 -0
  104. package/scaffold/src/modules/security/components/SecurityDashboard.tsx.hbs +315 -0
  105. package/scaffold/src/modules/security/config.ts +55 -0
  106. package/scaffold/src/modules/security/hooks/useAuth.ts +7 -0
  107. package/scaffold/src/modules/security/hooks/useDebouncedSearch.ts +13 -0
  108. package/scaffold/src/modules/security/hooks/usePermission.ts +35 -0
  109. package/scaffold/src/modules/security/host-access.ts +59 -0
  110. package/scaffold/src/modules/security/index.ts +44 -0
  111. package/scaffold/src/modules/security/navigation.ts +41 -0
  112. package/scaffold/src/modules/security/server/avatar-routes.ts +86 -0
  113. package/scaffold/src/modules/security/server/public-api-base-url.ts +26 -0
  114. package/scaffold/src/modules/security/server/session-routes.ts.hbs +322 -0
  115. package/scaffold/src/modules/security/server/session-server.ts.hbs +30 -0
  116. package/scaffold/src/modules/security/services/auth-service.ts +332 -0
  117. package/scaffold/src/modules/security/services/security-api.test.ts +178 -0
  118. package/scaffold/src/modules/security/services/security-api.ts +281 -0
  119. package/scaffold/src/modules/security/services/security-service.test.ts +71 -0
  120. package/scaffold/src/modules/security/services/security-service.ts +727 -0
  121. package/scaffold/src/modules/security/services/session-storage.ts +80 -0
  122. package/scaffold/src/modules/security/types.ts +295 -0
  123. package/scaffold/src/modules/security/useSecurityAppShellSections.tsx.hbs +180 -0
  124. package/scaffold/src/modules/security/utils.ts +194 -0
@@ -0,0 +1,269 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+
5
+ import {
6
+ SgButton,
7
+ SgConfirmationDialog,
8
+ SgDatatable,
9
+ SgInputText,
10
+ SgStack,
11
+ sgWhistle,
12
+ } from "@seedgrid/fe-components";
13
+ import { Pencil, Trash2 } from "lucide-react";
14
+ import { useRouter } from "next/navigation";
15
+
16
+ import { useI18n } from "@/i18n";
17
+ import {
18
+ InlineNotice,
19
+ PageFrame,
20
+ RouteGuard,
21
+ extractApiMessage,
22
+ securityConfig,
23
+ securityService,
24
+ useCrudPermission,
25
+ useDebouncedSearch,
26
+ type SortState,
27
+ type RoleSummary,
28
+ } from "@/modules/security";
29
+ import { isSearchReady } from "@/modules/security/utils";
30
+
31
+ export default function RolesListPage() {
32
+ return (
33
+ <RouteGuard permissions={["ROLE_READ"]}>
34
+ <RolesListScreen />
35
+ </RouteGuard>
36
+ );
37
+ }
38
+
39
+ function RolesListScreen() {
40
+ const router = useRouter();
41
+ const { t } = useI18n();
42
+ const permissions = useCrudPermission("ROLE");
43
+ const [roles, setRoles] = useState<RoleSummary[]>([]);
44
+ const [page, setPage] = useState(0);
45
+ const [rows, setRows] = useState<number>(
46
+ securityConfig.pagination.defaultPageSize
47
+ );
48
+ const [totalRecords, setTotalRecords] = useState(0);
49
+ const [loading, setLoading] = useState(true);
50
+ const [searchValue, setSearchValue] = useState("");
51
+ const [sort, setSort] = useState<SortState>({
52
+ sortField: "name",
53
+ sortOrder: 1,
54
+ });
55
+ const [selectedRole, setSelectedRole] = useState<RoleSummary | null>(null);
56
+ const debouncedSearch = useDebouncedSearch(searchValue);
57
+ const effectiveSearch = isSearchReady(debouncedSearch) ? debouncedSearch : "";
58
+ const searchInputProps = {
59
+ value: searchValue,
60
+ onChange: (event: { target: { value: string } }) => {
61
+ setSearchValue(event.target.value);
62
+ setPage(0);
63
+ },
64
+ };
65
+
66
+ useEffect(() => {
67
+ let active = true;
68
+ setLoading(true);
69
+
70
+ void (async () => {
71
+ try {
72
+ const response = await securityService.listRolesPage({
73
+ page,
74
+ size: rows,
75
+ search: effectiveSearch,
76
+ sort,
77
+ });
78
+
79
+ if (!active) {
80
+ return;
81
+ }
82
+
83
+ setRoles(response.items);
84
+ setTotalRecords(response.total);
85
+ } catch (error) {
86
+ if (!active) {
87
+ return;
88
+ }
89
+
90
+ sgWhistle.error({
91
+ message: extractApiMessage(error) ?? t("security.roles.form.load_error"),
92
+ });
93
+ } finally {
94
+ if (active) {
95
+ setLoading(false);
96
+ }
97
+ }
98
+ })();
99
+
100
+ return () => {
101
+ active = false;
102
+ };
103
+ }, [effectiveSearch, page, rows, sort, t]);
104
+
105
+ const columns = useMemo(
106
+ () => [
107
+ {
108
+ field: "name",
109
+ header: t("security.roles.list.column.name"),
110
+ sortable: true,
111
+ },
112
+ {
113
+ field: "permissions",
114
+ header: t("security.roles.list.column.permissions"),
115
+ body: (role: RoleSummary) =>
116
+ role.permissions?.length
117
+ ? role.permissions.map((permission) => permission.name).join(", ")
118
+ : "-",
119
+ },
120
+ {
121
+ field: "forIntegration",
122
+ header: t("security.roles.list.column.integration"),
123
+ body: (role: RoleSummary) =>
124
+ role.forIntegration ? t("security.common.yes") : t("security.common.no"),
125
+ },
126
+ {
127
+ header: t("security.common.actions"),
128
+ body: (role: RoleSummary) => (
129
+ <SgStack direction="row" wrap gap={8} data-sg-stop-row-select="true">
130
+ <SgButton
131
+ appearance="ghost"
132
+ severity="info"
133
+ iconOnly
134
+ leftIcon={<Pencil size={16} />}
135
+ title={
136
+ permissions.canUpdate
137
+ ? t("security.roles.list.action.edit")
138
+ : t("security.roles.list.action.view")
139
+ }
140
+ aria-label={
141
+ permissions.canUpdate
142
+ ? t("security.roles.list.action.edit")
143
+ : t("security.roles.list.action.view")
144
+ }
145
+ onClick={() => {
146
+ router.push(`${securityConfig.routes.roles}/${role.publicId}`);
147
+ }}
148
+ className="border border-slate-200/80 bg-slate-50/55 text-slate-700 hover:border-sky-200 hover:bg-slate-50 hover:text-sky-700"
149
+ />
150
+ {permissions.canDelete ? (
151
+ <SgButton
152
+ appearance="ghost"
153
+ severity="danger"
154
+ iconOnly
155
+ leftIcon={<Trash2 size={16} />}
156
+ title={t("security.roles.list.action.delete")}
157
+ aria-label={t("security.roles.list.action.delete")}
158
+ onClick={() => {
159
+ setSelectedRole(role);
160
+ }}
161
+ className="border border-rose-200/80 bg-rose-50/45 text-rose-600 hover:border-rose-200 hover:bg-rose-50/70 hover:text-rose-700"
162
+ />
163
+ ) : null}
164
+ </SgStack>
165
+ ),
166
+ },
167
+ ],
168
+ [permissions.canDelete, permissions.canUpdate, router, t]
169
+ );
170
+
171
+ async function handleDelete() {
172
+ if (!selectedRole) {
173
+ return;
174
+ }
175
+
176
+ try {
177
+ await securityService.deleteRole(selectedRole.publicId);
178
+ const response = await securityService.listRolesPage({
179
+ page,
180
+ size: rows,
181
+ search: effectiveSearch,
182
+ sort,
183
+ });
184
+ setRoles(response.items);
185
+ setTotalRecords(response.total);
186
+ setSelectedRole(null);
187
+ sgWhistle.success({ message: t("security.roles.list.delete_success") });
188
+ } catch (error) {
189
+ sgWhistle.error({
190
+ message: extractApiMessage(error) ?? t("security.roles.list.delete_error"),
191
+ });
192
+ }
193
+ }
194
+
195
+ return (
196
+ <PageFrame
197
+ title={t("security.roles.list.title")}
198
+ description={t("security.roles.list.description")}
199
+ readonlyLabel={permissions.isReadOnly ? t("security.common.readonly") : null}
200
+ actions={
201
+ permissions.canCreate ? (
202
+ <SgButton
203
+ appearance="solid"
204
+ severity="primary"
205
+ shape="rounded"
206
+ label={t("security.roles.list.new")}
207
+ onClick={() => {
208
+ router.push(`${securityConfig.routes.roles}/new`);
209
+ }}
210
+ />
211
+ ) : undefined
212
+ }
213
+ >
214
+ <SgStack gap={16}>
215
+ <SgInputText
216
+ id="security-roles-search"
217
+ label={t("security.common.search")}
218
+ inputProps={searchInputProps}
219
+ placeholder={t("security.roles.list.search_placeholder")}
220
+ />
221
+
222
+ {!isSearchReady(searchValue) && searchValue.trim().length > 0 ? (
223
+ <InlineNotice>{t("security.common.search_hint")}</InlineNotice>
224
+ ) : null}
225
+
226
+ <SgDatatable<RoleSummary>
227
+ title=" "
228
+ value={roles}
229
+ columns={columns}
230
+ dataKey="publicId"
231
+ paginator
232
+ lazy
233
+ first={page * rows}
234
+ rows={rows}
235
+ totalRecords={totalRecords}
236
+ onPage={(event) => {
237
+ setPage(event.page ?? 0);
238
+ setRows(event.rows ?? securityConfig.pagination.defaultPageSize);
239
+ }}
240
+ sortField={sort.sortField ?? undefined}
241
+ sortOrder={sort.sortOrder}
242
+ onSort={(event) => {
243
+ setPage(0);
244
+ setSort({
245
+ sortField: event.sortField ?? "name",
246
+ sortOrder:
247
+ event.sortOrder === 1 || event.sortOrder === -1
248
+ ? event.sortOrder
249
+ : 1,
250
+ });
251
+ }}
252
+ loading={loading}
253
+ emptyMessage={t("security.roles.list.empty")}
254
+ />
255
+ </SgStack>
256
+
257
+ <SgConfirmationDialog
258
+ open={Boolean(selectedRole)}
259
+ severity="danger"
260
+ title={t("security.roles.list.delete_title")}
261
+ message={t("security.roles.list.delete_message")}
262
+ onCancel={() => {
263
+ setSelectedRole(null);
264
+ }}
265
+ onConfirm={handleDelete}
266
+ />
267
+ </PageFrame>
268
+ );
269
+ }
@@ -0,0 +1,328 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+
5
+ import {
6
+ SgButton,
7
+ SgCheckboxGroup,
8
+ SgConfirmationDialog,
9
+ SgGrid,
10
+ SgInputText,
11
+ SgStack,
12
+ SgToggleSwitch,
13
+ sgWhistle,
14
+ } from "@seedgrid/fe-components";
15
+ import { useRouter } from "next/navigation";
16
+ import { useForm } from "react-hook-form";
17
+
18
+ import { useI18n } from "@/i18n";
19
+ import {
20
+ InlineNotice,
21
+ PageFrame,
22
+ extractApiMessage,
23
+ securityConfig,
24
+ securityService,
25
+ useCrudPermission,
26
+ type PermissionSummary,
27
+ type RoleSummary,
28
+ } from "@/modules/security";
29
+ import {
30
+ extractFirstFormErrorMessage,
31
+ showFormValidationWhistle,
32
+ } from "@/modules/security/utils";
33
+
34
+ type RoleEditorProps = {
35
+ publicId?: string;
36
+ };
37
+
38
+ type RoleFormValues = {
39
+ name: string;
40
+ description: string;
41
+ forIntegration: boolean;
42
+ permissionIds: Array<string | number>;
43
+ };
44
+
45
+ const EMPTY_FORM: RoleFormValues = {
46
+ name: "",
47
+ description: "",
48
+ forIntegration: false,
49
+ permissionIds: [],
50
+ };
51
+
52
+ export function RoleEditor({ publicId }: RoleEditorProps) {
53
+ const router = useRouter();
54
+ const { t } = useI18n();
55
+ const permissions = useCrudPermission("ROLE");
56
+ const isEditing = Boolean(publicId);
57
+ const isReadOnly = isEditing
58
+ ? permissions.canRead && !permissions.canUpdate
59
+ : !permissions.canCreate;
60
+ const [availablePermissions, setAvailablePermissions] = useState<
61
+ PermissionSummary[]
62
+ >([]);
63
+ const [loading, setLoading] = useState(true);
64
+ const [saving, setSaving] = useState(false);
65
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
66
+ const [loadError, setLoadError] = useState<string | null>(null);
67
+ const {
68
+ control,
69
+ handleSubmit,
70
+ register,
71
+ reset,
72
+ } = useForm<RoleFormValues>({
73
+ defaultValues: EMPTY_FORM,
74
+ });
75
+
76
+ useEffect(() => {
77
+ let active = true;
78
+
79
+ void (async () => {
80
+ try {
81
+ const [loadedPermissions, loadedRole] = await Promise.all([
82
+ securityService.listPermissions(),
83
+ publicId ? securityService.getRole(publicId) : Promise.resolve(null),
84
+ ]);
85
+
86
+ if (!active) {
87
+ return;
88
+ }
89
+
90
+ setAvailablePermissions(loadedPermissions);
91
+
92
+ if (loadedRole) {
93
+ reset({
94
+ name: loadedRole.name ?? "",
95
+ description: loadedRole.description ?? "",
96
+ forIntegration: Boolean(loadedRole.forIntegration),
97
+ permissionIds: (loadedRole.permissions ?? []).map(
98
+ (permission) => permission.id ?? permission.publicId
99
+ ),
100
+ });
101
+ } else {
102
+ reset(EMPTY_FORM);
103
+ }
104
+ } catch (error) {
105
+ if (!active) {
106
+ return;
107
+ }
108
+
109
+ setLoadError(
110
+ extractApiMessage(error) ?? t("security.roles.form.load_error")
111
+ );
112
+ } finally {
113
+ if (active) {
114
+ setLoading(false);
115
+ }
116
+ }
117
+ })();
118
+
119
+ return () => {
120
+ active = false;
121
+ };
122
+ }, [publicId, reset, t]);
123
+
124
+ const permissionOptions = useMemo(
125
+ () =>
126
+ availablePermissions.map((permission) => ({
127
+ label: permission.description
128
+ ? `${permission.name} - ${permission.description}`
129
+ : permission.name,
130
+ value: permission.id ?? permission.publicId,
131
+ })),
132
+ [availablePermissions]
133
+ );
134
+
135
+ async function handleValidSubmit(values: RoleFormValues) {
136
+ if (isReadOnly) {
137
+ return;
138
+ }
139
+
140
+ setSaving(true);
141
+
142
+ try {
143
+ const payload = {
144
+ name: values.name.trim(),
145
+ description: values.description.trim() || undefined,
146
+ forIntegration: values.forIntegration,
147
+ permissionIds: values.permissionIds,
148
+ };
149
+
150
+ const savedRole = publicId
151
+ ? await securityService.updateRole(publicId, payload)
152
+ : await securityService.createRole(payload);
153
+
154
+ sgWhistle.success({ message: t("security.roles.form.save_success") });
155
+ router.push(`${securityConfig.routes.roles}/${savedRole.publicId}`);
156
+ } catch (error) {
157
+ sgWhistle.error({
158
+ message: extractApiMessage(error) ?? t("security.roles.form.save_error"),
159
+ });
160
+ } finally {
161
+ setSaving(false);
162
+ }
163
+ }
164
+
165
+ function handleInvalidSubmit(errors: unknown) {
166
+ const message =
167
+ extractFirstFormErrorMessage(errors) ?? t("security.common.field_required");
168
+
169
+ showFormValidationWhistle("security-role-form-validation", message);
170
+ }
171
+
172
+ async function handleDelete() {
173
+ if (!publicId) {
174
+ return;
175
+ }
176
+
177
+ try {
178
+ await securityService.deleteRole(publicId);
179
+ sgWhistle.success({ message: t("security.roles.form.delete_success") });
180
+ router.push(securityConfig.routes.roles);
181
+ } catch (error) {
182
+ sgWhistle.error({
183
+ message: extractApiMessage(error) ?? t("security.roles.list.delete_error"),
184
+ });
185
+ } finally {
186
+ setDeleteDialogOpen(false);
187
+ }
188
+ }
189
+
190
+ return (
191
+ <PageFrame
192
+ title={
193
+ isEditing
194
+ ? t("security.roles.form.title_edit")
195
+ : t("security.roles.form.title_new")
196
+ }
197
+ description={
198
+ isEditing
199
+ ? t("security.roles.form.description_edit")
200
+ : t("security.roles.form.description_new")
201
+ }
202
+ readonlyLabel={isReadOnly ? t("security.common.readonly") : null}
203
+ actions={
204
+ <SgStack direction="row" wrap gap={8}>
205
+ <SgButton
206
+ appearance="outline"
207
+ severity="primary"
208
+ shape="rounded"
209
+ label={t("security.common.back")}
210
+ onClick={() => {
211
+ router.push(securityConfig.routes.roles);
212
+ }}
213
+ />
214
+ {isEditing && permissions.canDelete ? (
215
+ <SgButton
216
+ appearance="outline"
217
+ severity="danger"
218
+ shape="rounded"
219
+ label={t("security.common.delete")}
220
+ onClick={() => {
221
+ setDeleteDialogOpen(true);
222
+ }}
223
+ />
224
+ ) : null}
225
+ </SgStack>
226
+ }
227
+ >
228
+ {loadError ? <InlineNotice tone="danger">{loadError}</InlineNotice> : null}
229
+
230
+ {loading ? (
231
+ <InlineNotice>{t("security.common.loading")}</InlineNotice>
232
+ ) : (
233
+ <form
234
+ noValidate
235
+ onSubmit={handleSubmit(handleValidSubmit, handleInvalidSubmit)}
236
+ >
237
+ <SgStack gap={24}>
238
+ <SgGrid minItemWidth={280} gap={16}>
239
+ <SgInputText
240
+ id="security-role-name"
241
+ name="name"
242
+ label={t("security.roles.form.name")}
243
+ register={register}
244
+ required
245
+ rules={ {
246
+ required: t("security.common.field_required"),
247
+ }}
248
+ readOnly={isReadOnly}
249
+ />
250
+ <SgInputText
251
+ id="security-role-description"
252
+ name="description"
253
+ label={t("security.roles.form.description")}
254
+ register={register}
255
+ readOnly={isReadOnly}
256
+ />
257
+ </SgGrid>
258
+
259
+ <SgToggleSwitch
260
+ id="security-role-integration"
261
+ name="forIntegration"
262
+ label={t("security.roles.form.for_integration")}
263
+ control={control}
264
+ readOnly={isReadOnly}
265
+ />
266
+
267
+ <SgStack gap={12}>
268
+ <div className="text-sm text-slate-500">
269
+ {t("security.roles.form.permissions_hint")}
270
+ </div>
271
+
272
+ {permissionOptions.length ? (
273
+ <SgCheckboxGroup
274
+ id="security-role-permissions"
275
+ name="permissionIds"
276
+ title={t("security.roles.form.permissions")}
277
+ source={permissionOptions}
278
+ control={control}
279
+ orientation="vertical"
280
+ showCheckAll
281
+ disabled={isReadOnly}
282
+ readOnly={isReadOnly}
283
+ required
284
+ rules={ {
285
+ validate: (value) =>
286
+ Array.isArray(value) && value.length > 0
287
+ ? true
288
+ : t("security.roles.form.permissions_required"),
289
+ }}
290
+ />
291
+ ) : (
292
+ <InlineNotice tone="warning">
293
+ {t("security.roles.form.permissions_empty")}
294
+ </InlineNotice>
295
+ )}
296
+ </SgStack>
297
+
298
+ {!isReadOnly ? (
299
+ <SgStack direction="row" justify="end">
300
+ <SgButton
301
+ appearance="solid"
302
+ severity="primary"
303
+ shape="rounded"
304
+ label={t("security.common.save")}
305
+ loading={saving}
306
+ disabled={saving}
307
+ type="submit"
308
+ />
309
+ </SgStack>
310
+ ) : null}
311
+ </SgStack>
312
+ </form>
313
+ )}
314
+
315
+ <SgConfirmationDialog
316
+ open={deleteDialogOpen}
317
+ severity="danger"
318
+ title={t("security.roles.list.delete_title")}
319
+ message={t("security.roles.list.delete_message")}
320
+ onCancel={() => {
321
+ setDeleteDialogOpen(false);
322
+ }}
323
+ onConfirm={handleDelete}
324
+ />
325
+ </PageFrame>
326
+ );
327
+ }
328
+
@@ -0,0 +1,10 @@
1
+ import { RouteGuard } from "@/modules/security";
2
+ import { UserEditor } from "@/app/security/users/editors/UserEditor";
3
+
4
+ export default function NewUserPage() {
5
+ return (
6
+ <RouteGuard permissions={["USER_CREATE"]}>
7
+ <UserEditor />
8
+ </RouteGuard>
9
+ );
10
+ }
@@ -0,0 +1,16 @@
1
+ import { RouteGuard } from "@/modules/security";
2
+ import { UserEditor } from "@/app/security/users/editors/UserEditor";
3
+
4
+ export default async function EditUserPage({
5
+ params,
6
+ }: {
7
+ params: Promise<{ publicId: string }>;
8
+ }) {
9
+ const resolvedParams = await params;
10
+
11
+ return (
12
+ <RouteGuard permissions={["USER_READ"]}>
13
+ <UserEditor publicId={resolvedParams.publicId} />
14
+ </RouteGuard>
15
+ );
16
+ }