@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.
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +5 -0
- package/dist/client/permission-provider.d.ts +18 -0
- package/dist/client/permission-provider.d.ts.map +1 -0
- package/dist/client/permission-provider.js +57 -0
- package/dist/client/pick-list-field.d.ts +15 -0
- package/dist/client/pick-list-field.d.ts.map +1 -0
- package/dist/client/pick-list-field.js +30 -0
- package/dist/client/status-badge.d.ts +6 -0
- package/dist/client/status-badge.d.ts.map +1 -0
- package/dist/client/status-badge.js +6 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +4 -0
- package/dist/dtos.d.ts +88 -0
- package/dist/dtos.d.ts.map +1 -0
- package/dist/dtos.js +3 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/menu.d.ts +19 -0
- package/dist/menu.d.ts.map +1 -0
- package/dist/menu.js +41 -0
- package/dist/messages.d.ts +1858 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +990 -0
- package/dist/module.d.ts +3 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +37 -0
- package/dist/module.json +21 -0
- package/dist/paths.d.ts +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +3 -0
- package/dist/permissions.d.ts +14 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +39 -0
- package/dist/server/action.d.ts +3 -0
- package/dist/server/action.d.ts.map +1 -0
- package/dist/server/action.js +21 -0
- package/dist/server/create-security-server.d.ts +4 -0
- package/dist/server/create-security-server.d.ts.map +1 -0
- package/dist/server/create-security-server.js +166 -0
- package/dist/server/errors.d.ts +13 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/errors.js +26 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/problem-response.d.ts +3 -0
- package/dist/server/problem-response.d.ts.map +1 -0
- package/dist/server/problem-response.js +28 -0
- package/dist/server/route.d.ts +3 -0
- package/dist/server/route.d.ts.map +1 -0
- package/dist/server/route.js +22 -0
- package/dist/server/to-problem.d.ts +3 -0
- package/dist/server/to-problem.d.ts.map +1 -0
- package/dist/server/to-problem.js +22 -0
- package/dist/server-contract.d.ts +23 -0
- package/dist/server-contract.d.ts.map +1 -0
- package/dist/server-contract.js +4 -0
- package/dist/session-contract.d.ts +36 -0
- package/dist/session-contract.d.ts.map +1 -0
- package/dist/session-contract.js +8 -0
- package/dist/session-result.d.ts +22 -0
- package/dist/session-result.d.ts.map +1 -0
- package/dist/session-result.js +14 -0
- package/package.json +38 -3
- package/scaffold/README.md +97 -0
- package/scaffold/src/app/api/security/session/2fa/resend/route.ts +8 -0
- package/scaffold/src/app/api/security/session/2fa/verify/route.ts +8 -0
- package/scaffold/src/app/api/security/session/login/route.ts +7 -0
- package/scaffold/src/app/api/security/session/logout/route.ts +5 -0
- package/scaffold/src/app/api/security/session/refresh/route.ts +7 -0
- package/scaffold/src/app/api/security/users/[publicId]/avatar/route.ts +15 -0
- package/scaffold/src/app/auth/2fa/verify/page.tsx +286 -0
- package/scaffold/src/app/auth/forgot-password/confirm/page.tsx +96 -0
- package/scaffold/src/app/auth/forgot-password/page.tsx +165 -0
- package/scaffold/src/app/auth/forgot-password/success/page.tsx +31 -0
- package/scaffold/src/app/auth/login/page.tsx +386 -0
- package/scaffold/src/app/auth/welcome/page.tsx +90 -0
- package/scaffold/src/app/login/page.tsx +7 -0
- package/scaffold/src/app/page.tsx +12 -0
- package/scaffold/src/app/security/2fa/page.tsx +370 -0
- package/scaffold/src/app/security/companies/(create)/new/page.tsx +12 -0
- package/scaffold/src/app/security/companies/(edit)/[publicId]/page.tsx +18 -0
- package/scaffold/src/app/security/companies/(list)/page.tsx +298 -0
- package/scaffold/src/app/security/companies/editors/CompanyEditor.tsx +754 -0
- package/scaffold/src/app/security/profile/page.tsx +492 -0
- package/scaffold/src/app/security/roles/(create)/new/page.tsx +11 -0
- package/scaffold/src/app/security/roles/(edit)/[publicId]/page.tsx +17 -0
- package/scaffold/src/app/security/roles/(list)/page.tsx +269 -0
- package/scaffold/src/app/security/roles/editors/RoleEditor.tsx +328 -0
- package/scaffold/src/app/security/users/(create)/new/page.tsx +10 -0
- package/scaffold/src/app/security/users/(edit)/[publicId]/page.tsx +16 -0
- package/scaffold/src/app/security/users/(list)/page.tsx +289 -0
- package/scaffold/src/app/security/users/editors/UserEditor.tsx +478 -0
- package/scaffold/src/app/users/change-password/page.tsx +172 -0
- package/scaffold/src/modules/security/AuthProvider.tsx +405 -0
- package/scaffold/src/modules/security/components/AppShell.tsx.hbs +889 -0
- package/scaffold/src/modules/security/components/CompanyManagementGuard.tsx +59 -0
- package/scaffold/src/modules/security/components/PageFrame.tsx +105 -0
- package/scaffold/src/modules/security/components/RouteGuard.tsx +114 -0
- package/scaffold/src/modules/security/components/SecurityDashboard.tsx.hbs +315 -0
- package/scaffold/src/modules/security/config.ts +55 -0
- package/scaffold/src/modules/security/hooks/useAuth.ts +7 -0
- package/scaffold/src/modules/security/hooks/useDebouncedSearch.ts +13 -0
- package/scaffold/src/modules/security/hooks/usePermission.ts +35 -0
- package/scaffold/src/modules/security/host-access.ts +59 -0
- package/scaffold/src/modules/security/index.ts +44 -0
- package/scaffold/src/modules/security/navigation.ts +41 -0
- package/scaffold/src/modules/security/server/avatar-routes.ts +86 -0
- package/scaffold/src/modules/security/server/public-api-base-url.ts +26 -0
- package/scaffold/src/modules/security/server/session-routes.ts.hbs +322 -0
- package/scaffold/src/modules/security/server/session-server.ts.hbs +30 -0
- package/scaffold/src/modules/security/services/auth-service.ts +332 -0
- package/scaffold/src/modules/security/services/security-api.test.ts +178 -0
- package/scaffold/src/modules/security/services/security-api.ts +281 -0
- package/scaffold/src/modules/security/services/security-service.test.ts +71 -0
- package/scaffold/src/modules/security/services/security-service.ts +727 -0
- package/scaffold/src/modules/security/services/session-storage.ts +80 -0
- package/scaffold/src/modules/security/types.ts +295 -0
- package/scaffold/src/modules/security/useSecurityAppShellSections.tsx.hbs +180 -0
- package/scaffold/src/modules/security/utils.ts +194 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApiKeyCreatePayload,
|
|
3
|
+
ApiKeyCreateResponse,
|
|
4
|
+
ApiKeyListItem,
|
|
5
|
+
ChangePasswordPayload,
|
|
6
|
+
CompanyMutationPayload,
|
|
7
|
+
CompanySummary,
|
|
8
|
+
DisableTwoFactorPayload,
|
|
9
|
+
EntryRequest,
|
|
10
|
+
PagedResult,
|
|
11
|
+
PermissionSummary,
|
|
12
|
+
RoleMutationPayload,
|
|
13
|
+
RoleSummary,
|
|
14
|
+
SelfEnterInvitation,
|
|
15
|
+
SelfEnterInvitationCompletionPayload,
|
|
16
|
+
SelfEnterInvitationCreatePayload,
|
|
17
|
+
SelfEnterRequestEmailConfirmationPayload,
|
|
18
|
+
SelfEnterRequestPayload,
|
|
19
|
+
SortState,
|
|
20
|
+
TwoFactorEnableResponse,
|
|
21
|
+
TwoFactorStatus,
|
|
22
|
+
UserMutationPayload,
|
|
23
|
+
UserSummary,
|
|
24
|
+
} from "../types";
|
|
25
|
+
import {
|
|
26
|
+
createPagedResult,
|
|
27
|
+
hasActiveSearch,
|
|
28
|
+
normalizeSearchTerm,
|
|
29
|
+
} from "../utils";
|
|
30
|
+
import { securityConfig } from "../config";
|
|
31
|
+
import {
|
|
32
|
+
extractApiMessage,
|
|
33
|
+
isApiClientError,
|
|
34
|
+
securityApi,
|
|
35
|
+
} from "./security-api";
|
|
36
|
+
|
|
37
|
+
type PageRequest<T> = {
|
|
38
|
+
page?: number;
|
|
39
|
+
size?: number;
|
|
40
|
+
search?: string;
|
|
41
|
+
sort?: SortState;
|
|
42
|
+
path: string;
|
|
43
|
+
searchParamNames?: string[];
|
|
44
|
+
extraQuery?: Record<string, string | number | boolean | null | undefined>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const securityService = {
|
|
48
|
+
async listUsersPage(request: {
|
|
49
|
+
page?: number;
|
|
50
|
+
size?: number;
|
|
51
|
+
search?: string;
|
|
52
|
+
sort?: SortState;
|
|
53
|
+
}) {
|
|
54
|
+
return loadPage<UserSummary>({
|
|
55
|
+
...request,
|
|
56
|
+
path: "/users",
|
|
57
|
+
searchParamNames: ["search", "name", "email"],
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async getUser(publicId: string) {
|
|
62
|
+
return securityApi.get<UserSummary>(`/users/${publicId}`, {
|
|
63
|
+
retry: false,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async createUser(payload: UserMutationPayload) {
|
|
68
|
+
return securityApi.post<UserSummary>("/users", {
|
|
69
|
+
body: payload,
|
|
70
|
+
retry: false,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async updateUser(publicId: string, payload: Partial<UserMutationPayload>) {
|
|
75
|
+
return securityApi.patch<UserSummary>(`/users/${publicId}`, {
|
|
76
|
+
body: payload,
|
|
77
|
+
retry: false,
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
buildUserAvatarUrl(publicId: string, cacheBust?: string | number | null) {
|
|
82
|
+
const normalizedPublicId = publicId.trim();
|
|
83
|
+
|
|
84
|
+
if (!normalizedPublicId) {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const baseUrl = `/api/security/users/${encodeURIComponent(normalizedPublicId)}/avatar`;
|
|
89
|
+
|
|
90
|
+
if (cacheBust === undefined || cacheBust === null || `${cacheBust}`.trim() === "") {
|
|
91
|
+
return baseUrl;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return `${baseUrl}?v=${encodeURIComponent(String(cacheBust))}`;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async fetchUserAvatarObjectUrl(
|
|
98
|
+
publicId: string,
|
|
99
|
+
cacheBust?: string | number | null
|
|
100
|
+
) {
|
|
101
|
+
const normalizedPublicId = publicId.trim();
|
|
102
|
+
|
|
103
|
+
if (!normalizedPublicId || typeof URL === "undefined") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const response = await securityApi.get<Response>(
|
|
109
|
+
`/users/${encodeURIComponent(normalizedPublicId)}/avatar`,
|
|
110
|
+
{
|
|
111
|
+
parseAs: "response",
|
|
112
|
+
query:
|
|
113
|
+
cacheBust === undefined ||
|
|
114
|
+
cacheBust === null ||
|
|
115
|
+
`${cacheBust}`.trim() === ""
|
|
116
|
+
? undefined
|
|
117
|
+
: { v: String(cacheBust) },
|
|
118
|
+
retry: false,
|
|
119
|
+
captureExceptions: false,
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
const blob = await response.blob();
|
|
123
|
+
|
|
124
|
+
if (!blob.size) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return URL.createObjectURL(blob);
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
async uploadUserAvatar(
|
|
135
|
+
publicId: string,
|
|
136
|
+
payload: {
|
|
137
|
+
filename: string;
|
|
138
|
+
contentType: string;
|
|
139
|
+
base64: string;
|
|
140
|
+
}
|
|
141
|
+
) {
|
|
142
|
+
return securityApi.post<UserSummary>(`/users/${publicId}/avatar`, {
|
|
143
|
+
body: payload,
|
|
144
|
+
retry: false,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async deleteUser(publicId: string) {
|
|
149
|
+
return securityApi.delete<void>(`/users/${publicId}`, {
|
|
150
|
+
retry: false,
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
async listRolesPage(request: {
|
|
155
|
+
page?: number;
|
|
156
|
+
size?: number;
|
|
157
|
+
search?: string;
|
|
158
|
+
sort?: SortState;
|
|
159
|
+
}) {
|
|
160
|
+
return loadPage<RoleSummary>({
|
|
161
|
+
...request,
|
|
162
|
+
path: "/roles",
|
|
163
|
+
searchParamNames: ["search", "name"],
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
async listRoles() {
|
|
168
|
+
return securityApi.get<RoleSummary[]>("/roles", {
|
|
169
|
+
retry: false,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async getRole(publicId: string) {
|
|
174
|
+
return securityApi.get<RoleSummary>(`/roles/${publicId}`, {
|
|
175
|
+
retry: false,
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
async listPermissions() {
|
|
180
|
+
const response = await securityApi.get<unknown>("/permissions", {
|
|
181
|
+
retry: false,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return normalizeCollectionResponse<PermissionSummary>(response);
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
async createRole(payload: RoleMutationPayload) {
|
|
188
|
+
return securityApi.post<RoleSummary>("/roles", {
|
|
189
|
+
body: payload,
|
|
190
|
+
retry: false,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async updateRole(publicId: string, payload: Partial<RoleMutationPayload>) {
|
|
195
|
+
return securityApi.patch<RoleSummary>(`/roles/${publicId}`, {
|
|
196
|
+
body: payload,
|
|
197
|
+
retry: false,
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async deleteRole(publicId: string) {
|
|
202
|
+
return securityApi.delete<void>(`/roles/${publicId}`, {
|
|
203
|
+
retry: false,
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async listIntegrationRoles() {
|
|
208
|
+
const response = await securityApi.get<unknown>("/roles/integration", {
|
|
209
|
+
retry: false,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return normalizeCollectionResponse<RoleSummary>(response);
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
async listCompaniesPage(request: {
|
|
216
|
+
page?: number;
|
|
217
|
+
size?: number;
|
|
218
|
+
search?: string;
|
|
219
|
+
sort?: SortState;
|
|
220
|
+
}) {
|
|
221
|
+
return loadPage<CompanySummary>({
|
|
222
|
+
...request,
|
|
223
|
+
path: "/companies",
|
|
224
|
+
searchParamNames: ["search", "tradeName", "corporateName"],
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
async listAllCompanies() {
|
|
229
|
+
return securityApi.get<CompanySummary[]>("/companies", {
|
|
230
|
+
retry: false,
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async getCompany(publicId: string) {
|
|
235
|
+
return securityApi.get<CompanySummary>(`/companies/${publicId}`, {
|
|
236
|
+
retry: false,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async createCompany(payload: CompanyMutationPayload) {
|
|
241
|
+
return securityApi.post<CompanySummary>("/companies", {
|
|
242
|
+
body: payload,
|
|
243
|
+
retry: false,
|
|
244
|
+
});
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
async updateCompany(
|
|
248
|
+
publicId: string,
|
|
249
|
+
payload: Partial<CompanyMutationPayload>
|
|
250
|
+
) {
|
|
251
|
+
return securityApi.patch<CompanySummary>(`/companies/${publicId}`, {
|
|
252
|
+
body: payload,
|
|
253
|
+
retry: false,
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
async deleteCompany(publicId: string) {
|
|
258
|
+
return securityApi.delete<void>(`/companies/${publicId}`, {
|
|
259
|
+
retry: false,
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
async getTwoFactorStatus() {
|
|
264
|
+
return securityApi.get<TwoFactorStatus>("/2f/status", {
|
|
265
|
+
retry: false,
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
async enableTwoFactor(method: string, includeQrImage = true) {
|
|
270
|
+
return securityApi.post<TwoFactorEnableResponse>("/2f/enable", {
|
|
271
|
+
query: {
|
|
272
|
+
includeQrImage,
|
|
273
|
+
},
|
|
274
|
+
body: {
|
|
275
|
+
method,
|
|
276
|
+
},
|
|
277
|
+
retry: false,
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async confirmTotp(code: string) {
|
|
282
|
+
return securityApi.post<TwoFactorEnableResponse>("/2f/totp/confirm", {
|
|
283
|
+
body: {
|
|
284
|
+
code,
|
|
285
|
+
},
|
|
286
|
+
retry: false,
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
async disableTwoFactor(payload: DisableTwoFactorPayload) {
|
|
291
|
+
return securityApi.post<{ status?: string }>("/2f/disable", {
|
|
292
|
+
body: payload,
|
|
293
|
+
retry: false,
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
async changePassword(payload: ChangePasswordPayload) {
|
|
298
|
+
return securityApi.put<UserSummary>("/users/change-password", {
|
|
299
|
+
body: payload,
|
|
300
|
+
retry: false,
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
async createSelfEnterRequest(payload: SelfEnterRequestPayload) {
|
|
305
|
+
return securityApi.public.post<EntryRequest>("/public/self-enter/requests", {
|
|
306
|
+
body: payload,
|
|
307
|
+
retry: false,
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
async confirmSelfEnterRequestEmail(
|
|
312
|
+
id: string | number,
|
|
313
|
+
payload: SelfEnterRequestEmailConfirmationPayload
|
|
314
|
+
) {
|
|
315
|
+
return securityApi.public.post<EntryRequest>(
|
|
316
|
+
`/public/self-enter/requests/${id}/confirm-email`,
|
|
317
|
+
{
|
|
318
|
+
body: payload,
|
|
319
|
+
retry: false,
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
async resendSelfEnterRequestEmail(id: string | number) {
|
|
325
|
+
return securityApi.public.post<EntryRequest>(
|
|
326
|
+
`/public/self-enter/requests/${id}/resend-email`,
|
|
327
|
+
{
|
|
328
|
+
retry: false,
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
async getSelfEnterInvitation(token: string) {
|
|
334
|
+
return securityApi.public.get<SelfEnterInvitation>(
|
|
335
|
+
`/public/self-enter/invitations/${encodeURIComponent(token)}`,
|
|
336
|
+
{
|
|
337
|
+
retry: false,
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
async createSelfEnterInvitation(payload: SelfEnterInvitationCreatePayload) {
|
|
343
|
+
return securityApi.post<EntryRequest>("/self-enter/invitations", {
|
|
344
|
+
body: payload,
|
|
345
|
+
retry: false,
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
async listSelfEnterInvitationsPage(request: {
|
|
350
|
+
page?: number;
|
|
351
|
+
size?: number;
|
|
352
|
+
search?: string;
|
|
353
|
+
sort?: SortState;
|
|
354
|
+
}) {
|
|
355
|
+
return loadPage<EntryRequest>({
|
|
356
|
+
...request,
|
|
357
|
+
path: "/self-enter/invitations",
|
|
358
|
+
searchParamNames: ["search", "name", "email"],
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
async resendSelfEnterInvitation(id: string | number) {
|
|
363
|
+
return securityApi.post<{ message?: string; requestId?: string }>(
|
|
364
|
+
`/self-enter/invitations/${id}/resend`,
|
|
365
|
+
{
|
|
366
|
+
retry: false,
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
async completeSelfEnterInvitation(
|
|
372
|
+
token: string,
|
|
373
|
+
payload: SelfEnterInvitationCompletionPayload
|
|
374
|
+
) {
|
|
375
|
+
return securityApi.public.post<EntryRequest>(
|
|
376
|
+
`/public/self-enter/invitations/${encodeURIComponent(token)}/complete`,
|
|
377
|
+
{
|
|
378
|
+
body: payload,
|
|
379
|
+
retry: false,
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
async listSelfEnterRequestsPage(request: {
|
|
385
|
+
page?: number;
|
|
386
|
+
size?: number;
|
|
387
|
+
search?: string;
|
|
388
|
+
sort?: SortState;
|
|
389
|
+
}) {
|
|
390
|
+
return loadPage<EntryRequest>({
|
|
391
|
+
...request,
|
|
392
|
+
path: "/self-enter/requests/to-approve",
|
|
393
|
+
searchParamNames: ["search", "name", "email"],
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
async approveSelfEnterRequest(id: string | number) {
|
|
398
|
+
return securityApi.post<{ message?: string }>(
|
|
399
|
+
`/self-enter/requests/${id}/approve`,
|
|
400
|
+
{
|
|
401
|
+
retry: false,
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
async rejectSelfEnterRequest(id: string | number) {
|
|
407
|
+
return securityApi.post<{ message?: string }>(
|
|
408
|
+
`/self-enter/requests/${id}/reject`,
|
|
409
|
+
{
|
|
410
|
+
retry: false,
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
async listApiKeysPage(request: {
|
|
416
|
+
page?: number;
|
|
417
|
+
size?: number;
|
|
418
|
+
search?: string;
|
|
419
|
+
roleName?: string;
|
|
420
|
+
sort?: SortState;
|
|
421
|
+
}) {
|
|
422
|
+
const normalizedRoleName = request.roleName?.trim();
|
|
423
|
+
|
|
424
|
+
if (normalizedRoleName) {
|
|
425
|
+
const response = await securityApi.get<ApiKeyListItem[]>(
|
|
426
|
+
`/apikeys/role/${encodeURIComponent(normalizedRoleName)}`,
|
|
427
|
+
{
|
|
428
|
+
retry: false,
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
return normalizePagedResponse<ApiKeyListItem>(
|
|
433
|
+
response,
|
|
434
|
+
request.page,
|
|
435
|
+
request.size
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return loadPage<ApiKeyListItem>({
|
|
440
|
+
...request,
|
|
441
|
+
path: "/apikeys",
|
|
442
|
+
searchParamNames: ["search", "description", "name"],
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
async createApiKey(payload: ApiKeyCreatePayload) {
|
|
447
|
+
return securityApi.post<ApiKeyCreateResponse>(
|
|
448
|
+
`/apikeys/role/${encodeURIComponent(payload.roleName)}`,
|
|
449
|
+
{
|
|
450
|
+
body: {
|
|
451
|
+
name: payload.name,
|
|
452
|
+
expiresDays: payload.expiresDays ?? null,
|
|
453
|
+
},
|
|
454
|
+
retry: false,
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
async revokeApiKey(publicId: string) {
|
|
460
|
+
return securityApi.post<{ revoked?: boolean; revokedAt?: string }>(
|
|
461
|
+
`/apikeys/${publicId}/revoke`,
|
|
462
|
+
{
|
|
463
|
+
retry: false,
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
async function loadPage<T>({
|
|
470
|
+
page,
|
|
471
|
+
size,
|
|
472
|
+
search,
|
|
473
|
+
sort,
|
|
474
|
+
path,
|
|
475
|
+
searchParamNames = ["search"],
|
|
476
|
+
extraQuery = {},
|
|
477
|
+
}: PageRequest<T>): Promise<PagedResult<T>> {
|
|
478
|
+
const normalizedSearch = normalizeSearchTerm(search ?? "");
|
|
479
|
+
|
|
480
|
+
const response = await securityApi.get<unknown>(path, {
|
|
481
|
+
query: buildPagedQuery({
|
|
482
|
+
page,
|
|
483
|
+
size,
|
|
484
|
+
search: normalizedSearch,
|
|
485
|
+
searchParamNames,
|
|
486
|
+
sort,
|
|
487
|
+
extraQuery,
|
|
488
|
+
}),
|
|
489
|
+
retry: false,
|
|
490
|
+
captureExceptions: false,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
return normalizePagedResponse<T>(response, page, size);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function buildPagedQuery({
|
|
497
|
+
page,
|
|
498
|
+
size,
|
|
499
|
+
search,
|
|
500
|
+
searchParamNames,
|
|
501
|
+
sort,
|
|
502
|
+
extraQuery,
|
|
503
|
+
}: {
|
|
504
|
+
page?: number;
|
|
505
|
+
size?: number;
|
|
506
|
+
search: string;
|
|
507
|
+
searchParamNames: string[];
|
|
508
|
+
sort?: SortState;
|
|
509
|
+
extraQuery: Record<string, string | number | boolean | null | undefined>;
|
|
510
|
+
}) {
|
|
511
|
+
const shouldPaginate =
|
|
512
|
+
typeof page === "number" &&
|
|
513
|
+
Number.isFinite(page) &&
|
|
514
|
+
typeof size === "number" &&
|
|
515
|
+
Number.isFinite(size);
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
page: shouldPaginate ? page : undefined,
|
|
519
|
+
size: shouldPaginate ? size : undefined,
|
|
520
|
+
sortField: sort?.sortField ?? undefined,
|
|
521
|
+
sortOrder: sort?.sortOrder ?? undefined,
|
|
522
|
+
...buildSearchQuery(search, searchParamNames),
|
|
523
|
+
...extraQuery,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function buildSearchQuery(search: string, searchParamNames: string[]) {
|
|
528
|
+
if (!hasActiveSearch(search)) {
|
|
529
|
+
return {};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return searchParamNames.reduce<Record<string, string>>((query, paramName) => {
|
|
533
|
+
query[paramName] = search;
|
|
534
|
+
return query;
|
|
535
|
+
}, {});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function normalizePagedResponse<T>(
|
|
539
|
+
value: unknown,
|
|
540
|
+
fallbackPage?: number,
|
|
541
|
+
fallbackSize?: number
|
|
542
|
+
): PagedResult<T> {
|
|
543
|
+
const emptyPage =
|
|
544
|
+
typeof fallbackPage === "number" && Number.isFinite(fallbackPage)
|
|
545
|
+
? fallbackPage
|
|
546
|
+
: 0;
|
|
547
|
+
const emptySize =
|
|
548
|
+
typeof fallbackSize === "number" && Number.isFinite(fallbackSize)
|
|
549
|
+
? fallbackSize
|
|
550
|
+
: 0;
|
|
551
|
+
|
|
552
|
+
if (!value || typeof value !== "object") {
|
|
553
|
+
return createPagedResult([], 0, emptyPage, emptySize);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const items = normalizeCollectionResponse<T>(value);
|
|
557
|
+
|
|
558
|
+
if (Array.isArray(value)) {
|
|
559
|
+
return createPagedResult(items, items.length, emptyPage, items.length);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const record = value as Record<string, unknown>;
|
|
563
|
+
const total =
|
|
564
|
+
typeof record.total === "number" && Number.isFinite(record.total)
|
|
565
|
+
? record.total
|
|
566
|
+
: items.length;
|
|
567
|
+
const page =
|
|
568
|
+
typeof record.page === "number" && Number.isFinite(record.page)
|
|
569
|
+
? record.page
|
|
570
|
+
: emptyPage;
|
|
571
|
+
const size =
|
|
572
|
+
typeof record.size === "number" && Number.isFinite(record.size)
|
|
573
|
+
? record.size
|
|
574
|
+
: typeof fallbackSize === "number" && Number.isFinite(fallbackSize)
|
|
575
|
+
? fallbackSize
|
|
576
|
+
: items.length;
|
|
577
|
+
|
|
578
|
+
return createPagedResult(items, total, page, size);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function normalizeCollectionResponse<T>(value: unknown) {
|
|
582
|
+
if (Array.isArray(value)) {
|
|
583
|
+
return value as T[];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (value && typeof value === "object") {
|
|
587
|
+
const record = value as Record<string, unknown>;
|
|
588
|
+
|
|
589
|
+
if (Array.isArray(record.items)) {
|
|
590
|
+
return record.items as T[];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return [] as T[];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const SELF_ENTER_INVITATION_CONSUMED_PATTERNS = [
|
|
598
|
+
"already used",
|
|
599
|
+
"already been used",
|
|
600
|
+
"already consumed",
|
|
601
|
+
"consumed",
|
|
602
|
+
"ja foi usado",
|
|
603
|
+
"ja utilizada",
|
|
604
|
+
"ja utilizado",
|
|
605
|
+
"consumido",
|
|
606
|
+
"consumida",
|
|
607
|
+
"ya fue usado",
|
|
608
|
+
"ya ha sido usado",
|
|
609
|
+
"consumido",
|
|
610
|
+
"utilise",
|
|
611
|
+
"deja utilise",
|
|
612
|
+
];
|
|
613
|
+
|
|
614
|
+
const SELF_ENTER_INVITATION_INVALID_PATTERNS = [
|
|
615
|
+
"invalid",
|
|
616
|
+
"expired",
|
|
617
|
+
"not found",
|
|
618
|
+
"unknown invitation",
|
|
619
|
+
"invalido",
|
|
620
|
+
"invalida",
|
|
621
|
+
"expirado",
|
|
622
|
+
"expirada",
|
|
623
|
+
"nao encontrado",
|
|
624
|
+
"nao encontrada",
|
|
625
|
+
"nao existe",
|
|
626
|
+
"no encontrado",
|
|
627
|
+
"no encontrada",
|
|
628
|
+
"expiro",
|
|
629
|
+
"expire",
|
|
630
|
+
"introuvable",
|
|
631
|
+
];
|
|
632
|
+
|
|
633
|
+
const SELF_ENTER_INVITATION_EMAIL_MISMATCH_PATTERNS = [
|
|
634
|
+
"email does not match",
|
|
635
|
+
"email mismatch",
|
|
636
|
+
"email must match",
|
|
637
|
+
"email diferente",
|
|
638
|
+
"email diverge",
|
|
639
|
+
"email nao corresponde",
|
|
640
|
+
"email no coincide",
|
|
641
|
+
"correo no coincide",
|
|
642
|
+
];
|
|
643
|
+
|
|
644
|
+
const SELF_ENTER_INVITATION_ADMIN_DUPLICATE_PATTERNS = [
|
|
645
|
+
"already exists",
|
|
646
|
+
"active entry request already exists",
|
|
647
|
+
"usuario ja existe",
|
|
648
|
+
"recurso ja existe",
|
|
649
|
+
"ja existe uma solicitacao ativa",
|
|
650
|
+
"ja existe um usuario",
|
|
651
|
+
];
|
|
652
|
+
|
|
653
|
+
export function classifySelfEnterInvitationError(error: unknown) {
|
|
654
|
+
if (isApiClientError(error)) {
|
|
655
|
+
if (error.status === 409) {
|
|
656
|
+
return "consumed" as const;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (error.status === 404 || error.status === 410) {
|
|
660
|
+
return "invalid" as const;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const normalizedMessage = normalizeComparableMessage(extractApiMessage(error));
|
|
665
|
+
|
|
666
|
+
if (!normalizedMessage) {
|
|
667
|
+
return "unknown" as const;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (includesAnyPattern(normalizedMessage, SELF_ENTER_INVITATION_CONSUMED_PATTERNS)) {
|
|
671
|
+
return "consumed" as const;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (includesAnyPattern(normalizedMessage, SELF_ENTER_INVITATION_INVALID_PATTERNS)) {
|
|
675
|
+
return "invalid" as const;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return "unknown" as const;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export function isSelfEnterInvitationEmailMismatchError(error: unknown) {
|
|
682
|
+
const normalizedMessage = normalizeComparableMessage(extractApiMessage(error));
|
|
683
|
+
|
|
684
|
+
return (
|
|
685
|
+
normalizedMessage !== null &&
|
|
686
|
+
includesAnyPattern(
|
|
687
|
+
normalizedMessage,
|
|
688
|
+
SELF_ENTER_INVITATION_EMAIL_MISMATCH_PATTERNS
|
|
689
|
+
)
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function classifySelfEnterInvitationAdminError(error: unknown) {
|
|
694
|
+
if (isApiClientError(error) && error.status === 409) {
|
|
695
|
+
return "duplicate_email" as const;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const normalizedMessage = normalizeComparableMessage(extractApiMessage(error));
|
|
699
|
+
|
|
700
|
+
if (
|
|
701
|
+
normalizedMessage !== null &&
|
|
702
|
+
includesAnyPattern(
|
|
703
|
+
normalizedMessage,
|
|
704
|
+
SELF_ENTER_INVITATION_ADMIN_DUPLICATE_PATTERNS
|
|
705
|
+
)
|
|
706
|
+
) {
|
|
707
|
+
return "duplicate_email" as const;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return "unknown" as const;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function normalizeComparableMessage(message: string | null) {
|
|
714
|
+
if (!message) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return message
|
|
719
|
+
.normalize("NFD")
|
|
720
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
721
|
+
.trim()
|
|
722
|
+
.toLowerCase();
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function includesAnyPattern(message: string, patterns: string[]) {
|
|
726
|
+
return patterns.some((pattern) => message.includes(pattern));
|
|
727
|
+
}
|