serverest 3.2.2 → 3.3.0-beta.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/README.md CHANGED
@@ -124,6 +124,48 @@ Em ambos os comandos de subida de ambiente local será utilizado a última vers
124
124
 
125
125
  Você pode encontrar as versões disponíveis na [lista de tags no Docker Hub](https://hub.docker.com/r/paulogoncalvesbh/serverest/tags) e na [lista de versões do NPM](https://www.npmjs.com/package/serverest).
126
126
 
127
+ ## Etapas da requisição
128
+
129
+ Toda resposta das rotas `/login`, `/usuarios`, `/produtos` e `/carrinhos` informa por quais etapas do servidor a requisição passou, quanto tempo cada uma levou e qual delas interrompeu a requisição. Assim é possível entender, sem ler o código, por que a resposta foi 401 e não 400 quando o token e o body estão errados ao mesmo tempo.
130
+
131
+ | Cabeçalho | Conteúdo |
132
+ |---|---|
133
+ | `X-ServeRest-Stages` | Todas as etapas da rota, em ordem de execução, com status, duração e detalhes |
134
+
135
+ O cabeçalho é listado em `Access-Control-Expose-Headers`. Requisições `OPTIONS` (preflight de CORS) e as demais rotas (`/`, `/version`, `/swagger.json`) não possuem esse cabeçalho.
136
+
137
+ Exemplo de 401 sem token em `POST /carrinhos`:
138
+
139
+ ```
140
+ X-ServeRest-Stages: body;st=ok;dur=0.21, rate_limit;st=skipped, auth;st=failed;dur=0.05;reason=token_missing, schema;st=not_run, rules;st=not_run, write;st=not_run
141
+ ```
142
+
143
+ Cada entrada é um nome seguido de parâmetros `;chave=valor`, separadas por vírgula. Nome e valores usam apenas letras, números, `_` e `.`.
144
+
145
+ | Parâmetro | Valores | Presente quando |
146
+ |---|---|---|
147
+ | `st` | `ok`, `failed`, `skipped`, `not_run` | Sempre |
148
+ | `dur` | Milissegundos com duas casas decimais | A etapa executou (`ok` ou `failed`) |
149
+ | `reason` | `token_missing`, `bad_format`, `token_expired`, `token_invalid`, `user_not_found`, `not_admin` | A etapa `auth` falhou |
150
+ | `item` | Índice, a partir de zero, do item em `produtos` | Uma regra falhou em um item específico |
151
+
152
+ | Status | Significado |
153
+ |---|---|
154
+ | `ok` | Executou e deixou a requisição continuar |
155
+ | `failed` | Executou e interrompeu a requisição. No máximo uma etapa e uma regra por resposta |
156
+ | `skipped` | Não se aplica neste ambiente (rate limit ao executar localmente) |
157
+ | `not_run` | Não executou porque uma etapa anterior interrompeu a requisição |
158
+
159
+ As etapas são `body`, `rate_limit`, `auth`, `schema`, `read` (rotas GET), `rules` (regras de negócio e, enquanto a rota não for detalhada, também a gravação) e `write` (gravação, hoje apenas em `POST /carrinhos`). Entradas no formato `<etapa>.<regra>`, como `rules.enough_stock`, são regras da etapa citada antes do ponto: vêm logo depois dela e nunca possuem `dur`.
160
+
161
+ Casos que merecem atenção:
162
+
163
+ - Status 400 ou maior sem nenhuma etapa `failed` significa que a resposta veio de fora das etapas: rota inexistente (405) ou URL malformada (400).
164
+ - Em um timeout (408) a etapa que estava executando quando o tempo acabou é a marcada como `failed`.
165
+ - Em `POST /carrinhos` as regras `rules.product_exists` e `rules.enough_stock` são avaliadas item a item, na ordem do array, e param no primeiro item que falha. Portanto `rules.product_exists;st=ok` ao lado de `rules.enough_stock;st=failed;item=1` significa "nenhum produto inexistente até o item que falhou", e não "todos os produtos existem".
166
+
167
+ Evolução: versões futuras apenas adicionam entradas, como uma etapa `write` depois de `rules` ou entradas `rules.<regra>` em mais rotas. A gramática, os nomes dos parâmetros, os valores de status e os nomes das etapas acima são estáveis; renomear ou remover algum deles é uma quebra de compatibilidade.
168
+
127
169
  ## Teste de carga
128
170
 
129
171
  ### IMPORTANTE
package/docs/swagger.json CHANGED
@@ -52,6 +52,11 @@
52
52
  "responses": {
53
53
  "200": {
54
54
  "description": "paths.login.post.responses.200.description",
55
+ "headers": {
56
+ "X-ServeRest-Stages": {
57
+ "$ref": "#/components/headers/XServeRestStages"
58
+ }
59
+ },
55
60
  "content": {
56
61
  "application/json": {
57
62
  "schema": {
@@ -62,6 +67,11 @@
62
67
  },
63
68
  "401": {
64
69
  "description": "paths.login.post.responses.401.description",
70
+ "headers": {
71
+ "X-ServeRest-Stages": {
72
+ "$ref": "#/components/headers/XServeRestStages"
73
+ }
74
+ },
65
75
  "content": {
66
76
  "application/json": {
67
77
  "schema": {
@@ -99,6 +109,11 @@
99
109
  "responses": {
100
110
  "200": {
101
111
  "description": "paths.usuarios.get.responses.200.description",
112
+ "headers": {
113
+ "X-ServeRest-Stages": {
114
+ "$ref": "#/components/headers/XServeRestStages"
115
+ }
116
+ },
102
117
  "content": {
103
118
  "application/json": {
104
119
  "schema": {
@@ -121,6 +136,11 @@
121
136
  "responses": {
122
137
  "201": {
123
138
  "description": "paths.usuarios.post.responses.201.description",
139
+ "headers": {
140
+ "X-ServeRest-Stages": {
141
+ "$ref": "#/components/headers/XServeRestStages"
142
+ }
143
+ },
124
144
  "content": {
125
145
  "application/json": {
126
146
  "schema": {
@@ -156,6 +176,11 @@
156
176
  "responses": {
157
177
  "200": {
158
178
  "description": "paths.usuariosById.get.responses.200.description",
179
+ "headers": {
180
+ "X-ServeRest-Stages": {
181
+ "$ref": "#/components/headers/XServeRestStages"
182
+ }
183
+ },
159
184
  "content": {
160
185
  "application/json": {
161
186
  "schema": {
@@ -166,6 +191,11 @@
166
191
  },
167
192
  "400": {
168
193
  "description": "paths.usuariosById.get.responses.400.description",
194
+ "headers": {
195
+ "X-ServeRest-Stages": {
196
+ "$ref": "#/components/headers/XServeRestStages"
197
+ }
198
+ },
169
199
  "content": {
170
200
  "application/json": {
171
201
  "schema": {
@@ -203,6 +233,11 @@
203
233
  "responses": {
204
234
  "200": {
205
235
  "description": "paths.usuariosById.delete.responses.200.description",
236
+ "headers": {
237
+ "X-ServeRest-Stages": {
238
+ "$ref": "#/components/headers/XServeRestStages"
239
+ }
240
+ },
206
241
  "content": {
207
242
  "application/json": {
208
243
  "schema": {
@@ -219,6 +254,11 @@
219
254
  },
220
255
  "400": {
221
256
  "description": "paths.usuariosById.delete.responses.400.description",
257
+ "headers": {
258
+ "X-ServeRest-Stages": {
259
+ "$ref": "#/components/headers/XServeRestStages"
260
+ }
261
+ },
222
262
  "content": {
223
263
  "application/json": {
224
264
  "schema": {
@@ -253,6 +293,11 @@
253
293
  "responses": {
254
294
  "200": {
255
295
  "description": "paths.usuariosById.put.responses.200.description",
296
+ "headers": {
297
+ "X-ServeRest-Stages": {
298
+ "$ref": "#/components/headers/XServeRestStages"
299
+ }
300
+ },
256
301
  "content": {
257
302
  "application/json": {
258
303
  "schema": {
@@ -263,6 +308,11 @@
263
308
  },
264
309
  "201": {
265
310
  "description": "paths.usuariosById.put.responses.201.description",
311
+ "headers": {
312
+ "X-ServeRest-Stages": {
313
+ "$ref": "#/components/headers/XServeRestStages"
314
+ }
315
+ },
266
316
  "content": {
267
317
  "application/json": {
268
318
  "schema": {
@@ -303,6 +353,11 @@
303
353
  "responses": {
304
354
  "200": {
305
355
  "description": "paths.produtos.get.responses.200.description",
356
+ "headers": {
357
+ "X-ServeRest-Stages": {
358
+ "$ref": "#/components/headers/XServeRestStages"
359
+ }
360
+ },
306
361
  "content": {
307
362
  "application/json": {
308
363
  "schema": {
@@ -330,6 +385,11 @@
330
385
  "responses": {
331
386
  "201": {
332
387
  "description": "paths.produtos.post.responses.201.description",
388
+ "headers": {
389
+ "X-ServeRest-Stages": {
390
+ "$ref": "#/components/headers/XServeRestStages"
391
+ }
392
+ },
333
393
  "content": {
334
394
  "application/json": {
335
395
  "schema": {
@@ -371,6 +431,11 @@
371
431
  "responses": {
372
432
  "200": {
373
433
  "description": "paths.produtosById.get.responses.200.description",
434
+ "headers": {
435
+ "X-ServeRest-Stages": {
436
+ "$ref": "#/components/headers/XServeRestStages"
437
+ }
438
+ },
374
439
  "content": {
375
440
  "application/json": {
376
441
  "schema": {
@@ -381,6 +446,11 @@
381
446
  },
382
447
  "400": {
383
448
  "description": "paths.produtosById.get.responses.400.description",
449
+ "headers": {
450
+ "X-ServeRest-Stages": {
451
+ "$ref": "#/components/headers/XServeRestStages"
452
+ }
453
+ },
384
454
  "content": {
385
455
  "application/json": {
386
456
  "schema": {
@@ -423,6 +493,11 @@
423
493
  "responses": {
424
494
  "200": {
425
495
  "description": "paths.produtosById.delete.responses.200.description",
496
+ "headers": {
497
+ "X-ServeRest-Stages": {
498
+ "$ref": "#/components/headers/XServeRestStages"
499
+ }
500
+ },
426
501
  "content": {
427
502
  "application/json": {
428
503
  "schema": {
@@ -439,6 +514,11 @@
439
514
  },
440
515
  "400": {
441
516
  "description": "paths.produtosById.delete.responses.400.description",
517
+ "headers": {
518
+ "X-ServeRest-Stages": {
519
+ "$ref": "#/components/headers/XServeRestStages"
520
+ }
521
+ },
442
522
  "content": {
443
523
  "application/json": {
444
524
  "schema": {
@@ -484,6 +564,11 @@
484
564
  "responses": {
485
565
  "200": {
486
566
  "description": "paths.produtosById.put.responses.200.description",
567
+ "headers": {
568
+ "X-ServeRest-Stages": {
569
+ "$ref": "#/components/headers/XServeRestStages"
570
+ }
571
+ },
487
572
  "content": {
488
573
  "application/json": {
489
574
  "schema": {
@@ -494,6 +579,11 @@
494
579
  },
495
580
  "201": {
496
581
  "description": "paths.produtosById.put.responses.201.description",
582
+ "headers": {
583
+ "X-ServeRest-Stages": {
584
+ "$ref": "#/components/headers/XServeRestStages"
585
+ }
586
+ },
497
587
  "content": {
498
588
  "application/json": {
499
589
  "schema": {
@@ -538,6 +628,11 @@
538
628
  "responses": {
539
629
  "200": {
540
630
  "description": "paths.carrinhos.get.responses.200.description",
631
+ "headers": {
632
+ "X-ServeRest-Stages": {
633
+ "$ref": "#/components/headers/XServeRestStages"
634
+ }
635
+ },
541
636
  "content": {
542
637
  "application/json": {
543
638
  "schema": {
@@ -565,6 +660,11 @@
565
660
  "responses": {
566
661
  "201": {
567
662
  "description": "paths.carrinhos.post.responses.201.description",
663
+ "headers": {
664
+ "X-ServeRest-Stages": {
665
+ "$ref": "#/components/headers/XServeRestStages"
666
+ }
667
+ },
568
668
  "content": {
569
669
  "application/json": {
570
670
  "schema": {
@@ -575,6 +675,11 @@
575
675
  },
576
676
  "400": {
577
677
  "description": "paths.carrinhos.post.responses.400.description",
678
+ "headers": {
679
+ "X-ServeRest-Stages": {
680
+ "$ref": "#/components/headers/XServeRestStages"
681
+ }
682
+ },
578
683
  "content": {
579
684
  "application/json": {
580
685
  "schema": {
@@ -616,6 +721,11 @@
616
721
  "responses": {
617
722
  "200": {
618
723
  "description": "paths.carrinhosById.get.responses.200.description",
724
+ "headers": {
725
+ "X-ServeRest-Stages": {
726
+ "$ref": "#/components/headers/XServeRestStages"
727
+ }
728
+ },
619
729
  "content": {
620
730
  "application/json": {
621
731
  "schema": {
@@ -626,6 +736,11 @@
626
736
  },
627
737
  "400": {
628
738
  "description": "paths.carrinhosById.get.responses.400.description",
739
+ "headers": {
740
+ "X-ServeRest-Stages": {
741
+ "$ref": "#/components/headers/XServeRestStages"
742
+ }
743
+ },
629
744
  "content": {
630
745
  "application/json": {
631
746
  "schema": {
@@ -658,6 +773,11 @@
658
773
  "responses": {
659
774
  "200": {
660
775
  "description": "paths.carrinhosConcluirCompra.delete.responses.200.description",
776
+ "headers": {
777
+ "X-ServeRest-Stages": {
778
+ "$ref": "#/components/headers/XServeRestStages"
779
+ }
780
+ },
661
781
  "content": {
662
782
  "application/json": {
663
783
  "schema": {
@@ -693,6 +813,11 @@
693
813
  "responses": {
694
814
  "200": {
695
815
  "description": "paths.carrinhosCancelarCompra.delete.responses.200.description",
816
+ "headers": {
817
+ "X-ServeRest-Stages": {
818
+ "$ref": "#/components/headers/XServeRestStages"
819
+ }
820
+ },
696
821
  "content": {
697
822
  "application/json": {
698
823
  "schema": {
@@ -894,6 +1019,11 @@
894
1019
  "responses": {
895
1020
  "EmailJaUtilizado": {
896
1021
  "description": "components.responses.EmailJaUtilizado.description",
1022
+ "headers": {
1023
+ "X-ServeRest-Stages": {
1024
+ "$ref": "#/components/headers/XServeRestStages"
1025
+ }
1026
+ },
897
1027
  "content": {
898
1028
  "application/json": {
899
1029
  "schema": {
@@ -904,6 +1034,11 @@
904
1034
  },
905
1035
  "ProdutoNomeDuplicado": {
906
1036
  "description": "components.responses.ProdutoNomeDuplicado.description",
1037
+ "headers": {
1038
+ "X-ServeRest-Stages": {
1039
+ "$ref": "#/components/headers/XServeRestStages"
1040
+ }
1041
+ },
907
1042
  "content": {
908
1043
  "application/json": {
909
1044
  "schema": {
@@ -914,6 +1049,11 @@
914
1049
  },
915
1050
  "TokenInvalido": {
916
1051
  "description": "components.responses.TokenInvalido.description",
1052
+ "headers": {
1053
+ "X-ServeRest-Stages": {
1054
+ "$ref": "#/components/headers/XServeRestStages"
1055
+ }
1056
+ },
917
1057
  "content": {
918
1058
  "application/json": {
919
1059
  "schema": {
@@ -924,6 +1064,11 @@
924
1064
  },
925
1065
  "RotaParaAdministradores": {
926
1066
  "description": "components.responses.RotaParaAdministradores.description",
1067
+ "headers": {
1068
+ "X-ServeRest-Stages": {
1069
+ "$ref": "#/components/headers/XServeRestStages"
1070
+ }
1071
+ },
927
1072
  "content": {
928
1073
  "application/json": {
929
1074
  "schema": {
@@ -933,6 +1078,15 @@
933
1078
  }
934
1079
  }
935
1080
  },
1081
+ "headers": {
1082
+ "XServeRestStages": {
1083
+ "description": "components.headers.XServeRestStages.description",
1084
+ "schema": {
1085
+ "type": "string",
1086
+ "example": "body;st=ok;dur=0.21, rate_limit;st=skipped, auth;st=failed;dur=0.05;reason=token_missing, schema;st=not_run, rules;st=not_run, write;st=not_run"
1087
+ }
1088
+ }
1089
+ },
936
1090
  "securitySchemes": {
937
1091
  "ApiKeyAuth": {
938
1092
  "type": "apiKey",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverest",
3
- "version": "3.2.2",
3
+ "version": "3.3.0-beta.1",
4
4
  "description": "Servidor REST local de forma rápida e simples para estudo de testes de API",
5
5
  "author": "Paulo Gonçalves <author@serverest.dev> (https://www.linkedin.com/in/paulo-goncalves/)",
6
6
  "license": "GPL-3.0",
@@ -66,6 +66,7 @@
66
66
  "moesif-nodejs": "^3.11.0",
67
67
  "morgan": "^1.10.1",
68
68
  "nedb-promises": "^6.2.3",
69
+ "on-headers": "^1.1.0",
69
70
  "open": "^11.0.0",
70
71
  "rate-limiter-flexible": "^9.1.0",
71
72
  "swagger-ui-express": "^5.0.1",
package/src/app.js CHANGED
@@ -26,6 +26,7 @@ const moesifMiddleware = require('./middlewares/moesif-monitor-middleware')
26
26
  const { version } = require('../package.json')
27
27
  const swaggerDocument = require('../docs/swagger.json')
28
28
  const rateLimiter = require('./middlewares/rate-limiter')
29
+ const { iniciar, etapa } = require('./utils/etapas')
29
30
  const { fetchLatestRelease } = require('./utils/github-release')
30
31
  const { buildCustomJsStr, customCss } = require('./swagger')
31
32
  const { localizeSwaggerDocument } = require('./swagger/translations')
@@ -55,13 +56,15 @@ app.set('json spaces', 4)
55
56
  // o Express passaria a ler o primeiro endereço do X-Forwarded-For, que o próprio cliente
56
57
  // pode forjar, então o número de saltos precisa ser explícito.
57
58
  app.set('trust proxy', 1)
58
- app.use(express.json())
59
- app.use(express.urlencoded({ extended: false }))
59
+ app.use(['/login', '/usuarios', '/produtos', '/carrinhos'], iniciar)
60
+ const parsersDoBody = express.Router()
61
+ parsersDoBody.use(express.json(), express.urlencoded({ extended: false }))
62
+ app.use(etapa('body', parsersDoBody))
60
63
  app.use(queryParser())
61
64
  app.use(timeout())
62
- app.use(cors())
65
+ app.use(cors({ exposedHeaders: ['X-ServeRest-Stages'] }))
63
66
  app.use(moesifMiddleware)
64
- app.use(rateLimiter)
67
+ app.use(etapa('rate_limit', rateLimiter))
65
68
 
66
69
  app.disable('etag')
67
70
 
@@ -18,7 +18,11 @@ exports.getOne = async (req, res) => {
18
18
  }
19
19
 
20
20
  exports.post = async (req, res) => {
21
+ const { etapas } = res.locals
22
+ etapas.regras('single_cart', 'no_duplicate_product', 'product_exists', 'enough_stock')
23
+
21
24
  const { idUsuario, possuiCarrinho } = await service.usuarioJaPossuiCarrinho(req.headers.authorization)
25
+ etapas.regra('single_cart', !possuiCarrinho)
22
26
  if (possuiCarrinho) {
23
27
  return res.status(400).send({ message: constant.LIMIT_JUST_ONE_CART })
24
28
  }
@@ -26,14 +30,26 @@ exports.post = async (req, res) => {
26
30
  const { produtos } = req.body
27
31
 
28
32
  const idProdutosDuplicados = service.extrairProdutosDuplicados(produtos)
33
+ etapas.regra('no_duplicate_product', !idProdutosDuplicados.length)
29
34
  if (idProdutosDuplicados.length) {
30
35
  return res.status(400).send({ message: constant.CART_WITH_DUPLICATE_PRODUCT, idProdutosDuplicados })
31
36
  }
32
37
 
33
38
  const { produtosComPrecoUnitario, error } = await service.getProdutosComPrecoUnitarioOuErro(produtos)
34
39
  if (error) {
40
+ // As duas regras são avaliadas item a item e param no primeiro item que falha.
41
+ if (error.message === constant.PRODUCT_NOT_FOUND) {
42
+ etapas.regra('product_exists', false, { item: error.index })
43
+ } else {
44
+ etapas.regra('product_exists', true)
45
+ etapas.regra('enough_stock', false, { item: error.index })
46
+ }
35
47
  return res.status(error.statusCode).send({ message: error.message, item: error.item })
36
48
  }
49
+ etapas.regra('product_exists', true)
50
+ etapas.regra('enough_stock', true)
51
+
52
+ etapas.abrir('write')
37
53
  const precoTotal = await service.precoTotal(produtosComPrecoUnitario)
38
54
  const quantidadeTotal = await service.quantidadeTotal(produtosComPrecoUnitario)
39
55
 
@@ -1,41 +1,45 @@
1
1
  'use strict'
2
2
 
3
+ const { TokenExpiredError } = require('jsonwebtoken')
4
+
3
5
  const authService = require('../services/auth-service')
4
6
  const constant = require('../utils/constants')
5
7
  const usuariosService = require('../services/usuarios-service')
8
+ const { etapa } = require('../utils/etapas')
6
9
 
7
- exports.checkAdm = async (req, res, next) => {
8
- if (!await tokenValido(req.headers)) {
10
+ exports.checkAdm = etapa('auth', async (req, res, next) => {
11
+ const motivo = await motivoDoTokenInvalido(req.headers)
12
+ if (motivo) {
13
+ res.locals.etapas.detalhar({ reason: motivo })
9
14
  return res.status(401).send({ message: constant.INVALID_TOKEN })
10
15
  }
11
16
  const tokenDecodificado = authService.verifyToken(req.headers.authorization)
12
17
  if (!await usuariosService.usuarioEhAdministrador(tokenDecodificado)) {
18
+ res.locals.etapas.detalhar({ reason: 'not_admin' })
13
19
  return res.status(403).send({ message: constant.REQUIRED_ADMIN })
14
20
  }
15
21
  next()
16
- }
22
+ })
17
23
 
18
- exports.checkToken = async (req, res, next) => {
19
- if (!await tokenValido(req.headers)) {
24
+ exports.checkToken = etapa('auth', async (req, res, next) => {
25
+ const motivo = await motivoDoTokenInvalido(req.headers)
26
+ if (motivo) {
27
+ res.locals.etapas.detalhar({ reason: motivo })
20
28
  return res.status(401).send({ message: constant.INVALID_TOKEN })
21
29
  }
22
30
  next()
23
- }
31
+ })
24
32
 
25
- async function tokenValido ({ authorization }) {
26
- if (authorization === undefined) return false
33
+ async function motivoDoTokenInvalido ({ authorization }) {
34
+ if (authorization === undefined) return 'token_missing'
27
35
 
28
- const semBearer = authorization.split(' ')[0] !== 'Bearer'
29
- const semToken = authorization.split(' ')[1] === undefined
30
-
31
- if (semBearer || semToken) {
32
- return false
33
- }
36
+ const [tipo, token] = authorization.split(' ')
37
+ if (tipo !== 'Bearer' || !token) return 'bad_format'
34
38
 
35
39
  const tokenDecodificado = authService.verifyToken(authorization)
36
- if (tokenDecodificado.email === undefined) {
37
- return false
38
- }
40
+ if (tokenDecodificado instanceof TokenExpiredError) return 'token_expired'
41
+ if (tokenDecodificado.email === undefined) return 'token_invalid'
39
42
 
40
- return usuariosService.existeUsuario({ email: tokenDecodificado.email, password: tokenDecodificado.password })
43
+ const usuario = await usuariosService.existeUsuario({ email: tokenDecodificado.email, password: tokenDecodificado.password })
44
+ if (!usuario) return 'user_not_found'
41
45
  }
@@ -24,6 +24,7 @@ const limiteGlobal = new RateLimiterMemory({
24
24
 
25
25
  module.exports = async (req, res, next) => {
26
26
  if (aplicacaoExecutandoLocalmente() || ehAmbienteDeTestes) {
27
+ res.locals.etapas?.ignorar()
27
28
  return next()
28
29
  }
29
30
 
@@ -6,12 +6,13 @@ const authentication = require('../middlewares/authentication-middleware')
6
6
  const controller = require('../controllers/carrinhos-controller')
7
7
  const model = require('../models/carrinhos-model')
8
8
  const validateSchema = require('../services/validateSchema-service')
9
+ const { etapa, plano } = require('../utils/etapas')
9
10
 
10
11
  const router = express.Router()
11
- router.get('/', validateSchema(model.schemaGet), controller.get)
12
- router.get('/:id', validateSchema(model.schemaGetOne), controller.getOne)
13
- router.post('/', authentication.checkToken, validateSchema(model.schemaPost), controller.post)
14
- router.delete('/concluir-compra', authentication.checkToken, controller.concluirCompra)
15
- router.delete('/cancelar-compra', authentication.checkToken, controller.cancelarCompra)
12
+ router.get('/', plano('schema', 'read'), validateSchema(model.schemaGet), etapa('read', controller.get))
13
+ router.get('/:id', plano('schema', 'read'), validateSchema(model.schemaGetOne), etapa('read', controller.getOne))
14
+ router.post('/', plano('auth', 'schema', 'rules', 'write'), authentication.checkToken, validateSchema(model.schemaPost), etapa('rules', controller.post))
15
+ router.delete('/concluir-compra', plano('auth', 'rules'), authentication.checkToken, etapa('rules', controller.concluirCompra))
16
+ router.delete('/cancelar-compra', plano('auth', 'rules'), authentication.checkToken, etapa('rules', controller.cancelarCompra))
16
17
 
17
18
  module.exports = router
@@ -5,8 +5,9 @@ const express = require('express')
5
5
  const controller = require('../controllers/login-controller')
6
6
  const model = require('../models/login-model')
7
7
  const validateSchema = require('../services/validateSchema-service')
8
+ const { etapa, plano } = require('../utils/etapas')
8
9
 
9
10
  const router = express.Router()
10
- router.post('/', validateSchema(model.schemaPost), controller.post)
11
+ router.post('/', plano('schema', 'rules'), validateSchema(model.schemaPost), etapa('rules', controller.post))
11
12
 
12
13
  module.exports = router
@@ -6,12 +6,13 @@ const authentication = require('../middlewares/authentication-middleware')
6
6
  const controller = require('../controllers/produtos-controller')
7
7
  const model = require('../models/produtos-model')
8
8
  const validateSchema = require('../services/validateSchema-service')
9
+ const { etapa, plano } = require('../utils/etapas')
9
10
 
10
11
  const router = express.Router()
11
- router.get('/', validateSchema(model.schemaGet), controller.get)
12
- router.get('/:id', validateSchema(model.schemaGetOne), controller.getOne)
13
- router.post('/', authentication.checkAdm, validateSchema(model.schemaPost), controller.post)
14
- router.delete('/:id', authentication.checkAdm, validateSchema(model.schemaDelete), controller.delete)
15
- router.put('/:id', authentication.checkAdm, validateSchema(model.schemaPut), controller.put)
12
+ router.get('/', plano('schema', 'read'), validateSchema(model.schemaGet), etapa('read', controller.get))
13
+ router.get('/:id', plano('schema', 'read'), validateSchema(model.schemaGetOne), etapa('read', controller.getOne))
14
+ router.post('/', plano('auth', 'schema', 'rules'), authentication.checkAdm, validateSchema(model.schemaPost), etapa('rules', controller.post))
15
+ router.delete('/:id', plano('auth', 'schema', 'rules'), authentication.checkAdm, validateSchema(model.schemaDelete), etapa('rules', controller.delete))
16
+ router.put('/:id', plano('auth', 'schema', 'rules'), authentication.checkAdm, validateSchema(model.schemaPut), etapa('rules', controller.put))
16
17
 
17
18
  module.exports = router
@@ -5,12 +5,13 @@ const express = require('express')
5
5
  const controller = require('../controllers/usuarios-controller')
6
6
  const model = require('../models/usuarios-model')
7
7
  const validateSchema = require('../services/validateSchema-service')
8
+ const { etapa, plano } = require('../utils/etapas')
8
9
 
9
10
  const router = express.Router()
10
- router.get('/', validateSchema(model.schemaGet), controller.get)
11
- router.get('/:id', validateSchema(model.schemaGetOne), controller.getOne)
12
- router.post('/', validateSchema(model.schemaPost), controller.post)
13
- router.delete('/:id', validateSchema(model.schemaDelete), controller.delete)
14
- router.put('/:id', validateSchema(model.schemaPut), controller.put)
11
+ router.get('/', plano('schema', 'read'), validateSchema(model.schemaGet), etapa('read', controller.get))
12
+ router.get('/:id', plano('schema', 'read'), validateSchema(model.schemaGetOne), etapa('read', controller.getOne))
13
+ router.post('/', plano('schema', 'rules'), validateSchema(model.schemaPost), etapa('rules', controller.post))
14
+ router.delete('/:id', plano('schema', 'rules'), validateSchema(model.schemaDelete), etapa('rules', controller.delete))
15
+ router.put('/:id', plano('schema', 'rules'), validateSchema(model.schemaPut), etapa('rules', controller.put))
15
16
 
16
17
  module.exports = router
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { validate } = require('express-validation')
4
4
 
5
+ const { etapa } = require('../utils/etapas')
6
+
5
7
  module.exports = schema => {
6
- return validate(schema, { keyByField: false }, { abortEarly: false })
8
+ return etapa('schema', validate(schema, { keyByField: false }, { abortEarly: false }))
7
9
  }
@@ -93,5 +93,6 @@ module.exports = {
93
93
  'components.responses.ProdutoNomeDuplicado.description': 'A product with this name already exists',
94
94
  'components.responses.TokenInvalido.description': 'Token missing, invalid or expired',
95
95
  'components.responses.RotaParaAdministradores.description': 'Route exclusive to administrators (`administrador = true`)',
96
+ 'components.headers.XServeRestStages.description': 'Stages of the route in execution order. Each entry has `st` (`ok`, `failed`, `skipped` or `not_run`), `dur` in milliseconds when the stage ran, `reason` when `auth` fails and `item` when a rule fails on a specific item. Entries named `<stage>.<rule>` detail the rules of that stage. Full format in the project README.',
96
97
  'components.securitySchemes.ApiKeyAuth.description': 'Provide the `Authorization` value returned by the _/login_ route'
97
98
  }
@@ -93,5 +93,6 @@ module.exports = {
93
93
  'components.responses.ProdutoNomeDuplicado.description': 'Ya existe un producto con ese nombre',
94
94
  'components.responses.TokenInvalido.description': 'Token ausente, inválido o expirado',
95
95
  'components.responses.RotaParaAdministradores.description': 'Ruta exclusiva para administradores (`administrador = true`)',
96
+ 'components.headers.XServeRestStages.description': 'Etapas de la ruta en orden de ejecución. Cada entrada tiene `st` (`ok`, `failed`, `skipped` o `not_run`), `dur` en milisegundos cuando la etapa se ejecutó, `reason` cuando `auth` falla e `item` cuando una regla falla en un ítem específico. Las entradas `<etapa>.<regla>` detallan las reglas de la etapa. Formato completo en el README del proyecto.',
96
97
  'components.securitySchemes.ApiKeyAuth.description': 'Indica el valor de `Authorization` devuelto por la ruta _/login_'
97
98
  }
@@ -93,5 +93,6 @@ module.exports = {
93
93
  'components.responses.ProdutoNomeDuplicado.description': 'Já existe produto com esse nome',
94
94
  'components.responses.TokenInvalido.description': 'Token ausente, inválido ou expirado',
95
95
  'components.responses.RotaParaAdministradores.description': 'Rota exclusiva para administradores (`administrador = true`)',
96
+ 'components.headers.XServeRestStages.description': 'Etapas da rota em ordem de execução. Cada entrada tem `st` (`ok`, `failed`, `skipped` ou `not_run`), `dur` em milissegundos quando a etapa executou, `reason` quando `auth` falha e `item` quando uma regra falha em um item específico. Entradas `<etapa>.<regra>` detalham as regras da etapa. Formato completo no README do projeto.',
96
97
  'components.securitySchemes.ApiKeyAuth.description': 'Informe o valor de `Authorization` retornado na rota _/login_'
97
98
  }
@@ -0,0 +1,98 @@
1
+ 'use strict'
2
+
3
+ const onHeaders = require('on-headers')
4
+
5
+ const emMilissegundos = nanossegundos => (Number(nanossegundos) / 1e6).toFixed(2)
6
+
7
+ class Registro {
8
+ constructor () {
9
+ this.entradas = []
10
+ this.regrasDeclaradas = new Map()
11
+ this.aberta = undefined
12
+ }
13
+
14
+ planejar (nomes) {
15
+ nomes.forEach(nome => this.entradas.push({ nome, st: 'not_run', detalhes: {} }))
16
+ }
17
+
18
+ abrir (nome) {
19
+ this.fechar('ok')
20
+ this.aberta = this.entradas.find(entrada => entrada.nome === nome)
21
+ this.aberta.inicio = process.hrtime.bigint()
22
+ }
23
+
24
+ fechar (st) {
25
+ if (!this.aberta) return
26
+ this.aberta.st = st
27
+ this.aberta.dur = emMilissegundos(process.hrtime.bigint() - this.aberta.inicio)
28
+ this.aberta = undefined
29
+ }
30
+
31
+ ignorar () {
32
+ this.aberta.st = 'skipped'
33
+ this.aberta = undefined
34
+ }
35
+
36
+ detalhar (objeto) {
37
+ Object.assign(this.aberta.detalhes, objeto)
38
+ }
39
+
40
+ regras (...nomes) {
41
+ const novas = nomes.map(nome => {
42
+ const entrada = { nome: `${this.aberta.nome}.${nome}`, st: 'not_run', detalhes: {} }
43
+ this.regrasDeclaradas.set(nome, entrada)
44
+ return entrada
45
+ })
46
+ this.entradas.splice(this.entradas.indexOf(this.aberta) + 1, 0, ...novas)
47
+ }
48
+
49
+ regra (nome, passou, detalhes) {
50
+ const entrada = this.regrasDeclaradas.get(nome)
51
+ entrada.st = passou ? 'ok' : 'failed'
52
+ Object.assign(entrada.detalhes, detalhes)
53
+ }
54
+
55
+ // A etapa ainda aberta quando os headers são escritos é a que produziu a resposta.
56
+ finalizar (statusCode) {
57
+ this.fechar(statusCode >= 400 ? 'failed' : 'ok')
58
+ }
59
+
60
+ stages () {
61
+ return this.entradas
62
+ .map(({ nome, st, dur, detalhes }) => [
63
+ nome,
64
+ `st=${st}`,
65
+ ...(dur === undefined ? [] : [`dur=${dur}`]),
66
+ ...Object.entries(detalhes).map(([chave, valor]) => `${chave}=${valor}`)
67
+ ].join(';'))
68
+ .join(', ')
69
+ }
70
+ }
71
+
72
+ exports.iniciar = (req, res, next) => {
73
+ // O cors responde o preflight antes do rate limiter, então não há etapa a relatar.
74
+ if (req.method === 'OPTIONS') return next()
75
+ const registro = res.locals.etapas = new Registro()
76
+ registro.planejar(['body', 'rate_limit'])
77
+ onHeaders(res, () => {
78
+ registro.finalizar(res.statusCode)
79
+ res.setHeader('X-ServeRest-Stages', registro.stages())
80
+ })
81
+ next()
82
+ }
83
+
84
+ // Exatamente três parâmetros: com quatro o Express trataria o retorno como error handler.
85
+ exports.etapa = (nome, middleware) => (req, res, next) => {
86
+ const registro = res.locals.etapas
87
+ if (!registro) return middleware(req, res, next)
88
+ registro.abrir(nome)
89
+ return middleware(req, res, erro => {
90
+ registro.fechar(erro ? 'failed' : 'ok')
91
+ next(erro)
92
+ })
93
+ }
94
+
95
+ exports.plano = (...nomes) => (req, res, next) => {
96
+ res.locals.etapas.planejar(nomes)
97
+ next()
98
+ }