@seedgrid/fe-security 2026.7.0 → 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,289 @@
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
+ StatusBadge,
22
+ extractApiMessage,
23
+ securityConfig,
24
+ securityService,
25
+ useCrudPermission,
26
+ useDebouncedSearch,
27
+ type SortState,
28
+ type UserSummary,
29
+ } from "@/modules/security";
30
+ import { isSearchReady } from "@/modules/security/utils";
31
+
32
+ export default function UsersListPage() {
33
+ return (
34
+ <RouteGuard permissions={["USER_READ"]}>
35
+ <UsersListScreen />
36
+ </RouteGuard>
37
+ );
38
+ }
39
+
40
+ function UsersListScreen() {
41
+ const router = useRouter();
42
+ const { t } = useI18n();
43
+ const permissions = useCrudPermission("USER");
44
+ const [users, setUsers] = useState<UserSummary[]>([]);
45
+ const [page, setPage] = useState(0);
46
+ const [rows, setRows] = useState<number>(
47
+ securityConfig.pagination.defaultPageSize
48
+ );
49
+ const [totalRecords, setTotalRecords] = useState(0);
50
+ const [loading, setLoading] = useState(true);
51
+ const [searchValue, setSearchValue] = useState("");
52
+ const [sort, setSort] = useState<SortState>({
53
+ sortField: "name",
54
+ sortOrder: 1,
55
+ });
56
+ const [selectedUser, setSelectedUser] = useState<UserSummary | null>(null);
57
+ const debouncedSearch = useDebouncedSearch(searchValue);
58
+ const effectiveSearch = isSearchReady(debouncedSearch) ? debouncedSearch : "";
59
+
60
+ const searchInputProps = {
61
+ value: searchValue,
62
+ onChange: (event: { target: { value: string } }) => {
63
+ setSearchValue(event.target.value);
64
+ setPage(0);
65
+ },
66
+ };
67
+
68
+ useEffect(() => {
69
+ let active = true;
70
+ setLoading(true);
71
+
72
+ void (async () => {
73
+ try {
74
+ const response = await securityService.listUsersPage({
75
+ page,
76
+ size: rows,
77
+ search: effectiveSearch,
78
+ sort,
79
+ });
80
+
81
+ if (!active) {
82
+ return;
83
+ }
84
+
85
+ setUsers(response.items);
86
+ setTotalRecords(response.total);
87
+ } catch (error) {
88
+ if (!active) {
89
+ return;
90
+ }
91
+
92
+ sgWhistle.error({
93
+ message:
94
+ extractApiMessage(error) ?? t("security.users.list.delete_error"),
95
+ });
96
+ } finally {
97
+ if (active) {
98
+ setLoading(false);
99
+ }
100
+ }
101
+ })();
102
+
103
+ return () => {
104
+ active = false;
105
+ };
106
+ }, [effectiveSearch, page, rows, sort, t]);
107
+
108
+ const columns = useMemo(
109
+ () => [
110
+ {
111
+ field: "name",
112
+ header: t("security.users.list.column.name"),
113
+ sortable: true,
114
+ },
115
+ {
116
+ field: "email",
117
+ header: t("security.users.list.column.email"),
118
+ sortable: true,
119
+ },
120
+ {
121
+ field: "whatsApp",
122
+ header: t("security.users.list.column.whatsapp"),
123
+ },
124
+ {
125
+ field: "roles",
126
+ header: t("security.users.list.column.roles"),
127
+ body: (user: UserSummary) =>
128
+ user.roles?.map((role) => role.name).join(", ") || "-",
129
+ },
130
+ {
131
+ field: "active",
132
+ header: t("security.users.list.column.status"),
133
+ body: (user: UserSummary) => (
134
+ <StatusBadge
135
+ active={Boolean(user.active)}
136
+ activeLabel={t("security.common.enabled")}
137
+ inactiveLabel={t("security.common.disabled")}
138
+ />
139
+ ),
140
+ },
141
+ {
142
+ header: t("security.common.actions"),
143
+ body: (user: UserSummary) => (
144
+ <SgStack direction="row" wrap gap={8} data-sg-stop-row-select="true">
145
+ <SgButton
146
+ appearance="ghost"
147
+ severity="info"
148
+ iconOnly
149
+ leftIcon={<Pencil size={16} />}
150
+ title={
151
+ permissions.canUpdate
152
+ ? t("security.users.list.action.edit")
153
+ : t("security.users.list.action.view")
154
+ }
155
+ aria-label={
156
+ permissions.canUpdate
157
+ ? t("security.users.list.action.edit")
158
+ : t("security.users.list.action.view")
159
+ }
160
+ onClick={() => {
161
+ router.push(`${securityConfig.routes.users}/${user.publicId}`);
162
+ }}
163
+ 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"
164
+ />
165
+ {permissions.canDelete ? (
166
+ <SgButton
167
+ appearance="ghost"
168
+ severity="danger"
169
+ iconOnly
170
+ leftIcon={<Trash2 size={16} />}
171
+ title={t("security.users.list.action.delete")}
172
+ aria-label={t("security.users.list.action.delete")}
173
+ onClick={() => {
174
+ setSelectedUser(user);
175
+ }}
176
+ 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"
177
+ />
178
+ ) : null}
179
+ </SgStack>
180
+ ),
181
+ },
182
+ ],
183
+ [permissions.canDelete, permissions.canUpdate, router, t]
184
+ );
185
+
186
+ async function handleDelete() {
187
+ if (!selectedUser) {
188
+ return;
189
+ }
190
+
191
+ try {
192
+ await securityService.deleteUser(selectedUser.publicId);
193
+ sgWhistle.success({ message: t("security.users.list.delete_success") });
194
+ setSelectedUser(null);
195
+ setLoading(true);
196
+ const response = await securityService.listUsersPage({
197
+ page,
198
+ size: rows,
199
+ search: effectiveSearch,
200
+ sort,
201
+ });
202
+ setUsers(response.items);
203
+ setTotalRecords(response.total);
204
+ } catch (error) {
205
+ sgWhistle.error({
206
+ message: extractApiMessage(error) ?? t("security.users.list.delete_error"),
207
+ });
208
+ } finally {
209
+ setLoading(false);
210
+ }
211
+ }
212
+
213
+ return (
214
+ <PageFrame
215
+ title={t("security.users.list.title")}
216
+ description={t("security.users.list.description")}
217
+ readonlyLabel={
218
+ permissions.isReadOnly ? t("security.common.readonly") : null
219
+ }
220
+ actions={
221
+ permissions.canCreate ? (
222
+ <SgButton
223
+ appearance="solid"
224
+ severity="primary"
225
+ shape="rounded"
226
+ label={t("security.users.list.new")}
227
+ onClick={() => {
228
+ router.push(`${securityConfig.routes.users}/new`);
229
+ }}
230
+ />
231
+ ) : undefined
232
+ }
233
+ >
234
+ <SgStack gap={16}>
235
+ <SgInputText
236
+ id="security-users-search"
237
+ label={t("security.common.search")}
238
+ inputProps={searchInputProps}
239
+ placeholder={t("security.users.list.search_placeholder")}
240
+ />
241
+
242
+ {!isSearchReady(searchValue) && searchValue.trim().length > 0 ? (
243
+ <InlineNotice>{t("security.common.search_hint")}</InlineNotice>
244
+ ) : null}
245
+
246
+ <SgDatatable<UserSummary>
247
+ title=" "
248
+ value={users}
249
+ columns={columns}
250
+ dataKey="publicId"
251
+ lazy
252
+ paginator
253
+ rows={rows}
254
+ first={page * rows}
255
+ totalRecords={totalRecords}
256
+ sortField={sort.sortField}
257
+ sortOrder={sort.sortOrder}
258
+ loading={loading}
259
+ emptyMessage={t("security.users.list.empty")}
260
+ onPage={(event) => {
261
+ setRows(event.rows ?? securityConfig.pagination.defaultPageSize);
262
+ setPage(event.page ?? 0);
263
+ }}
264
+ onSort={(event) => {
265
+ setPage(0);
266
+ setSort({
267
+ sortField: event.sortField ?? "name",
268
+ sortOrder:
269
+ event.sortOrder === 1 || event.sortOrder === -1
270
+ ? event.sortOrder
271
+ : 1,
272
+ });
273
+ }}
274
+ />
275
+ </SgStack>
276
+
277
+ <SgConfirmationDialog
278
+ open={Boolean(selectedUser)}
279
+ severity="danger"
280
+ title={t("security.users.list.delete_title")}
281
+ message={t("security.users.list.delete_message")}
282
+ onCancel={() => {
283
+ setSelectedUser(null);
284
+ }}
285
+ onConfirm={handleDelete}
286
+ />
287
+ </PageFrame>
288
+ );
289
+ }