@design-edito/publisher-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +265 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +5105 -0
  4. package/dist/index.js.map +7 -0
  5. package/dist/public/empty.txt +0 -0
  6. package/dist/types/api/admin/temp/flush/index.d.ts +6 -0
  7. package/dist/types/api/admin/users/create/index.d.ts +23 -0
  8. package/dist/types/api/admin/users/delete/index.d.ts +12 -0
  9. package/dist/types/api/admin/users/get/index.d.ts +18 -0
  10. package/dist/types/api/admin/users/get-upload-quota/index.d.ts +12 -0
  11. package/dist/types/api/admin/users/list/index.d.ts +24 -0
  12. package/dist/types/api/admin/users/reset-upload-quota/index.d.ts +15 -0
  13. package/dist/types/api/admin/users/revoke-auth-tokens/index.d.ts +10 -0
  14. package/dist/types/api/admin/users/revoke-email-validation-tokens/index.d.ts +10 -0
  15. package/dist/types/api/admin/users/revoke-pasword-renewal-tokens/index.d.ts +10 -0
  16. package/dist/types/api/admin/users/update/index.d.ts +23 -0
  17. package/dist/types/api/auth/login/index.d.ts +15 -0
  18. package/dist/types/api/auth/logout/index.d.ts +5 -0
  19. package/dist/types/api/auth/logout-everywhere/index.d.ts +5 -0
  20. package/dist/types/api/auth/refresh-token/index.d.ts +5 -0
  21. package/dist/types/api/auth/request-email-verification-token/index.d.ts +9 -0
  22. package/dist/types/api/auth/request-new-password/index.d.ts +9 -0
  23. package/dist/types/api/auth/signup/index.d.ts +13 -0
  24. package/dist/types/api/auth/submit-new-password/index.d.ts +11 -0
  25. package/dist/types/api/auth/verify-email/index.d.ts +12 -0
  26. package/dist/types/api/auth/whoami/index.d.ts +8 -0
  27. package/dist/types/api/csrf/get-token/index.d.ts +8 -0
  28. package/dist/types/api/image/format/index.d.ts +28 -0
  29. package/dist/types/api/image/transform/index.d.ts +11 -0
  30. package/dist/types/api/index.d.ts +102 -0
  31. package/dist/types/api/internals.d.ts +270 -0
  32. package/dist/types/api/system/kill/index.d.ts +3 -0
  33. package/dist/types/api/system/ping/index.d.ts +6 -0
  34. package/dist/types/api/system/send-mail/index.d.ts +11 -0
  35. package/dist/types/api/system/status-check/index.d.ts +22 -0
  36. package/dist/types/auth/index.d.ts +11 -0
  37. package/dist/types/csrf/index.d.ts +5 -0
  38. package/dist/types/database/index.d.ts +69 -0
  39. package/dist/types/dto/index.d.ts +25 -0
  40. package/dist/types/email/index.d.ts +6 -0
  41. package/dist/types/env/index.d.ts +83 -0
  42. package/dist/types/errors/index.d.ts +295 -0
  43. package/dist/types/fs/index.d.ts +110 -0
  44. package/dist/types/index.d.ts +21 -0
  45. package/dist/types/index.js +1 -0
  46. package/dist/types/index.js.map +7 -0
  47. package/dist/types/init/index.d.ts +1 -0
  48. package/dist/types/jwt/index.d.ts +7 -0
  49. package/dist/types/logs/index.d.ts +46 -0
  50. package/dist/types/plugins/index.d.ts +3 -0
  51. package/dist/types/plugins/internals.d.ts +177 -0
  52. package/dist/types/schema/index.d.ts +156 -0
  53. package/dist/types/temp/index.d.ts +7 -0
  54. package/dist/types/transformers/index.d.ts +3 -0
  55. package/dist/types/transformers/user/index.d.ts +5 -0
  56. package/dist/types/transformers/user-upload-quota/index.d.ts +3 -0
  57. package/dist/types/uploads/index.d.ts +73 -0
  58. package/dist/types/validation/index.d.ts +4 -0
  59. package/package.json +280 -0
package/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # LM-Publisher
2
+
3
+ `lm-publisher`, est serveur qui ne répond que du JSON ou des fichiers, pas de frontend.
4
+
5
+ ## API
6
+
7
+ Il y a pour l'instant 5 types de réponses : `void`, `file`, `json`, `list`, `error`. Si le serveur ne répond pas une réponse de type `error`, c'est que l'opération a abouti. Ci-dessous, d'abord le détail des réponses HTTP avec les headers avant de détailler la forme de la donnée JSON reçue pour les réponses de type `json`, `list` et `error`.
8
+
9
+ ### Les réponses HTTP
10
+
11
+ #### Type VOID
12
+
13
+ Une réponse de type `void`, est une réponse vide avec un statut HTTP 204, ce sont des opérations qui ne nécessitent pas de récupérer de la donnée en réponse. Ça ne va pas concerner beaucoup de routes normalement, typiquement :
14
+
15
+ - `POST /auth/logout`
16
+ - `POST /auth/logout-everywhere`
17
+ - `POST /auth/refresh-token (celle là elle est un peu spéciale, more on that later)`
18
+ - `POST /auth/request-email-verification-token`
19
+ - `POST /auth/submit-new-password`
20
+ - Quelques autres dans la section `/storage` du serveur, mais cette partie là est encore expérimentale, on va pas en avoir besoin tout de suite
21
+
22
+ Le détail de comment la réponse est formée :
23
+
24
+ ```ts
25
+ // Headers par défaut pour chaque route (possiblement overridées plus bas en fonction du type de réponse)
26
+ res.setHeader('Cache-Control', 'no-store')
27
+ .setHeader('Content-Security-Policy', `default-src 'none';`)
28
+ .setHeader('Referrer-Policy', 'no-referrer')
29
+ .setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
30
+ .setHeader('X-Content-Type-Options', 'nosniff')
31
+ .setHeader('X-Request-ID', initMeta.requestId)
32
+ .setHeader('X-Response-Time', `${Date.now() - initMeta.timestamp}ms`)
33
+ .setHeader('X-Server-Meta', JSON.stringify({
34
+ userId: initMeta.userId,
35
+ requestId: initMeta.requestId,
36
+ timestamp: initMeta.timestamp
37
+ }))
38
+ // Fin des headers par défaut
39
+ .status(204)
40
+ .end()
41
+ ```
42
+
43
+ #### Type FILE
44
+
45
+ Lorsque le serveur répond par un fichier.
46
+
47
+ > Note pour plus tard : pour l'instant elle n'est implémentée nulle part, mais on va en faire usage pour nos générateurs d'image, et après réflexion, on va pas passer par un truc intermédiaire de genre la route renvoie une liste d'urls temporaires sur lesquelles trouver les fichiers, on va effectivement répondre un zip, j'ai vu qu'on pouvait unzip côté client et récupérer le contenu des images, donc on va faire comme ça je pense. Anyway, la réponse est créée comme ça :
48
+
49
+ ```ts
50
+ // Headers par défaut, puis
51
+ res.status(fileResponse.httpStatus)
52
+ .type(fileResponse.contentType)
53
+ .setHeader('Content-Disposition', `attachment; filename="${fileResponse.filename}"; filename*=UTF-8''${encodeURIComponent(fileResponse.filename)}`)
54
+ .setHeader('Cache-Control', fileResponse.cacheControl)
55
+ .setHeader('Content-Length', fileResponse.contentLengthBytes)
56
+ ```
57
+ Ensuite, soit le fichier est envoyé d'un bloc via `res.send(fileBuffer)` parce que c'est un buffer côté serveur, soit il est "streamé" via `fileStream.pipe(res)`.
58
+
59
+ > Qu'est-ce que ça veut dire ? Aucune idée.
60
+
61
+ #### Type JSON et LIST
62
+
63
+ Pour la plupart des routes, le serveur va répondre du JSON :
64
+ - soit un seul objet (type `json`) :
65
+ ```ts
66
+ // Réponse JSON, headers par défaut, puis
67
+ res.status(fullJsonResponse.httpStatus)
68
+ .type('application/json; charset=utf-8')
69
+ .setHeader('Content-Disposition', 'inline')
70
+ .json(fullJsonResponse)
71
+ ```
72
+ - soit une liste d'objets partielle et paginée (type `list`) :
73
+ ```ts
74
+ // Réponse LIST, headers par défaut, puis
75
+ res.status(fullListResponse.httpStatus)
76
+ .type('application/json; charset=utf-8')
77
+ .setHeader('Content-Disposition', 'inline')
78
+ .setHeader('X-Total-Count', fullListResponse.total)
79
+ .setHeader('X-Page', fullResponse.page)
80
+ .setHeader('X-Next-Page', fullListResponse.next ?? '')
81
+ .setHeader('X-Prev-Page', fullListResponse.prev ?? '')
82
+ .json(fullListResponse)
83
+ ```
84
+
85
+ #### Type ERROR
86
+
87
+ À chaque fois que quelque chose s'est mal passé, soit côté client (auth manquante, requête malformée, ...), soit côté serveur (mauvaise implémentation, db non accessible, etc...) :
88
+
89
+ ```ts
90
+ // Headers par défaut
91
+ res.status(fullResponse.httpStatus)
92
+ .type('application/problem+json')
93
+ .setHeader('Content-Disposition', 'inline')
94
+ .json(fullResponse)
95
+ ```
96
+
97
+ ### Le contenu des réponses JSON, LIST et ERROR
98
+
99
+ Quand le serveur renvoie autre chose qu'une réponse `void` ou `file`, il renvoie du JSON dans `response.body`. La structure sera toujours :
100
+
101
+ ```ts
102
+ type ServerErrorResponse<C extends Codes> = {
103
+ success: false
104
+ httpStatus: number
105
+ type: 'error'
106
+ error: ErrorData<C> // Voir plus bas
107
+ meta: {
108
+ userId: string | null
109
+ requestId: string
110
+ timestampMs: number
111
+ elapsedMs: number
112
+ }
113
+ }
114
+
115
+ type ServerJsonResponse<P extends object> = {
116
+ success: true
117
+ httpStatus: number
118
+ type: 'json'
119
+ payload: P
120
+ meta: {
121
+ userId: string | null
122
+ requestId: string
123
+ timestampMs: number
124
+ elapsedMs: number
125
+ }
126
+ }
127
+
128
+ type ServerListResponse<P extends object> = {
129
+ success: true
130
+ httpStatus: number
131
+ type: 'list'
132
+ payload: P[]
133
+ page: number
134
+ total: number
135
+ prev: string | null
136
+ next: string | null
137
+ meta: {
138
+ userId: string | null
139
+ requestId: string
140
+ timestampMs: number
141
+ elapsedMs: number
142
+ }
143
+ }
144
+ ```
145
+
146
+ Ce qui compte, c'est le contenu de `response.body.error` ou `response.body.payload`, et éventuellement les détails de pagination pour les réponses `list`: `page`, `total`, `prev` (`null` si première), `next` (`null` si dernière).
147
+
148
+ ### Routes, Méthodes, Requêtes et Réponses
149
+
150
+ Toute la partie API du serveur est définie à partir d'ici :
151
+ https://github.com/lm-design-edito/lm-publisher/tree/master/src/api
152
+
153
+ Dans [/api/index.ts](https://github.com/lm-design-edito/lm-publisher/blob/master/src/api/index.ts), il y a :
154
+
155
+ ```ts
156
+ enum ENDPOINT = {
157
+ // Liste des endpoints sous la forme METHOD:PATH, ex: GET:/system/status-check, POST:/auth/login, etc...
158
+ }
159
+
160
+ const ROUTES = {
161
+ [ENDPOINT]: handlerFunction
162
+ }
163
+ ```
164
+
165
+ Ensuite, chaque handler est défini dans son sous-dossier dans [/api](https://github.com/lm-design-edito/lm-publisher/blob/master/src/api), exemple : [/api/auth/login](https://github.com/lm-design-edito/lm-publisher/blob/master/src/api/auth/login)
166
+
167
+ Pour chaque sous-dossier de handler, il y a :
168
+ - `index.ts` — Rien d'intéressant ici
169
+ - `operation.ts` — La fonction qui est éxécutée pour effectuer l'opération. On y trouve normalement toujours :
170
+ - `const silentOperation`, le cœur de la fonction éxécutée
171
+ - `const operation`, la fonction embarquant les logs qu'elle implique
172
+ - `type OperationErrorCodes`, la liste des codes d'erreurs que peut renvoyer l'opération
173
+ - `type DTO`, le "[data transfer object](https://en.wikipedia.org/wiki/Data_transfer_object)", c'est à dire la forme de l'objet qu'on va trouver dans `response.body.payload`, si la réponse est de type `json` ou `list`
174
+ - `validation.ts` — Optionnel, la fonction qui s'occupe de valider l'input utilisateur avant de la passer à `operation.ts`. On y trouve :
175
+ - `type ExpectedBody`, qui décrit la forme de l'objet à envoyer via `request.body` pour passer la validation
176
+ - Plus tard, potentiellement `type ExpectedQuery`, je ne vois pas trop de cas d'usage à transmettre de la donnée via les query params de l'URL, à part pour une route qui répondrait une liste, `?page=2` pourrait avoir du sens.
177
+ - `authentication.ts` — Optionnel, la fonction qui s'occupe de savoir si l'utilisateur qui fait la reqûete a les droits suffisants. Normalement assez straigntforward, ça ressemblera très souvent à ça : [/api/storage/credentials/create/authentication.ts](https://github.com/lm-design-edito/lm-publisher/blob/master/src/api/storage/credentials/create/authentication.ts)
178
+
179
+ ### Les codes d'erreur
180
+
181
+ La liste des codes d'erreurs que peut renvoyer le serveur en cas de réponse de type `error`, ainsi que le message et l'objet de détails qui y sont associés (`ErrorData<Code>`) est trouvable sur : [/errors/index.ts](https://github.com/lm-design-edito/lm-publisher/blob/master/src/errors/index.ts)
182
+
183
+ ## Authentification
184
+
185
+ L'authentification d'un client lorsqu'il envoie une requête au serveur repose sur deux concepts :
186
+ - Des [JSON Web Tokens](https://fr.wikipedia.org/wiki/JSON_Web_Token), avec une logique de access & refresh tokens
187
+ - Une protection contre les [attaques CSRF](https://fr.wikipedia.org/wiki/Cross-site_request_forgery), via le package NPM [csurf](https://www.npmjs.com/package/csurf), qui... a été archivé la veille de la rédaction de ce README 😭😭😭😭. Il n'y a pas trop de raison de bouger pour l'instant et surtout pas d'autre solution viable donc on va continuer et régler ça plus tard.
188
+
189
+ ### CSRF
190
+
191
+ Que l'on fasse une requête sur un endpoint qui demande l'authentification ou non, si la requête n'est pas en `GET`, la protection CSRF va s'interposer en premier, et par défaut, elle va répondre une réponse de type `error`, avec le code `invalid-csrf-token`.
192
+
193
+ Avant toute requête hors `GET`, il faut donc aller récupérer le token généré par le serveur, sur `/csrf/get-token`. Dans l'idée :
194
+
195
+ ```ts
196
+ const res = await fetch('https://<hostname>/csrf/get-token', { credentials: 'include' })
197
+ const data = await res.json()
198
+ const csrfToken = data.payload.token
199
+ ```
200
+
201
+ Ensuite, le token vit uniquement dans la mémoire, dans une variable, et disparait donc quand l'onglet ou le navigateur est fermé. Une fois le token récupéré, les requêtes doivent embarquer le header `'X-CSRF-Token': csrfToken`, et spécifier `credentials: 'include'`. Ex:
202
+
203
+ ```ts
204
+ await fetch('https://<hostname>/some/path', {
205
+ method: 'POST',
206
+ headers: {
207
+ 'Content-Type': 'application/json',
208
+ 'X-CSRF-Token': csrfToken
209
+ },
210
+ credentials: 'include',
211
+ body: JSON.stringify({ ... })
212
+ })
213
+ ```
214
+
215
+ Le token a une durée de vie de 2h, il faut aller le générer à nouveau sur `/csrf/get-token` à chaque fois que le serveur répond `invalid-csrf-token` (en limitant les retries successifs, si la procédure ne fonctionne pas du premier coup, c'est probablement qu'il y a un problême côté serveur).
216
+
217
+ ### JWT
218
+
219
+ Dans l'idée, lorsqu'un client va s'authentifier sur `/auth/login`, si l'authentification réussit, le serveur va répondre avec :
220
+ - Un access token via le Header `X-Access-Token` :
221
+ ```ts
222
+ res.setHeader('X-Access-Token', `Bearer ${token}`)
223
+ ```
224
+ - Un refresh token via le cookie `refreshToken` :
225
+ ```ts
226
+ res.cookie('refreshToken', token, {
227
+ httpOnly: true,
228
+ secure: MODE === 'production',
229
+ sameSite: 'none',
230
+ expires: new Date(Date.now() + REFRESH_TOKEN_EXPIRATION_SECONDS * 1000)
231
+ })
232
+ ```
233
+
234
+ Une fois ces tokens récupérés, le client doit idéalement persister la valeur de l'access token dans `localStorage` ou `sessionStorage`. Il peut le garder uniquement en mémoire dans une variable comme le token CSRF, mais en revanche il ne doit **SURTOUT PAS** être persisté via un cookie. La valeur de cet `accessToken` est ensuite utilisée pour chaque requête (du moins celles qui nécessitent authentification, mais c'est bien de le mettre tout le temps) :
235
+
236
+ ```ts
237
+ await fetch('https://<hostname>/some/path', {
238
+ method: 'POST',
239
+ headers: {
240
+ 'Content-Type': 'application/json',
241
+ 'Authorization': `Bearer ${accessToken}`,
242
+ 'X-CSRF-Token': csrfToken
243
+ },
244
+ body: JSON.stringify({ ... }),
245
+ credentials: 'include'
246
+ })
247
+ ```
248
+
249
+ Si l'`accessToken` a expiré, le serveur ne va pas forcément répondre par une erreur explicite. Le middleware chargé de récupérer le contenu du token dans le header va juste considérer que la requête vient d'un utilisateur qui n'est pas authentifié, laisse passer la requête qui continue sa route vers le handler correspondant à l'URL demandée, qui lui se charge de répondre que l'utilisateur n'est pas authentifié si la route est protégée, via le code d'erreur `user-not-authenticated`.
250
+
251
+ Il faut donc à ce moment demander un renouvellement de l'`accessToken`, sur `/auth/refresh-token`. Pour ce faire, pas besoin de se préoccuper de la valeur du `refreshToken` : le serveur nous l'a fourni initialement via un cookie dont le contenu n'est pas accessible à JavaScript, et sera automatiquement inclus dans la requête :
252
+
253
+ ```ts
254
+ await fetch('https://<hostname>/auth/refresh-token', {
255
+ method: 'POST',
256
+ headers: {
257
+ 'Content-Type': 'application/json',
258
+ 'X-CSRF-Token': csrfToken
259
+ },
260
+ credentials: 'include'
261
+ })
262
+ ```
263
+
264
+ [WIP] à continuer, détails sur les erreurs retournées par `/auth/refresh-token`, etc...
265
+
@@ -0,0 +1 @@
1
+ export declare function start(): Promise<void>;