feat: integrate NextAuth for authentication, refactor login and registration processes, and enhance middleware for session management

This commit is contained in:
Julien Froidefond
2025-10-16 15:50:37 +02:00
parent 9ecdd72804
commit 7426bfb33c
33 changed files with 417 additions and 729 deletions

17
src/lib/auth-utils.ts Normal file
View File

@@ -0,0 +1,17 @@
import { auth } from "@/lib/auth";
import type { UserData } from "@/lib/services/auth-server.service";
export async function getCurrentUser(): Promise<UserData | null> {
const session = await auth();
if (!session?.user) {
return null;
}
return {
id: session.user.id,
email: session.user.email,
roles: session.user.roles,
authenticated: true,
};
}

61
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,61 @@
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { AuthServerService } from "@/lib/services/auth-server.service";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
remember: { label: "Remember me", type: "checkbox" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
try {
const userData = await AuthServerService.loginUser(
credentials.email as string,
credentials.password as string
);
return {
id: userData.id,
email: userData.email,
roles: userData.roles,
};
} catch (error) {
console.error("Auth error:", error);
return null;
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
// Convertir le tableau en string pour éviter les problèmes de clonage
token.roles = JSON.stringify(user.roles);
}
return token;
},
async session({ session, token }) {
if (token) {
session.user.id = token.sub!;
// Reconvertir la string en tableau
session.user.roles = JSON.parse(token.roles as string);
}
return session;
},
},
pages: {
signIn: "/login",
},
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
});

View File

@@ -10,7 +10,14 @@ export function withPageTiming(pageName: string, Component: PageComponent) {
// Ensure params is awaited before using it
const params = props.params ? await Promise.resolve(props.params) : {};
await DebugService.logPageRender(pageName + JSON.stringify(params), duration);
// Only log if debug is enabled and user is authenticated
try {
await DebugService.logPageRender(pageName + JSON.stringify(params), duration);
} catch {
// Silently fail if user is not authenticated or debug is disabled
// This prevents errors on public pages like /login
}
return result;
};

View File

@@ -0,0 +1,26 @@
import { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
export async function getAuthSession(request: NextRequest) {
try {
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET
});
if (!token) {
return null;
}
return {
user: {
id: token.sub!,
email: token.email!,
roles: JSON.parse(token.roles as string),
}
};
} catch (error) {
console.error("Auth error in middleware:", error);
return null;
}
}

View File

@@ -1,7 +1,6 @@
import { cookies } from "next/headers";
import connectDB from "@/lib/mongodb";
import { UserModel } from "@/lib/models/user.model";
import bcrypt from "bcrypt";
import bcrypt from "bcryptjs";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
@@ -15,7 +14,7 @@ export interface UserData {
export class AuthServerService {
private static readonly SALT_ROUNDS = 10;
static async createUser(email: string, password: string): Promise<UserData> {
static async registerUser(email: string, password: string): Promise<UserData> {
await connectDB();
//check if password is strong
@@ -67,37 +66,6 @@ export class AuthServerService {
return true;
}
static async setUserCookie(userData: UserData, remember: boolean = false): Promise<void> {
// Encode user data in base64
const encodedUserData = Buffer.from(JSON.stringify(userData)).toString("base64");
// Set cookie with user data
const cookieStore = await cookies();
cookieStore.set("stripUser", encodedUserData, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: remember ? 30 * 24 * 60 * 60 : 24 * 60 * 60, // 30 days if remember, 24 hours otherwise
});
}
static async getCurrentUser(): Promise<UserData | null> {
const cookieStore = await cookies();
const userCookie = cookieStore.get("stripUser");
if (!userCookie) {
return null;
}
try {
return JSON.parse(atob(userCookie.value));
} catch (error) {
console.error("Error while getting user from cookie:", error);
return null;
}
}
static async loginUser(email: string, password: string): Promise<UserData> {
await connectDB();

View File

@@ -1,105 +0,0 @@
"use client";
import type { AppErrorType } from "@/types/global";
import { ERROR_CODES } from "@/constants/errorCodes";
class AuthService {
private static instance: AuthService;
// Constructeur privé pour le pattern Singleton
private constructor() {
// Pas d'initialisation nécessaire
}
public static getInstance(): AuthService {
if (!AuthService.instance) {
AuthService.instance = new AuthService();
}
return AuthService.instance;
}
/**
* Authentifie un utilisateur
*/
async login(email: string, password: string, remember: boolean = false): Promise<void> {
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password, remember }),
});
if (!response.ok) {
const data = await response.json();
throw data.error;
}
} catch (error) {
if ((error as AppErrorType).code) {
throw error;
}
throw {
code: ERROR_CODES.AUTH.INVALID_CREDENTIALS,
name: "Invalid credentials",
message: "The email or password is incorrect",
} as AppErrorType;
}
}
/**
* Crée un nouvel utilisateur
*/
async register(email: string, password: string): Promise<void> {
try {
const response = await fetch("/api/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const data = await response.json();
throw data.error;
}
} catch (error) {
if ((error as AppErrorType).code) {
throw error;
}
throw {
code: ERROR_CODES.AUTH.INVALID_USER_DATA,
name: "Invalid user data",
message: "The email or password is incorrect",
} as AppErrorType;
}
}
/**
* Déconnecte l'utilisateur
*/
async logout(): Promise<void> {
try {
const response = await fetch("/api/auth/logout", {
method: "POST",
});
if (!response.ok) {
const data = await response.json();
throw data.error;
}
} catch (error) {
if ((error as AppErrorType).code) {
throw error;
}
throw {
code: ERROR_CODES.AUTH.LOGOUT_ERROR,
name: "Logout error",
message: "The logout failed",
} as AppErrorType;
}
}
}
export const authService = AuthService.getInstance();

View File

@@ -2,14 +2,14 @@ import connectDB from "@/lib/mongodb";
import { KomgaConfig as KomgaConfigModel } from "@/lib/models/config.model";
import { TTLConfig as TTLConfigModel } from "@/lib/models/ttl-config.model";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { User, KomgaConfigData, TTLConfigData, KomgaConfig, TTLConfig } from "@/types/komga";
export class ConfigDBService {
private static async getCurrentUser(): Promise<User> {
const user: User | null = await AuthServerService.getCurrentUser();
const user: User | null = await getCurrentUser();
if (!user) {
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}

View File

@@ -1,8 +1,8 @@
import fs from "fs/promises";
import path from "path";
import type { CacheType } from "./base-api.service";
import { AuthServerService } from "./auth-server.service";
import { PreferencesService } from "./preferences.service";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
@@ -28,7 +28,7 @@ export class DebugService {
private static writeQueues = new Map<string, Promise<void>>();
private static async getCurrentUserId(): Promise<string> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}
@@ -49,7 +49,7 @@ export class DebugService {
}
private static async isDebugEnabled(): Promise<boolean> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
return false;
}

View File

@@ -1,7 +1,7 @@
import connectDB from "@/lib/mongodb";
import { FavoriteModel } from "@/lib/models/favorite.model";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { User } from "@/types/komga";
@@ -17,7 +17,7 @@ export class FavoriteService {
}
private static async getCurrentUser(): Promise<User> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}

View File

@@ -1,5 +1,5 @@
import { PreferencesModel } from "@/lib/models/preferences.model";
import { AuthServerService } from "./auth-server.service";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { UserPreferences } from "@/types/preferences";
@@ -9,7 +9,7 @@ import connectDB from "@/lib/mongodb";
export class PreferencesService {
static async getCurrentUser(): Promise<User> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}

View File

@@ -33,6 +33,7 @@ class RequestMonitor {
} else if (count >= this.thresholds.high) {
console.warn(`[REQUEST-MONITOR] ⚠️ HIGH concurrency: ${count} active requests`);
} else if (count >= this.thresholds.warning) {
// eslint-disable-next-line no-console
console.log(`[REQUEST-MONITOR] ⚡ Warning concurrency: ${count} active requests`);
}
}

View File

@@ -2,7 +2,7 @@ import fs from "fs";
import path from "path";
import { PreferencesService } from "./preferences.service";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
import { getCurrentUser } from "../auth-utils";
export type CacheMode = "file" | "memory";
@@ -45,7 +45,7 @@ class ServerCacheService {
private async initializeCacheMode(): Promise<void> {
try {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
this.setCacheMode("memory");
return;
@@ -293,7 +293,7 @@ class ServerCacheService {
* Supprime une entrée du cache
*/
async delete(key: string): Promise<void> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
}
@@ -313,7 +313,7 @@ class ServerCacheService {
* Supprime toutes les entrées du cache qui commencent par un préfixe
*/
async deleteAll(prefix: string): Promise<void> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
}
@@ -390,7 +390,7 @@ class ServerCacheService {
type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"
): Promise<T> {
const startTime = performance.now();
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
}