rl-core-front 0.18.10 → 0.19.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
CHANGED
|
@@ -244,17 +244,17 @@ export interface paths {
|
|
|
244
244
|
patch?: never;
|
|
245
245
|
trace?: never;
|
|
246
246
|
};
|
|
247
|
-
"/auth/password-token/
|
|
247
|
+
"/auth/password-token/inspect": {
|
|
248
248
|
parameters: {
|
|
249
249
|
query?: never;
|
|
250
250
|
header?: never;
|
|
251
251
|
path?: never;
|
|
252
252
|
cookie?: never;
|
|
253
253
|
};
|
|
254
|
-
|
|
255
|
-
get: operations["AuthController_inspectPasswordToken"];
|
|
254
|
+
get?: never;
|
|
256
255
|
put?: never;
|
|
257
|
-
|
|
256
|
+
/** Verifica a validade do link de senha sem consumi-lo */
|
|
257
|
+
post: operations["AuthController_inspectPasswordToken"];
|
|
258
258
|
delete?: never;
|
|
259
259
|
options?: never;
|
|
260
260
|
head?: never;
|
|
@@ -838,6 +838,38 @@ export interface paths {
|
|
|
838
838
|
patch: operations["NotificationsController_markAsRead"];
|
|
839
839
|
trace?: never;
|
|
840
840
|
};
|
|
841
|
+
"/notifications/read": {
|
|
842
|
+
parameters: {
|
|
843
|
+
query?: never;
|
|
844
|
+
header?: never;
|
|
845
|
+
path?: never;
|
|
846
|
+
cookie?: never;
|
|
847
|
+
};
|
|
848
|
+
get?: never;
|
|
849
|
+
put?: never;
|
|
850
|
+
post?: never;
|
|
851
|
+
delete: operations["NotificationsController_dismissRead"];
|
|
852
|
+
options?: never;
|
|
853
|
+
head?: never;
|
|
854
|
+
patch?: never;
|
|
855
|
+
trace?: never;
|
|
856
|
+
};
|
|
857
|
+
"/notifications/{id}": {
|
|
858
|
+
parameters: {
|
|
859
|
+
query?: never;
|
|
860
|
+
header?: never;
|
|
861
|
+
path?: never;
|
|
862
|
+
cookie?: never;
|
|
863
|
+
};
|
|
864
|
+
get?: never;
|
|
865
|
+
put?: never;
|
|
866
|
+
post?: never;
|
|
867
|
+
delete: operations["NotificationsController_dismiss"];
|
|
868
|
+
options?: never;
|
|
869
|
+
head?: never;
|
|
870
|
+
patch?: never;
|
|
871
|
+
trace?: never;
|
|
872
|
+
};
|
|
841
873
|
"/exports/{handler}": {
|
|
842
874
|
parameters: {
|
|
843
875
|
query?: never;
|
|
@@ -1148,6 +1180,9 @@ export interface components {
|
|
|
1148
1180
|
ForgotPasswordCommand: {
|
|
1149
1181
|
email: string;
|
|
1150
1182
|
};
|
|
1183
|
+
InspectPasswordTokenCommand: {
|
|
1184
|
+
token: string;
|
|
1185
|
+
};
|
|
1151
1186
|
ResetPasswordCommand: {
|
|
1152
1187
|
token: string;
|
|
1153
1188
|
newPassword: string;
|
|
@@ -1588,6 +1623,13 @@ export interface components {
|
|
|
1588
1623
|
sortBy?: string;
|
|
1589
1624
|
/** @enum {string} */
|
|
1590
1625
|
sortDir?: "ASC" | "DESC";
|
|
1626
|
+
/**
|
|
1627
|
+
* @description Parâmetros do documento, validados pelo relatório pedido
|
|
1628
|
+
* @example {
|
|
1629
|
+
* "referenceMonth": "2026-09"
|
|
1630
|
+
* }
|
|
1631
|
+
*/
|
|
1632
|
+
params?: Record<string, never>;
|
|
1591
1633
|
};
|
|
1592
1634
|
ExportAcceptedResponse: {
|
|
1593
1635
|
/** @example 184 */
|
|
@@ -2038,12 +2080,14 @@ export interface operations {
|
|
|
2038
2080
|
parameters: {
|
|
2039
2081
|
query?: never;
|
|
2040
2082
|
header?: never;
|
|
2041
|
-
path
|
|
2042
|
-
token: string;
|
|
2043
|
-
};
|
|
2083
|
+
path?: never;
|
|
2044
2084
|
cookie?: never;
|
|
2045
2085
|
};
|
|
2046
|
-
requestBody
|
|
2086
|
+
requestBody: {
|
|
2087
|
+
content: {
|
|
2088
|
+
"application/json": components["schemas"]["InspectPasswordTokenCommand"];
|
|
2089
|
+
};
|
|
2090
|
+
};
|
|
2047
2091
|
responses: {
|
|
2048
2092
|
200: {
|
|
2049
2093
|
headers: {
|
|
@@ -3338,6 +3382,42 @@ export interface operations {
|
|
|
3338
3382
|
};
|
|
3339
3383
|
};
|
|
3340
3384
|
};
|
|
3385
|
+
NotificationsController_dismissRead: {
|
|
3386
|
+
parameters: {
|
|
3387
|
+
query?: never;
|
|
3388
|
+
header?: never;
|
|
3389
|
+
path?: never;
|
|
3390
|
+
cookie?: never;
|
|
3391
|
+
};
|
|
3392
|
+
requestBody?: never;
|
|
3393
|
+
responses: {
|
|
3394
|
+
204: {
|
|
3395
|
+
headers: {
|
|
3396
|
+
[name: string]: unknown;
|
|
3397
|
+
};
|
|
3398
|
+
content?: never;
|
|
3399
|
+
};
|
|
3400
|
+
};
|
|
3401
|
+
};
|
|
3402
|
+
NotificationsController_dismiss: {
|
|
3403
|
+
parameters: {
|
|
3404
|
+
query?: never;
|
|
3405
|
+
header?: never;
|
|
3406
|
+
path: {
|
|
3407
|
+
id: string;
|
|
3408
|
+
};
|
|
3409
|
+
cookie?: never;
|
|
3410
|
+
};
|
|
3411
|
+
requestBody?: never;
|
|
3412
|
+
responses: {
|
|
3413
|
+
204: {
|
|
3414
|
+
headers: {
|
|
3415
|
+
[name: string]: unknown;
|
|
3416
|
+
};
|
|
3417
|
+
content?: never;
|
|
3418
|
+
};
|
|
3419
|
+
};
|
|
3420
|
+
};
|
|
3341
3421
|
ExportController_request: {
|
|
3342
3422
|
parameters: {
|
|
3343
3423
|
query?: never;
|
|
@@ -3395,7 +3475,7 @@ export interface operations {
|
|
|
3395
3475
|
};
|
|
3396
3476
|
requestBody?: never;
|
|
3397
3477
|
responses: {
|
|
3398
|
-
/** @description
|
|
3478
|
+
/** @description O arquivo gerado */
|
|
3399
3479
|
200: {
|
|
3400
3480
|
headers: {
|
|
3401
3481
|
[name: string]: unknown;
|
|
@@ -84,10 +84,9 @@ export const authService = {
|
|
|
84
84
|
|
|
85
85
|
/** Verifica o link de senha antes de exibir o formulário (não consome o token). */
|
|
86
86
|
inspectPasswordToken(token: string): Promise<PasswordTokenCheck> {
|
|
87
|
+
// No corpo, não na URL: token em path vai parar em log de proxy.
|
|
87
88
|
return api
|
|
88
|
-
.
|
|
89
|
-
`/auth/password-token/${encodeURIComponent(token)}`,
|
|
90
|
-
)
|
|
89
|
+
.post<PasswordTokenCheck>("/auth/password-token/inspect", { token })
|
|
91
90
|
.then((r) => r.data);
|
|
92
91
|
},
|
|
93
92
|
|
package/src/_utils/upload-url.ts
CHANGED
|
@@ -18,6 +18,11 @@ const API_ORIGIN = (process.env.NEXT_PUBLIC_API_BASE_URL ?? "").replace(
|
|
|
18
18
|
* avatar gravado por versões anteriores e de qualquer endereço externo que
|
|
19
19
|
* ainda esteja em base.
|
|
20
20
|
*/
|
|
21
|
+
/** Imagem ainda não confirmada por um salvar — vive em `/uploads/<pasta>/pending/`. */
|
|
22
|
+
export function isPendingImagePath(path?: string | null): boolean {
|
|
23
|
+
return /^\/uploads\/[^/]+\/pending\//.test(path ?? "");
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export function uploadUrlOf(path?: string | null): string | undefined {
|
|
22
27
|
if (!path) {
|
|
23
28
|
return undefined;
|
|
@@ -177,7 +177,9 @@ const ADMIN_CHILDREN: NavItem[] = [
|
|
|
177
177
|
perm: anyOrTeam("users:read"),
|
|
178
178
|
},
|
|
179
179
|
{ key: "nav.rbac", href: "/rbac", icon: ShieldCheck, perm: "roles:read:any" },
|
|
180
|
-
|
|
180
|
+
// Gerente vê o log da própria equipe — a rota poda pelo mesmo alcance da
|
|
181
|
+
// listagem de usuários.
|
|
182
|
+
{ key: "nav.logs", href: "/logs", icon: ScrollText, perm: anyOrTeam("audit:read") },
|
|
181
183
|
// Logo abaixo do log de requisições, que é o vizinho natural: os dois contam
|
|
182
184
|
// o que aconteceu no sistema, um por chamada e outro por alteração de dado.
|
|
183
185
|
// Restrito a administradores — só quem tem audit:read-trail:any (só Super
|
|
@@ -5,7 +5,7 @@ import type { ChangeEvent, ClipboardEvent, DragEvent, JSX } from "react";
|
|
|
5
5
|
import { useRef, useState } from "react";
|
|
6
6
|
|
|
7
7
|
import { imagesService } from "#core/_services/images";
|
|
8
|
-
import { uploadUrlOf } from "#core/_utils/upload-url";
|
|
8
|
+
import { isPendingImagePath, uploadUrlOf } from "#core/_utils/upload-url";
|
|
9
9
|
import { Button } from "#core/components/ui/button";
|
|
10
10
|
import { Input } from "#core/components/ui/input";
|
|
11
11
|
import { Spinner } from "#core/components/ui/spinner";
|
|
@@ -102,9 +102,13 @@ export function ImageUpload({
|
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
105
|
+
// Só a pendente sai do disco por aqui — é o upload desta sessão que ninguém
|
|
106
|
+
// referencia. A definitiva pertence ao registro: quem a apaga é o salvar
|
|
107
|
+
// (`discard()` no service da feature), e a API recusa o caminho por esta
|
|
108
|
+
// rota de propósito.
|
|
109
|
+
if (isPendingImagePath(value)) {
|
|
110
|
+
await imagesService.remove(profile, value).catch(() => undefined);
|
|
111
|
+
}
|
|
108
112
|
onChange(null);
|
|
109
113
|
clearError();
|
|
110
114
|
};
|
package/src/middleware.ts
CHANGED
|
@@ -7,9 +7,119 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
7
7
|
*
|
|
8
8
|
* Observação: a validação forte do token é feita no backend; aqui é só
|
|
9
9
|
* o "portão" de roteamento (UX), evitando flicker de telas privadas.
|
|
10
|
+
*
|
|
11
|
+
* Também é aqui que a Content-Security-Policy nasce, porque o nonce precisa
|
|
12
|
+
* ser novo a cada requisição e chegar ao `layout.tsx` antes de a página
|
|
13
|
+
* renderizar — ver `buildContentSecurityPolicy`.
|
|
10
14
|
*/
|
|
11
15
|
const PUBLIC_PATHS = ["/login", "/forgot-password", "/reset-password"];
|
|
12
16
|
|
|
17
|
+
/** Cabeçalho por onde o nonce chega ao `layout.tsx` (`headers().get("x-nonce")`). */
|
|
18
|
+
export const NONCE_HEADER = "x-nonce";
|
|
19
|
+
|
|
20
|
+
export interface ContentSecurityPolicyOptions {
|
|
21
|
+
/** Nonce desta requisição — vai nos scripts inline do Next e no de tema do layout. */
|
|
22
|
+
nonce: string;
|
|
23
|
+
/** Origens extras de `connect-src` (outra API, um serviço de terceiros). */
|
|
24
|
+
extraConnectSrc?: string[];
|
|
25
|
+
/** Origens extras de `img-src` (CDN de imagens, S3). */
|
|
26
|
+
extraImgSrc?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Os mesmos padrões do `axios.factory` e do `socket-context`: sem a variável, a
|
|
31
|
+
* CSP precisa liberar exatamente para onde o cliente vai cair, senão o navegador
|
|
32
|
+
* bloqueia toda chamada.
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_API_BASE_URL = "http://localhost:3108/api/v1";
|
|
35
|
+
const DEFAULT_SOCKET_URL = "http://localhost:3108";
|
|
36
|
+
|
|
37
|
+
const originOf = (url: string): string | null => {
|
|
38
|
+
try {
|
|
39
|
+
return new URL(url).origin;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Origem da API (`NEXT_PUBLIC_API_BASE_URL`), sem prefixo — é de lá que vêm avatar e download. */
|
|
46
|
+
const apiOrigin = (): string | null =>
|
|
47
|
+
originOf(process.env.NEXT_PUBLIC_API_BASE_URL ?? DEFAULT_API_BASE_URL);
|
|
48
|
+
|
|
49
|
+
/** Origem do socket (`NEXT_PUBLIC_SOCKET_URL`) — costuma ser a da API, mas pode divergir. */
|
|
50
|
+
const socketOrigin = (): string | null =>
|
|
51
|
+
originOf(process.env.NEXT_PUBLIC_SOCKET_URL ?? DEFAULT_SOCKET_URL);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A CSP do core.
|
|
55
|
+
*
|
|
56
|
+
* Defesa em profundidade: hoje não há sink de XSS na interface, e a CSP é o
|
|
57
|
+
* que limita o estrago de um que apareça num componente de terceiro. Script só
|
|
58
|
+
* com nonce (`strict-dynamic` deixa o Next carregar os chunks dele); `img` e
|
|
59
|
+
* `connect` liberam a origem da API, porque avatar, axios e socket.io falam
|
|
60
|
+
* com ela. `style` aceita inline porque Tailwind e Radix escrevem `style=`
|
|
61
|
+
* direto no elemento — bloquear isso quebraria todo popover e menu.
|
|
62
|
+
*
|
|
63
|
+
* `unsafe-eval` só em desenvolvimento: o React o usa para reconstruir stack
|
|
64
|
+
* de erro do servidor no navegador; em produção nem React nem Next o usam.
|
|
65
|
+
*
|
|
66
|
+
* Exportada para o projeto que precisar de mais origens montar a própria: chama
|
|
67
|
+
* esta com `extraConnectSrc`/`extraImgSrc` dentro do middleware dele.
|
|
68
|
+
*/
|
|
69
|
+
export const buildContentSecurityPolicy = ({
|
|
70
|
+
nonce,
|
|
71
|
+
extraConnectSrc = [],
|
|
72
|
+
extraImgSrc = [],
|
|
73
|
+
}: ContentSecurityPolicyOptions): string => {
|
|
74
|
+
const api = apiOrigin();
|
|
75
|
+
const socket = socketOrigin();
|
|
76
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
77
|
+
// O socket.io começa por polling HTTP e sobe para WebSocket: as duas formas.
|
|
78
|
+
const connect = [
|
|
79
|
+
"'self'",
|
|
80
|
+
api,
|
|
81
|
+
socket,
|
|
82
|
+
socket?.replace(/^http/, "ws") ?? null,
|
|
83
|
+
...extraConnectSrc,
|
|
84
|
+
].filter(Boolean);
|
|
85
|
+
const img = ["'self'", "blob:", "data:", api, ...extraImgSrc].filter(Boolean);
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
"default-src 'self'",
|
|
89
|
+
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""}`,
|
|
90
|
+
"style-src 'self' 'unsafe-inline'",
|
|
91
|
+
`img-src ${img.join(" ")}`,
|
|
92
|
+
"font-src 'self' data:",
|
|
93
|
+
`connect-src ${connect.join(" ")}`,
|
|
94
|
+
"object-src 'none'",
|
|
95
|
+
"base-uri 'self'",
|
|
96
|
+
"form-action 'self'",
|
|
97
|
+
"frame-ancestors 'none'",
|
|
98
|
+
].join("; ");
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Devolve a resposta "segue em frente" já com o nonce e a CSP nos dois lados:
|
|
103
|
+
* no request (é de onde o Next lê o nonce para os scripts dele) e no response.
|
|
104
|
+
*/
|
|
105
|
+
export const nextWithSecurityHeaders = (
|
|
106
|
+
req: NextRequest,
|
|
107
|
+
options: Omit<ContentSecurityPolicyOptions, "nonce"> = {},
|
|
108
|
+
): NextResponse => {
|
|
109
|
+
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
|
|
110
|
+
const csp = buildContentSecurityPolicy({ nonce, ...options });
|
|
111
|
+
|
|
112
|
+
const requestHeaders = new Headers(req.headers);
|
|
113
|
+
requestHeaders.set(NONCE_HEADER, nonce);
|
|
114
|
+
requestHeaders.set("Content-Security-Policy", csp);
|
|
115
|
+
|
|
116
|
+
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
117
|
+
response.headers.set("Content-Security-Policy", csp);
|
|
118
|
+
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
119
|
+
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
120
|
+
return response;
|
|
121
|
+
};
|
|
122
|
+
|
|
13
123
|
export function middleware(req: NextRequest): NextResponse<unknown> {
|
|
14
124
|
const { pathname } = req.nextUrl;
|
|
15
125
|
const hasSession =
|
|
@@ -34,7 +144,7 @@ export function middleware(req: NextRequest): NextResponse<unknown> {
|
|
|
34
144
|
return NextResponse.redirect(url);
|
|
35
145
|
}
|
|
36
146
|
|
|
37
|
-
return
|
|
147
|
+
return nextWithSecurityHeaders(req);
|
|
38
148
|
}
|
|
39
149
|
|
|
40
150
|
export const config = {
|