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

View File

@@ -0,0 +1,3 @@
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;

View File

@@ -1,44 +0,0 @@
import { NextResponse } from "next/server";
import { AuthServerService } from "@/lib/services/auth-server.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
import type { UserData } from "@/lib/services/auth-server.service";
import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
try {
const { email, password, remember } = await request.json();
try {
const userData: UserData = await AuthServerService.loginUser(email, password);
await AuthServerService.setUserCookie(userData, remember);
return NextResponse.json({
message: "✅ Connexion réussie",
user: userData,
});
} catch (error) {
if (error instanceof AppError) {
return NextResponse.json(
{
error,
},
{ status: 401 }
);
}
throw error;
}
} catch (error) {
console.error("Erreur lors de la connexion:", error);
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.INVALID_CREDENTIALS,
name: "Invalid credentials",
message: getErrorMessage(ERROR_CODES.AUTH.INVALID_CREDENTIALS),
} as AppError,
},
{ status: 500 }
);
}
}

View File

@@ -1,27 +0,0 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { ERROR_CODES } from "@/constants/errorCodes";
import { getErrorMessage } from "@/utils/errors";
import type { AppErrorType } from "@/types/global";
export async function POST() {
try {
// Supprimer le cookie
const cookieStore = await cookies();
cookieStore.delete("stripUser");
return NextResponse.json({ message: "👋 Déconnexion réussie" });
} catch (error) {
console.error("Erreur lors de la déconnexion:", error);
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.LOGOUT_ERROR,
name: "Logout error",
message: getErrorMessage(ERROR_CODES.AUTH.LOGOUT_ERROR),
} as AppErrorType,
},
{ status: 500 }
);
}
}

View File

@@ -1,48 +1,52 @@
import { NextResponse } from "next/server";
import type { UserData } from "@/lib/services/auth-server.service";
import { NextRequest, NextResponse } from "next/server";
import { AuthServerService } from "@/lib/services/auth-server.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { ERROR_MESSAGES } from "@/constants/errorMessages";
import { AppError } from "@/utils/errors";
import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
try {
const userData: UserData = await AuthServerService.createUser(email, password);
await AuthServerService.setUserCookie(userData);
return NextResponse.json({
message: "✅ Inscription réussie",
user: userData,
});
} catch (error) {
if (error instanceof AppError) {
const status =
error.code === ERROR_CODES.AUTH.EMAIL_EXISTS ||
error.code === ERROR_CODES.AUTH.PASSWORD_NOT_STRONG
? 400
: 500;
return NextResponse.json(
{
error,
},
{ status }
);
}
throw error;
if (!email || !password) {
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.INVALID_USER_DATA,
name: "Invalid user data",
message: ERROR_MESSAGES[ERROR_CODES.AUTH.INVALID_USER_DATA],
} as AppError,
},
{ status: 400 }
);
}
const userData = await AuthServerService.registerUser(email, password);
return NextResponse.json({ success: true, user: userData });
} catch (error) {
console.error("Erreur lors de l'inscription:", error);
console.error("Registration error:", error);
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
name: error.name,
message: error.message,
} as AppError,
},
{ status: 400 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.INVALID_USER_DATA,
name: "Invalid user data",
message: getErrorMessage(ERROR_CODES.AUTH.INVALID_USER_DATA),
},
code: ERROR_CODES.AUTH.REGISTRATION_FAILED,
name: "Registration failed",
message: ERROR_MESSAGES[ERROR_CODES.AUTH.REGISTRATION_FAILED],
} as AppError,
},
{ status: 500 }
);

View File

@@ -6,6 +6,7 @@ import ClientLayout from "@/components/layout/ClientLayout";
import { PreferencesService } from "@/lib/services/preferences.service";
import { PreferencesProvider } from "@/contexts/PreferencesContext";
import { I18nProvider } from "@/components/providers/I18nProvider";
import { AuthProvider } from "@/components/providers/AuthProvider";
import "@/i18n/i18n"; // Import i18next configuration
import { cookies } from "next/headers";
import { defaultPreferences } from "@/types/preferences";
@@ -158,13 +159,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<body
className={cn("min-h-screen bg-background font-sans antialiased h-full", inter.className)}
>
<I18nProvider locale={locale}>
<PreferencesProvider initialPreferences={preferences}>
<ClientLayout initialLibraries={libraries} initialFavorites={favorites}>
{children}
</ClientLayout>
</PreferencesProvider>
</I18nProvider>
<AuthProvider>
<I18nProvider locale={locale}>
<PreferencesProvider initialPreferences={preferences}>
<ClientLayout initialLibraries={libraries} initialFavorites={favorites}>
{children}
</ClientLayout>
</PreferencesProvider>
</I18nProvider>
</AuthProvider>
</body>
</html>
);

View File

@@ -2,9 +2,7 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { authService } from "@/lib/services/auth.service";
import type { AppErrorType } from "@/types/global";
import { ErrorMessage } from "@/components/ui/ErrorMessage";
import { signIn } from "next-auth/react";
import { useTranslate } from "@/hooks/useTranslate";
interface LoginFormProps {
@@ -14,7 +12,7 @@ interface LoginFormProps {
export function LoginForm({ from }: LoginFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<AppErrorType | null>(null);
const [error, setError] = useState<string | null>(null);
const { t } = useTranslate();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
@@ -28,11 +26,21 @@ export function LoginForm({ from }: LoginFormProps) {
const remember = formData.get("remember") === "on";
try {
await authService.login(email, password, remember);
router.push(from || "/");
router.refresh();
} catch (error) {
setError(error as AppErrorType);
const result = await signIn("credentials", {
email,
password,
remember,
redirect: false,
});
if (result?.error) {
setError("Email ou mot de passe incorrect");
} else {
router.push(from || "/");
router.refresh();
}
} catch (_error) {
setError("Une erreur est survenue lors de la connexion : " + _error);
} finally {
setIsLoading(false);
}
@@ -89,7 +97,11 @@ export function LoginForm({ from }: LoginFormProps) {
{t("login.form.remember")}
</label>
</div>
{error && <ErrorMessage errorCode={error.code} variant="form" />}
{error && (
<div className="text-red-600 text-sm">
{error}
</div>
)}
<button
type="submit"
disabled={isLoading}

View File

@@ -2,18 +2,16 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { authService } from "@/lib/services/auth.service";
import type { AppErrorType } from "@/types/global";
import { ERROR_CODES } from "@/constants/errorCodes";
import { signIn } from "next-auth/react";
import { ErrorMessage } from "@/components/ui/ErrorMessage";
import { useTranslate } from "@/hooks/useTranslate";
import { getErrorMessage } from "@/utils/errors";
import type { AppErrorType } from "@/types/global";
interface RegisterFormProps {
from?: string;
}
export function RegisterForm({ from }: RegisterFormProps) {
export function RegisterForm({ from: _from }: RegisterFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<AppErrorType | null>(null);
@@ -31,20 +29,57 @@ export function RegisterForm({ from }: RegisterFormProps) {
if (password !== confirmPassword) {
setError({
code: ERROR_CODES.AUTH.PASSWORD_MISMATCH,
code: "AUTH_PASSWORD_MISMATCH",
name: "Password mismatch",
message: getErrorMessage(ERROR_CODES.AUTH.PASSWORD_MISMATCH),
message: "Les mots de passe ne correspondent pas",
});
setIsLoading(false);
return;
}
try {
await authService.register(email, password);
router.push(from || "/");
router.refresh();
} catch (error) {
setError(error as AppErrorType);
// Étape 1: Inscription via l'API
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();
setError(data.error || {
code: "AUTH_REGISTRATION_FAILED",
name: "Registration failed",
message: "Erreur lors de l'inscription",
});
return;
}
// Étape 2: Connexion automatique via NextAuth
const signInResult = await signIn("credentials", {
email,
password,
redirect: false,
});
if (signInResult?.error) {
setError({
code: "AUTH_INVALID_CREDENTIALS",
name: "Login failed",
message: "Inscription réussie mais erreur lors de la connexion automatique",
});
} else {
router.push("/");
router.refresh();
}
} catch {
setError({
code: "AUTH_REGISTRATION_FAILED",
name: "Registration failed",
message: "Une erreur est survenue lors de l'inscription",
});
} finally {
setIsLoading(false);
}

View File

@@ -3,7 +3,7 @@
import { Home, Library, Settings, LogOut, RefreshCw, Star, Download } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { authService } from "@/lib/services/auth.service";
import { signOut } from "next-auth/react";
import { useEffect, useState, useCallback } from "react";
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
import { usePreferences } from "@/contexts/PreferencesContext";
@@ -118,19 +118,15 @@ export function Sidebar({ isOpen, onClose, initialLibraries, initialFavorites }:
const handleLogout = async () => {
try {
await authService.logout();
await signOut({ callbackUrl: "/login" });
setLibraries([]);
setFavorites([]);
onClose();
router.push("/login");
} catch (error) {
console.error("Erreur lors de la déconnexion:", error);
toast({
title: "Erreur",
description:
error instanceof AppError
? error.message
: getErrorMessage(ERROR_CODES.AUTH.LOGOUT_ERROR),
description: "Une erreur est survenue lors de la déconnexion",
variant: "destructive",
});
}

View File

@@ -3,7 +3,7 @@
import { SeriesGrid } from "./SeriesGrid";
import { Pagination } from "@/components/ui/Pagination";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import type { KomgaSeries } from "@/types/komga";
@@ -27,7 +27,7 @@ export function PaginatedSeriesGrid({
series,
currentPage,
totalPages,
totalElements,
totalElements: _totalElements,
defaultShowOnlyUnread,
showOnlyUnread: initialShowOnlyUnread,
}: PaginatedSeriesGridProps) {
@@ -36,10 +36,10 @@ export function PaginatedSeriesGrid({
const searchParams = useSearchParams();
const [isChangingPage, setIsChangingPage] = useState(false);
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
const { isCompact, itemsPerPage } = useDisplayPreferences();
const { isCompact, itemsPerPage: _itemsPerPage } = useDisplayPreferences();
const { t } = useTranslate();
const updateUrlParams = async (
const updateUrlParams = useCallback(async (
updates: Record<string, string | null>,
replace: boolean = false
) => {
@@ -59,7 +59,7 @@ export function PaginatedSeriesGrid({
} else {
await router.push(`${pathname}?${params.toString()}`);
}
};
}, [router, pathname, searchParams]);
// Reset loading state when series change
useEffect(() => {
@@ -76,7 +76,7 @@ export function PaginatedSeriesGrid({
if (defaultShowOnlyUnread && !searchParams.has("unread")) {
updateUrlParams({ page: "1", unread: "true" }, true);
}
}, [defaultShowOnlyUnread, pathname, router, searchParams]);
}, [defaultShowOnlyUnread, pathname, router, searchParams, updateUrlParams]);
const handlePageChange = async (page: number) => {
await updateUrlParams({ page: page.toString() });

View File

@@ -0,0 +1,12 @@
"use client";
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
return <SessionProvider>{children}</SessionProvider>;
}

View File

@@ -1,6 +1,5 @@
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import { cn } from "@/lib/utils";
import { useState } from "react";
interface ZoomablePageProps {
pageUrl: string | null;
@@ -23,10 +22,7 @@ export const ZoomablePage = ({
order = "first",
onZoomChange,
}: ZoomablePageProps) => {
const [currentScale, setCurrentScale] = useState(1);
const handleTransform = (ref: any, state: { scale: number; positionX: number; positionY: number }) => {
setCurrentScale(state.scale);
onZoomChange?.(state.scale > 1.1);
};
return (

View File

@@ -3,7 +3,7 @@
import { BookGrid } from "./BookGrid";
import { Pagination } from "@/components/ui/Pagination";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import type { KomgaBook } from "@/types/komga";
@@ -38,7 +38,7 @@ export function PaginatedBookGrid({
const { isCompact, itemsPerPage } = useDisplayPreferences();
const { t } = useTranslate();
const updateUrlParams = async (
const updateUrlParams = useCallback(async (
updates: Record<string, string | null>,
replace: boolean = false
) => {
@@ -58,7 +58,7 @@ export function PaginatedBookGrid({
} else {
await router.push(`${pathname}?${params.toString()}`);
}
};
}, [router, pathname, searchParams]);
// Reset loading state when books change
useEffect(() => {
@@ -75,7 +75,7 @@ export function PaginatedBookGrid({
if (defaultShowOnlyUnread && !searchParams.has("unread")) {
updateUrlParams({ page: "1", unread: "true" }, true);
}
}, [defaultShowOnlyUnread, pathname, router, searchParams]);
}, [defaultShowOnlyUnread, pathname, router, searchParams, updateUrlParams]);
const handlePageChange = async (page: number) => {
await updateUrlParams({ page: page.toString() });

View File

@@ -13,6 +13,7 @@ export const ERROR_CODES = {
EMAIL_EXISTS: "AUTH_EMAIL_EXISTS",
INVALID_USER_DATA: "AUTH_INVALID_USER_DATA",
LOGOUT_ERROR: "AUTH_LOGOUT_ERROR",
REGISTRATION_FAILED: "AUTH_REGISTRATION_FAILED",
},
KOMGA: {
MISSING_CONFIG: "KOMGA_MISSING_CONFIG",

View File

@@ -19,6 +19,7 @@ export const ERROR_MESSAGES: Record<string, string> = {
[ERROR_CODES.AUTH.EMAIL_EXISTS]: "📧 This email is already in use",
[ERROR_CODES.AUTH.INVALID_USER_DATA]: "👤 Invalid user data",
[ERROR_CODES.AUTH.LOGOUT_ERROR]: "🚪 Error during logout",
[ERROR_CODES.AUTH.REGISTRATION_FAILED]: "❌ Registration failed",
// Komga
[ERROR_CODES.KOMGA.MISSING_CONFIG]: "⚙️ Komga configuration not found",

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é");
}

View File

@@ -1,52 +1,32 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { ERROR_CODES } from "./constants/errorCodes";
import type { UserData } from "./lib/services/auth-server.service";
import { getErrorMessage } from "./utils/errors";
import { NextResponse, NextRequest } from "next/server";
import { getAuthSession } from "@/lib/middleware-auth";
// Routes qui ne nécessitent pas d'authentification
const publicRoutes = ["/login", "/register", "/images"];
// Routes d'API qui ne nécessitent pas d'authentification
const publicApiRoutes = ["/api/auth/login", "/api/auth/register", "/api/komga/test"];
const publicApiRoutes = ["/api/auth/register", "/api/komga/test"];
// Langues supportées
const locales = ["fr", "en"];
const defaultLocale = "fr";
export function middleware(request: NextRequest) {
export default async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Gestion de la langue
let locale = request.cookies.get("NEXT_LOCALE")?.value;
let locale = request.headers.get("cookie")?.match(/NEXT_LOCALE=([^;]+)/)?.[1];
// Si pas de cookie de langue ou langue non supportée, on utilise la langue par défaut
if (!locale || !locales.includes(locale)) {
locale = defaultLocale;
// On s'assure que la réponse est bien une redirection si nécessaire
const response =
pathname === "/login"
? NextResponse.next()
: NextResponse.redirect(new URL("/login", request.url));
response.cookies.set("NEXT_LOCALE", locale, {
path: "/",
maxAge: 365 * 24 * 60 * 60, // 1 an
secure: true, // Ajout de secure pour HTTPS
sameSite: "lax", // Protection CSRF
});
return response;
}
// Gestion de l'authentification
const user = request.cookies.get("stripUser");
// Vérifier si c'est une route publique avant de gérer l'authentification
if (
publicRoutes.includes(pathname) ||
publicApiRoutes.includes(pathname) ||
pathname.startsWith("/api/auth/") ||
pathname.startsWith("/images/") ||
pathname.startsWith("/_next/") ||
pathname.startsWith("/fonts/")
@@ -54,14 +34,16 @@ export function middleware(request: NextRequest) {
return NextResponse.next();
}
// Pour toutes les routes protégées, vérifier la présence de l'utilisateur
if (!user?.value) {
// Vérifier l'authentification avec NextAuth v5
const session = await getAuthSession(request);
if (!session) {
if (pathname.startsWith("/api/")) {
return NextResponse.json(
{
error: {
code: ERROR_CODES.MIDDLEWARE.UNAUTHORIZED,
message: getErrorMessage(ERROR_CODES.MIDDLEWARE.UNAUTHORIZED),
code: "UNAUTHORIZED",
message: "Unauthorized access",
name: "Unauthorized",
},
},
@@ -70,35 +52,21 @@ export function middleware(request: NextRequest) {
}
const loginUrl = new URL("/login", request.url);
// loginUrl.searchParams.set("from", encodeURIComponent(pathname));
return NextResponse.redirect(loginUrl);
}
try {
const userData: UserData = JSON.parse(atob(user.value));
if (!userData || !userData.authenticated || !userData.id || !userData.email) {
throw new Error(getErrorMessage(ERROR_CODES.MIDDLEWARE.INVALID_SESSION));
}
} catch (error) {
console.error("Erreur de validation du cookie:", error);
if (pathname.startsWith("/api/")) {
return NextResponse.json(
{
error: {
code: ERROR_CODES.MIDDLEWARE.INVALID_TOKEN,
message: getErrorMessage(ERROR_CODES.MIDDLEWARE.INVALID_TOKEN),
name: "Invalid token",
},
},
{ status: 401 }
);
}
const loginUrl = new URL("/login", request.url);
// loginUrl.searchParams.set("from", pathname);
return NextResponse.redirect(loginUrl);
// Définir le cookie de langue si nécessaire
const response = NextResponse.next();
if (!request.headers.get("cookie")?.includes("NEXT_LOCALE") && locale) {
response.cookies.set("NEXT_LOCALE", locale, {
path: "/",
maxAge: 365 * 24 * 60 * 60, // 1 an
secure: true, // Ajout de secure pour HTTPS
sameSite: "lax", // Protection CSRF
});
}
return NextResponse.next();
return response;
}
// Configuration des routes à protéger
@@ -106,12 +74,12 @@ export const config = {
matcher: [
/*
* Match all request paths except:
* 1. /api/auth/* (authentication routes)
* 1. /api/auth/* (NextAuth routes)
* 2. /_next/* (Next.js internals)
* 3. /fonts/* (inside public directory)
* 4. /images/* (inside public directory)
* 5. Static files (manifest.json, favicon.ico, etc.)
*/
"/((?!api/auth/*|_next/static|_next/image|fonts|images|manifest.json|favicon.ico|sitemap.xml|sw.js|offline.html).*)",
"/((?!api/auth|_next/static|_next/image|fonts|images|manifest.json|favicon.ico|sitemap.xml|sw.js|offline.html).*)",
],
};
};

23
src/types/next-auth.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import type { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
email: string;
roles: string[];
} & DefaultSession["user"];
}
interface User {
id: string;
email: string;
roles: string[];
}
}
declare module "next-auth/jwt" {
interface JWT {
roles: string; // Stocké comme string JSON
}
}