feat: integrate NextAuth for authentication, refactor login and registration processes, and enhance middleware for session management
This commit is contained in:
3
src/app/api/auth/[...nextauth]/route.ts
Normal file
3
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/lib/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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() });
|
||||
|
||||
12
src/components/providers/AuthProvider.tsx
Normal file
12
src/components/providers/AuthProvider.tsx
Normal 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>;
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
17
src/lib/auth-utils.ts
Normal 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
61
src/lib/auth.ts
Normal 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,
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
26
src/lib/middleware-auth.ts
Normal file
26
src/lib/middleware-auth.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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é");
|
||||
}
|
||||
|
||||
@@ -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
23
src/types/next-auth.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user