rl-core-front 0.3.0 → 0.5.0
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/package.json +1 -1
- package/src/_services/api/app-error-code.enum.ts +23 -0
- package/src/_services/api/schema.d.ts +86 -132
- package/src/_services/auth/index.ts +2 -0
- package/src/_utils/filter.ts +15 -34
- package/src/_utils/password.ts +40 -0
- package/src/_utils/permission.ts +14 -0
- package/src/_utils/storage.ts +64 -0
- package/src/_utils/user.ts +19 -0
- package/src/components/app-shell.tsx +85 -11
- package/src/components/brand-header.tsx +23 -8
- package/src/components/brand-panel.tsx +41 -0
- package/src/components/index.ts +1 -0
- package/src/components/password-field.tsx +49 -22
- package/src/components/ui/alert.tsx +34 -5
- package/src/components/ui/badge.tsx +2 -2
- package/src/components/ui/button.tsx +5 -0
- package/src/components/ui/confirm-dialog.tsx +3 -1
- package/src/components/ui/data-table.tsx +13 -4
- package/src/components/ui/dialog.tsx +120 -21
- package/src/components/ui/filter-sheet.tsx +47 -25
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/password-requirements.tsx +105 -0
- package/src/components/ui/segmented-control.tsx +75 -0
- package/src/components/ui/simple-select.tsx +74 -0
- package/src/components/ui/switch.tsx +3 -1
- package/src/components/ui/tabs.tsx +4 -8
- package/src/components/ui/track.styles.ts +31 -0
- package/src/contexts/brand-context.tsx +15 -0
- package/src/contexts/color-mode-context.tsx +4 -2
- package/src/contexts/i18n-context.tsx +72 -6
- package/src/contexts/socket-context.tsx +27 -1
- package/src/features/audit/audit-screen.tsx +13 -140
- package/src/features/audit/components/audit-diff.tsx +156 -0
- package/src/features/audit/components/index.ts +3 -0
- package/src/features/audit/components/record-audit-button.tsx +176 -0
- package/src/features/audit/enums/audit-data-action.enum.ts +20 -0
- package/src/features/audit/hooks/use-audit-trail.ts +9 -2
- package/src/features/audit/services/audit.service.ts +43 -1
- package/src/features/login/components/backup-codes-dialog.tsx +4 -4
- package/src/features/login/components/login-form.tsx +18 -3
- package/src/features/login/components/two-factor-setup.tsx +32 -2
- package/src/features/login/hooks/use-login-flow.ts +60 -7
- package/src/features/login/login-screen.tsx +99 -40
- package/src/features/logs/enums/error-type.enum.ts +21 -0
- package/src/features/logs/enums/request-outcome.enum.ts +14 -0
- package/src/features/logs/hooks/use-request-logs.ts +26 -3
- package/src/features/logs/logs-screen.tsx +2 -14
- package/src/features/logs/panels/index.ts +0 -1
- package/src/features/logs/panels/request-logs-panel.tsx +177 -25
- package/src/features/logs/services/logs.service.ts +13 -51
- package/src/features/notifications/components/notification-item.tsx +10 -8
- package/src/features/notifications/enums/notification-type.enum.ts +8 -0
- package/src/features/profile/force-password-change.tsx +18 -3
- package/src/features/profile/profile-screen.tsx +10 -2
- package/src/features/rbac/panels/groups-panel.tsx +78 -30
- package/src/features/rbac/panels/roles-panel.tsx +2 -2
- package/src/features/rbac/services/rbac.service.ts +23 -27
- package/src/features/recovery/reset-password-screen.tsx +16 -2
- package/src/features/users/components/user-form-dialog.tsx +65 -9
- package/src/features/users/hooks/use-users.ts +24 -3
- package/src/features/users/services/users.service.ts +16 -0
- package/src/features/users/users-screen.tsx +17 -12
- package/src/hooks/use-column-visibility.ts +13 -12
- package/src/hooks/use-countdown.ts +70 -0
- package/src/hooks/use-filters.ts +25 -2
- package/src/hooks/use-list-query.ts +14 -1
- package/src/hooks/use-request.ts +21 -2
- package/src/i18n/messages/en.ts +37 -6
- package/src/i18n/messages/pt.ts +41 -6
- package/src/index.ts +15 -1
- package/src/styles/core.css +99 -2
- package/src/styles/fonts/poppins-latin-400.woff2 +0 -0
- package/src/styles/fonts/poppins-latin-500.woff2 +0 -0
- package/src/styles/fonts/poppins-latin-600.woff2 +0 -0
- package/src/styles/fonts/poppins-latin-700.woff2 +0 -0
- package/tailwind-preset.ts +17 -0
- package/src/components/ui/filter-chips.tsx +0 -88
- package/src/features/logs/hooks/use-error-logs.ts +0 -65
- package/src/features/logs/panels/error-logs-panel.tsx +0 -183
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rl-core-front",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Telas e componentes Next.js do core: login com 2FA, usu\u00e1rios, RBAC, auditoria, logs e listagens com filtro din\u00e2mico",
|
|
5
5
|
"author": "Rodrigo Liberti",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Espelho do `AppErrorCode` do backend (core/filters/appErrorCode.enum.ts).
|
|
3
|
+
*
|
|
4
|
+
* Chave de protocolo, então continua string (§7): o valor é o que trafega no
|
|
5
|
+
* corpo do erro, e a tradução de cada um mora em `errors.codes.<CÓDIGO>` no
|
|
6
|
+
* dicionário de i18n.
|
|
7
|
+
*
|
|
8
|
+
* Só entra aqui o código que a interface precisa **reconhecer** para mudar de
|
|
9
|
+
* comportamento — mostrar a mensagem não exige nada disto, porque o
|
|
10
|
+
* `useRequest` já traduz pelo código que veio.
|
|
11
|
+
*/
|
|
12
|
+
export enum AppErrorCode {
|
|
13
|
+
/** Conta travada por excesso de tentativas. Só um admin reabre. */
|
|
14
|
+
ACCOUNT_LOCKED = "ACCOUNT_LOCKED",
|
|
15
|
+
/** Conta desativada por um administrador. */
|
|
16
|
+
ACCOUNT_INACTIVE = "ACCOUNT_INACTIVE",
|
|
17
|
+
/** E-mail/senha não conferem. */
|
|
18
|
+
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
|
|
19
|
+
/** Código de 2FA não confere. */
|
|
20
|
+
INVALID_TWO_FACTOR_CODE = "INVALID_TWO_FACTOR_CODE",
|
|
21
|
+
/** A janela entre a senha e o 2FA venceu — refazer o login resolve. */
|
|
22
|
+
PENDING_TOKEN_EXPIRED = "PENDING_TOKEN_EXPIRED",
|
|
23
|
+
}
|
|
@@ -528,6 +528,22 @@ export interface paths {
|
|
|
528
528
|
patch?: never;
|
|
529
529
|
trace?: never;
|
|
530
530
|
};
|
|
531
|
+
"/rbac/groups/managed": {
|
|
532
|
+
parameters: {
|
|
533
|
+
query?: never;
|
|
534
|
+
header?: never;
|
|
535
|
+
path?: never;
|
|
536
|
+
cookie?: never;
|
|
537
|
+
};
|
|
538
|
+
get: operations["RbacController_managedGroups"];
|
|
539
|
+
put?: never;
|
|
540
|
+
post?: never;
|
|
541
|
+
delete?: never;
|
|
542
|
+
options?: never;
|
|
543
|
+
head?: never;
|
|
544
|
+
patch?: never;
|
|
545
|
+
trace?: never;
|
|
546
|
+
};
|
|
531
547
|
"/rbac/groups/{id}": {
|
|
532
548
|
parameters: {
|
|
533
549
|
query?: never;
|
|
@@ -608,46 +624,14 @@ export interface paths {
|
|
|
608
624
|
patch?: never;
|
|
609
625
|
trace?: never;
|
|
610
626
|
};
|
|
611
|
-
"/audit/
|
|
612
|
-
parameters: {
|
|
613
|
-
query?: never;
|
|
614
|
-
header?: never;
|
|
615
|
-
path?: never;
|
|
616
|
-
cookie?: never;
|
|
617
|
-
};
|
|
618
|
-
get: operations["AuditController_changes"];
|
|
619
|
-
put?: never;
|
|
620
|
-
post?: never;
|
|
621
|
-
delete?: never;
|
|
622
|
-
options?: never;
|
|
623
|
-
head?: never;
|
|
624
|
-
patch?: never;
|
|
625
|
-
trace?: never;
|
|
626
|
-
};
|
|
627
|
-
"/audit/changes/filter-schema": {
|
|
628
|
-
parameters: {
|
|
629
|
-
query?: never;
|
|
630
|
-
header?: never;
|
|
631
|
-
path?: never;
|
|
632
|
-
cookie?: never;
|
|
633
|
-
};
|
|
634
|
-
get: operations["AuditController_changesFilterSchema"];
|
|
635
|
-
put?: never;
|
|
636
|
-
post?: never;
|
|
637
|
-
delete?: never;
|
|
638
|
-
options?: never;
|
|
639
|
-
head?: never;
|
|
640
|
-
patch?: never;
|
|
641
|
-
trace?: never;
|
|
642
|
-
};
|
|
643
|
-
"/audit/errors": {
|
|
627
|
+
"/audit/requests": {
|
|
644
628
|
parameters: {
|
|
645
629
|
query?: never;
|
|
646
630
|
header?: never;
|
|
647
631
|
path?: never;
|
|
648
632
|
cookie?: never;
|
|
649
633
|
};
|
|
650
|
-
get: operations["
|
|
634
|
+
get: operations["AuditController_requests"];
|
|
651
635
|
put?: never;
|
|
652
636
|
post?: never;
|
|
653
637
|
delete?: never;
|
|
@@ -656,14 +640,14 @@ export interface paths {
|
|
|
656
640
|
patch?: never;
|
|
657
641
|
trace?: never;
|
|
658
642
|
};
|
|
659
|
-
"/audit/
|
|
643
|
+
"/audit/requests/filter-schema": {
|
|
660
644
|
parameters: {
|
|
661
645
|
query?: never;
|
|
662
646
|
header?: never;
|
|
663
647
|
path?: never;
|
|
664
648
|
cookie?: never;
|
|
665
649
|
};
|
|
666
|
-
get: operations["
|
|
650
|
+
get: operations["AuditController_requestsFilterSchema"];
|
|
667
651
|
put?: never;
|
|
668
652
|
post?: never;
|
|
669
653
|
delete?: never;
|
|
@@ -919,6 +903,7 @@ export interface components {
|
|
|
919
903
|
/** @default true */
|
|
920
904
|
isActive: boolean;
|
|
921
905
|
roleIds?: string[];
|
|
906
|
+
groupIds?: string[];
|
|
922
907
|
};
|
|
923
908
|
UpdateUserCommand: {
|
|
924
909
|
name?: string;
|
|
@@ -928,6 +913,7 @@ export interface components {
|
|
|
928
913
|
/** @default true */
|
|
929
914
|
isActive: boolean;
|
|
930
915
|
roleIds?: string[];
|
|
916
|
+
groupIds?: string[];
|
|
931
917
|
};
|
|
932
918
|
ResourceResponse: {
|
|
933
919
|
/** Format: uuid */
|
|
@@ -1081,6 +1067,8 @@ export interface components {
|
|
|
1081
1067
|
type: components["schemas"]["GroupType"];
|
|
1082
1068
|
/** Format: uuid */
|
|
1083
1069
|
parentId?: string | null;
|
|
1070
|
+
/** Format: uuid */
|
|
1071
|
+
managerId?: string | null;
|
|
1084
1072
|
isActive: boolean;
|
|
1085
1073
|
roles?: components["schemas"]["RoleResponse"][];
|
|
1086
1074
|
};
|
|
@@ -1094,6 +1082,8 @@ export interface components {
|
|
|
1094
1082
|
type: "company" | "department" | "team";
|
|
1095
1083
|
/** Format: uuid */
|
|
1096
1084
|
parentId?: string;
|
|
1085
|
+
/** Format: uuid */
|
|
1086
|
+
managerId?: string | null;
|
|
1097
1087
|
roleIds?: string[];
|
|
1098
1088
|
};
|
|
1099
1089
|
UpdateGroupCommand: {
|
|
@@ -1106,6 +1096,8 @@ export interface components {
|
|
|
1106
1096
|
type: "company" | "department" | "team";
|
|
1107
1097
|
/** Format: uuid */
|
|
1108
1098
|
parentId?: string;
|
|
1099
|
+
/** Format: uuid */
|
|
1100
|
+
managerId?: string | null;
|
|
1109
1101
|
roleIds?: string[];
|
|
1110
1102
|
isActive?: boolean;
|
|
1111
1103
|
};
|
|
@@ -1122,6 +1114,11 @@ export interface components {
|
|
|
1122
1114
|
permissionIds?: string[];
|
|
1123
1115
|
isActive?: boolean;
|
|
1124
1116
|
};
|
|
1117
|
+
/**
|
|
1118
|
+
* @description 0 = sucesso, 1 = erro, 2 = abortada
|
|
1119
|
+
* @enum {number}
|
|
1120
|
+
*/
|
|
1121
|
+
RequestOutcome: 0 | 1 | 2;
|
|
1125
1122
|
AuditUserRefResponse: {
|
|
1126
1123
|
/** Format: uuid */
|
|
1127
1124
|
id: string;
|
|
@@ -1129,58 +1126,43 @@ export interface components {
|
|
|
1129
1126
|
/** @example admin@admin.com */
|
|
1130
1127
|
email: string;
|
|
1131
1128
|
};
|
|
1129
|
+
/**
|
|
1130
|
+
* @description 0 = validação, 1 = autenticação, 2 = não encontrado, 3 = limite, 4 = banco, 5 = integração, 6 = interno
|
|
1131
|
+
* @enum {number}
|
|
1132
|
+
*/
|
|
1133
|
+
ErrorType: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
1132
1134
|
RequestLogResponse: {
|
|
1133
1135
|
/** Format: uuid */
|
|
1134
1136
|
id: string;
|
|
1135
|
-
/**
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
/** @example
|
|
1137
|
+
/**
|
|
1138
|
+
* @description 0 = sucesso, 1 = erro, 2 = abortada
|
|
1139
|
+
* @example 0
|
|
1140
|
+
*/
|
|
1141
|
+
outcome: components["schemas"]["RequestOutcome"];
|
|
1142
|
+
/** @example Login */
|
|
1143
|
+
action?: string | null;
|
|
1144
|
+
/** @example POST */
|
|
1141
1145
|
method?: string | null;
|
|
1146
|
+
/** @example /api/v1/users/9f3c… */
|
|
1142
1147
|
path?: string | null;
|
|
1148
|
+
/** @example /users/:id */
|
|
1149
|
+
route?: string | null;
|
|
1143
1150
|
/** @example 200 */
|
|
1144
1151
|
statusCode?: number | null;
|
|
1145
1152
|
/** @example 42 */
|
|
1146
1153
|
durationMs?: number | null;
|
|
1147
|
-
/** @example 10.0.0.1 */
|
|
1148
|
-
ipAddress?: string | null;
|
|
1149
|
-
/** Format: uuid */
|
|
1150
|
-
requestId?: string | null;
|
|
1151
|
-
metadata?: Record<string, never> | null;
|
|
1152
1154
|
userId?: string | null;
|
|
1153
1155
|
user?: components["schemas"]["AuditUserRefResponse"] | null;
|
|
1154
|
-
/**
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
updatedAt: string;
|
|
1158
|
-
/** Format: date-time */
|
|
1159
|
-
deletedAt?: string | null;
|
|
1160
|
-
};
|
|
1161
|
-
PaginatedRequestLogsResponse: {
|
|
1162
|
-
items: components["schemas"]["RequestLogResponse"][];
|
|
1163
|
-
meta: components["schemas"]["PaginationMetaResponse"];
|
|
1164
|
-
};
|
|
1165
|
-
/** @enum {string} */
|
|
1166
|
-
ErrorType: "VALIDATION" | "AUTH" | "NOT_FOUND" | "RATE_LIMIT" | "DATABASE" | "INTEGRATION" | "INTERNAL";
|
|
1167
|
-
ErrorLogResponse: {
|
|
1168
|
-
/** Format: uuid */
|
|
1169
|
-
id: string;
|
|
1170
|
-
type: components["schemas"]["ErrorType"];
|
|
1171
|
-
/** @example 500 */
|
|
1172
|
-
statusCode?: number | null;
|
|
1173
|
-
message: string;
|
|
1174
|
-
stack?: string | null;
|
|
1175
|
-
/** @example POST */
|
|
1176
|
-
method?: string | null;
|
|
1177
|
-
path?: string | null;
|
|
1178
|
-
/** @example 10.0.0.1 */
|
|
1179
|
-
ipAddress?: string | null;
|
|
1156
|
+
/** @description HMAC do IP — não é reversível ao endereço original */
|
|
1157
|
+
ipHash?: string | null;
|
|
1158
|
+
userAgent?: string | null;
|
|
1180
1159
|
/** Format: uuid */
|
|
1181
1160
|
requestId?: string | null;
|
|
1182
|
-
|
|
1183
|
-
|
|
1161
|
+
/** @description 0 = validação, 1 = autenticação, 2 = não encontrado, 3 = limite, 4 = banco, 5 = integração, 6 = interno */
|
|
1162
|
+
errorType?: components["schemas"]["ErrorType"] | null;
|
|
1163
|
+
errorCode?: string | null;
|
|
1164
|
+
errorMessage?: string | null;
|
|
1165
|
+
errorStack?: string | null;
|
|
1184
1166
|
/** Format: date-time */
|
|
1185
1167
|
createdAt: string;
|
|
1186
1168
|
/** Format: date-time */
|
|
@@ -1188,12 +1170,12 @@ export interface components {
|
|
|
1188
1170
|
/** Format: date-time */
|
|
1189
1171
|
deletedAt?: string | null;
|
|
1190
1172
|
};
|
|
1191
|
-
|
|
1192
|
-
items: components["schemas"]["
|
|
1173
|
+
PaginatedRequestLogsResponse: {
|
|
1174
|
+
items: components["schemas"]["RequestLogResponse"][];
|
|
1193
1175
|
meta: components["schemas"]["PaginationMetaResponse"];
|
|
1194
1176
|
};
|
|
1195
|
-
/** @enum {
|
|
1196
|
-
AuditDataAction:
|
|
1177
|
+
/** @enum {number} */
|
|
1178
|
+
AuditDataAction: 0 | 1 | 2;
|
|
1197
1179
|
AuditDataChangeResponse: {
|
|
1198
1180
|
/** Format: uuid */
|
|
1199
1181
|
id: string;
|
|
@@ -1218,8 +1200,8 @@ export interface components {
|
|
|
1218
1200
|
items: components["schemas"]["AuditDataChangeResponse"][];
|
|
1219
1201
|
meta: components["schemas"]["PaginationMetaResponse"];
|
|
1220
1202
|
};
|
|
1221
|
-
/** @enum {
|
|
1222
|
-
NotificationType:
|
|
1203
|
+
/** @enum {number} */
|
|
1204
|
+
NotificationType: 0 | 1 | 2 | 3;
|
|
1223
1205
|
UserNotificationResponse: {
|
|
1224
1206
|
/** Format: uuid */
|
|
1225
1207
|
id: string;
|
|
@@ -1245,8 +1227,8 @@ export interface components {
|
|
|
1245
1227
|
/** @example 3 */
|
|
1246
1228
|
unread: number;
|
|
1247
1229
|
};
|
|
1248
|
-
/** @enum {
|
|
1249
|
-
ScopeType:
|
|
1230
|
+
/** @enum {number} */
|
|
1231
|
+
ScopeType: 0 | 1 | 2;
|
|
1250
1232
|
NotificationTargetCommand: {
|
|
1251
1233
|
type: components["schemas"]["ScopeType"];
|
|
1252
1234
|
/** @description Id do usuário quando type=USER, nome do papel quando type=ROLE. Ignorado em ALL. */
|
|
@@ -2231,6 +2213,25 @@ export interface operations {
|
|
|
2231
2213
|
};
|
|
2232
2214
|
};
|
|
2233
2215
|
};
|
|
2216
|
+
RbacController_managedGroups: {
|
|
2217
|
+
parameters: {
|
|
2218
|
+
query?: never;
|
|
2219
|
+
header?: never;
|
|
2220
|
+
path?: never;
|
|
2221
|
+
cookie?: never;
|
|
2222
|
+
};
|
|
2223
|
+
requestBody?: never;
|
|
2224
|
+
responses: {
|
|
2225
|
+
200: {
|
|
2226
|
+
headers: {
|
|
2227
|
+
[name: string]: unknown;
|
|
2228
|
+
};
|
|
2229
|
+
content: {
|
|
2230
|
+
"application/json": components["schemas"]["GroupResponse"][];
|
|
2231
|
+
};
|
|
2232
|
+
};
|
|
2233
|
+
};
|
|
2234
|
+
};
|
|
2234
2235
|
RbacController_group: {
|
|
2235
2236
|
parameters: {
|
|
2236
2237
|
query?: never;
|
|
@@ -2487,7 +2488,7 @@ export interface operations {
|
|
|
2487
2488
|
};
|
|
2488
2489
|
};
|
|
2489
2490
|
};
|
|
2490
|
-
|
|
2491
|
+
AuditController_requests: {
|
|
2491
2492
|
parameters: {
|
|
2492
2493
|
query?: {
|
|
2493
2494
|
page?: number;
|
|
@@ -2515,54 +2516,7 @@ export interface operations {
|
|
|
2515
2516
|
};
|
|
2516
2517
|
};
|
|
2517
2518
|
};
|
|
2518
|
-
|
|
2519
|
-
parameters: {
|
|
2520
|
-
query?: never;
|
|
2521
|
-
header?: never;
|
|
2522
|
-
path?: never;
|
|
2523
|
-
cookie?: never;
|
|
2524
|
-
};
|
|
2525
|
-
requestBody?: never;
|
|
2526
|
-
responses: {
|
|
2527
|
-
200: {
|
|
2528
|
-
headers: {
|
|
2529
|
-
[name: string]: unknown;
|
|
2530
|
-
};
|
|
2531
|
-
content: {
|
|
2532
|
-
"application/json": components["schemas"]["FilterFieldResponse"][];
|
|
2533
|
-
};
|
|
2534
|
-
};
|
|
2535
|
-
};
|
|
2536
|
-
};
|
|
2537
|
-
AuditController_errors: {
|
|
2538
|
-
parameters: {
|
|
2539
|
-
query?: {
|
|
2540
|
-
page?: number;
|
|
2541
|
-
limit?: number;
|
|
2542
|
-
search?: string;
|
|
2543
|
-
/** @description Coluna para ordenar (whitelist por endpoint) */
|
|
2544
|
-
sortBy?: string;
|
|
2545
|
-
sortDir?: "ASC" | "DESC";
|
|
2546
|
-
/** @description JSON, ex.: {"name":{"$SUBSTRING":"ana"}} */
|
|
2547
|
-
filter?: string;
|
|
2548
|
-
};
|
|
2549
|
-
header?: never;
|
|
2550
|
-
path?: never;
|
|
2551
|
-
cookie?: never;
|
|
2552
|
-
};
|
|
2553
|
-
requestBody?: never;
|
|
2554
|
-
responses: {
|
|
2555
|
-
200: {
|
|
2556
|
-
headers: {
|
|
2557
|
-
[name: string]: unknown;
|
|
2558
|
-
};
|
|
2559
|
-
content: {
|
|
2560
|
-
"application/json": components["schemas"]["PaginatedErrorLogsResponse"];
|
|
2561
|
-
};
|
|
2562
|
-
};
|
|
2563
|
-
};
|
|
2564
|
-
};
|
|
2565
|
-
AuditController_errorsFilterSchema: {
|
|
2519
|
+
AuditController_requestsFilterSchema: {
|
|
2566
2520
|
parameters: {
|
|
2567
2521
|
query?: never;
|
|
2568
2522
|
header?: never;
|
|
@@ -6,6 +6,8 @@ export type LoginStatus = "SETUP_REQUIRED" | "TOTP_REQUIRED";
|
|
|
6
6
|
export interface LoginResponse {
|
|
7
7
|
status: LoginStatus;
|
|
8
8
|
pendingToken: string;
|
|
9
|
+
/** Segundos até a etapa do código vencer — alimenta o contador da tela. */
|
|
10
|
+
expiresInSeconds: number;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface TotpSetup {
|
package/src/_utils/filter.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { components } from "#core/_services/api/schema";
|
|
2
|
-
import {
|
|
2
|
+
import { fromIsoDay, toIsoDay } from "#core/_utils/calendar";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Tipos derivados do `openapi.json` (§5): o catálogo é servido pelo backend,
|
|
@@ -63,8 +63,20 @@ export const hasValue = (draft: FilterDraftValue | undefined): boolean => {
|
|
|
63
63
|
);
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Quantos filtros o botão "Filtros" anuncia.
|
|
68
|
+
*
|
|
69
|
+
* `ignore` são os campos que a barra da listagem controla: eles filtram de
|
|
70
|
+
* verdade, mas o contador não pode cobrá-los, senão a tela abre marcando 1 num
|
|
71
|
+
* painel onde não há nada marcado.
|
|
72
|
+
*/
|
|
73
|
+
export const activeFilterCount = (
|
|
74
|
+
values: FilterValues,
|
|
75
|
+
ignore: string[] = [],
|
|
76
|
+
): number =>
|
|
77
|
+
Object.entries(values).filter(
|
|
78
|
+
([field, draft]) => hasValue(draft) && !ignore.includes(field),
|
|
79
|
+
).length;
|
|
68
80
|
|
|
69
81
|
/**
|
|
70
82
|
* Ponta de um intervalo de datas em ISO **com o fuso de quem filtrou**.
|
|
@@ -280,34 +292,3 @@ const draftFrom = (
|
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
};
|
|
283
|
-
|
|
284
|
-
/** Resumo de um filtro ativo, para o chip. */
|
|
285
|
-
export const describeValue = (
|
|
286
|
-
field: FilterField,
|
|
287
|
-
draft: FilterDraftValue,
|
|
288
|
-
labels: { from: string; to: string; yes: string; no: string; locale: string },
|
|
289
|
-
): string => {
|
|
290
|
-
const show = (value: string): string =>
|
|
291
|
-
field.type === "DATE" || field.type === "DATETIME"
|
|
292
|
-
? formatDay(value, labels.locale)
|
|
293
|
-
: value;
|
|
294
|
-
|
|
295
|
-
if (field.type === "BOOLEAN") {
|
|
296
|
-
return draft.value === "true" ? labels.yes : labels.no;
|
|
297
|
-
}
|
|
298
|
-
if (draft.values?.length) {
|
|
299
|
-
const optionLabel = (value: string): string =>
|
|
300
|
-
field.options?.find((option) => option.value === value)?.label ?? value;
|
|
301
|
-
return draft.values.map(optionLabel).join(", ");
|
|
302
|
-
}
|
|
303
|
-
if (draft.from && draft.to) {
|
|
304
|
-
return `${show(draft.from)} – ${show(draft.to)}`;
|
|
305
|
-
}
|
|
306
|
-
if (draft.from) {
|
|
307
|
-
return `${labels.from} ${show(draft.from)}`;
|
|
308
|
-
}
|
|
309
|
-
if (draft.to) {
|
|
310
|
-
return `${labels.to} ${show(draft.to)}`;
|
|
311
|
-
}
|
|
312
|
-
return draft.value ?? "";
|
|
313
|
-
};
|
package/src/_utils/password.ts
CHANGED
|
@@ -5,3 +5,43 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const STRONG_PASSWORD_REGEX =
|
|
7
7
|
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>]).{8,}$/;
|
|
8
|
+
|
|
9
|
+
/** Tamanho mínimo — o mesmo `{8,}` do regex acima, nomeado. */
|
|
10
|
+
export const PASSWORD_MIN_LENGTH = 8;
|
|
11
|
+
|
|
12
|
+
export interface PasswordRule {
|
|
13
|
+
/** Chave de i18n do texto da regra (`validation.rules.*`). */
|
|
14
|
+
key: string;
|
|
15
|
+
/** Parâmetros da tradução, quando o texto tem número. */
|
|
16
|
+
params?: Record<string, string | number>;
|
|
17
|
+
test: (value: string) => boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* As exigências do regex, uma a uma.
|
|
22
|
+
*
|
|
23
|
+
* O regex sozinho só sabe dizer "não passou", e uma senha recusada sem dizer o
|
|
24
|
+
* que falta obriga a pessoa a adivinhar. Aqui cada condição responde por si, e
|
|
25
|
+
* a tela mostra quais já foram cumpridas enquanto se digita.
|
|
26
|
+
*
|
|
27
|
+
* A lista é derivada do mesmo regex e existe **só para explicar**: quem valida
|
|
28
|
+
* de fato continua sendo `STRONG_PASSWORD_REGEX`, e o backend por último.
|
|
29
|
+
*/
|
|
30
|
+
export const PASSWORD_RULES: PasswordRule[] = [
|
|
31
|
+
{
|
|
32
|
+
key: "validation.rules.minLength",
|
|
33
|
+
params: { count: PASSWORD_MIN_LENGTH },
|
|
34
|
+
test: (value) => value.length >= PASSWORD_MIN_LENGTH,
|
|
35
|
+
},
|
|
36
|
+
{ key: "validation.rules.lowercase", test: (value) => /[a-z]/.test(value) },
|
|
37
|
+
{ key: "validation.rules.uppercase", test: (value) => /[A-Z]/.test(value) },
|
|
38
|
+
{ key: "validation.rules.digit", test: (value) => /\d/.test(value) },
|
|
39
|
+
{
|
|
40
|
+
key: "validation.rules.special",
|
|
41
|
+
test: (value) => /[!@#$%^&*(),.?":{}|<>]/.test(value),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Quantas regras a senha já cumpre — base do medidor de força. */
|
|
46
|
+
export const passwordScore = (value: string): number =>
|
|
47
|
+
PASSWORD_RULES.filter((rule) => rule.test(value)).length;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Os dois códigos que autorizam a mesma ação em alcances diferentes.
|
|
3
|
+
*
|
|
4
|
+
* `anyOrTeam("users:update")` → `["users:update:any", "users:update:team"]`.
|
|
5
|
+
*
|
|
6
|
+
* Espelha o helper de mesmo nome do backend (`core/auth/permissionScope.util`),
|
|
7
|
+
* como os enums do §7: são dois pacotes publicados separadamente, e o front não
|
|
8
|
+
* importa do outro. O que não pode divergir é o texto dos sufixos — e é
|
|
9
|
+
* justamente por isso que ele fica escrito num lugar só de cada lado.
|
|
10
|
+
*/
|
|
11
|
+
export const anyOrTeam = (action: string): [string, string] => [
|
|
12
|
+
`${action}:any`,
|
|
13
|
+
`${action}:team`,
|
|
14
|
+
];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acesso ao `localStorage` que nunca derruba a tela.
|
|
3
|
+
*
|
|
4
|
+
* Ler `localStorage` **lança** — não devolve `null` — quando o navegador está
|
|
5
|
+
* com dados de site bloqueados: Chrome em "bloquear todos os cookies", Safari
|
|
6
|
+
* com prevenção de rastreamento em contexto de terceiros, políticas
|
|
7
|
+
* corporativas. Como quem lê são providers da raiz e o shell, um erro ali não
|
|
8
|
+
* degradava um pedaço da interface: derrubava a aplicação inteira, com tela
|
|
9
|
+
* branca no login e nenhuma pista para o usuário.
|
|
10
|
+
*
|
|
11
|
+
* O script anti-flash do `layout.tsx` já se protegia assim; aqui isso deixa de
|
|
12
|
+
* ser cuidado de um lugar só e vira a única porta de acesso.
|
|
13
|
+
*
|
|
14
|
+
* O que está guardado aqui é conveniência — tema, idioma, menu recolhido,
|
|
15
|
+
* colunas escondidas. Não poder ler significa cair no padrão, nunca falhar.
|
|
16
|
+
*
|
|
17
|
+
* Também cobre o servidor: no primeiro render do Next não existe `window`.
|
|
18
|
+
*/
|
|
19
|
+
const memory = new Map<string, string>();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Espelho em memória para quando o navegador recusa o armazenamento.
|
|
23
|
+
*
|
|
24
|
+
* Sem ele, trocar o tema com cookies bloqueados não teria efeito nenhum dentro
|
|
25
|
+
* da própria sessão — a escrita se perdia e a leitura seguinte devolvia o valor
|
|
26
|
+
* antigo. Assim a preferência vale enquanto a aba está aberta; ela só não
|
|
27
|
+
* sobrevive ao recarregamento, que é o máximo possível nesse cenário.
|
|
28
|
+
*/
|
|
29
|
+
export const safeStorage = {
|
|
30
|
+
get(key: string): string | null {
|
|
31
|
+
try {
|
|
32
|
+
if (typeof window === "undefined") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return window.localStorage.getItem(key);
|
|
36
|
+
} catch {
|
|
37
|
+
return memory.get(key) ?? null;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
set(key: string, value: string): void {
|
|
42
|
+
try {
|
|
43
|
+
if (typeof window === "undefined") {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
window.localStorage.setItem(key, value);
|
|
47
|
+
} catch {
|
|
48
|
+
// Cota estourada ou armazenamento negado: guarda na sessão e segue.
|
|
49
|
+
memory.set(key, value);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
remove(key: string): void {
|
|
54
|
+
try {
|
|
55
|
+
memory.delete(key);
|
|
56
|
+
if (typeof window === "undefined") {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
window.localStorage.removeItem(key);
|
|
60
|
+
} catch {
|
|
61
|
+
// Já saiu do espelho em memória; não há mais nada a fazer.
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Forma mínima que basta para rotular uma pessoa na interface. */
|
|
2
|
+
export interface NamedUser {
|
|
3
|
+
name?: string | null;
|
|
4
|
+
lastName?: string | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Nome completo para exibição — espelha o getter `fullName` da entidade `User`
|
|
9
|
+
* do backend, inclusive no comportamento com sobrenome ausente.
|
|
10
|
+
*
|
|
11
|
+
* Existe para não haver três concatenações diferentes espalhadas pelas telas:
|
|
12
|
+
* quem mostra pessoa mostra o nome inteiro, e "Ana" e "Ana" deixam de ser a
|
|
13
|
+
* mesma linha numa lista de auditoria.
|
|
14
|
+
*
|
|
15
|
+
* Saudação é a exceção e continua usando só o primeiro nome — "Bem-vindo, Ana
|
|
16
|
+
* Paula de Souza" soa como carta de cobrança.
|
|
17
|
+
*/
|
|
18
|
+
export const fullName = (user?: NamedUser | null): string =>
|
|
19
|
+
[user?.name, user?.lastName].filter(Boolean).join(" ");
|