feat: username login support - recherche par username OU email

This commit is contained in:
Manus Deploy
2026-04-21 06:00:43 -04:00
parent 171386d333
commit 145b3dd971
8 changed files with 792 additions and 41 deletions

View File

@@ -9,6 +9,8 @@ import {
appSettings,
importLogs,
InsertLocalUser,
ideas,
InsertIdea,
} from "../drizzle/schema";
import { ENV } from "./_core/env";
@@ -70,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,
@@ -315,3 +318,45 @@ export async function getImportStats() {
totalNewRows,
};
}
// ─── Boîte à idées ────────────────────────────────────────────────────────────
export async function createIdea(data: InsertIdea) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.insert(ideas).values(data);
}
export async function getAllIdeas() {
const db = await getDb();
if (!db) return [];
return db.select().from(ideas).orderBy(desc(ideas.createdAt));
}
export async function getIdeasByUser(userId: number) {
const db = await getDb();
if (!db) return [];
return db.select().from(ideas).where(eq(ideas.userId, userId)).orderBy(desc(ideas.createdAt));
}
export async function repondreIdea(
id: number,
reponseAdmin: string,
reponduPar: string,
statut: "ouvert" | "en_cours" | "resolu" | "ferme"
) {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(ideas).set({
reponseAdmin,
reponduPar,
reponduAt: new Date(),
statut,
}).where(eq(ideas.id, id));
}
export async function updateIdeaStatut(id: number, statut: "ouvert" | "en_cours" | "resolu" | "ferme") {
const db = await getDb();
if (!db) throw new Error("Database not available");
await db.update(ideas).set({ statut }).where(eq(ideas.id, id));
}

View File

@@ -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<string> {
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!");
}
}

View File

@@ -17,6 +17,11 @@ import {
createLocalUser,
updateLocalUser,
deleteLocalUser,
createIdea,
getAllIdeas,
getIdeasByUser,
repondreIdea,
updateIdeaStatut,
} from "./db";
import { importVeille, importAAP, runFullImport, getImportConfig } from "./importer";
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
@@ -190,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"),
})
@@ -199,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,
@@ -212,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(),
@@ -238,6 +246,67 @@ export const appRouter = router({
return { success: true };
}),
}),
// ─── Boîte à idées ───────────────────────────────────────────────────────────
ideas: router({
// Créer une nouvelle idée / question
create: protectedProcedure
.input(
z.object({
titre: z.string().min(3).max(512),
message: z.string().min(10),
})
)
.mutation(async ({ input, ctx }) => {
await createIdea({
userId: ctx.user.id,
userName: ctx.user.name ?? ctx.user.email ?? "Utilisateur",
titre: input.titre,
message: input.message,
});
return { success: true };
}),
// Lister toutes les idées (admin) ou les siennes (user)
list: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.role === "admin") {
return getAllIdeas();
}
return getIdeasByUser(ctx.user.id);
}),
// Répondre à une idée (admin uniquement)
repondre: adminProcedure
.input(
z.object({
id: z.number().int().positive(),
reponseAdmin: z.string().min(1),
statut: z.enum(["ouvert", "en_cours", "resolu", "ferme"]),
})
)
.mutation(async ({ input, ctx }) => {
await repondreIdea(
input.id,
input.reponseAdmin,
ctx.user.name ?? ctx.user.email ?? "Admin",
input.statut
);
return { success: true };
}),
// Changer le statut (admin uniquement)
updateStatut: adminProcedure
.input(
z.object({
id: z.number().int().positive(),
statut: z.enum(["ouvert", "en_cours", "resolu", "ferme"]),
})
)
.mutation(async ({ input }) => {
await updateIdeaStatut(input.id, input.statut);
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;