From b3137e8f278b411bc24cc80eea9b61afc0d7eb17 Mon Sep 17 00:00:00 2001 From: Manus Date: Tue, 21 Apr 2026 05:31:44 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20du=20champ=20username=20d?= =?UTF-8?q?ans=20la=20table=20local=5Fusers,=20adaptation=20de=20l'auth=20?= =?UTF-8?q?backend=20(connexion=20par=20username=20OU=20email),=20mise=20?= =?UTF-8?q?=C3=A0=20jour=20de=20la=20page=20Gestion=20des=20utilisateurs?= =?UTF-8?q?=20avec=20le=20champ=20username=20visible=20et=20=C3=A9ditable,?= =?UTF-8?q?=20compte=20adminItinova=20migr=C3=A9=20avec=20username=20propr?= =?UTF-8?q?e.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/public/__manus__/version.json | 4 +- client/src/pages/UsersAdmin.tsx | 165 +++++- drizzle/0003_shocking_secret_warriors.sql | 4 + drizzle/meta/0003_snapshot.json | 670 ++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 3 +- server/db.ts | 1 + server/localAuth.ts | 41 +- server/routers.ts | 7 +- todo.md | 7 + 10 files changed, 866 insertions(+), 43 deletions(-) create mode 100644 drizzle/0003_shocking_secret_warriors.sql create mode 100644 drizzle/meta/0003_snapshot.json diff --git a/client/public/__manus__/version.json b/client/public/__manus__/version.json index 2d111b8..3a7d148 100644 --- a/client/public/__manus__/version.json +++ b/client/public/__manus__/version.json @@ -1,4 +1,4 @@ { - "version": "4ba97843", - "timestamp": 1776437827804 + "version": "59dcc8d3", + "timestamp": 1776763904784 } \ No newline at end of file diff --git a/client/src/pages/UsersAdmin.tsx b/client/src/pages/UsersAdmin.tsx index 9aece9f..3f94cf7 100644 --- a/client/src/pages/UsersAdmin.tsx +++ b/client/src/pages/UsersAdmin.tsx @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, @@ -26,6 +26,7 @@ import { Eye, CheckCircle, XCircle, + AtSign, } from "lucide-react"; import { toast } from "sonner"; import { format } from "date-fns"; @@ -37,7 +38,8 @@ type Role = "admin" | "user" | "readonly"; interface LocalUser { id: number; name: string; - email: string; + username: string | null; + email: string | null; role: Role; isActive: boolean; createdAt: Date; @@ -64,13 +66,21 @@ const ROLE_ICONS: Record = { interface UserFormData { name: string; + username: string; email: string; password: string; role: Role; isActive: boolean; } -const DEFAULT_FORM: UserFormData = { name: "", email: "", password: "", role: "user", isActive: true }; +const DEFAULT_FORM: UserFormData = { + name: "", + username: "", + email: "", + password: "", + role: "user", + isActive: true, +}; export default function UsersAdmin() { const [showDialog, setShowDialog] = useState(false); @@ -118,7 +128,14 @@ export default function UsersAdmin() { const openEdit = (user: LocalUser) => { setEditingUser(user); - setForm({ name: user.name, email: user.email, password: "", role: user.role, isActive: user.isActive }); + setForm({ + name: user.name, + username: user.username ?? "", + email: user.email ?? "", + password: "", + role: user.role, + isActive: user.isActive, + }); setShowDialog(true); }; @@ -127,14 +144,21 @@ export default function UsersAdmin() { const data: Parameters[0] = { id: editingUser.id, name: form.name, - email: form.email, + username: form.username || undefined, + email: form.email || undefined, role: form.role, isActive: form.isActive, }; if (form.password) data.password = form.password; updateMutation.mutate(data); } else { - createMutation.mutate({ name: form.name, email: form.email, password: form.password, role: form.role }); + createMutation.mutate({ + name: form.name, + username: form.username || undefined, + email: form.email || undefined, + password: form.password, + role: form.role, + }); } }; @@ -149,7 +173,9 @@ export default function UsersAdmin() {

Gestion des utilisateurs

-

{users.length} utilisateur{users.length !== 1 ? "s" : ""} enregistré{users.length !== 1 ? "s" : ""}

+

+ {users.length} utilisateur{users.length !== 1 ? "s" : ""} enregistré{users.length !== 1 ? "s" : ""} +

- @@ -241,23 +295,65 @@ export default function UsersAdmin() { {editingUser ? "Modifier l'utilisateur" : "Nouvel utilisateur"} - {editingUser ? "Modifiez les informations de l'utilisateur" : "Créez un nouveau compte utilisateur"} + {editingUser + ? "Modifiez les informations de l'utilisateur" + : "Créez un nouveau compte. L'identifiant ou l'e-mail servira à la connexion."}
+ {/* Nom complet */}
- - setForm((f) => ({ ...f, name: e.target.value }))} /> + + setForm((f) => ({ ...f, name: e.target.value }))} + />
+ {/* Identifiant */}
- - setForm((f) => ({ ...f, email: e.target.value }))} /> + + setForm((f) => ({ ...f, username: e.target.value }))} + /> +

+ Permet la connexion sans e-mail. Ex : adminItinova +

+ {/* Email */}
- - setForm((f) => ({ ...f, password: e.target.value }))} /> + + setForm((f) => ({ ...f, email: e.target.value }))} + />
+ {/* Mot de passe */} +
+ + setForm((f) => ({ ...f, password: e.target.value }))} + /> +
+ {/* Rôle */}
+ {/* Statut (modification uniquement) */} {editingUser && (
- setForm((f) => ({ ...f, isActive: v }))} /> + setForm((f) => ({ ...f, isActive: v }))} + /> {form.isActive ? "Actif" : "Inactif"}
@@ -283,9 +383,16 @@ export default function UsersAdmin() {
- - + @@ -297,11 +404,19 @@ export default function UsersAdmin() { Supprimer l'utilisateur - Cette action est irréversible. L'utilisateur ne pourra plus se connecter. + + Cette action est irréversible. L'utilisateur ne pourra plus se connecter. + - - + diff --git a/drizzle/0003_shocking_secret_warriors.sql b/drizzle/0003_shocking_secret_warriors.sql new file mode 100644 index 0000000..28c3830 --- /dev/null +++ b/drizzle/0003_shocking_secret_warriors.sql @@ -0,0 +1,4 @@ +ALTER TABLE `local_users` DROP INDEX `local_users_email_unique`;--> statement-breakpoint +ALTER TABLE `local_users` MODIFY COLUMN `email` varchar(320);--> statement-breakpoint +ALTER TABLE `local_users` ADD `username` varchar(128);--> statement-breakpoint +ALTER TABLE `local_users` ADD CONSTRAINT `local_users_username_unique` UNIQUE(`username`); \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..c9d74d2 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,670 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "c42bd6aa-6824-4752-9e80-d410188548cf", + "prevId": "46aedace-2016-466b-a1f0-6dc05f8f383e", + "tables": { + "aap_items": { + "name": "aap_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "dedupKey": { + "name": "dedupKey", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "titre": { + "name": "titre", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "categorie": { + "name": "categorie", + "type": "enum('Handicap','PA','Enfance','Précarité','Sanitaire','Autre')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "departement": { + "name": "departement", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateCloture": { + "name": "dateCloture", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "datePublication": { + "name": "datePublication", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lien": { + "name": "lien", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importedAt": { + "name": "importedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "aap_items_id": { + "name": "aap_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "aap_items_dedupKey_unique": { + "name": "aap_items_dedupKey_unique", + "columns": [ + "dedupKey" + ] + } + }, + "checkConstraint": {} + }, + "app_settings": { + "name": "app_settings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "key": { + "name": "key", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "app_settings_id": { + "name": "app_settings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "app_settings_key_unique": { + "name": "app_settings_key_unique", + "columns": [ + "key" + ] + } + }, + "checkConstraint": {} + }, + "ideas": { + "name": "ideas", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userName": { + "name": "userName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "titre": { + "name": "titre", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('ouvert','en_cours','resolu','ferme')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ouvert'" + }, + "reponseAdmin": { + "name": "reponseAdmin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reponduPar": { + "name": "reponduPar", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reponduAt": { + "name": "reponduAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "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": { + "ideas_id": { + "name": "ideas_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "import_logs": { + "name": "import_logs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "fileType": { + "name": "fileType", + "type": "enum('veille','aap')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('success','partial','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalRows": { + "name": "totalRows", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "newRows": { + "name": "newRows", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "skippedRows": { + "name": "skippedRows", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "json", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "startedAt": { + "name": "startedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "completedAt": { + "name": "completedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "import_logs_id": { + "name": "import_logs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "local_users": { + "name": "local_users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('admin','user','readonly')", + "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": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "local_users_id": { + "name": "local_users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "local_users_username_unique": { + "name": "local_users_username_unique", + "columns": [ + "username" + ] + } + }, + "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 + }, + "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'" + }, + "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" + ] + } + }, + "checkConstraint": {} + }, + "veille_items": { + "name": "veille_items", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "dedupKey": { + "name": "dedupKey", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "titre": { + "name": "titre", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "categorie": { + "name": "categorie", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "niveau": { + "name": "niveau", + "type": "varchar(128)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "territoire": { + "name": "territoire", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resume": { + "name": "resume", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "passage": { + "name": "passage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lien": { + "name": "lien", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeVeille": { + "name": "typeVeille", + "type": "enum('reglementaire','concurrentielle','technologique','generale')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "datePublication": { + "name": "datePublication", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importedAt": { + "name": "importedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "veille_items_id": { + "name": "veille_items_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "veille_items_dedupKey_unique": { + "name": "veille_items_dedupKey_unique", + "columns": [ + "dedupKey" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 55e15ce..36693d8 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1776437589031, "tag": "0002_sticky_the_watchers", "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1776763582959, + "tag": "0003_shocking_secret_warriors", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 871d075..f9c8d18 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -31,7 +31,8 @@ export type InsertUser = typeof users.$inferInsert; export const localUsers = mysqlTable("local_users", { id: int("id").autoincrement().primaryKey(), name: varchar("name", { length: 255 }).notNull(), - email: varchar("email", { length: 320 }).notNull().unique(), + username: varchar("username", { length: 128 }).unique(), + email: varchar("email", { length: 320 }), passwordHash: varchar("passwordHash", { length: 255 }).notNull(), role: mysqlEnum("role", ["admin", "user", "readonly"]).default("user").notNull(), isActive: boolean("isActive").default(true).notNull(), diff --git a/server/db.ts b/server/db.ts index 00b5f83..f3104e3 100644 --- a/server/db.ts +++ b/server/db.ts @@ -72,6 +72,7 @@ export async function getLocalUsers() { .select({ id: localUsers.id, name: localUsers.name, + username: localUsers.username, email: localUsers.email, role: localUsers.role, isActive: localUsers.isActive, diff --git a/server/localAuth.ts b/server/localAuth.ts index d348db7..b00e10d 100644 --- a/server/localAuth.ts +++ b/server/localAuth.ts @@ -7,7 +7,7 @@ import { ENV } from "./_core/env"; const SALT_ROUNDS = 12; const JWT_EXPIRY = "7d"; -const LOCAL_AUTH_COOKIE = "veille_local_auth"; +export const LOCAL_AUTH_COOKIE = "veille_local_auth"; export async function hashPassword(password: string): Promise { return bcrypt.hash(password, SALT_ROUNDS); @@ -37,24 +37,30 @@ export async function verifyLocalToken(token: string): Promise<{ userId: number; } } -export async function loginLocalUser(email: string, password: string) { +/** + * Connexion par username OU email (insensible à la casse pour l'email). + * Le champ `identifier` peut être un nom d'utilisateur libre ou une adresse e-mail. + */ +export async function loginLocalUser(identifier: string, password: string) { const db = await getDb(); if (!db) throw new Error("Base de données indisponible"); - // Recherche par e-mail (insensible à la casse) OU par identifiant exact - const identifier = email.trim(); - const users = await db + const id = identifier.trim(); + + // Cherche d'abord par username exact, puis par email (insensible à la casse) + const results = await db .select() .from(localUsers) .where( or( - eq(localUsers.email, identifier.toLowerCase()), - eq(localUsers.email, identifier) + eq(localUsers.username, id), + eq(localUsers.email, id.toLowerCase()), + eq(localUsers.email, id) ) ) .limit(1); - const user = users[0]; + const user = results[0]; if (!user || !user.isActive) { throw new Error("Identifiants incorrects ou compte désactivé"); } @@ -62,21 +68,29 @@ export async function loginLocalUser(email: string, password: string) { const valid = await verifyPassword(password, user.passwordHash); if (!valid) throw new Error("Identifiants incorrects ou compte désactivé"); - // Mise à jour lastSignedIn await db .update(localUsers) .set({ lastSignedIn: new Date() }) .where(eq(localUsers.id, user.id)); const token = await generateLocalToken(user.id, user.role); - return { token, user: { id: user.id, name: user.name, email: user.email, role: user.role } }; + return { + token, + user: { + id: user.id, + name: user.name, + username: user.username ?? null, + email: user.email ?? null, + role: user.role, + }, + }; } export async function getLocalUserById(id: number) { const db = await getDb(); if (!db) return null; - const users = await db.select().from(localUsers).where(eq(localUsers.id, id)).limit(1); - return users[0] ?? null; + const results = await db.select().from(localUsers).where(eq(localUsers.id, id)).limit(1); + return results[0] ?? null; } export async function ensureAdminExists() { @@ -93,11 +107,12 @@ export async function ensureAdminExists() { const hash = await hashPassword("Admin@Itinova2024!"); await db.insert(localUsers).values({ name: "Administrateur", + username: "admin", email: "admin@itinova.fr", passwordHash: hash, role: "admin", isActive: true, }); - console.log("[LocalAuth] Compte admin par défaut créé : admin@itinova.fr / Admin@Itinova2024!"); + console.log("[LocalAuth] Compte admin par défaut créé : admin / Admin@Itinova2024!"); } } diff --git a/server/routers.ts b/server/routers.ts index 19b371e..2a9598c 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -195,7 +195,8 @@ export const appRouter = router({ .input( z.object({ name: z.string().min(2).max(255), - email: z.string().email(), + username: z.string().min(2).max(128).optional(), + email: z.string().email().optional(), password: z.string().min(8), role: z.enum(["admin", "user", "readonly"]).default("user"), }) @@ -204,7 +205,8 @@ export const appRouter = router({ const passwordHash = await hashPassword(input.password); await createLocalUser({ name: input.name, - email: input.email.toLowerCase(), + username: input.username ?? null, + email: input.email ? input.email.toLowerCase() : null, passwordHash, role: input.role, isActive: true, @@ -217,6 +219,7 @@ export const appRouter = router({ z.object({ id: z.number().int().positive(), name: z.string().min(2).max(255).optional(), + username: z.string().min(2).max(128).optional(), email: z.string().email().optional(), password: z.string().min(8).optional(), role: z.enum(["admin", "user", "readonly"]).optional(), diff --git a/todo.md b/todo.md index 006b68a..a1a681d 100644 --- a/todo.md +++ b/todo.md @@ -53,3 +53,10 @@ - [x] Page BoiteAIdees : liste avec date, titre, demandeur, texte, statut, réponse admin - [x] Bouton "Nouvelle demande" en haut à droite ouvrant une boîte de dialogue - [x] Menu "Boîte à idées" dans la sidebar accessible à tous les utilisateurs + +## Authentification username +- [ ] BDD : ajouter colonne username (unique, nullable) dans la table users +- [ ] Backend : loginLocalUser cherche par username OU email +- [ ] Backend : createLocalUser accepte username optionnel +- [ ] Frontend : page Gestion utilisateurs affiche et permet de saisir le username +- [ ] Mettre à jour le compte adminItinova avec username = adminItinova