feat: add authentication system with NextAuth integration
This commit is contained in:
38
apps/fabrikanabytok/app/(auth)/login/page.tsx
Normal file
38
apps/fabrikanabytok/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { LoginForm } from "@/components/auth/login-form"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-brand-50 via-white to-brand-50 p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-block mb-6">
|
||||
<div className="text-3xl font-bold text-brand-600">FABRIKA NABYTOK</div>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">Bejelentkezés</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Jelentkezzen be fiókjába a folytatáshoz
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/80 mt-2">
|
||||
Ügyfelek, munkatársak és adminisztrátorok számára
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LoginForm />
|
||||
|
||||
<div className="mt-6 space-y-3">
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
Még nincs fiókja?{" "}
|
||||
<Link href="/register" className="text-brand-600 hover:underline font-medium">
|
||||
Regisztráljon itt
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="text-center text-xs text-muted-foreground/70 pt-2 border-t border-muted">
|
||||
<p>A bejelentkezés után automatikusan átirányítjuk Önt a megfelelő felületre.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
apps/fabrikanabytok/app/(auth)/register/page.tsx
Normal file
27
apps/fabrikanabytok/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { RegisterForm } from "@/components/auth/register-form"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function RegisterPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-brand-50 via-white to-brand-50 p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-block mb-6">
|
||||
<div className="text-3xl font-bold text-brand-600">FABRIKA NABYTOK</div>
|
||||
</Link>
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">Regisztráció</h1>
|
||||
<p className="text-muted-foreground">Hozzon létre új fiókot a kezdéshez</p>
|
||||
</div>
|
||||
|
||||
<RegisterForm />
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground mt-6">
|
||||
Már van fiókja?{" "}
|
||||
<Link href="/login" className="text-brand-600 hover:underline font-medium">
|
||||
Jelentkezzen be itt
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
apps/fabrikanabytok/app/(auth)/setup-wizard/page.tsx
Normal file
31
apps/fabrikanabytok/app/(auth)/setup-wizard/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { isSystemInitialized } from "@/lib/db/setup"
|
||||
import { redirect } from "next/navigation"
|
||||
import { SetupWizardForm } from "@/components/auth/setup-wizard-form"
|
||||
|
||||
export default async function SetupWizardPage() {
|
||||
const initialized = await isSystemInitialized()
|
||||
|
||||
if (initialized) {
|
||||
redirect("/")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-brand-50 via-white to-brand-50 p-4">
|
||||
<div className="w-full max-w-2xl">
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 bg-brand-600 rounded-2xl mb-4">
|
||||
<svg className="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-foreground mb-2">Üdvözöljük a FABRIKA NABYTOK rendszerben</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
A kezdéshez konfigurálja a rendszert és hozza létre a superadmin fiókot
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SetupWizardForm />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
3
apps/fabrikanabytok/app/api/auth/[...nextauth]/route.ts
Normal file
3
apps/fabrikanabytok/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/lib/auth/auth"
|
||||
|
||||
export const { GET, POST } = handlers
|
||||
119
apps/fabrikanabytok/components/auth/login-form.tsx
Normal file
119
apps/fabrikanabytok/components/auth/login-form.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Loader2, AlertCircle, CheckCircle2 } from "lucide-react"
|
||||
import { loginAction } from "@/lib/actions/auth.actions"
|
||||
|
||||
export function LoginForm() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const setupSuccess = searchParams.get("setup") === "success"
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
|
||||
const formData = new FormData(e.currentTarget)
|
||||
|
||||
try {
|
||||
const result = await loginAction(formData)
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.message)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect based on callback URL or user role
|
||||
const callbackUrl = searchParams.get("callbackUrl")
|
||||
|
||||
if (callbackUrl) {
|
||||
router.push(callbackUrl)
|
||||
} else if (result.redirectTo) {
|
||||
// Use the role-based redirect from the server
|
||||
router.push(result.redirectTo)
|
||||
} else {
|
||||
// Fallback to home
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
router.refresh()
|
||||
} catch (err) {
|
||||
setError("Hiba történt a bejelentkezés során")
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Üdvözöljük vissza</CardTitle>
|
||||
<CardDescription>Adja meg bejelentkezési adatait</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{setupSuccess && (
|
||||
<div className="flex items-center gap-2 p-3 mb-4 bg-brand-50 border border-brand-200 rounded-lg text-brand-700">
|
||||
<CheckCircle2 className="w-5 h-5 flex-shrink-0" />
|
||||
<p className="text-sm">Rendszer sikeresen inicializálva! Jelentkezzen be.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email cím</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="pelda@email.hu"
|
||||
disabled={loading}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Jelszó</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
disabled={loading}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive">
|
||||
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||
<p className="text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Bejelentkezés...
|
||||
</>
|
||||
) : (
|
||||
"Bejelentkezés"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
149
apps/fabrikanabytok/components/auth/register-form.tsx
Normal file
149
apps/fabrikanabytok/components/auth/register-form.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Loader2, AlertCircle } from "lucide-react"
|
||||
import { registerAction } from "@/lib/actions/auth.actions"
|
||||
|
||||
export function RegisterForm() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
|
||||
const formData = new FormData(e.currentTarget)
|
||||
const password = formData.get("password") as string
|
||||
const confirmPassword = formData.get("confirmPassword") as string
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("A jelszavak nem egyeznek")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await registerAction(formData)
|
||||
|
||||
if (!result.success) {
|
||||
setError(result.message)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect to profile after successful registration
|
||||
router.push("/profile")
|
||||
router.refresh()
|
||||
} catch (err) {
|
||||
setError("Hiba történt a regisztráció során")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Új fiók létrehozása</CardTitle>
|
||||
<CardDescription>Töltse ki az alábbi mezőket a regisztrációhoz</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">Keresztnév</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
required
|
||||
placeholder="János"
|
||||
disabled={loading}
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Vezetéknév</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Kovács"
|
||||
disabled={loading}
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email cím</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="pelda@email.hu"
|
||||
disabled={loading}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Jelszó</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Minimum 8 karakter"
|
||||
disabled={loading}
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Jelszó megerősítése</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Írja be újra a jelszót"
|
||||
disabled={loading}
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive">
|
||||
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||
<p className="text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" size="lg" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Regisztráció...
|
||||
</>
|
||||
) : (
|
||||
"Fiók létrehozása"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
245
apps/fabrikanabytok/components/auth/setup-wizard-form.tsx
Normal file
245
apps/fabrikanabytok/components/auth/setup-wizard-form.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"
|
||||
|
||||
export function SetupWizardForm() {
|
||||
const router = useRouter()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [step, setStep] = useState(1)
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
companyName: "",
|
||||
mongoUri: "",
|
||||
})
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value,
|
||||
}))
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError(null)
|
||||
|
||||
// Validation
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError("A jelszavak nem egyeznek")
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.password.length < 8) {
|
||||
setError("A jelszónak legalább 8 karakter hosszúnak kell lennie")
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/setup/initialize", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
firstName: formData.firstName,
|
||||
lastName: formData.lastName,
|
||||
companyName: formData.companyName || undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || "Hiba történt az inicializálás során")
|
||||
}
|
||||
|
||||
// Success - redirect to login
|
||||
router.push("/login?setup=success")
|
||||
} catch (err) {
|
||||
setError((err as Error).message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-2">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex gap-2">
|
||||
{[1, 2].map((s) => (
|
||||
<div
|
||||
key={s}
|
||||
className={`h-2 w-24 rounded-full transition-colors ${s <= step ? "bg-brand-600" : "bg-muted"}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm font-medium text-muted-foreground">{step}/2 lépés</span>
|
||||
</div>
|
||||
<CardTitle className="text-2xl">
|
||||
{step === 1 ? "Superadmin fiók létrehozása" : "Vállalati információk"}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{step === 1
|
||||
? "Hozza létre a fő adminisztrátori fiókot a rendszer kezeléséhez"
|
||||
: "Adja meg a vállalat adatait (opcionális)"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{step === 1 ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">Keresztnév *</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
type="text"
|
||||
required
|
||||
value={formData.firstName}
|
||||
onChange={handleInputChange}
|
||||
placeholder="János"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Vezetéknév *</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
type="text"
|
||||
required
|
||||
value={formData.lastName}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Kovács"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email cím *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
placeholder="admin@pelda.hu"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Jelszó *</Label>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Minimum 8 karakter"
|
||||
disabled={loading}
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Jelszó megerősítése *</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Írja be újra a jelszót"
|
||||
disabled={loading}
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setStep(2)}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
disabled={loading || !formData.email || !formData.password || !formData.firstName || !formData.lastName}
|
||||
>
|
||||
Tovább
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="companyName">Cég neve</Label>
|
||||
<Input
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
type="text"
|
||||
value={formData.companyName}
|
||||
onChange={handleInputChange}
|
||||
placeholder="FABRIKA NABYTOK Kft."
|
||||
disabled={loading}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">Később módosítható a beállításokban</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive">
|
||||
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
||||
<p className="text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setStep(1)}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
size="lg"
|
||||
disabled={loading}
|
||||
>
|
||||
Vissza
|
||||
</Button>
|
||||
<Button type="submit" className="flex-1" size="lg" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Inicializálás...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||
Rendszer indítása
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
285
apps/fabrikanabytok/lib/auth/AUTHENTICATION_FLOW.md
Normal file
285
apps/fabrikanabytok/lib/auth/AUTHENTICATION_FLOW.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# Authentication Flow Diagram
|
||||
|
||||
## Login Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ User Login │
|
||||
│ (All user types use │
|
||||
│ same /login page) │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Submit Email + Password │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Step 1: Check Users Collection │
|
||||
│ db.collection("users").findOne({email}) │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
├─────► User not found ──► Error
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Step 2: Validate Password │
|
||||
│ bcrypt.compare(password, user.password) │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
├─────► Invalid ──► Error
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Step 3: Check Employee Collection │
|
||||
│ db.collection("employees").findOne({userId}) │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
Employee Found Employee NOT Found
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ Update employee │ │ Regular user │
|
||||
│ lastLogin │ │ (customer, etc.) │
|
||||
└──────────┬───────────┘ └──────────┬───────────┘
|
||||
│ │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Step 4: Construct Authenticated User │
|
||||
│ - Basic user data (id, email, name, role) │
|
||||
│ - Employee data if applicable: │
|
||||
│ * isEmployee: true │
|
||||
│ * employeeId, employeeNumber │
|
||||
│ * department, position │
|
||||
│ * permissions │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Step 5: Update Last Login │
|
||||
│ db.collection("users").updateOne( │
|
||||
│ {_id}, {$set: {lastLoginAt: new Date()}} │
|
||||
│ ) │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Step 6: Create JWT Session │
|
||||
│ - Include all user + employee data │
|
||||
│ - 7 day expiry │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Step 7: Determine Redirect │
|
||||
│ getDefaultRedirectForRole(role, isEmployee) │
|
||||
└────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┬──────────────┐
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Employee │ │ Admin │ │Distribut.│ │ Customer │
|
||||
│ /internal│ │ /admin │ │/distribu.│ │ /profile │
|
||||
│/dashboard│ │/dashboard│ │/dashboard│ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## Database Structure
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ USERS Collection │
|
||||
│ (All authenticated users including employees) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ { │
|
||||
│ _id: ObjectId │
|
||||
│ email: "user@example.com" │
|
||||
│ password: "hashed_bcrypt" │
|
||||
│ firstName: "John" │
|
||||
│ lastName: "Doe" │
|
||||
│ role: "warehouse_manager" | "customer" | "admin" | ... │
|
||||
│ emailVerified: true │
|
||||
│ lastLoginAt: Date │
|
||||
│ createdAt: Date │
|
||||
│ updatedAt: Date │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ Linked by userId
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ EMPLOYEES Collection │
|
||||
│ (Additional employee-specific data) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ { │
|
||||
│ _id: ObjectId │
|
||||
│ userId: "user_id_reference" ◄─── References users._id │
|
||||
│ employeeNumber: "EMP-001" │
|
||||
│ role: "warehouse_manager" │
|
||||
│ department: "Logistics" │
|
||||
│ position: "Manager" │
|
||||
│ permissions: ["inventory.view", "orders.pick", ...] │
|
||||
│ status: "active" | "inactive" | "suspended" │
|
||||
│ hireDate: Date │
|
||||
│ lastLogin: Date │
|
||||
│ metrics: {...} │
|
||||
│ schedule: {...} │
|
||||
│ createdAt: Date │
|
||||
│ updatedAt: Date │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Session Structure
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SESSION │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ For Regular User (Customer): │
|
||||
│ { │
|
||||
│ user: { │
|
||||
│ id: "user_123" │
|
||||
│ email: "customer@example.com" │
|
||||
│ firstName: "Jane" │
|
||||
│ lastName: "Smith" │
|
||||
│ role: "customer" │
|
||||
│ avatar: "https://..." │
|
||||
│ emailVerified: true │
|
||||
│ isEmployee: false │
|
||||
│ } │
|
||||
│ } │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ For Employee: │
|
||||
│ { │
|
||||
│ user: { │
|
||||
│ id: "user_456" │
|
||||
│ email: "employee@example.com" │
|
||||
│ firstName: "John" │
|
||||
│ lastName: "Doe" │
|
||||
│ role: "warehouse_manager" │
|
||||
│ avatar: "https://..." │
|
||||
│ emailVerified: true │
|
||||
│ isEmployee: true │
|
||||
│ employeeId: "emp_789" │
|
||||
│ employeeNumber: "EMP-001" │
|
||||
│ department: "Logistics" │
|
||||
│ position: "Manager" │
|
||||
│ permissions: ["inventory.view", "orders.pick", ...] │
|
||||
│ } │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Role-Based Access Control
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Protected Routes │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┬──────────────┐
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
|
||||
/internal/* /admin/* /distributor/* /profile
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│Check: │ │Check: │ │Check: │ │Check: │
|
||||
│isEmployee│ │isAdmin │ │role == │ │Any auth │
|
||||
│== true │ │Role │ │distribut.│ │user │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
Warehouse Dashboard Distributor Personal
|
||||
Inventory Analytics Portal Profile
|
||||
Orders Users Orders Settings
|
||||
Shipping Products Catalogs History
|
||||
Reports Settings Commission Wishlist
|
||||
```
|
||||
|
||||
## Permission Hierarchy
|
||||
|
||||
```
|
||||
┌───────────────┐
|
||||
│ SUPERADMIN │
|
||||
│ (All access) │
|
||||
└───────┬───────┘
|
||||
│
|
||||
┌───────▼───────┐
|
||||
│ ADMIN │
|
||||
│ (Admin panel)│
|
||||
└───────┬───────┘
|
||||
│
|
||||
┌─────────────────┼─────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌────────────┐ ┌────────────┐ ┌────────────┐
|
||||
│ DISTRIBUTOR│ │ EMPLOYEE │ │ CUSTOMER │
|
||||
│ (Business) │ │ (Internal) │ │ (Public) │
|
||||
└────────────┘ └─────┬──────┘ └────────────┘
|
||||
│
|
||||
┌─────────────────┼─────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│Warehouse │ │Inventory │ │ Picker │
|
||||
│ Manager │ │ Clerk │ │ Packer │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
(Full access) (Limited) (Task-based)
|
||||
```
|
||||
|
||||
## Migration Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ OLD STRUCTURE │
|
||||
│ Employee has separate login credentials │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ employees collection: │
|
||||
│ { │
|
||||
│ email: "employee@example.com" │
|
||||
│ password: "hashed_password" │
|
||||
│ firstName: "John" │
|
||||
│ lastName: "Doe" │
|
||||
│ ... other employee data ... │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ MIGRATION SCRIPT
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ NEW STRUCTURE │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Step 1: Create user in users collection │
|
||||
│ { │
|
||||
│ email: "employee@example.com" │
|
||||
│ password: "hashed_password" (copied from employee) │
|
||||
│ firstName: "John" │
|
||||
│ lastName: "Doe" │
|
||||
│ role: "warehouse_manager" │
|
||||
│ } │
|
||||
│ │ │
|
||||
│ │ userId = user._id │
|
||||
│ │ │
|
||||
│ Step 2: Update employee record │
|
||||
│ { │
|
||||
│ userId: "user_id_reference" ◄─── NEW │
|
||||
│ employeeNumber: "EMP-001" │
|
||||
│ department: "Logistics" │
|
||||
│ ... other employee data ... │
|
||||
│ (password field removed) │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
314
apps/fabrikanabytok/lib/auth/MIGRATION_GUIDE.md
Normal file
314
apps/fabrikanabytok/lib/auth/MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# Employee Migration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide explains how to use the Employee Migration Panel to migrate existing employees from the old authentication system to the new unified authentication system.
|
||||
|
||||
## Access
|
||||
|
||||
The migration panel is **only accessible to superadmin users** and can be found at:
|
||||
|
||||
**Path:** `/admin/users`
|
||||
|
||||
The panel appears at the top of the Users page as an orange-highlighted card.
|
||||
|
||||
## Migration Process
|
||||
|
||||
### Step 1: Preview the Migration
|
||||
|
||||
Before running the migration, **always preview** what will happen:
|
||||
|
||||
1. Click the **"Előnézet" (Preview)** button
|
||||
2. Review the information displayed:
|
||||
- **Total employees to migrate**: Number of employees without `userId`
|
||||
- **New users to create**: Employees without existing user accounts
|
||||
- **Link to existing**: Employees with existing user accounts
|
||||
- **Role updates**: Existing users whose roles will be updated
|
||||
- **Employee list**: Preview of first 10 employees
|
||||
|
||||
#### What the Preview Shows
|
||||
|
||||
```
|
||||
Migráció előnézet
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Found X employees to migrate
|
||||
|
||||
┌─────────────────────┬──────────────────────┐
|
||||
│ Migrálásra vár: 15 │ Új felhasználók: 12 │
|
||||
├─────────────────────┼──────────────────────┤
|
||||
│ Meglévőkhöz: 3 │ Szerepkör frissítés: 2│
|
||||
└─────────────────────┴──────────────────────┘
|
||||
|
||||
Első 10 munkatárs:
|
||||
• John Doe (john@example.com) [Új]
|
||||
• Jane Smith (jane@example.com) [Meglévő] customer → warehouse_manager
|
||||
• ...
|
||||
```
|
||||
|
||||
### Step 2: Run the Migration
|
||||
|
||||
Once you've reviewed the preview and are ready to proceed:
|
||||
|
||||
1. Click **"Migráció futtatása" (Run Migration)**
|
||||
2. A confirmation dialog appears
|
||||
3. Read the warning carefully
|
||||
4. Click **"Migráció indítása" (Start Migration)**
|
||||
5. Wait for the process to complete
|
||||
|
||||
The migration will:
|
||||
- ✅ Create user accounts for employees without existing accounts
|
||||
- ✅ Link employees to existing user accounts (preserves existing users)
|
||||
- ✅ Update roles if needed (employee role takes precedence)
|
||||
- ✅ Remove duplicate passwords from employees collection
|
||||
- ✅ Update timestamps
|
||||
|
||||
### Step 3: Automatic Verification
|
||||
|
||||
After successful migration, the system automatically runs a verification check.
|
||||
|
||||
The verification displays:
|
||||
- **Total employees**: Total count in employees collection
|
||||
- **Missing userId**: Should be 0 after successful migration
|
||||
- **Invalid userId references**: Should be 0 after successful migration
|
||||
|
||||
### Step 4: Manual Verification (Optional)
|
||||
|
||||
You can manually verify the migration at any time:
|
||||
|
||||
1. Click **"Ellenőrzés" (Verify)**
|
||||
2. Review the results
|
||||
|
||||
## Understanding the Results
|
||||
|
||||
### Successful Migration
|
||||
|
||||
```
|
||||
✅ Sikeres migráció
|
||||
|
||||
Successfully migrated 15 employees
|
||||
|
||||
Migrált munkatársak: 15 / 15
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
✅ Ellenőrzés eredménye
|
||||
|
||||
Migration verified successfully
|
||||
|
||||
Összes munkatárs: 15
|
||||
Hiányzó userId: 0 ✓
|
||||
Érvénytelen userId referenciák: 0 ✓
|
||||
```
|
||||
|
||||
### Migration with Errors
|
||||
|
||||
```
|
||||
⚠️ Sikeres migráció
|
||||
|
||||
Successfully migrated 13 employees
|
||||
|
||||
Migrált munkatársak: 13 / 15
|
||||
|
||||
Hibák:
|
||||
• Failed to migrate employee@example.com: Email already exists
|
||||
• Failed to migrate test@example.com: Invalid email format
|
||||
```
|
||||
|
||||
If you see errors, check the employee records manually and fix any data issues.
|
||||
|
||||
## What Gets Migrated
|
||||
|
||||
### Before Migration
|
||||
|
||||
**Employees Collection:**
|
||||
```json
|
||||
{
|
||||
"_id": "emp123",
|
||||
"email": "john@example.com",
|
||||
"password": "hashed_password", // ← Stored here
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"role": "warehouse_manager",
|
||||
"department": "Logistics",
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
**Users Collection:**
|
||||
```json
|
||||
// No user exists yet for this employee
|
||||
```
|
||||
|
||||
### After Migration
|
||||
|
||||
**Employees Collection:**
|
||||
```json
|
||||
{
|
||||
"_id": "emp123",
|
||||
"userId": "user456", // ← NEW: Links to user
|
||||
"email": "john@example.com",
|
||||
// password field removed
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"role": "warehouse_manager",
|
||||
"department": "Logistics",
|
||||
// ... other fields
|
||||
}
|
||||
```
|
||||
|
||||
**Users Collection:**
|
||||
```json
|
||||
{
|
||||
"_id": "user456", // ← NEW: User created
|
||||
"email": "john@example.com",
|
||||
"password": "hashed_password", // ← Moved here
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"role": "warehouse_manager",
|
||||
"emailVerified": true,
|
||||
"createdAt": "2025-11-26T...",
|
||||
"updatedAt": "2025-11-26T..."
|
||||
}
|
||||
```
|
||||
|
||||
## Preserving Existing Users
|
||||
|
||||
### Scenario: Employee with Existing User Account
|
||||
|
||||
If an employee's email matches an existing user:
|
||||
|
||||
**Before:**
|
||||
- Users: `{email: "john@example.com", role: "customer"}`
|
||||
- Employees: `{email: "john@example.com", role: "warehouse_manager"}`
|
||||
|
||||
**After:**
|
||||
- Users: `{email: "john@example.com", role: "warehouse_manager"}` ← Role updated
|
||||
- Employees: `{userId: "user123", email: "john@example.com"}` ← Linked
|
||||
|
||||
The existing user account is **preserved** and only the role is updated to match the employee role.
|
||||
|
||||
## Safety Features
|
||||
|
||||
### 1. Preview Before Migration
|
||||
Always shows what will happen before making changes.
|
||||
|
||||
### 2. Existing User Protection
|
||||
- Preserves existing user accounts
|
||||
- Only updates roles when necessary
|
||||
- Never deletes data
|
||||
|
||||
### 3. Verification
|
||||
Automatic and manual verification ensures data integrity.
|
||||
|
||||
### 4. Rollback Capability
|
||||
For testing purposes, you can rollback the migration.
|
||||
|
||||
⚠️ **WARNING**: Rollback should only be used in development/testing!
|
||||
|
||||
## Rollback (Testing Only)
|
||||
|
||||
If you need to test the migration multiple times:
|
||||
|
||||
1. Click **"Visszaállítás" (Rollback)**
|
||||
2. Confirm the action
|
||||
3. This removes the `userId` field from all employees
|
||||
4. You can now run the migration again
|
||||
|
||||
⚠️ **IMPORTANT**:
|
||||
- Rollback does NOT delete created user accounts
|
||||
- It only removes the `userId` link
|
||||
- Use with caution in production
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Preview shows 0 employees
|
||||
|
||||
**Cause**: All employees already have `userId` set.
|
||||
|
||||
**Solution**: Migration already completed or not needed.
|
||||
|
||||
### Issue: Migration fails for some employees
|
||||
|
||||
**Possible causes**:
|
||||
- Invalid email format
|
||||
- Missing required fields
|
||||
- Database connection issues
|
||||
|
||||
**Solution**:
|
||||
1. Check error messages
|
||||
2. Fix data issues in the employees collection
|
||||
3. Run migration again (already migrated employees are skipped)
|
||||
|
||||
### Issue: Verification shows missing userId
|
||||
|
||||
**Cause**: Migration was interrupted or failed.
|
||||
|
||||
**Solution**: Run the migration again. It will only process employees without `userId`.
|
||||
|
||||
### Issue: Invalid userId references
|
||||
|
||||
**Cause**: User was deleted but employee still references it.
|
||||
|
||||
**Solution**:
|
||||
1. Find the affected employee(s)
|
||||
2. Either create the missing user or update the employee's userId
|
||||
|
||||
## Post-Migration Checklist
|
||||
|
||||
After successful migration:
|
||||
|
||||
- [ ] Verification shows 0 missing userId
|
||||
- [ ] Verification shows 0 invalid references
|
||||
- [ ] Test employee login on `/login`
|
||||
- [ ] Verify session contains employee data
|
||||
- [ ] Check role-based redirects work correctly
|
||||
- [ ] Test employee-specific features
|
||||
- [ ] Review migration logs for any errors
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. **Superadmin Only**: Only superadmin users can access the migration panel
|
||||
2. **Password Security**: Existing hashed passwords are moved, not re-hashed
|
||||
3. **Session Integrity**: Existing sessions are not affected
|
||||
4. **Database Backups**: Always backup before running in production
|
||||
5. **Audit Trail**: Migration actions are logged
|
||||
|
||||
## Production Checklist
|
||||
|
||||
Before running migration in production:
|
||||
|
||||
1. [ ] **Backup database** completely
|
||||
2. [ ] Run preview and review results
|
||||
3. [ ] Test migration in staging environment first
|
||||
4. [ ] Schedule during low-traffic period
|
||||
5. [ ] Notify team members
|
||||
6. [ ] Run migration
|
||||
7. [ ] Verify results immediately
|
||||
8. [ ] Test employee logins
|
||||
9. [ ] Monitor for issues
|
||||
10. [ ] Document completion
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
1. Check the error messages in the migration panel
|
||||
2. Review the employee and user collections manually
|
||||
3. Check the server logs for detailed error information
|
||||
4. Contact technical support with:
|
||||
- Screenshot of error
|
||||
- Number of employees to migrate
|
||||
- Any error messages from verification
|
||||
|
||||
## Migration Statistics
|
||||
|
||||
The migration panel tracks:
|
||||
- Total employees processed
|
||||
- Successfully migrated
|
||||
- Errors encountered
|
||||
- New users created
|
||||
- Existing users linked
|
||||
- Roles updated
|
||||
|
||||
Use this information to ensure complete and accurate migration.
|
||||
|
||||
263
apps/fabrikanabytok/lib/auth/README.md
Normal file
263
apps/fabrikanabytok/lib/auth/README.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Unified Authentication System
|
||||
|
||||
## Overview
|
||||
|
||||
The authentication system has been refactored to support a unified login experience for all user types:
|
||||
- **Visitors** (unauthenticated users)
|
||||
- **Customers** (registered users)
|
||||
- **Distributors** (business partners)
|
||||
- **Employees** (internal staff with various roles)
|
||||
- **Administrators** (admin and superadmin)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Database Structure
|
||||
|
||||
#### Users Collection
|
||||
All authenticated users (including employees) are stored in the `users` collection:
|
||||
|
||||
```typescript
|
||||
{
|
||||
_id: ObjectId,
|
||||
email: string,
|
||||
password: string (hashed),
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
role: UserRole,
|
||||
avatar?: string,
|
||||
emailVerified: boolean,
|
||||
lastLoginAt?: Date,
|
||||
createdAt: Date,
|
||||
updatedAt: Date,
|
||||
// ... other user fields
|
||||
}
|
||||
```
|
||||
|
||||
#### Employees Collection
|
||||
Employee-specific data is stored separately and linked via `userId`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
_id: ObjectId,
|
||||
userId: string, // Reference to users._id
|
||||
employeeNumber: string,
|
||||
role: EmployeeRole,
|
||||
department: string,
|
||||
position: string,
|
||||
permissions: Permission[],
|
||||
status: "active" | "inactive" | "suspended",
|
||||
lastLogin?: Date,
|
||||
// ... other employee fields
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. **User Login**
|
||||
- User submits email and password
|
||||
- System checks `users` collection for authentication
|
||||
- Password is validated using bcrypt
|
||||
|
||||
2. **Employee Check**
|
||||
- After successful authentication, system checks `employees` collection
|
||||
- Searches for employee record with matching `userId`
|
||||
- If found, employee data is merged into the session
|
||||
|
||||
3. **Session Creation**
|
||||
- JWT token is created with user data
|
||||
- Employee-specific fields are included if applicable:
|
||||
- `isEmployee`: boolean
|
||||
- `employeeId`: string
|
||||
- `employeeNumber`: string
|
||||
- `department`: string
|
||||
- `position`: string
|
||||
- `permissions`: Permission[]
|
||||
|
||||
4. **Role-Based Redirect**
|
||||
- System determines appropriate redirect based on:
|
||||
- User role
|
||||
- Employee status
|
||||
- Callback URL (if provided)
|
||||
|
||||
### Redirect Logic
|
||||
|
||||
The `getDefaultRedirectForRole()` function determines where users are sent after login:
|
||||
|
||||
| User Type | Role | Redirect Path |
|
||||
|-----------|------|---------------|
|
||||
| Employee | warehouse_manager, inventory_clerk, etc. | `/internal/dashboard` |
|
||||
| Admin | admin, superadmin | `/admin/dashboard` |
|
||||
| Distributor | distributor | `/distributor/dashboard` |
|
||||
| Customer | customer | `/profile` |
|
||||
| Visitor | visitor | `/` |
|
||||
|
||||
## Key Files
|
||||
|
||||
### Auth Configuration
|
||||
- **`lib/auth/auth.ts`** - NextAuth configuration and credentials provider
|
||||
- **`lib/auth/auth.config.ts`** - Base auth configuration
|
||||
- **`lib/auth/auth-helpers.ts`** - Role-based utilities and redirect logic
|
||||
|
||||
### Types
|
||||
- **`lib/types/user.types.ts`** - User and role type definitions
|
||||
- **`lib/types/employee.types.ts`** - Employee-specific types
|
||||
- **`lib/types/next-auth.d.ts`** - NextAuth type extensions
|
||||
|
||||
### Actions
|
||||
- **`lib/actions/auth.actions.ts`** - Server actions for login/logout/register
|
||||
|
||||
### Components
|
||||
- **`components/auth/login-form.tsx`** - Login form component
|
||||
- **`app/(auth)/login/page.tsx`** - Login page
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating an Employee
|
||||
|
||||
To create a new employee:
|
||||
|
||||
1. First, create a user account in the `users` collection:
|
||||
```typescript
|
||||
const user = {
|
||||
email: "employee@example.com",
|
||||
password: await hash("password", 12),
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
role: "warehouse_manager", // Employee role
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
const result = await db.collection("users").insertOne(user)
|
||||
```
|
||||
|
||||
2. Then, create the employee record linked to the user:
|
||||
```typescript
|
||||
const employee = {
|
||||
userId: result.insertedId.toString(),
|
||||
employeeNumber: "EMP-001",
|
||||
role: "warehouse_manager",
|
||||
department: "Logistics",
|
||||
position: "Manager",
|
||||
permissions: [...], // Role-based permissions
|
||||
status: "active",
|
||||
hireDate: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
await db.collection("employees").insertOne(employee)
|
||||
```
|
||||
|
||||
### Accessing Session Data
|
||||
|
||||
In server components:
|
||||
```typescript
|
||||
import { auth } from "@/lib/auth/auth"
|
||||
|
||||
const session = await auth()
|
||||
|
||||
if (session?.user) {
|
||||
const { role, isEmployee, employeeId, permissions } = session.user
|
||||
|
||||
if (isEmployee) {
|
||||
// Employee-specific logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In client components:
|
||||
```typescript
|
||||
import { useSession } from "next-auth/react"
|
||||
|
||||
const { data: session } = useSession()
|
||||
|
||||
if (session?.user.isEmployee) {
|
||||
// Employee-specific UI
|
||||
}
|
||||
```
|
||||
|
||||
### Checking Permissions
|
||||
|
||||
```typescript
|
||||
import { canAccessAdmin, canAccessInternal } from "@/lib/auth/auth-helpers"
|
||||
|
||||
const session = await auth()
|
||||
|
||||
if (canAccessAdmin(session.user.role)) {
|
||||
// Admin panel access
|
||||
}
|
||||
|
||||
if (canAccessInternal(session.user.role, session.user.isEmployee)) {
|
||||
// Internal employee portal access
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Password Storage**: All passwords are hashed using bcrypt with 12 rounds
|
||||
2. **Session Management**: JWT-based sessions with 7-day expiry
|
||||
3. **Role Validation**: Role and permission checks are performed server-side
|
||||
4. **Database Queries**: Protected against injection via MongoDB driver
|
||||
5. **Employee Verification**: Two-step verification (user + employee record)
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Migrating Existing Employees
|
||||
|
||||
If you have employees with separate login credentials:
|
||||
|
||||
1. **Backup existing data**
|
||||
2. **For each employee:**
|
||||
```typescript
|
||||
// Create user account
|
||||
const user = await db.collection("users").insertOne({
|
||||
email: employee.email,
|
||||
password: employee.password, // Already hashed
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
role: employee.role,
|
||||
emailVerified: true,
|
||||
createdAt: employee.createdAt,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
// Update employee record
|
||||
await db.collection("employees").updateOne(
|
||||
{ _id: employee._id },
|
||||
{
|
||||
$set: {
|
||||
userId: user.insertedId.toString(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "Hibás email vagy jelszó"
|
||||
- Verify user exists in `users` collection
|
||||
- Check password hash matches
|
||||
- Ensure user is not suspended/inactive
|
||||
|
||||
### Issue: Employee not recognized
|
||||
- Verify `employees` collection has record with matching `userId`
|
||||
- Check that `userId` matches the `_id` from users collection
|
||||
- Ensure employee status is "active"
|
||||
|
||||
### Issue: Wrong redirect after login
|
||||
- Check user role in database
|
||||
- Verify `isEmployee` flag in session
|
||||
- Review `getDefaultRedirectForRole()` logic
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Two-factor authentication (2FA)
|
||||
- [ ] OAuth providers (Google, Microsoft)
|
||||
- [ ] Remember me functionality
|
||||
- [ ] Login attempt rate limiting
|
||||
- [ ] Account lockout after failed attempts
|
||||
- [ ] Password reset flow
|
||||
- [ ] Email verification flow
|
||||
- [ ] Session management dashboard
|
||||
|
||||
135
apps/fabrikanabytok/lib/auth/auth-helpers.ts
Normal file
135
apps/fabrikanabytok/lib/auth/auth-helpers.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Authentication Helper Utilities
|
||||
* Role-based redirects and user type detection
|
||||
*/
|
||||
|
||||
import type { UserRole } from "@/lib/types/user.types"
|
||||
|
||||
/**
|
||||
* Get default redirect path based on user role
|
||||
*/
|
||||
export function getDefaultRedirectForRole(role: UserRole, isEmployee?: boolean): string {
|
||||
// Employee roles
|
||||
if (isEmployee) {
|
||||
switch (role) {
|
||||
case "warehouse_manager":
|
||||
case "inventory_clerk":
|
||||
case "picker":
|
||||
case "packer":
|
||||
case "shipper":
|
||||
case "location_manager":
|
||||
case "outsource_coordinator":
|
||||
case "quality_controller":
|
||||
case "assistant":
|
||||
return "/internal/dashboard"
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Admin and superadmin
|
||||
if (role === "admin" || role === "superadmin") {
|
||||
return "/admin/dashboard"
|
||||
}
|
||||
|
||||
// Distributor
|
||||
if (role === "distributor") {
|
||||
return "/distributor/dashboard"
|
||||
}
|
||||
|
||||
// Customer
|
||||
if (role === "customer") {
|
||||
return "/profile"
|
||||
}
|
||||
|
||||
// Visitor (default)
|
||||
return "/"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user role is an employee role
|
||||
*/
|
||||
export function isEmployeeRole(role: UserRole): boolean {
|
||||
const employeeRoles: UserRole[] = [
|
||||
"warehouse_manager",
|
||||
"inventory_clerk",
|
||||
"picker",
|
||||
"packer",
|
||||
"shipper",
|
||||
"location_manager",
|
||||
"outsource_coordinator",
|
||||
"quality_controller",
|
||||
"assistant",
|
||||
]
|
||||
return employeeRoles.includes(role)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user role is an admin role
|
||||
*/
|
||||
export function isAdminRole(role: UserRole): boolean {
|
||||
return role === "admin" || role === "superadmin"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has access to admin panel
|
||||
*/
|
||||
export function canAccessAdmin(role: UserRole): boolean {
|
||||
return isAdminRole(role)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has access to internal employee portal
|
||||
*/
|
||||
export function canAccessInternal(role: UserRole, isEmployee?: boolean): boolean {
|
||||
return isEmployee === true || isEmployeeRole(role)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user type label for display
|
||||
*/
|
||||
export function getUserTypeLabel(role: UserRole, isEmployee?: boolean): string {
|
||||
if (isEmployee || isEmployeeRole(role)) {
|
||||
return "Munkatárs"
|
||||
}
|
||||
|
||||
switch (role) {
|
||||
case "superadmin":
|
||||
return "Szuper adminisztrátor"
|
||||
case "admin":
|
||||
return "Adminisztrátor"
|
||||
case "distributor":
|
||||
return "Viszonteladó"
|
||||
case "customer":
|
||||
return "Ügyfél"
|
||||
case "visitor":
|
||||
return "Látogató"
|
||||
default:
|
||||
return "Felhasználó"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role display name in Hungarian
|
||||
*/
|
||||
export function getRoleDisplayName(role: UserRole): string {
|
||||
const roleNames: Record<UserRole, string> = {
|
||||
visitor: "Látogató",
|
||||
customer: "Ügyfél",
|
||||
distributor: "Viszonteladó",
|
||||
admin: "Adminisztrátor",
|
||||
superadmin: "Szuper adminisztrátor",
|
||||
warehouse_manager: "Raktárvezető",
|
||||
inventory_clerk: "Készletkezelő",
|
||||
picker: "Komissiózó",
|
||||
packer: "Csomagoló",
|
||||
shipper: "Szállítmányozó",
|
||||
location_manager: "Helyszín menedzser",
|
||||
outsource_coordinator: "Beszerzési koordinátor",
|
||||
quality_controller: "Minőségellenőr",
|
||||
assistant: "Asszisztens",
|
||||
}
|
||||
|
||||
return roleNames[role] || role
|
||||
}
|
||||
|
||||
8
apps/fabrikanabytok/lib/auth/auth.config.ts
Normal file
8
apps/fabrikanabytok/lib/auth/auth.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { NextAuthConfig } from "next-auth"
|
||||
import Credentials from "next-auth/providers/credentials"
|
||||
|
||||
export default {
|
||||
providers: [
|
||||
Credentials
|
||||
]
|
||||
} satisfies NextAuthConfig
|
||||
162
apps/fabrikanabytok/lib/auth/auth.ts
Normal file
162
apps/fabrikanabytok/lib/auth/auth.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import NextAuth from "next-auth"
|
||||
import authConfig from "./auth.config"
|
||||
import { MongoClient } from "mongodb"
|
||||
import { MongoDBAdapter } from "@auth/mongodb-adapter"
|
||||
import Credentials from "next-auth/providers/credentials"
|
||||
import { getDb, getClient } from "@/lib/db/mongodb"
|
||||
import bcrypt from "bcryptjs"
|
||||
import type { UserRole } from "@/lib/types/user.types"
|
||||
import { ObjectId } from "mongodb"
|
||||
import { Permission } from "../types/employee.types"
|
||||
|
||||
const client = await getClient()
|
||||
const db = await getDb()
|
||||
|
||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
...authConfig,
|
||||
adapter: MongoDBAdapter(client as unknown as MongoClient) as any,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 days
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
error: "/login",
|
||||
},
|
||||
providers: [
|
||||
Credentials({
|
||||
id: "credentials",
|
||||
name: "credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
// @ts-ignore
|
||||
async authorize(credentials) {
|
||||
|
||||
if (!db) {
|
||||
throw new Error("Database not connected")
|
||||
}
|
||||
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
throw new Error("Hibás email vagy jelszó")
|
||||
}
|
||||
|
||||
// Step 1: Authenticate against users collection
|
||||
const user = await db
|
||||
.collection("users")
|
||||
.findOne({ email: credentials.email })
|
||||
|
||||
if (!user) {
|
||||
throw new Error("Hibás email vagy jelszó")
|
||||
}
|
||||
|
||||
// Step 2: Verify password
|
||||
const isValidPassword = await bcrypt.compare(
|
||||
credentials.password as string,
|
||||
user.password
|
||||
)
|
||||
|
||||
if (!isValidPassword) {
|
||||
throw new Error("Hibás email vagy jelszó")
|
||||
}
|
||||
|
||||
// Step 3: Check if user is an employee
|
||||
const employee = await db
|
||||
.collection("employees")
|
||||
.findOne({ userId: user._id.toString() })
|
||||
|
||||
// Step 4: Update last login timestamp
|
||||
await db.collection("users").updateOne(
|
||||
{ _id: new ObjectId(user._id) },
|
||||
{
|
||||
$set: {
|
||||
lastLoginAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Step 5: If employee exists, update their last login
|
||||
if (employee) {
|
||||
await db.collection("employees").updateOne(
|
||||
{ _id: new ObjectId(employee._id) },
|
||||
{
|
||||
$set: {
|
||||
lastLogin: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Step 6: Construct the authenticated user object
|
||||
const authenticatedUser = {
|
||||
id: user._id.toString(),
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
name: `${user.firstName || ''} ${user.lastName || ''}`.trim() || user.email,
|
||||
role: employee ? employee.role : user.role,
|
||||
avatar: user.avatar,
|
||||
emailVerified: user.emailVerified,
|
||||
// Include employee-specific data if applicable
|
||||
employeeId: employee?._id?.toString(),
|
||||
employeeNumber: employee?.employeeNumber,
|
||||
department: employee?.department,
|
||||
position: employee?.position,
|
||||
permissions: employee?.permissions || [],
|
||||
isEmployee: !!employee,
|
||||
}
|
||||
|
||||
return authenticatedUser
|
||||
}
|
||||
})
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id
|
||||
token.role = user.role
|
||||
token.firstName = user.firstName
|
||||
token.lastName = user.lastName
|
||||
token.avatar = user.avatar
|
||||
token.emailVerified = user.emailVerified as boolean | Date | undefined
|
||||
// Employee-specific fields
|
||||
token.isEmployee = user.isEmployee
|
||||
token.employeeId = user.employeeId
|
||||
token.employeeNumber = user.employeeNumber
|
||||
token.department = user.department
|
||||
token.position = user.position
|
||||
token.permissions = user.permissions
|
||||
}
|
||||
return token
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string
|
||||
session.user.role = token.role as UserRole
|
||||
session.user.firstName = token.firstName as string
|
||||
session.user.lastName = token.lastName as string
|
||||
session.user.avatar = token.avatar as string
|
||||
session.user.emailVerified = token.emailVerified as Date & boolean
|
||||
// Employee-specific fields
|
||||
session.user.isEmployee = token.isEmployee as boolean
|
||||
session.user.employeeId = token.employeeId as string
|
||||
session.user.employeeNumber = token.employeeNumber as string
|
||||
session.user.department = token.department as string
|
||||
session.user.position = token.position as string
|
||||
session.user.permissions = token.permissions as Permission[]
|
||||
}
|
||||
return session
|
||||
},
|
||||
async redirect({ url, baseUrl }) {
|
||||
// Allows relative callback URLs
|
||||
if (url.startsWith("/")) return `${baseUrl}${url}`
|
||||
// Allows callback URLs on the same origin
|
||||
else if (new URL(url).origin === baseUrl) return url
|
||||
return baseUrl
|
||||
},
|
||||
},
|
||||
trustHost: true,
|
||||
})
|
||||
259
apps/fabrikanabytok/lib/auth/migrate-employees.ts
Normal file
259
apps/fabrikanabytok/lib/auth/migrate-employees.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/**
|
||||
* Employee Migration Script
|
||||
* Migrates employees from separate authentication to unified user system
|
||||
*/
|
||||
|
||||
import { getDb } from "@/lib/db/mongodb"
|
||||
import { ObjectId } from "mongodb"
|
||||
|
||||
interface OldEmployee {
|
||||
_id: ObjectId
|
||||
email: string
|
||||
password: string // Already hashed
|
||||
firstName: string
|
||||
lastName: string
|
||||
role: string
|
||||
department: string
|
||||
position: string
|
||||
permissions: string[]
|
||||
status: string
|
||||
hireDate: Date
|
||||
createdAt: Date
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate employees to the new unified authentication system
|
||||
*
|
||||
* This script:
|
||||
* 1. Finds all employees without a userId
|
||||
* 2. Creates corresponding user accounts
|
||||
* 3. Links employees to users via userId
|
||||
*/
|
||||
export async function migrateEmployeesToUnifiedAuth() {
|
||||
const db = await getDb()
|
||||
|
||||
if (!db) {
|
||||
throw new Error("Database not connected")
|
||||
}
|
||||
|
||||
console.log("🔄 Starting employee migration...")
|
||||
|
||||
try {
|
||||
// Find employees without userId (old structure)
|
||||
const employeesWithoutUser = await db
|
||||
.collection("employees")
|
||||
.find({
|
||||
$or: [
|
||||
{ userId: { $exists: false } },
|
||||
{ userId: null },
|
||||
{ userId: "" }
|
||||
]
|
||||
})
|
||||
.toArray() as unknown as OldEmployee[]
|
||||
|
||||
console.log(`📊 Found ${employeesWithoutUser.length} employees to migrate`)
|
||||
|
||||
if (employeesWithoutUser.length === 0) {
|
||||
console.log("✅ No employees to migrate")
|
||||
return {
|
||||
success: true,
|
||||
migrated: 0,
|
||||
errors: [],
|
||||
}
|
||||
}
|
||||
|
||||
const errors: string[] = []
|
||||
let migratedCount = 0
|
||||
|
||||
for (const employee of employeesWithoutUser) {
|
||||
try {
|
||||
console.log(`\n👤 Processing: ${employee.firstName} ${employee.lastName} (${employee.email})`)
|
||||
|
||||
// Check if user already exists with this email
|
||||
const existingUser = await db
|
||||
.collection("users")
|
||||
.findOne({ email: employee.email })
|
||||
|
||||
let userId: string
|
||||
|
||||
if (existingUser) {
|
||||
console.log(` ℹ️ User already exists (${existingUser.role}), linking to employee...`)
|
||||
userId = existingUser._id.toString()
|
||||
|
||||
// If the existing user has a different role, update it to the employee role
|
||||
// This ensures employees have the correct role in the users collection
|
||||
if (existingUser.role !== employee.role) {
|
||||
console.log(` 🔄 Updating user role from ${existingUser.role} to ${employee.role}`)
|
||||
await db.collection("users").updateOne(
|
||||
{ _id: existingUser._id },
|
||||
{
|
||||
$set: {
|
||||
role: employee.role,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Create new user account
|
||||
console.log(` ➕ Creating new user account...`)
|
||||
|
||||
const newUser = {
|
||||
email: employee.email,
|
||||
password: employee.password, // Use existing hashed password
|
||||
firstName: employee.firstName,
|
||||
lastName: employee.lastName,
|
||||
role: employee.role,
|
||||
avatar: employee.avatar || null,
|
||||
phone: employee.phone || null,
|
||||
emailVerified: true, // Employees are pre-verified
|
||||
isActive: employee.status === "active",
|
||||
creditBalance: 0,
|
||||
createdAt: employee.createdAt || new Date(),
|
||||
updatedAt: new Date(),
|
||||
lastLoginAt: employee.lastLogin || null,
|
||||
}
|
||||
|
||||
const userResult = await db.collection("users").insertOne(newUser)
|
||||
userId = userResult.insertedId.toString()
|
||||
console.log(` ✅ User created with ID: ${userId}`)
|
||||
}
|
||||
|
||||
// Update employee record with userId
|
||||
await db.collection("employees").updateOne(
|
||||
{ _id: employee._id },
|
||||
{
|
||||
$set: {
|
||||
userId: userId,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
// Remove password from employee record (now stored in users)
|
||||
$unset: { password: "" }
|
||||
}
|
||||
)
|
||||
|
||||
console.log(` ✅ Employee linked to user`)
|
||||
migratedCount++
|
||||
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to migrate ${employee.email}: ${error}`
|
||||
console.error(` ❌ ${errorMsg}`)
|
||||
errors.push(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n${"=".repeat(50)}`)
|
||||
console.log(`✅ Migration complete!`)
|
||||
console.log(` Migrated: ${migratedCount}/${employeesWithoutUser.length}`)
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(` Errors: ${errors.length}`)
|
||||
console.log(`\n❌ Errors:`)
|
||||
errors.forEach(err => console.log(` - ${err}`))
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
migrated: migratedCount,
|
||||
total: employeesWithoutUser.length,
|
||||
errors,
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Migration failed:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify migration was successful
|
||||
* Checks that all employees have valid userId references
|
||||
*/
|
||||
export async function verifyEmployeeMigration() {
|
||||
const db = await getDb()
|
||||
|
||||
if (!db) {
|
||||
throw new Error("Database not connected")
|
||||
}
|
||||
|
||||
console.log("🔍 Verifying employee migration...")
|
||||
|
||||
// Check for employees without userId
|
||||
const employeesWithoutUser = await db
|
||||
.collection("employees")
|
||||
.countDocuments({
|
||||
$or: [
|
||||
{ userId: { $exists: false } },
|
||||
{ userId: null },
|
||||
{ userId: "" }
|
||||
]
|
||||
})
|
||||
|
||||
// Check for employees with invalid userId
|
||||
const employees = await db.collection("employees").find({}).toArray()
|
||||
let invalidUserIds = 0
|
||||
|
||||
for (const employee of employees) {
|
||||
if (employee.userId) {
|
||||
const user = await db
|
||||
.collection("users")
|
||||
.findOne({ _id: new ObjectId(employee.userId) })
|
||||
|
||||
if (!user) {
|
||||
console.log(`❌ Employee ${employee.email} has invalid userId: ${employee.userId}`)
|
||||
invalidUserIds++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n📊 Verification Results:`)
|
||||
console.log(` Total employees: ${employees.length}`)
|
||||
console.log(` Without userId: ${employeesWithoutUser}`)
|
||||
console.log(` Invalid userId references: ${invalidUserIds}`)
|
||||
|
||||
const isValid = employeesWithoutUser === 0 && invalidUserIds === 0
|
||||
|
||||
if (isValid) {
|
||||
console.log(`\n✅ Migration verified successfully!`)
|
||||
} else {
|
||||
console.log(`\n❌ Migration has issues that need to be resolved`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: isValid,
|
||||
totalEmployees: employees.length,
|
||||
missingUserId: employeesWithoutUser,
|
||||
invalidUserIdReferences: invalidUserIds,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback migration (for testing purposes)
|
||||
* WARNING: This will remove the userId field from employees
|
||||
*/
|
||||
export async function rollbackEmployeeMigration() {
|
||||
const db = await getDb()
|
||||
|
||||
if (!db) {
|
||||
throw new Error("Database not connected")
|
||||
}
|
||||
|
||||
console.log("⚠️ WARNING: Rolling back employee migration...")
|
||||
console.log("This will remove userId from all employee records")
|
||||
|
||||
const result = await db.collection("employees").updateMany(
|
||||
{},
|
||||
{
|
||||
$unset: { userId: "" }
|
||||
}
|
||||
)
|
||||
|
||||
console.log(`✅ Rolled back ${result.modifiedCount} employee records`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
modified: result.modifiedCount,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user