Checkpoint: Ajout de l'identifiant de connexion et modification du mot de passe dans la gestion des utilisateurs

Modifications apportées :
- Ajout du champ username (identifiant de connexion) dans le schéma de la base de données avec contrainte UNIQUE
- Mise à jour des procédures tRPC create et update pour gérer username et password
- Implémentation du hashage du mot de passe avec bcryptjs dans createUser et updateUser
- Ajout de la colonne "Identifiant" dans le tableau de la page de gestion des utilisateurs
- Ajout des champs "Identifiant de connexion" et "Mot de passe" dans le formulaire de création
- Ajout des champs "Identifiant de connexion" et "Nouveau mot de passe" dans le formulaire d'édition avec notes explicatives
- Les mots de passe sont hashés avant d'être stockés en base de données pour la sécurité
- Dans le formulaire d'édition, les champs username et password sont optionnels (laissez vide pour ne pas modifier)
This commit is contained in:
Manus Sandbox
2025-11-21 10:08:56 -05:00
parent fad88c5257
commit 8a61f9b823
13 changed files with 1011 additions and 3 deletions

View File

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE users ADD COLUMN username VARCHAR(100) AFTER email",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE users ADD COLUMN username VARCHAR(100) AFTER email",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 507
}

View File

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE users ADD UNIQUE KEY unique_username (username)",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE users ADD UNIQUE KEY unique_username (username)",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 575
}

View File

@@ -0,0 +1,8 @@
{
"query": "ALTER TABLE users ADD COLUMN username VARCHAR(100) UNIQUE AFTER email",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE users ADD COLUMN username VARCHAR(100) UNIQUE AFTER email",
"returncode": 1,
"logs": [
"ERROR 8200 (HY000) at line 1: unsupported add column 'username' constraint UNIQUE KEY when altering '7PAT67UmWoxv8vwp8Bbcv6.users'"
]
}

View File

@@ -44,11 +44,15 @@ export default function AdminUsers() {
openId: "",
name: "",
email: "",
username: "",
password: "",
role: "user" as "user" | "admin",
});
const [editForm, setEditForm] = useState({
name: "",
email: "",
username: "",
password: "",
role: "user" as "user" | "admin",
});
@@ -63,6 +67,8 @@ export default function AdminUsers() {
openId: "",
name: "",
email: "",
username: "",
password: "",
role: "user",
});
},
@@ -127,6 +133,8 @@ export default function AdminUsers() {
openId: createForm.openId,
name: createForm.name || undefined,
email: createForm.email || undefined,
username: createForm.username || undefined,
password: createForm.password || undefined,
role: createForm.role,
isActive: true,
});
@@ -137,6 +145,8 @@ export default function AdminUsers() {
setEditForm({
name: user.name || "",
email: user.email || "",
username: user.username || "",
password: "", // Toujours vide pour la sécurité
role: user.role,
});
setEditDialogOpen(true);
@@ -145,12 +155,23 @@ export default function AdminUsers() {
const handleSaveEdit = () => {
if (!selectedUser) return;
updateMutation.mutate({
// Ne pas envoyer username et password s'ils sont vides
const updateData: any = {
id: selectedUser.id,
name: editForm.name || undefined,
email: editForm.email || undefined,
role: editForm.role,
});
};
if (editForm.username) {
updateData.username = editForm.username;
}
if (editForm.password) {
updateData.password = editForm.password;
}
updateMutation.mutate(updateData);
};
const handleToggleStatus = (user: any) => {
@@ -281,6 +302,7 @@ export default function AdminUsers() {
<TableHead>ID</TableHead>
<TableHead>Nom</TableHead>
<TableHead>Email</TableHead>
<TableHead>Identifiant</TableHead>
<TableHead>Rôle</TableHead>
<TableHead>Statut</TableHead>
<TableHead>Dernière connexion</TableHead>
@@ -291,7 +313,7 @@ export default function AdminUsers() {
<TableBody>
{filteredUsers.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
<TableCell colSpan={9} className="text-center py-8 text-muted-foreground">
Aucun utilisateur trouvé
</TableCell>
</TableRow>
@@ -301,6 +323,7 @@ export default function AdminUsers() {
<TableCell className="font-medium">{user.id}</TableCell>
<TableCell>{user.name || "N/A"}</TableCell>
<TableCell>{user.email || "N/A"}</TableCell>
<TableCell>{user.username || "N/A"}</TableCell>
<TableCell>
<Badge variant={user.role === "admin" ? "default" : "secondary"}>
{user.role === "admin" ? "Administrateur" : "Utilisateur"}
@@ -403,6 +426,27 @@ export default function AdminUsers() {
/>
</div>
<div>
<Label htmlFor="createUsername">Identifiant de connexion</Label>
<Input
id="createUsername"
value={createForm.username}
onChange={(e) => setCreateForm({ ...createForm, username: e.target.value })}
placeholder="Identifiant unique pour la connexion"
/>
</div>
<div>
<Label htmlFor="createPassword">Mot de passe</Label>
<Input
id="createPassword"
type="password"
value={createForm.password}
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
placeholder="Minimum 6 caractères"
/>
</div>
<div>
<Label htmlFor="createRole">Rôle</Label>
<Select
@@ -465,6 +509,29 @@ export default function AdminUsers() {
/>
</div>
<div>
<Label htmlFor="editUsername">Identifiant de connexion</Label>
<Input
id="editUsername"
value={editForm.username}
onChange={(e) => setEditForm({ ...editForm, username: e.target.value })}
placeholder="Identifiant unique pour la connexion"
/>
<p className="text-xs text-muted-foreground mt-1">Laissez vide pour ne pas modifier</p>
</div>
<div>
<Label htmlFor="editPassword">Nouveau mot de passe</Label>
<Input
id="editPassword"
type="password"
value={editForm.password}
onChange={(e) => setEditForm({ ...editForm, password: e.target.value })}
placeholder="Laissez vide pour ne pas modifier"
/>
<p className="text-xs text-muted-foreground mt-1">Minimum 6 caractères si modifié</p>
</div>
<div>
<Label htmlFor="editRole">Rôle</Label>
<Select

View File

@@ -0,0 +1,3 @@
ALTER TABLE `users` ADD `username` varchar(100);--> statement-breakpoint
ALTER TABLE `users` ADD `password` varchar(255);--> statement-breakpoint
ALTER TABLE `users` ADD CONSTRAINT `users_username_unique` UNIQUE(`username`);

View File

@@ -0,0 +1,854 @@
{
"version": "5",
"dialect": "mysql",
"id": "95760afb-5e4d-4e32-afc6-fbbcec28c208",
"prevId": "443cedd2-fcbf-4247-a935-e69461bb7446",
"tables": {
"apprenants": {
"name": "apprenants",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"prenom": {
"name": "prenom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"codeEtablissement": {
"name": "codeEtablissement",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fonction": {
"name": "fonction",
"type": "enum('directeur','chef_service','autre')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"apprenants_id": {
"name": "apprenants_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"apprenants_email_unique": {
"name": "apprenants_email_unique",
"columns": [
"email"
]
}
},
"checkConstraint": {}
},
"datesFormation": {
"name": "datesFormation",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"sequenceId": {
"name": "sequenceId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dateDebut": {
"name": "dateDebut",
"type": "datetime",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dateFin": {
"name": "dateFin",
"type": "datetime",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ordre": {
"name": "ordre",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"datesFormation_id": {
"name": "datesFormation_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"emailConfig": {
"name": "emailConfig",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"provider": {
"name": "provider",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'resend'"
},
"apiKey": {
"name": "apiKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fromEmail": {
"name": "fromEmail",
"type": "varchar(320)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromName": {
"name": "fromName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'Formation Manager Itinova'"
},
"mode": {
"name": "mode",
"type": "enum('simulation','production')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'simulation'"
},
"domainVerified": {
"name": "domainVerified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"emailConfig_id": {
"name": "emailConfig_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"emailTemplates": {
"name": "emailTemplates",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"type": {
"name": "type",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"logoUrl": {
"name": "logoUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"primaryColor": {
"name": "primaryColor",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#2563eb'"
},
"headerBgColor": {
"name": "headerBgColor",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#2563eb'"
},
"headerTextColor": {
"name": "headerTextColor",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#ffffff'"
},
"headerTitle": {
"name": "headerTitle",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'Formation Manager Itinova'"
},
"footerText": {
"name": "footerText",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"emailTemplates_id": {
"name": "emailTemplates_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"emailTemplates_type_unique": {
"name": "emailTemplates_type_unique",
"columns": [
"type"
]
}
},
"checkConstraint": {}
},
"formateurs": {
"name": "formateurs",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"formateurs_id": {
"name": "formateurs_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"formations": {
"name": "formations",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"lienUnique": {
"name": "lienUnique",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"actif": {
"name": "actif",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"formations_id": {
"name": "formations_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"formations_lienUnique_unique": {
"name": "formations_lienUnique_unique",
"columns": [
"lienUnique"
]
}
},
"checkConstraint": {}
},
"inscriptions": {
"name": "inscriptions",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"apprenantId": {
"name": "apprenantId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sequenceId": {
"name": "sequenceId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"statut": {
"name": "statut",
"type": "enum('confirmee','liste_attente','annulee')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dateInscription": {
"name": "dateInscription",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"inscriptions_id": {
"name": "inscriptions_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"passwordResetTokens": {
"name": "passwordResetTokens",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"userId": {
"name": "userId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"used": {
"name": "used",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"passwordResetTokens_id": {
"name": "passwordResetTokens_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"passwordResetTokens_token_unique": {
"name": "passwordResetTokens_token_unique",
"columns": [
"token"
]
}
},
"checkConstraint": {}
},
"sequences": {
"name": "sequences",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"formationId": {
"name": "formationId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lieu": {
"name": "lieu",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"publicCible": {
"name": "publicCible",
"type": "enum('directeur','chef_service','autre')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"formateurId": {
"name": "formateurId",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"capaciteMax": {
"name": "capaciteMax",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 12
},
"dateBlocage": {
"name": "dateBlocage",
"type": "datetime",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"statut": {
"name": "statut",
"type": "enum('ouverte','bloquee','terminee')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'ouverte'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"sequences_id": {
"name": "sequences_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"username": {
"name": "username",
"type": "varchar(100)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"password": {
"name": "password",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
},
"users_username_unique": {
"name": "users_username_unique",
"columns": [
"username"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@@ -85,6 +85,13 @@
"when": 1763630439521,
"tag": "0011_natural_cyclops",
"breakpoints": true
},
{
"idx": 12,
"version": "5",
"when": 1763737191050,
"tag": "0012_dapper_ronan",
"breakpoints": true
}
]
}

View File

@@ -15,6 +15,10 @@ export const users = mysqlTable("users", {
openId: varchar("openId", { length: 64 }).notNull().unique(),
name: text("name"),
email: varchar("email", { length: 320 }),
/** Identifiant de connexion unique pour l'utilisateur */
username: varchar("username", { length: 100 }).unique(),
/** Mot de passe hashé (bcrypt) */
password: varchar("password", { length: 255 }),
loginMethod: varchar("loginMethod", { length: 64 }),
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
/** Statut du compte (actif/inactif) */

View File

@@ -46,6 +46,7 @@
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
"axios": "^1.12.0",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -85,6 +86,7 @@
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.1.3",
"@types/bcryptjs": "^3.0.0",
"@types/express": "4.17.21",
"@types/google.maps": "^3.58.1",
"@types/node": "^24.7.0",

20
pnpm-lock.yaml generated
View File

@@ -115,6 +115,9 @@ importers:
axios:
specifier: ^1.12.0
version: 1.12.2
bcryptjs:
specifier: ^3.0.3
version: 3.0.3
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -227,6 +230,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.1.3
version: 4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))
'@types/bcryptjs':
specifier: ^3.0.0
version: 3.0.0
'@types/express':
specifier: 4.17.21
version: 4.17.21
@@ -2157,6 +2163,10 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/bcryptjs@3.0.0':
resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==}
deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.
'@types/body-parser@1.19.6':
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
@@ -2443,6 +2453,10 @@ packages:
resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==}
hasBin: true
bcryptjs@3.0.3:
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
hasBin: true
body-parser@1.20.3:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -6641,6 +6655,10 @@ snapshots:
dependencies:
'@babel/types': 7.28.4
'@types/bcryptjs@3.0.0':
dependencies:
bcryptjs: 3.0.3
'@types/body-parser@1.19.6':
dependencies:
'@types/connect': 3.4.38
@@ -6973,6 +6991,8 @@ snapshots:
baseline-browser-mapping@2.8.12: {}
bcryptjs@3.0.3: {}
body-parser@1.20.3:
dependencies:
bytes: 3.1.2

View File

@@ -418,6 +418,12 @@ export async function createUser(data: InsertUser) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Hasher le mot de passe si fourni
if (data.password) {
const bcrypt = await import('bcryptjs');
data.password = await bcrypt.hash(data.password, 10);
}
const result = await db.insert(users).values(data);
return result;
}
@@ -441,6 +447,12 @@ export async function updateUser(id: number, data: Partial<InsertUser>) {
const db = await getDb();
if (!db) throw new Error("Database not available");
// Hasher le mot de passe si fourni
if (data.password) {
const bcrypt = await import('bcryptjs');
data.password = await bcrypt.hash(data.password, 10);
}
await db.update(users).set(data).where(eq(users.id, id));
}

View File

@@ -618,6 +618,8 @@ export const appRouter = router({
openId: z.string().min(1),
name: z.string().optional(),
email: z.string().email().optional(),
username: z.string().min(3).optional(),
password: z.string().min(6).optional(),
role: z.enum(["user", "admin"]).default("user"),
isActive: z.boolean().default(true),
})).mutation(async ({ input }) => {
@@ -629,6 +631,8 @@ export const appRouter = router({
id: z.number(),
name: z.string().optional(),
email: z.string().email().optional(),
username: z.string().min(3).optional(),
password: z.string().min(6).optional(),
role: z.enum(["user", "admin"]).optional(),
isActive: z.boolean().optional(),
})).mutation(async ({ input }) => {

View File

@@ -383,3 +383,12 @@
- [x] Identifier la cause de la création excessive d'utilisateurs
- [x] Corriger le problème
- [x] Tester la correction
## Identifiant de connexion et modification du mot de passe
- [ ] Ajouter le champ username (identifiant de connexion) dans le schéma de la base de données
- [ ] Générer et appliquer la migration de la base de données
- [x] Mettre à jour les procédures tRPC pour gérer username et password
- [ ] Afficher l'identifiant de connexion dans la liste des utilisateurs
- [ ] Permettre la modification de l'identifiant et du mot de passe dans la fenêtre d'édition
- [x] Tester les modifications