feat(db): Login register and auth
This commit is contained in:
58
src/app/api/auth/login/route.ts
Normal file
58
src/app/api/auth/login/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
9
src/app/api/auth/logout/route.ts
Normal file
9
src/app/api/auth/logout/route.ts
Normal file
@@ -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" });
|
||||
}
|
||||
66
src/app/api/auth/register/route.ts
Normal file
66
src/app/api/auth/register/route.ts
Normal file
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
75
src/app/login/LoginContent.tsx
Normal file
75
src/app/login/LoginContent.tsx
Normal file
@@ -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 (
|
||||
<div className="container relative min-h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div className="relative hidden h-full flex-col bg-slate-800/80 p-10 text-white lg:flex dark:border-r overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-40 transition-opacity duration-200 ease-in-out"
|
||||
style={{
|
||||
backgroundImage: "url('/images/login-bg.jpg')",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-slate-800/20 to-slate-800/70" />
|
||||
<div className="relative z-20 flex items-center text-lg font-medium">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-6 w-6"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
StripStream
|
||||
</div>
|
||||
<div className="relative z-20 mt-auto">
|
||||
<blockquote className="space-y-2">
|
||||
<p className="text-lg">
|
||||
Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne
|
||||
et fluide.
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:p-8">
|
||||
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Bienvenue sur StripStream</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Connectez-vous ou créez un compte pour commencer
|
||||
</p>
|
||||
</div>
|
||||
<Tabs defaultValue={defaultTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="login">Connexion</TabsTrigger>
|
||||
<TabsTrigger value="register">Inscription</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="login">
|
||||
<LoginForm from={searchParams.from} />
|
||||
</TabsContent>
|
||||
<TabsContent value="register">
|
||||
<RegisterForm from={searchParams.from} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<AuthError | null>(null);
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
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 (
|
||||
<div className="container relative min-h-[calc(100vh)] flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||
<div className="relative hidden h-full flex-col bg-slate-800/80 p-10 text-white lg:flex dark:border-r overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-40 transition-opacity duration-200 ease-in-out"
|
||||
style={{
|
||||
backgroundImage: "url('/images/login-bg.jpg')",
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-slate-800/20 to-slate-800/70" />
|
||||
<div className="relative z-20 flex items-center text-lg font-medium">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-6 w-6"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
Stripstream
|
||||
</div>
|
||||
<div className="relative z-20 mt-auto">
|
||||
<blockquote className="space-y-2">
|
||||
<p className="text-lg">
|
||||
Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne
|
||||
et fluide.
|
||||
</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:p-8">
|
||||
<div className="mx-auto flex w-full flex-col justify-center space-y-8 sm:w-[350px]">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-primary to-indigo-600 rounded-full blur opacity-70"></div>
|
||||
<div className="relative bg-background rounded-full p-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="h-12 w-12 text-primary"
|
||||
>
|
||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 text-center">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary to-indigo-600 bg-clip-text text-transparent">
|
||||
Stripstream
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">Votre bibliothèque numérique de BD</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Connexion</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Connectez-vous pour accéder à votre bibliothèque
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
defaultValue="demo@stripstream.local"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
defaultValue="demo123"
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
id="remember"
|
||||
name="remember"
|
||||
type="checkbox"
|
||||
defaultChecked
|
||||
className="h-4 w-4 rounded border border-input ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
/>
|
||||
<label
|
||||
htmlFor="remember"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Se souvenir de moi
|
||||
</label>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error.message}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="inline-flex w-full items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? "Connexion en cours..." : "Se connecter"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<div className="flex items-center justify-center min-h-screen">Chargement...</div>}
|
||||
>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
);
|
||||
export default function LoginPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { from?: string; tab?: string };
|
||||
}) {
|
||||
return <LoginContent searchParams={searchParams} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user