Add authentication system with next-auth (CredentialsProvider + JWT)
- Login page with email/password - Auth middleware (proxy) protecting all routes - Seed endpoint for admin user creation (admin@optiquestock.com / admin123) - Session provider wrapping root layout - User info + logout button in header - Updated POS sales route to track authenticated user
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -42,6 +42,7 @@
|
|||||||
"@tanstack/react-query": "^5.82.0",
|
"@tanstack/react-query": "^5.82.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"bun-types": "^1.3.4",
|
"bun-types": "^1.3.4",
|
||||||
@@ -676,6 +678,8 @@
|
|||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
|
||||||
|
|
||||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||||
|
|
||||||
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||||
@@ -832,6 +836,8 @@
|
|||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg=="],
|
||||||
|
|
||||||
|
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|
||||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"@tanstack/react-query": "^5.82.0",
|
"@tanstack/react-query": "^5.82.0",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/bcryptjs": "^3.0.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"bun-types": "^1.3.4",
|
"bun-types": "^1.3.4",
|
||||||
|
|||||||
BIN
prisma/dev.db
BIN
prisma/dev.db
Binary file not shown.
6
src/app/api/auth/[...nextauth]/route.ts
Normal file
6
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import NextAuth from 'next-auth'
|
||||||
|
import { authOptions } from '@/lib/auth'
|
||||||
|
|
||||||
|
const handler = NextAuth(authOptions)
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST }
|
||||||
36
src/app/api/auth/seed/route.ts
Normal file
36
src/app/api/auth/seed/route.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
import { db } from '@/lib/db'
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
const existing = await db.employe.findUnique({
|
||||||
|
where: { email: 'admin@optiquestock.com' },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return NextResponse.json({ message: 'Admin already exists' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash('admin123', 12)
|
||||||
|
|
||||||
|
const employe = await db.employe.create({
|
||||||
|
data: {
|
||||||
|
email: 'admin@optiquestock.com',
|
||||||
|
nom: 'Admin',
|
||||||
|
prenom: 'Admin',
|
||||||
|
role: 'ADMIN',
|
||||||
|
motDePasse: hashedPassword,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: 'Admin user created',
|
||||||
|
email: employe.email,
|
||||||
|
password: 'admin123',
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Seed error:', error)
|
||||||
|
return NextResponse.json({ error: 'Seed failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { db } from '@/lib/db'
|
import { db } from '@/lib/db'
|
||||||
import { StatutVente, ModePaiement } from '@prisma/client'
|
import { StatutVente, ModePaiement } from '@prisma/client'
|
||||||
|
import { getCurrentUserId, requireAuth } from '@/lib/auth-utils'
|
||||||
|
|
||||||
// Helper function to generate sale number
|
// Helper function to generate sale number
|
||||||
async function generateSaleNumber(): Promise<string> {
|
async function generateSaleNumber(): Promise<string> {
|
||||||
@@ -55,6 +56,9 @@ export async function GET(request: NextRequest) {
|
|||||||
// POST /api/pos/sales - Create a new sale
|
// POST /api/pos/sales - Create a new sale
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
const authError = await requireAuth()
|
||||||
|
if (authError) return authError
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const {
|
const {
|
||||||
clientId,
|
clientId,
|
||||||
@@ -103,6 +107,9 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
const employeId = await getCurrentUserId()
|
||||||
|
|
||||||
// Generate sale number
|
// Generate sale number
|
||||||
const numero = await generateSaleNumber()
|
const numero = await generateSaleNumber()
|
||||||
|
|
||||||
@@ -158,7 +165,7 @@ export async function POST(request: NextRequest) {
|
|||||||
montant: paiement.montant,
|
montant: paiement.montant,
|
||||||
reference: paiement.reference || null,
|
reference: paiement.reference || null,
|
||||||
notes: paiement.notes || null,
|
notes: paiement.notes || null,
|
||||||
employeId: null // Can be updated with authentication
|
employeId
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
import SessionProvider from "@/components/auth/SessionProvider";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -14,25 +15,8 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Z.ai Code Scaffold - AI-Powered Development",
|
title: "OptiqueStock - Gestion de Magasin d'Optique",
|
||||||
description: "Modern Next.js scaffold optimized for AI-powered development with Z.ai. Built with TypeScript, Tailwind CSS, and shadcn/ui.",
|
description: "Application de gestion de magasin d'optique : clients, produits, ventes, achats, atelier et rapports.",
|
||||||
keywords: ["Z.ai", "Next.js", "TypeScript", "Tailwind CSS", "shadcn/ui", "AI development", "React"],
|
|
||||||
authors: [{ name: "Z.ai Team" }],
|
|
||||||
icons: {
|
|
||||||
icon: "https://z-cdn.chatglm.cn/z-ai/static/logo.svg",
|
|
||||||
},
|
|
||||||
openGraph: {
|
|
||||||
title: "Z.ai Code Scaffold",
|
|
||||||
description: "AI-powered development with modern React stack",
|
|
||||||
url: "https://chat.z.ai",
|
|
||||||
siteName: "Z.ai",
|
|
||||||
type: "website",
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image",
|
|
||||||
title: "Z.ai Code Scaffold",
|
|
||||||
description: "AI-powered development with modern React stack",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -41,11 +25,13 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="fr" suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
||||||
>
|
>
|
||||||
{children}
|
<SessionProvider>
|
||||||
|
{children}
|
||||||
|
</SessionProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
88
src/app/login/page.tsx
Normal file
88
src/app/login/page.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { signIn } from 'next-auth/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { Eye } from 'lucide-react'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const result = await signIn('credentials', {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
redirect: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
setError('Email ou mot de passe incorrect')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push('/')
|
||||||
|
router.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100">
|
||||||
|
<Card className="w-full max-w-md mx-4">
|
||||||
|
<CardHeader className="text-center space-y-2">
|
||||||
|
<div className="flex justify-center mb-2">
|
||||||
|
<div className="p-3 bg-primary rounded-lg">
|
||||||
|
<Eye className="h-8 w-8 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-2xl">OptiqueStock</CardTitle>
|
||||||
|
<CardDescription>Connectez-vous pour accéder à l'application</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="email">Email</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="admin@optiquestock.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">Mot de passe</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm text-red-500 text-center">{error}</p>
|
||||||
|
)}
|
||||||
|
<Button type="submit" className="w-full" disabled={loading}>
|
||||||
|
{loading ? 'Connexion...' : 'Se connecter'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useSession, signOut } from 'next-auth/react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
@@ -13,7 +15,9 @@ import {
|
|||||||
BarChart3,
|
BarChart3,
|
||||||
Eye,
|
Eye,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Wrench
|
Wrench,
|
||||||
|
LogOut,
|
||||||
|
User
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import POSModule from '@/components/pos/POSModule'
|
import POSModule from '@/components/pos/POSModule'
|
||||||
import { ProduitListe } from '@/components/products/ProduitListe'
|
import { ProduitListe } from '@/components/products/ProduitListe'
|
||||||
@@ -90,8 +94,15 @@ const modules: ModuleCard[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const router = useRouter()
|
||||||
const [currentModule, setCurrentModule] = useState<Module>('HOME')
|
const [currentModule, setCurrentModule] = useState<Module>('HOME')
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
router.push('/login')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const renderModule = () => {
|
const renderModule = () => {
|
||||||
if (currentModule === 'HOME') {
|
if (currentModule === 'HOME') {
|
||||||
return (
|
return (
|
||||||
@@ -347,9 +358,18 @@ export default function Home() {
|
|||||||
<Badge variant="outline" className="text-sm">
|
<Badge variant="outline" className="text-sm">
|
||||||
v1.0.0
|
v1.0.0
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className="h-8 w-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground text-sm font-medium">
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||||
OP
|
<User className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">{session?.user?.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||||
|
title="Se déconnecter"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
src/components/auth/SessionProvider.tsx
Normal file
7
src/components/auth/SessionProvider.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react'
|
||||||
|
|
||||||
|
export default function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>
|
||||||
|
}
|
||||||
20
src/lib/auth-utils.ts
Normal file
20
src/lib/auth-utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { getServerSession } from 'next-auth'
|
||||||
|
import { authOptions } from './auth'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function getSession() {
|
||||||
|
return await getServerSession(authOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentUserId(): Promise<string | null> {
|
||||||
|
const session = await getSession()
|
||||||
|
return (session?.user as any)?.id || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requireAuth() {
|
||||||
|
const session = await getSession()
|
||||||
|
if (!session?.user) {
|
||||||
|
return NextResponse.json({ error: 'Non authentifié' }, { status: 401 })
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
58
src/lib/auth.ts
Normal file
58
src/lib/auth.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { NextAuthOptions } from 'next-auth'
|
||||||
|
import CredentialsProvider from 'next-auth/providers/credentials'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
import { db } from './db'
|
||||||
|
|
||||||
|
export const authOptions: NextAuthOptions = {
|
||||||
|
providers: [
|
||||||
|
CredentialsProvider({
|
||||||
|
name: 'credentials',
|
||||||
|
credentials: {
|
||||||
|
email: { label: 'Email', type: 'email' },
|
||||||
|
password: { label: 'Mot de passe', type: 'password' },
|
||||||
|
},
|
||||||
|
async authorize(credentials) {
|
||||||
|
if (!credentials?.email || !credentials?.password) return null
|
||||||
|
|
||||||
|
const employe = await db.employe.findUnique({
|
||||||
|
where: { email: credentials.email },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!employe || !employe.actif) return null
|
||||||
|
|
||||||
|
const isValid = await bcrypt.compare(credentials.password, employe.motDePasse)
|
||||||
|
if (!isValid) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: employe.id,
|
||||||
|
email: employe.email,
|
||||||
|
name: `${employe.prenom} ${employe.nom}`,
|
||||||
|
role: employe.role,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
callbacks: {
|
||||||
|
async jwt({ token, user }) {
|
||||||
|
if (user) {
|
||||||
|
token.id = user.id
|
||||||
|
token.role = (user as any).role
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
},
|
||||||
|
async session({ session, token }) {
|
||||||
|
if (session.user) {
|
||||||
|
(session.user as any).id = token.id
|
||||||
|
(session.user as any).role = token.role
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
signIn: '/login',
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
strategy: 'jwt',
|
||||||
|
},
|
||||||
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
|
}
|
||||||
30
src/proxy.ts
Normal file
30
src/proxy.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { getToken } from 'next-auth/jwt'
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function proxy(request: NextRequest) {
|
||||||
|
const { pathname } = request.nextUrl
|
||||||
|
|
||||||
|
if (
|
||||||
|
pathname.startsWith('/login') ||
|
||||||
|
pathname.startsWith('/api/auth') ||
|
||||||
|
pathname.startsWith('/_next/static') ||
|
||||||
|
pathname.startsWith('/_next/image') ||
|
||||||
|
pathname === '/favicon.ico'
|
||||||
|
) {
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET })
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
const loginUrl = new URL('/login', request.url)
|
||||||
|
loginUrl.searchParams.set('callbackUrl', pathname)
|
||||||
|
return NextResponse.redirect(loginUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user