feat: Implement NextAuth authentication with credentials, database and JSON fallback, including admin seeding and password management.

This commit is contained in:
Michael committed 2026-03-10 12:24:30 +01:00
1 parent 4e80f228fe
commit 69e2f9c325
3 files changed
+90 -26

No files matched your search

+48
View File
@@ -1,4 +1,15 @@
import { scryptSync, randomBytes, timingSafeEqual } from "crypto"; import { scryptSync, randomBytes, timingSafeEqual } from "crypto";
import fs from "fs";
import path from "path";
const USERS_JSON_PATH = path.join(process.cwd(), "data", "users.json");
export interface UserJson {
id: string;
username: string;
passwordHash: string;
role: string;
}
/** /**
* Hashes a password using scrypt. * Hashes a password using scrypt.
@@ -26,3 +37,40 @@ export function verifyPassword(password: string, storedHash: string): boolean {
return false; return false;
} }
} }
/**
* Reads users from local JSON fallback.
*/
export function getFallbackUsers(): UserJson[] {
try {
if (!fs.existsSync(USERS_JSON_PATH)) {
// Initial seed if file doesn't exist
const defaultAdmin: UserJson = {
id: "0",
username: "admin",
passwordHash: hashPassword("admin"),
role: "admin"
};
saveFallbackUsers([defaultAdmin]);
return [defaultAdmin];
}
const data = fs.readFileSync(USERS_JSON_PATH, "utf-8");
return JSON.parse(data);
} catch (e) {
console.error("[AUTH] Error reading users.json:", e);
return [];
}
}
/**
* Saves users to local JSON fallback.
*/
export function saveFallbackUsers(users: UserJson[]) {
try {
const dir = path.dirname(USERS_JSON_PATH);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(USERS_JSON_PATH, JSON.stringify(users, null, 2));
} catch (e) {
console.error("[AUTH] Error writing users.json:", e);
}
}
+20 -18
View File
@@ -1,6 +1,6 @@
import { db } from "@/db"; import { db } from "@/db";
import { users } from "@/db/schema"; import { users } from "@/db/schema";
import { hashPassword } from "./auth-logic"; import { hashPassword, getFallbackUsers, saveFallbackUsers } from "./auth-logic";
import { count, eq, sql } from "drizzle-orm"; import { count, eq, sql } from "drizzle-orm";
/** /**
@@ -9,13 +9,23 @@ import { count, eq, sql } from "drizzle-orm";
* Also ensures the table exists (auto-repair). * Also ensures the table exists (auto-repair).
*/ */
export async function ensureAdminExists() { export async function ensureAdminExists() {
try { // 1. Always ensure local JSON fallback has at least one admin
console.log("[AUTH] Testing database connection..."); const fallbackUsers = getFallbackUsers();
await db.execute(sql`SELECT 1`); if (fallbackUsers.length === 0) {
console.log("[AUTH] Database connection OK."); console.log("[AUTH] Initializing local JSON fallback with admin/admin...");
saveFallbackUsers([{
id: "0",
username: "admin",
passwordHash: hashPassword("admin"),
role: "admin"
}]);
}
// 1. Auto-repair: Ensure table exists (Fallback if db-init failed) try {
console.log("[AUTH] Checking/Creating users table..."); console.log("[AUTH] Testing database connection for seeding...");
await db.execute(sql`SELECT 1`);
// 2. Auto-repair: Ensure table exists
await db.execute(sql` await db.execute(sql`
CREATE TABLE IF NOT EXISTS "users" ( CREATE TABLE IF NOT EXISTS "users" (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
@@ -25,32 +35,24 @@ export async function ensureAdminExists() {
created_at TIMESTAMP DEFAULT NOW() created_at TIMESTAMP DEFAULT NOW()
); );
`); `);
console.log("[AUTH] users table verified/created.");
// 3. Sync admin to DB
const adminUser = await db.select() const adminUser = await db.select()
.from(users) .from(users)
.where(eq(users.username, "admin")) .where(eq(users.username, "admin"))
.limit(1); .limit(1);
if (adminUser.length === 0) { if (adminUser.length === 0) {
console.log("[AUTH] Admin user not found. Seeding default (admin/admin)..."); console.log("[AUTH] Seeding admin to DB...");
await db.insert(users).values({ await db.insert(users).values({
username: "admin", username: "admin",
passwordHash: hashPassword("admin"), passwordHash: hashPassword("admin"),
role: "admin" role: "admin"
}); });
console.log("[AUTH] Default admin seeded successfully.");
return true; return true;
} else {
console.log("[AUTH] Admin user already exists.");
} }
} catch (err: any) { } catch (err: any) {
console.error("[AUTH] !!! CRITICAL DATABASE ERROR !!!"); console.warn("[AUTH] Database seeding failed (expected if DB not configured yet):", err.message);
console.error("- Message:", err.message);
console.error("- PG Code:", err.code);
console.error("- Detail:", err.detail);
console.error("- Hint:", err.hint);
if (err.internalQuery) console.error("- Query:", err.internalQuery);
} }
return false; return false;
} }
+22 -8
View File
@@ -3,7 +3,7 @@ import Credentials from "next-auth/providers/credentials";
import { db } from "@/db"; import { db } from "@/db";
import { users } from "@/db/schema"; import { users } from "@/db/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { verifyPassword } from "@/features/auth/logic/auth-logic"; import { verifyPassword, getFallbackUsers } from "@/features/auth/logic/auth-logic";
import { ensureAdminExists } from "@/features/auth/logic/seed-admin"; import { ensureAdminExists } from "@/features/auth/logic/seed-admin";
import { authConfig } from "./auth.config"; import { authConfig } from "./auth.config";
@@ -22,15 +22,30 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
if (!credentials?.username || !credentials?.password) return null; if (!credentials?.username || !credentials?.password) return null;
try { try {
console.log(`[AUTH] Checking user "${credentials.username}" in database...`); console.log(`[AUTH] Checking user "${credentials.username}"...`);
const [user] = await db.select()
.from(users)
.where(eq(users.username, credentials.username as string));
console.log(`[AUTH] User found in DB: ${!!user}`); let user: any = null;
// Try DB first
try {
const [dbUser] = await db.select()
.from(users)
.where(eq(users.username, credentials.username as string));
user = dbUser;
console.log(`[AUTH] DB check: User found: ${!!user}`);
} catch (dbErr) {
console.warn("[AUTH] DB unreachable, falling back to JSON.");
}
// Fallback to JSON if DB failed or user not found
if (!user) {
const fallbackUsers = getFallbackUsers();
user = fallbackUsers.find(u => u.username === credentials.username);
console.log(`[AUTH] JSON check: User found: ${!!user}`);
}
if (!user) { if (!user) {
console.warn(`[AUTH] User "${credentials.username}" not found.`); console.warn(`[AUTH] User "${credentials.username}" not found anywhere.`);
return null; return null;
} }
@@ -49,7 +64,6 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
}; };
} catch (err: any) { } catch (err: any) {
console.error("[AUTH] CRITICAL ERROR during authorize callback:", err.message || err); console.error("[AUTH] CRITICAL ERROR during authorize callback:", err.message || err);
// Propage l'erreur pour que Next-Auth la gère (mais authorize doit retourner null ou l'utilisateur)
return null; return null;
} }
} }