feat: integrate NextAuth for authentication, refactor login and registration processes, and enhance middleware for session management
This commit is contained in:
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é");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user