diff --git a/bun.lock b/bun.lock index 599cee8..d180947 100644 --- a/bun.lock +++ b/bun.lock @@ -42,6 +42,7 @@ "@tanstack/react-query": "^5.82.0", "@tanstack/react-table": "^8.21.3", "@types/qrcode": "^1.5.6", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -76,6 +77,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^3.0.0", "@types/react": "^19", "@types/react-dom": "^19", "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=="], + "@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-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=="], + "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=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], diff --git a/package.json b/package.json index c5acd88..fea8893 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@tanstack/react-query": "^5.82.0", "@tanstack/react-table": "^8.21.3", "@types/qrcode": "^1.5.6", + "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -84,6 +85,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", + "@types/bcryptjs": "^3.0.0", "@types/react": "^19", "@types/react-dom": "^19", "bun-types": "^1.3.4", diff --git a/prisma/dev.db b/prisma/dev.db index 0ce3c52..86c4ce2 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..e10628f --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -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 } diff --git a/src/app/api/auth/seed/route.ts b/src/app/api/auth/seed/route.ts new file mode 100644 index 0000000..0d2c93e --- /dev/null +++ b/src/app/api/auth/seed/route.ts @@ -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 }) + } +} diff --git a/src/app/api/pos/sales/route.ts b/src/app/api/pos/sales/route.ts index 673b25f..97457ed 100644 --- a/src/app/api/pos/sales/route.ts +++ b/src/app/api/pos/sales/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { db } from '@/lib/db' import { StatutVente, ModePaiement } from '@prisma/client' +import { getCurrentUserId, requireAuth } from '@/lib/auth-utils' // Helper function to generate sale number async function generateSaleNumber(): Promise { @@ -55,6 +56,9 @@ export async function GET(request: NextRequest) { // POST /api/pos/sales - Create a new sale export async function POST(request: NextRequest) { try { + const authError = await requireAuth() + if (authError) return authError + const body = await request.json() const { clientId, @@ -103,6 +107,9 @@ export async function POST(request: NextRequest) { } } + // Get current user + const employeId = await getCurrentUserId() + // Generate sale number const numero = await generateSaleNumber() @@ -158,7 +165,7 @@ export async function POST(request: NextRequest) { montant: paiement.montant, reference: paiement.reference || null, notes: paiement.notes || null, - employeId: null // Can be updated with authentication + employeId } }) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e9179cf..322271d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { Toaster } from "@/components/ui/toaster"; +import SessionProvider from "@/components/auth/SessionProvider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -14,25 +15,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Z.ai Code Scaffold - AI-Powered Development", - description: "Modern Next.js scaffold optimized for AI-powered development with Z.ai. Built with TypeScript, Tailwind CSS, and shadcn/ui.", - 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", - }, + title: "OptiqueStock - Gestion de Magasin d'Optique", + description: "Application de gestion de magasin d'optique : clients, produits, ventes, achats, atelier et rapports.", }; export default function RootLayout({ @@ -41,11 +25,13 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - {children} + + {children} + diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..2914d5d --- /dev/null +++ b/src/app/login/page.tsx @@ -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 ( +
+ + +
+
+ +
+
+ OptiqueStock + Connectez-vous pour accéder à l'application +
+ +
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ {error && ( +

{error}

+ )} + +
+
+
+
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 975be0a..787bcb3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,8 @@ 'use client' 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 { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -13,7 +15,9 @@ import { BarChart3, Eye, LayoutDashboard, - Wrench + Wrench, + LogOut, + User } from 'lucide-react' import POSModule from '@/components/pos/POSModule' import { ProduitListe } from '@/components/products/ProduitListe' @@ -90,8 +94,15 @@ const modules: ModuleCard[] = [ ] export default function Home() { + const { data: session } = useSession() + const router = useRouter() const [currentModule, setCurrentModule] = useState('HOME') + if (!session) { + router.push('/login') + return null + } + const renderModule = () => { if (currentModule === 'HOME') { return ( @@ -347,9 +358,18 @@ export default function Home() { v1.0.0 -
- OP +
+ + {session?.user?.name}
+
diff --git a/src/components/auth/SessionProvider.tsx b/src/components/auth/SessionProvider.tsx new file mode 100644 index 0000000..bdac00a --- /dev/null +++ b/src/components/auth/SessionProvider.tsx @@ -0,0 +1,7 @@ +'use client' + +import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react' + +export default function SessionProvider({ children }: { children: React.ReactNode }) { + return {children} +} diff --git a/src/lib/auth-utils.ts b/src/lib/auth-utils.ts new file mode 100644 index 0000000..dc828cc --- /dev/null +++ b/src/lib/auth-utils.ts @@ -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 { + 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 +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..bf62679 --- /dev/null +++ b/src/lib/auth.ts @@ -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, +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..bddf5c9 --- /dev/null +++ b/src/proxy.ts @@ -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).*)'], +}