@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.
- package/README.md +265 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5105 -0
- package/dist/index.js.map +7 -0
- package/dist/public/empty.txt +0 -0
- package/dist/types/api/admin/temp/flush/index.d.ts +6 -0
- package/dist/types/api/admin/users/create/index.d.ts +23 -0
- package/dist/types/api/admin/users/delete/index.d.ts +12 -0
- package/dist/types/api/admin/users/get/index.d.ts +18 -0
- package/dist/types/api/admin/users/get-upload-quota/index.d.ts +12 -0
- package/dist/types/api/admin/users/list/index.d.ts +24 -0
- package/dist/types/api/admin/users/reset-upload-quota/index.d.ts +15 -0
- package/dist/types/api/admin/users/revoke-auth-tokens/index.d.ts +10 -0
- package/dist/types/api/admin/users/revoke-email-validation-tokens/index.d.ts +10 -0
- package/dist/types/api/admin/users/revoke-pasword-renewal-tokens/index.d.ts +10 -0
- package/dist/types/api/admin/users/update/index.d.ts +23 -0
- package/dist/types/api/auth/login/index.d.ts +15 -0
- package/dist/types/api/auth/logout/index.d.ts +5 -0
- package/dist/types/api/auth/logout-everywhere/index.d.ts +5 -0
- package/dist/types/api/auth/refresh-token/index.d.ts +5 -0
- package/dist/types/api/auth/request-email-verification-token/index.d.ts +9 -0
- package/dist/types/api/auth/request-new-password/index.d.ts +9 -0
- package/dist/types/api/auth/signup/index.d.ts +13 -0
- package/dist/types/api/auth/submit-new-password/index.d.ts +11 -0
- package/dist/types/api/auth/verify-email/index.d.ts +12 -0
- package/dist/types/api/auth/whoami/index.d.ts +8 -0
- package/dist/types/api/csrf/get-token/index.d.ts +8 -0
- package/dist/types/api/image/format/index.d.ts +28 -0
- package/dist/types/api/image/transform/index.d.ts +11 -0
- package/dist/types/api/index.d.ts +102 -0
- package/dist/types/api/internals.d.ts +270 -0
- package/dist/types/api/system/kill/index.d.ts +3 -0
- package/dist/types/api/system/ping/index.d.ts +6 -0
- package/dist/types/api/system/send-mail/index.d.ts +11 -0
- package/dist/types/api/system/status-check/index.d.ts +22 -0
- package/dist/types/auth/index.d.ts +11 -0
- package/dist/types/csrf/index.d.ts +5 -0
- package/dist/types/database/index.d.ts +69 -0
- package/dist/types/dto/index.d.ts +25 -0
- package/dist/types/email/index.d.ts +6 -0
- package/dist/types/env/index.d.ts +83 -0
- package/dist/types/errors/index.d.ts +295 -0
- package/dist/types/fs/index.d.ts +110 -0
- package/dist/types/index.d.ts +21 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +7 -0
- package/dist/types/init/index.d.ts +1 -0
- package/dist/types/jwt/index.d.ts +7 -0
- package/dist/types/logs/index.d.ts +46 -0
- package/dist/types/plugins/index.d.ts +3 -0
- package/dist/types/plugins/internals.d.ts +177 -0
- package/dist/types/schema/index.d.ts +156 -0
- package/dist/types/temp/index.d.ts +7 -0
- package/dist/types/transformers/index.d.ts +3 -0
- package/dist/types/transformers/user/index.d.ts +5 -0
- package/dist/types/transformers/user-upload-quota/index.d.ts +3 -0
- package/dist/types/uploads/index.d.ts +73 -0
- package/dist/types/validation/index.d.ts +4 -0
- 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
|
+
|
package/dist/index.d.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export declare function start(): Promise<void>;
|