@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.
- package/LICENSE +544 -0
- package/README.md +120 -0
- package/dist/index.js +16 -0
- package/dist/nodefony/contracts/IOAuthUserProvisioner.js +1 -0
- package/dist/nodefony/contracts/IPasswordBlocklist.js +1 -0
- package/dist/nodefony/contracts/IPasswordEncoder.js +1 -0
- package/dist/nodefony/contracts/IPasswordVerifier.js +1 -0
- package/dist/nodefony/contracts/IUser.js +1 -0
- package/dist/nodefony/contracts/IUserProfile.js +1 -0
- package/dist/nodefony/contracts/IUserProvider.js +1 -0
- package/dist/nodefony/contracts/IUserRepository.js +1 -0
- package/dist/nodefony/contracts/index.js +1 -0
- package/dist/nodefony/errors/UserNotFoundError.js +19 -0
- package/dist/nodefony/errors/WeakPasswordError.js +16 -0
- package/dist/nodefony/service/UserService.js +280 -0
- package/dist/nodefony/src/AnonymousUser.js +37 -0
- package/dist/nodefony/src/BaseUser.js +121 -0
- package/dist/nodefony/src/InMemoryUserRepository.js +230 -0
- package/dist/nodefony/src/admin/UserAdminApi.js +588 -0
- package/dist/nodefony/src/encoders/Argon2idEncoder.js +107 -0
- package/dist/nodefony/src/encoders/BcryptEncoder.js +75 -0
- package/dist/nodefony/src/encoders/MigratingEncoder.js +88 -0
- package/dist/nodefony/src/encoders/encoderFromConfig.js +37 -0
- package/dist/nodefony/src/userContract.js +247 -0
- package/dist/nodefony/src/userFilters.js +66 -0
- package/dist/nodefony/src/userProfile.js +195 -0
- package/dist/nodefony/src/userSort.js +53 -0
- package/dist/nodefony/src/userStoreRegistry.js +41 -0
- package/dist/types/index.d.ts +40 -0
- package/dist/types/nodefony/contracts/IOAuthUserProvisioner.d.ts +69 -0
- package/dist/types/nodefony/contracts/IPasswordBlocklist.d.ts +20 -0
- package/dist/types/nodefony/contracts/IPasswordEncoder.d.ts +49 -0
- package/dist/types/nodefony/contracts/IPasswordVerifier.d.ts +26 -0
- package/dist/types/nodefony/contracts/IUser.d.ts +68 -0
- package/dist/types/nodefony/contracts/IUserProfile.d.ts +29 -0
- package/dist/types/nodefony/contracts/IUserProvider.d.ts +44 -0
- package/dist/types/nodefony/contracts/IUserRepository.d.ts +129 -0
- package/dist/types/nodefony/contracts/index.d.ts +7 -0
- package/dist/types/nodefony/errors/UserNotFoundError.d.ts +15 -0
- package/dist/types/nodefony/errors/WeakPasswordError.d.ts +12 -0
- package/dist/types/nodefony/service/UserService.d.ts +179 -0
- package/dist/types/nodefony/src/AnonymousUser.d.ts +27 -0
- package/dist/types/nodefony/src/BaseUser.d.ts +98 -0
- package/dist/types/nodefony/src/InMemoryUserRepository.d.ts +73 -0
- package/dist/types/nodefony/src/admin/UserAdminApi.d.ts +119 -0
- package/dist/types/nodefony/src/encoders/Argon2idEncoder.d.ts +83 -0
- package/dist/types/nodefony/src/encoders/BcryptEncoder.d.ts +55 -0
- package/dist/types/nodefony/src/encoders/MigratingEncoder.d.ts +68 -0
- package/dist/types/nodefony/src/encoders/encoderFromConfig.d.ts +36 -0
- package/dist/types/nodefony/src/userContract.d.ts +198 -0
- package/dist/types/nodefony/src/userFilters.d.ts +80 -0
- package/dist/types/nodefony/src/userProfile.d.ts +56 -0
- package/dist/types/nodefony/src/userSort.d.ts +41 -0
- package/dist/types/nodefony/src/userStoreRegistry.d.ts +15 -0
- package/docs/ajouter-des-champs.md +189 -0
- package/docs/index.md +1113 -0
- 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.
|