@nodefony/user 10.0.0-alpha.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 (57) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +120 -0
  3. package/dist/index.js +16 -0
  4. package/dist/nodefony/contracts/IOAuthUserProvisioner.js +1 -0
  5. package/dist/nodefony/contracts/IPasswordBlocklist.js +1 -0
  6. package/dist/nodefony/contracts/IPasswordEncoder.js +1 -0
  7. package/dist/nodefony/contracts/IPasswordVerifier.js +1 -0
  8. package/dist/nodefony/contracts/IUser.js +1 -0
  9. package/dist/nodefony/contracts/IUserProfile.js +1 -0
  10. package/dist/nodefony/contracts/IUserProvider.js +1 -0
  11. package/dist/nodefony/contracts/IUserRepository.js +1 -0
  12. package/dist/nodefony/contracts/index.js +1 -0
  13. package/dist/nodefony/errors/UserNotFoundError.js +19 -0
  14. package/dist/nodefony/errors/WeakPasswordError.js +16 -0
  15. package/dist/nodefony/service/UserService.js +280 -0
  16. package/dist/nodefony/src/AnonymousUser.js +37 -0
  17. package/dist/nodefony/src/BaseUser.js +121 -0
  18. package/dist/nodefony/src/InMemoryUserRepository.js +230 -0
  19. package/dist/nodefony/src/admin/UserAdminApi.js +588 -0
  20. package/dist/nodefony/src/encoders/Argon2idEncoder.js +107 -0
  21. package/dist/nodefony/src/encoders/BcryptEncoder.js +75 -0
  22. package/dist/nodefony/src/encoders/MigratingEncoder.js +88 -0
  23. package/dist/nodefony/src/encoders/encoderFromConfig.js +37 -0
  24. package/dist/nodefony/src/userContract.js +247 -0
  25. package/dist/nodefony/src/userFilters.js +66 -0
  26. package/dist/nodefony/src/userProfile.js +195 -0
  27. package/dist/nodefony/src/userSort.js +53 -0
  28. package/dist/nodefony/src/userStoreRegistry.js +41 -0
  29. package/dist/types/index.d.ts +40 -0
  30. package/dist/types/nodefony/contracts/IOAuthUserProvisioner.d.ts +69 -0
  31. package/dist/types/nodefony/contracts/IPasswordBlocklist.d.ts +20 -0
  32. package/dist/types/nodefony/contracts/IPasswordEncoder.d.ts +49 -0
  33. package/dist/types/nodefony/contracts/IPasswordVerifier.d.ts +26 -0
  34. package/dist/types/nodefony/contracts/IUser.d.ts +68 -0
  35. package/dist/types/nodefony/contracts/IUserProfile.d.ts +29 -0
  36. package/dist/types/nodefony/contracts/IUserProvider.d.ts +44 -0
  37. package/dist/types/nodefony/contracts/IUserRepository.d.ts +129 -0
  38. package/dist/types/nodefony/contracts/index.d.ts +7 -0
  39. package/dist/types/nodefony/errors/UserNotFoundError.d.ts +15 -0
  40. package/dist/types/nodefony/errors/WeakPasswordError.d.ts +12 -0
  41. package/dist/types/nodefony/service/UserService.d.ts +179 -0
  42. package/dist/types/nodefony/src/AnonymousUser.d.ts +27 -0
  43. package/dist/types/nodefony/src/BaseUser.d.ts +98 -0
  44. package/dist/types/nodefony/src/InMemoryUserRepository.d.ts +73 -0
  45. package/dist/types/nodefony/src/admin/UserAdminApi.d.ts +119 -0
  46. package/dist/types/nodefony/src/encoders/Argon2idEncoder.d.ts +83 -0
  47. package/dist/types/nodefony/src/encoders/BcryptEncoder.d.ts +55 -0
  48. package/dist/types/nodefony/src/encoders/MigratingEncoder.d.ts +68 -0
  49. package/dist/types/nodefony/src/encoders/encoderFromConfig.d.ts +36 -0
  50. package/dist/types/nodefony/src/userContract.d.ts +198 -0
  51. package/dist/types/nodefony/src/userFilters.d.ts +80 -0
  52. package/dist/types/nodefony/src/userProfile.d.ts +56 -0
  53. package/dist/types/nodefony/src/userSort.d.ts +41 -0
  54. package/dist/types/nodefony/src/userStoreRegistry.d.ts +15 -0
  55. package/docs/ajouter-des-champs.md +189 -0
  56. package/docs/index.md +1113 -0
  57. package/package.json +90 -0
@@ -0,0 +1,189 @@
1
+ ---
2
+ title: "Ajouter tes champs à l'utilisateur"
3
+ navTitle: "Ajouter des champs"
4
+ lang: fr
5
+ module: "@nodefony/user"
6
+ topic: user-champs-personnalises
7
+ section: "Identité"
8
+ audience: [developer]
9
+ tags: [user, entity, migration, schema]
10
+ version: "doc"
11
+ status: stable
12
+ updated: 2026-09-01
13
+ source: "src/packages/@nodefony/user/docs/ajouter-des-champs.md"
14
+ ---
15
+
16
+ 📍 [Documentation](../../../../../docs/README.md) › [@nodefony/user](index.md) › **Ajouter des champs**
17
+
18
+ # Ajouter tes champs à l'utilisateur
19
+
20
+ > La table des utilisateurs t'appartient : tu peux y ajouter ce que tu veux. Cette page dit **où**
21
+ > mettre chaque champ — sur la table, dans `metadata`, ou dans une entité liée — et pourquoi ce
22
+ > choix n'est pas une affaire de goût.
23
+
24
+ ## Schéma général
25
+
26
+ ```mermaid
27
+ flowchart TD
28
+ Q["Un champ à ajouter<br/>à l'utilisateur"] --> S{"Sensible ?<br/>(secret, donnée<br/>réglementée)"}
29
+ S -- oui --> E["Entité LIÉE, chiffrée<br/>lue seulement quand on en a besoin"]
30
+ S -- non --> V{"Lu à presque<br/>chaque requête ?"}
31
+ V -- oui --> C["COLONNE sur User<br/>filtrable, triable, indexable"]
32
+ V -- non --> G{"Faut-il filtrer<br/>ou trier dessus<br/>en SQL ?"}
33
+ G -- oui --> C
34
+ G -- non --> M["metadata (JSON)<br/>déjà là, aucune migration"]
35
+ ```
36
+
37
+ ## Lexique
38
+
39
+ | Terme | Ce que c'est |
40
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | **Contrat de colonnes** | La liste des colonnes que le framework LIT sur un utilisateur (`USER_COLUMNS`). Ton entité doit les porter ; tu ajoutes les tiennes par-dessus. |
42
+ | **Entité** | La description, en TypeScript, d'une table. Celle de l'utilisateur vit dans TON application, sous `nodefony/entity/User.ts`. |
43
+ | **Migration** | Un fichier SQL versionné qui fait passer la base d'un état au suivant. `orm:generate` l'écrit, `orm:migrate` l'applique. |
44
+ | **Dépôt** (repository) | L'objet par lequel on lit et écrit des lignes. Il en existe deux ici : celui de l'identité (typé) et le générique (libre). |
45
+
46
+ ## Qu'est-ce que c'est ?
47
+
48
+ Toute application finit par vouloir en dire plus sur ses utilisateurs que « qui es-tu et qu'as-tu le
49
+ droit de faire » : un service de rattachement, une langue préférée, un numéro de client, un
50
+ consentement daté. La question n'est jamais _est-ce possible_ — ça l'est toujours — mais **où** la
51
+ donnée doit vivre.
52
+
53
+ Le réflexe est d'ajouter une colonne à la table des utilisateurs. C'est souvent le bon geste, et
54
+ parfois le pire : cette table n'est pas une table comme les autres.
55
+
56
+ ## La vision Nodefony
57
+
58
+ **La table des utilisateurs est relue à chaque requête portant une session authentifiée.** Ce n'est
59
+ pas un détail d'implémentation, c'est le cœur du modèle : une session ne transporte qu'un
60
+ identifiant, jamais l'utilisateur lui-même. À chaque requête, `SessionAuthenticator.authenticate()`
61
+ (`src/packages/@nodefony/security/nodefony/src/authenticator/SessionAuthenticator.ts:63`) redemande
62
+ l'identité vivante — `resolveSessionIdentity`
63
+ (`src/packages/@nodefony/security/nodefony/src/authenticator/SessionAuthenticator.ts:70`) — pour que
64
+ la désactivation d'un compte, un changement de rôle ou un verrouillage prennent effet
65
+ **immédiatement**, sans attendre l'expiration d'un jeton.
66
+
67
+ La conséquence est directe, et c'est elle qui décide de tout le reste : **toute colonne posée sur la
68
+ table des utilisateurs est ramenée en mémoire à chaque requête authentifiée.** Une colonne courte ne
69
+ se remarque pas. Un champ de texte libre, une pièce jointe encodée, un historique — si.
70
+
71
+ Et la même mécanique fait de cette table le pire endroit pour une donnée sensible : un numéro de
72
+ sécurité sociale posé là traverse le processus des milliers de fois par jour, apparaît dans les
73
+ vidages mémoire et les traces de débogage, sans qu'aucun de ces passages ait la moindre utilité.
74
+
75
+ ## Les trois voies, et le critère qui les départage
76
+
77
+ | Voie | Quand la choisir | Ce que ça coûte |
78
+ | -------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
79
+ | **Une colonne sur `User`** | Le champ est court, non sensible, et tu veux **filtrer, trier ou indexer** dessus en SQL | Relu à chaque requête authentifiée · une migration |
80
+ | **`metadata` (JSON)** | Le champ est occasionnel, sans filtre ni tri SQL — une préférence d'affichage, un drapeau | Relu à chaque requête aussi · **aucune migration**, la colonne existe |
81
+ | **Une entité liée** | La donnée est **sensible**, volumineuse, historisée, ou lue seulement dans un écran dédié | Une table et une migration de plus · une jointure quand on en a besoin |
82
+
83
+ Le test qui tranche en une question : **cette donnée doit-elle être en mémoire à chaque requête ?**
84
+
85
+ - Un service de rattachement qui apparaît dans l'en-tête de toutes les pages : oui — **colonne**.
86
+ - Une préférence de thème sombre : elle voyage de toute façon, et rien ne la filtre — **`metadata`**.
87
+ - Un numéro de sécurité sociale, un IBAN, un document d'identité : **non**, et jamais — **entité
88
+ liée**, et **chiffrée**.
89
+
90
+ ### La donnée sensible ne se pose pas en clair
91
+
92
+ Quand une donnée réglementée doit vivre en base, le framework a son précédent : le secret d'un
93
+ second facteur est stocké **chiffré**, en blob opaque, jamais en clair — `totpSecretEntity`
94
+ (`src/packages/@nodefony/drizzle/nodefony/entity/totpSecretEntity.ts:41`). Reprends ce patron :
95
+ une entité dédiée, un blob chiffré, et une lecture qui n'a lieu qu'au moment où la donnée sert.
96
+
97
+ ## Démarrage rapide (exemple minimal qui compile)
98
+
99
+ **Ne modifie pas `nodefony/entity/User.ts` à la main** : relance la commande avec tes champs, elle
100
+ réécrit l'entité avec les colonnes du contrat en clair, plus les tiennes.
101
+
102
+ ```bash
103
+ npx nodefony create entity User department:string(100)? locale:string=fr
104
+ npx nodefony orm:generate --name champs_utilisateur
105
+ npx nodefony orm:migrate
106
+ ```
107
+
108
+ Un champ **obligatoire** doit avoir une valeur par défaut (`locale:string=fr`) ou être **facultatif**
109
+ (`department:string(100)?`). Le framework crée des utilisateurs sans rien savoir de tes champs — au
110
+ semis d'un administrateur, à la première connexion par un fournisseur externe — et ces créations
111
+ échoueraient sur un champ obligatoire sans défaut.
112
+
113
+ Ensuite, dans un controller :
114
+
115
+ ```typescript
116
+ import { Controller, controller, Get } from "@nodefony/framework";
117
+ import type { Context } from "@nodefony/http";
118
+ import type { IOrm, IRepository } from "@nodefony/orm-core";
119
+
120
+ interface IUserRow {
121
+ id: string;
122
+ identifier: string;
123
+ department?: string | null;
124
+ locale?: string;
125
+ }
126
+
127
+ @controller("/profil")
128
+ export default class ProfilController extends Controller {
129
+ @Get("/")
130
+ async profil(): Promise<Record<string, unknown>> {
131
+ // `get` rend `null` quand le service n'est pas là : le dire, plutôt que
132
+ // laisser une erreur de propriété sur `null` remonter à l'utilisateur.
133
+ const orm = this.get<IOrm>("orm");
134
+ if (!orm) {
135
+ throw new Error("service « orm » absent du conteneur");
136
+ }
137
+ const users = orm.getRepository<IUserRow>("User");
138
+ const moi = await users.findOne({
139
+ identifier: String(this.context?.user ?? ""),
140
+ });
141
+ return { department: moi?.department ?? null, locale: moi?.locale ?? "fr" };
142
+ }
143
+ }
144
+ ```
145
+
146
+ ## Lire et écrire tes champs — la porte
147
+
148
+ Il y a **deux** dépôts, et c'est volontaire.
149
+
150
+ - **En LECTURE, il n'y a rien à faire.** Le dépôt d'identité reporte sur l'utilisateur toute colonne
151
+ qu'il ne connaît pas — `attachExtraColumns`
152
+ (`src/packages/@nodefony/user/nodefony/src/userContract.ts:288`). Un champ ajouté à la table arrive
153
+ donc sur l'objet utilisateur sans une ligne de code.
154
+ - **En ÉCRITURE, `IUserRepository` les refuse**, et c'est une garde, pas un manque : il est typé sur
155
+ le contrat d'identité, et laisser écrire n'importe quoi par cette porte reviendrait à laisser un
156
+ code d'authentification modifier des données métier. La porte des champs métier est le **dépôt
157
+ générique** : `orm.getRepository("User")`.
158
+
159
+ ## Pièges
160
+
161
+ | Symptôme | Cause | Correction |
162
+ | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
163
+ | Le démarrage refuse : « ne porte pas une colonne que le framework LIT » | Une colonne du contrat a disparu de ton entité — souvent en la réécrivant à la main | Relancer `nodefony create entity User …` avec tes champs : elle réécrit le contrat en entier |
164
+ | `npx nodefony orm:migrate` échoue sur « contains null values » | Un champ obligatoire sans défaut, sur une table qui porte déjà des comptes | Lui donner un défaut (`role:string=membre`) ou le déclarer facultatif (`role:string?`) |
165
+ | Sur MySQL, le même champ passe… et ne contient que du vide | MySQL/MariaDB accepte `NOT NULL` sans défaut et remplit les lignes existantes de `''`, mode strict compris | Même correction : un défaut, ou facultatif. Ne jamais se fier au fait que « ça passe » sur un moteur |
166
+ | Ton champ est bien en base mais `IUserRepository` refuse de l'écrire | C'est la garde de typage, pas un défaut | Passer par `orm.getRepository("User")` — cf « la porte » ci-dessus |
167
+ | La latence monte après l'ajout d'un champ | La colonne est ramenée en mémoire à chaque requête authentifiée | La déplacer dans une entité liée, lue seulement quand elle sert |
168
+
169
+ ## Tests
170
+
171
+ Ce que le dépôt éprouve autour de ces gestes :
172
+
173
+ - **Contrat de colonnes** — `src/packages/@nodefony/drizzle/tests/unit/userContractParity.test.ts` et
174
+ `src/packages/@nodefony/mongoose/tests/unit/userContractParity.test.ts` : la table produite rend le
175
+ contrat en entier, sur les trois dialectes SQL et en document.
176
+ - **Refus au démarrage** — `src/packages/@nodefony/drizzle/tests/integration/user-contrat-colonnes.test.ts` :
177
+ une entité d'application à qui manque une colonne fait échouer le démarrage, en nommant la colonne
178
+ et son lecteur.
179
+ - **Évolutions sur base peuplée** — `src/packages/@nodefony/drizzle/tests/integration/user-migrations.e2e.test.ts` :
180
+ ajout facultatif, ajout à valeur par défaut, ajout obligatoire sans défaut et retrait d'un champ,
181
+ sur sqlite, PostgreSQL et MySQL, avec des comptes réels en base. Derrière `NF_RUN_CLI_BOOT=1`.
182
+ - **Champs métier de bout en bout** — `src/packages/@nodefony/mongoose/tests/integration/user-champs-metier.test.ts`.
183
+
184
+ ## Pour aller plus loin
185
+
186
+ - ⬆️ **Retour au hub** : [@nodefony/user](index.md)
187
+ - [Le module `@nodefony/user`](index.md) — l'identité, ses contrats et son cycle de vie.
188
+ - [Les migrations de schéma](../../drizzle/docs/migrations.md) — écrire, éprouver et appliquer une
189
+ migration.