diff --git a/package-lock.json b/package-lock.json
index 854eaa5..2fe36ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slot": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@types/mongoose": "^5.11.97",
"class-variance-authority": "^0.7.1",
@@ -1339,6 +1340,35 @@
}
}
},
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
+ "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.2",
+ "@radix-ui/react-roving-focus": "1.1.2",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-toast": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
diff --git a/package.json b/package.json
index f80a281..dfdb2d5 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-slot": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@types/mongoose": "^5.11.97",
"class-variance-authority": "^0.7.1",
diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts
new file mode 100644
index 0000000..759a88d
--- /dev/null
+++ b/src/app/api/auth/login/route.ts
@@ -0,0 +1,58 @@
+import { NextResponse } from "next/server";
+import { cookies } from "next/headers";
+import connectDB from "@/lib/mongodb";
+import { UserModel } from "@/lib/models/user.model";
+
+export async function POST(request: Request) {
+ try {
+ const { email, password, remember } = await request.json();
+ await connectDB();
+
+ const user = await UserModel.findOne({ email: email.toLowerCase() });
+
+ if (!user || user.password !== password) {
+ return NextResponse.json(
+ {
+ error: {
+ code: "INVALID_CREDENTIALS",
+ message: "Email ou mot de passe incorrect",
+ },
+ },
+ { status: 401 }
+ );
+ }
+
+ const userData = {
+ id: user._id.toString(),
+ email: user.email,
+ roles: user.roles,
+ authenticated: true,
+ };
+
+ // Encoder les données utilisateur en base64
+ const encodedUserData = Buffer.from(JSON.stringify(userData)).toString("base64");
+
+ // Définir le cookie avec les données utilisateur
+ cookies().set("stripUser", encodedUserData, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/",
+ // 30 jours si "remember me" est coché, sinon 24 heures
+ maxAge: remember ? 30 * 24 * 60 * 60 : 24 * 60 * 60,
+ });
+
+ return NextResponse.json({ message: "Connexion réussie", user: userData });
+ } catch (error) {
+ console.error("Erreur lors de la connexion:", error);
+ return NextResponse.json(
+ {
+ error: {
+ code: "SERVER_ERROR",
+ message: "Une erreur est survenue lors de la connexion",
+ },
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts
new file mode 100644
index 0000000..8eee72d
--- /dev/null
+++ b/src/app/api/auth/logout/route.ts
@@ -0,0 +1,9 @@
+import { NextResponse } from "next/server";
+import { cookies } from "next/headers";
+
+export async function POST() {
+ // Supprimer le cookie
+ cookies().delete("stripUser");
+
+ return NextResponse.json({ message: "Déconnexion réussie" });
+}
diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts
new file mode 100644
index 0000000..9561b13
--- /dev/null
+++ b/src/app/api/auth/register/route.ts
@@ -0,0 +1,66 @@
+import { NextResponse } from "next/server";
+import { cookies } from "next/headers";
+import connectDB from "@/lib/mongodb";
+import { UserModel } from "@/lib/models/user.model";
+
+export async function POST(request: Request) {
+ try {
+ const { email, password } = await request.json();
+ await connectDB();
+
+ // Vérifier si l'utilisateur existe déjà
+ const existingUser = await UserModel.findOne({ email: email.toLowerCase() });
+ if (existingUser) {
+ return NextResponse.json(
+ {
+ error: {
+ code: "EMAIL_EXISTS",
+ message: "Cet email est déjà utilisé",
+ },
+ },
+ { status: 400 }
+ );
+ }
+
+ // Créer le nouvel utilisateur
+ const user = await UserModel.create({
+ email: email.toLowerCase(),
+ password,
+ roles: ["ROLE_USER"],
+ authenticated: true,
+ });
+
+ const userData = {
+ id: user._id.toString(),
+ email: user.email,
+ roles: user.roles,
+ authenticated: true,
+ };
+
+ // Encoder les données utilisateur en base64
+ const encodedUserData = Buffer.from(JSON.stringify(userData)).toString("base64");
+
+ // Définir le cookie avec les données utilisateur
+ cookies().set("stripUser", encodedUserData, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/",
+ // 24 heures par défaut pour les nouveaux utilisateurs
+ maxAge: 24 * 60 * 60,
+ });
+
+ return NextResponse.json({ message: "Inscription réussie", user: userData });
+ } catch (error) {
+ console.error("Erreur lors de l'inscription:", error);
+ return NextResponse.json(
+ {
+ error: {
+ code: "SERVER_ERROR",
+ message: "Une erreur est survenue lors de l'inscription",
+ },
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/app/login/LoginContent.tsx b/src/app/login/LoginContent.tsx
new file mode 100644
index 0000000..2de8157
--- /dev/null
+++ b/src/app/login/LoginContent.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { LoginForm } from "@/components/auth/LoginForm";
+import { RegisterForm } from "@/components/auth/RegisterForm";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+
+interface LoginContentProps {
+ searchParams: {
+ from?: string;
+ tab?: string;
+ };
+}
+
+export function LoginContent({ searchParams }: LoginContentProps) {
+ const defaultTab = searchParams.tab || "login";
+
+ return (
+
+
+
+
+
+
+
+
+ Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne
+ et fluide.
+
+
+
+
+
+
+
+
Bienvenue sur StripStream
+
+ Connectez-vous ou créez un compte pour commencer
+
+
+
+
+ Connexion
+ Inscription
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index 4e5abfe..5b1d583 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -1,187 +1,15 @@
-"use client";
+import { Metadata } from "next";
+import { LoginContent } from "./LoginContent";
-import { useState, Suspense } from "react";
-import { useRouter, useSearchParams } from "next/navigation";
-import { authService } from "@/lib/services/auth.service";
-import { AuthError } from "@/types/auth";
+export const metadata: Metadata = {
+ title: "Connexion",
+ description: "Connectez-vous à votre compte StripStream",
+};
-function LoginForm() {
- const router = useRouter();
- const searchParams = useSearchParams();
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const handleSubmit = async (event: React.FormEvent) => {
- event.preventDefault();
- setIsLoading(true);
- setError(null);
-
- const formData = new FormData(event.currentTarget);
- const email = formData.get("email") as string;
- const password = formData.get("password") as string;
- const remember = formData.get("remember") === "on";
-
- try {
- await authService.login(email, password, remember);
- const from = searchParams.get("from") || "/";
- router.push(from);
- } catch (error) {
- setError(error as AuthError);
- } finally {
- setIsLoading(false);
- }
- };
-
- return (
-
-
-
-
-
-
-
-
- Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne
- et fluide.
-
-
-
-
-
-
-
-
-
-
- Stripstream
-
-
Votre bibliothèque numérique de BD
-
-
-
-
-
Connexion
-
- Connectez-vous pour accéder à votre bibliothèque
-
-
-
-
-
-
- );
-}
-
-export default function LoginPage() {
- return (
- Chargement...}
- >
-
-
- );
+export default function LoginPage({
+ searchParams,
+}: {
+ searchParams: { from?: string; tab?: string };
+}) {
+ return ;
}
diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx
new file mode 100644
index 0000000..a4c347c
--- /dev/null
+++ b/src/components/auth/LoginForm.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { authService } from "@/lib/services/auth.service";
+import { AuthError } from "@/types/auth";
+
+interface LoginFormProps {
+ from?: string;
+}
+
+export function LoginForm({ from }: LoginFormProps) {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ setIsLoading(true);
+ setError(null);
+
+ const formData = new FormData(event.currentTarget);
+ const email = formData.get("email") as string;
+ const password = formData.get("password") as string;
+ const remember = formData.get("remember") === "on";
+
+ try {
+ await authService.login(email, password, remember);
+ router.push(from || "/");
+ } catch (error) {
+ setError(error as AuthError);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx
new file mode 100644
index 0000000..4b05fba
--- /dev/null
+++ b/src/components/auth/RegisterForm.tsx
@@ -0,0 +1,110 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { authService } from "@/lib/services/auth.service";
+import { AuthError } from "@/types/auth";
+
+interface RegisterFormProps {
+ from?: string;
+}
+
+export function RegisterForm({ from }: RegisterFormProps) {
+ const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ setIsLoading(true);
+ setError(null);
+
+ const formData = new FormData(event.currentTarget);
+ const email = formData.get("email") as string;
+ const password = formData.get("password") as string;
+ const confirmPassword = formData.get("confirmPassword") as string;
+
+ if (password !== confirmPassword) {
+ setError({
+ code: "SERVER_ERROR",
+ message: "Les mots de passe ne correspondent pas",
+ });
+ setIsLoading(false);
+ return;
+ }
+
+ try {
+ await authService.register(email, password);
+ router.push(from || "/");
+ } catch (error) {
+ setError(error as AuthError);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/layout/SidebarWrapper.tsx b/src/components/layout/SidebarWrapper.tsx
index 7a6415a..7f4cecb 100644
--- a/src/components/layout/SidebarWrapper.tsx
+++ b/src/components/layout/SidebarWrapper.tsx
@@ -1,6 +1,5 @@
import { FavoriteService } from "@/lib/services/favorite.service";
import { LibraryService } from "@/lib/services/library.service";
-import { ClientSidebar } from "./ClientSidebar";
export async function SidebarWrapper() {
// Récupérer les favoris depuis le serveur
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx
new file mode 100644
index 0000000..9651cac
--- /dev/null
+++ b/src/components/ui/tabs.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import * as React from "react";
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+import { cn } from "@/lib/utils";
+
+const Tabs = TabsPrimitive.Root;
+
+const TabsList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsList.displayName = TabsPrimitive.List.displayName;
+
+const TabsTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
+
+const TabsContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+TabsContent.displayName = TabsPrimitive.Content.displayName;
+
+export { Tabs, TabsList, TabsTrigger, TabsContent };
diff --git a/src/lib/models/user.model.ts b/src/lib/models/user.model.ts
new file mode 100644
index 0000000..2c3fa35
--- /dev/null
+++ b/src/lib/models/user.model.ts
@@ -0,0 +1,36 @@
+import mongoose from "mongoose";
+
+const userSchema = new mongoose.Schema(
+ {
+ email: {
+ type: String,
+ required: true,
+ unique: true,
+ lowercase: true,
+ trim: true,
+ },
+ password: {
+ type: String,
+ required: true,
+ },
+ roles: {
+ type: [String],
+ default: ["ROLE_USER"],
+ },
+ authenticated: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+// Middleware pour mettre à jour le champ updatedAt avant la sauvegarde
+userSchema.pre("save", function (next) {
+ this.updatedAt = new Date();
+ next();
+});
+
+export const UserModel = mongoose.models.User || mongoose.model("User", userSchema);
diff --git a/src/lib/services/auth.service.ts b/src/lib/services/auth.service.ts
index f331255..01f8c1e 100644
--- a/src/lib/services/auth.service.ts
+++ b/src/lib/services/auth.service.ts
@@ -1,3 +1,5 @@
+"use client";
+
import { AuthError } from "@/types/auth";
import { storageService } from "./storage.service";
@@ -7,19 +9,6 @@ interface AuthUser {
roles: string[];
authenticated: boolean;
}
-
-// Utilisateur de développement
-const DEV_USER = {
- email: "demo@stripstream.local",
- password: "demo123",
- userData: {
- id: "1",
- email: "demo@stripstream.local",
- roles: ["ROLE_USER"],
- authenticated: true,
- } as AuthUser,
-};
-
class AuthService {
private static instance: AuthService;
@@ -36,23 +25,79 @@ class AuthService {
* Authentifie un utilisateur
*/
async login(email: string, password: string, remember: boolean = false): Promise {
- // En développement, on vérifie juste l'utilisateur de démo
- if (email === DEV_USER.email && password === DEV_USER.password) {
- storageService.setUserData(DEV_USER.userData, remember);
- return;
- }
+ try {
+ const response = await fetch("/api/auth/login", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email, password, remember }),
+ });
- throw {
- code: "INVALID_CREDENTIALS",
- message: "Email ou mot de passe incorrect",
- } as AuthError;
+ if (!response.ok) {
+ const data = await response.json();
+ throw data.error;
+ }
+
+ const data = await response.json();
+ if (data.user) {
+ storageService.setUserData(data.user, remember);
+ }
+ } catch (error) {
+ if ((error as AuthError).code) {
+ throw error;
+ }
+ throw {
+ code: "SERVER_ERROR",
+ message: "Une erreur est survenue lors de la connexion",
+ } as AuthError;
+ }
+ }
+
+ /**
+ * Crée un nouvel utilisateur
+ */
+ async register(email: string, password: string): Promise {
+ 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;
+ }
+
+ const data = await response.json();
+ if (data.user) {
+ storageService.setUserData(data.user, false);
+ }
+ } catch (error) {
+ if ((error as AuthError).code) {
+ throw error;
+ }
+ throw {
+ code: "SERVER_ERROR",
+ message: "Une erreur est survenue lors de l'inscription",
+ } as AuthError;
+ }
}
/**
* Déconnecte l'utilisateur
*/
- logout(): void {
- storageService.clear();
+ async logout(): Promise {
+ try {
+ await fetch("/api/auth/logout", {
+ method: "POST",
+ });
+ } finally {
+ storageService.clear();
+ }
}
/**
diff --git a/src/middleware.ts b/src/middleware.ts
index 4cc6cff..4b8653f 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -14,14 +14,15 @@ export function middleware(request: NextRequest) {
if (
publicRoutes.includes(pathname) ||
publicApiRoutes.includes(pathname) ||
- pathname.startsWith("/images/")
+ pathname.startsWith("/images/") ||
+ pathname.startsWith("/_next/")
) {
return NextResponse.next();
}
// Pour toutes les routes protégées, vérifier la présence de l'utilisateur
const user = request.cookies.get("stripUser");
- if (!user) {
+ if (!user || !user.value) {
if (pathname.startsWith("/api/")) {
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
}
@@ -32,13 +33,11 @@ export function middleware(request: NextRequest) {
try {
const userData = JSON.parse(atob(user.value));
- if (!userData.authenticated) {
- if (pathname.startsWith("/api/")) {
- return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
- }
- throw new Error("User not authenticated");
+ if (!userData || !userData.authenticated || !userData.id || !userData.email) {
+ throw new Error("Invalid user data");
}
} catch (error) {
+ console.error("Erreur de validation du cookie:", error);
if (pathname.startsWith("/api/")) {
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
}
@@ -59,7 +58,7 @@ export const config = {
* 2. /_next/* (Next.js internals)
* 3. /fonts/* (inside public directory)
* 4. /images/* (inside public directory)
- * 5. /favicon.ico, /sitemap.xml (public files)
+ * 5. /favicon.ico, sitemap.xml (public files)
*/
"/((?!api/auth|_next/static|_next/image|fonts|images|favicon.ico|sitemap.xml).*)",
],
diff --git a/src/types/auth.ts b/src/types/auth.ts
index f332763..7397e6e 100644
--- a/src/types/auth.ts
+++ b/src/types/auth.ts
@@ -19,11 +19,4 @@ export interface AuthError {
message: string;
}
-export type AuthErrorCode =
- | "INVALID_CREDENTIALS"
- | "INVALID_SERVER_URL"
- | "SERVER_UNREACHABLE"
- | "NETWORK_ERROR"
- | "UNKNOWN_ERROR"
- | "CACHE_CLEAR_ERROR"
- | "TEST_CONNECTION_ERROR";
+export type AuthErrorCode = "INVALID_CREDENTIALS" | "SERVER_ERROR" | "EMAIL_EXISTS";