feat: username login support - recherche par username OU email
This commit is contained in:
45
server/db.ts
45
server/db.ts
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user