feat(db): Login register and auth
This commit is contained in:
30
package-lock.json
generated
30
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
"@types/mongoose": "^5.11.97",
|
"@types/mongoose": "^5.11.97",
|
||||||
"class-variance-authority": "^0.7.1",
|
"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": {
|
"node_modules/@radix-ui/react-toast": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
"@types/mongoose": "^5.11.97",
|
"@types/mongoose": "^5.11.97",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
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";
|
export const metadata: Metadata = {
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
title: "Connexion",
|
||||||
import { authService } from "@/lib/services/auth.service";
|
description: "Connectez-vous à votre compte StripStream",
|
||||||
import { AuthError } from "@/types/auth";
|
|
||||||
|
|
||||||
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 (
|
export default function LoginPage({
|
||||||
<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">
|
searchParams,
|
||||||
<div className="relative hidden h-full flex-col bg-slate-800/80 p-10 text-white lg:flex dark:border-r overflow-hidden">
|
}: {
|
||||||
<div
|
searchParams: { from?: string; tab?: string };
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-40 transition-opacity duration-200 ease-in-out"
|
}) {
|
||||||
style={{
|
return <LoginContent searchParams={searchParams} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
102
src/components/auth/LoginForm.tsx
Normal file
102
src/components/auth/LoginForm.tsx
Normal file
@@ -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<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);
|
||||||
|
router.push(from || "/");
|
||||||
|
} catch (error) {
|
||||||
|
setError(error as AuthError);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
110
src/components/auth/RegisterForm.tsx
Normal file
110
src/components/auth/RegisterForm.tsx
Normal file
@@ -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<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 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 (
|
||||||
|
<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
|
||||||
|
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="new-password"
|
||||||
|
required
|
||||||
|
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="confirmPassword"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Confirmer le mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
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>
|
||||||
|
{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 ? "Inscription en cours..." : "S'inscrire"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { FavoriteService } from "@/lib/services/favorite.service";
|
import { FavoriteService } from "@/lib/services/favorite.service";
|
||||||
import { LibraryService } from "@/lib/services/library.service";
|
import { LibraryService } from "@/lib/services/library.service";
|
||||||
import { ClientSidebar } from "./ClientSidebar";
|
|
||||||
|
|
||||||
export async function SidebarWrapper() {
|
export async function SidebarWrapper() {
|
||||||
// Récupérer les favoris depuis le serveur
|
// Récupérer les favoris depuis le serveur
|
||||||
|
|||||||
54
src/components/ui/tabs.tsx
Normal file
54
src/components/ui/tabs.tsx
Normal file
@@ -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<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
36
src/lib/models/user.model.ts
Normal file
36
src/lib/models/user.model.ts
Normal file
@@ -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);
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { AuthError } from "@/types/auth";
|
import { AuthError } from "@/types/auth";
|
||||||
import { storageService } from "./storage.service";
|
import { storageService } from "./storage.service";
|
||||||
|
|
||||||
@@ -7,19 +9,6 @@ interface AuthUser {
|
|||||||
roles: string[];
|
roles: string[];
|
||||||
authenticated: boolean;
|
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 {
|
class AuthService {
|
||||||
private static instance: AuthService;
|
private static instance: AuthService;
|
||||||
|
|
||||||
@@ -36,24 +25,80 @@ class AuthService {
|
|||||||
* Authentifie un utilisateur
|
* Authentifie un utilisateur
|
||||||
*/
|
*/
|
||||||
async login(email: string, password: string, remember: boolean = false): Promise<void> {
|
async login(email: string, password: string, remember: boolean = false): Promise<void> {
|
||||||
// En développement, on vérifie juste l'utilisateur de démo
|
try {
|
||||||
if (email === DEV_USER.email && password === DEV_USER.password) {
|
const response = await fetch("/api/auth/login", {
|
||||||
storageService.setUserData(DEV_USER.userData, remember);
|
method: "POST",
|
||||||
return;
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, password, remember }),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
throw {
|
||||||
code: "INVALID_CREDENTIALS",
|
code: "SERVER_ERROR",
|
||||||
message: "Email ou mot de passe incorrect",
|
message: "Une erreur est survenue lors de la connexion",
|
||||||
} as AuthError;
|
} as AuthError;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
* Déconnecte l'utilisateur
|
||||||
*/
|
*/
|
||||||
logout(): void {
|
async logout(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fetch("/api/auth/logout", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
storageService.clear();
|
storageService.clear();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si l'utilisateur est connecté
|
* Vérifie si l'utilisateur est connecté
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ export function middleware(request: NextRequest) {
|
|||||||
if (
|
if (
|
||||||
publicRoutes.includes(pathname) ||
|
publicRoutes.includes(pathname) ||
|
||||||
publicApiRoutes.includes(pathname) ||
|
publicApiRoutes.includes(pathname) ||
|
||||||
pathname.startsWith("/images/")
|
pathname.startsWith("/images/") ||
|
||||||
|
pathname.startsWith("/_next/")
|
||||||
) {
|
) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pour toutes les routes protégées, vérifier la présence de l'utilisateur
|
// Pour toutes les routes protégées, vérifier la présence de l'utilisateur
|
||||||
const user = request.cookies.get("stripUser");
|
const user = request.cookies.get("stripUser");
|
||||||
if (!user) {
|
if (!user || !user.value) {
|
||||||
if (pathname.startsWith("/api/")) {
|
if (pathname.startsWith("/api/")) {
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
}
|
}
|
||||||
@@ -32,13 +33,11 @@ export function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const userData = JSON.parse(atob(user.value));
|
const userData = JSON.parse(atob(user.value));
|
||||||
if (!userData.authenticated) {
|
if (!userData || !userData.authenticated || !userData.id || !userData.email) {
|
||||||
if (pathname.startsWith("/api/")) {
|
throw new Error("Invalid user data");
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
|
||||||
}
|
|
||||||
throw new Error("User not authenticated");
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Erreur de validation du cookie:", error);
|
||||||
if (pathname.startsWith("/api/")) {
|
if (pathname.startsWith("/api/")) {
|
||||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
}
|
}
|
||||||
@@ -59,7 +58,7 @@ export const config = {
|
|||||||
* 2. /_next/* (Next.js internals)
|
* 2. /_next/* (Next.js internals)
|
||||||
* 3. /fonts/* (inside public directory)
|
* 3. /fonts/* (inside public directory)
|
||||||
* 4. /images/* (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).*)",
|
"/((?!api/auth|_next/static|_next/image|fonts|images|favicon.ico|sitemap.xml).*)",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,11 +19,4 @@ export interface AuthError {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthErrorCode =
|
export type AuthErrorCode = "INVALID_CREDENTIALS" | "SERVER_ERROR" | "EMAIL_EXISTS";
|
||||||
| "INVALID_CREDENTIALS"
|
|
||||||
| "INVALID_SERVER_URL"
|
|
||||||
| "SERVER_UNREACHABLE"
|
|
||||||
| "NETWORK_ERROR"
|
|
||||||
| "UNKNOWN_ERROR"
|
|
||||||
| "CACHE_CLEAR_ERROR"
|
|
||||||
| "TEST_CONNECTION_ERROR";
|
|
||||||
|
|||||||
Reference in New Issue
Block a user