diff --git a/apps/fabrikanabytok/app/(auth)/login/page.tsx b/apps/fabrikanabytok/app/(auth)/login/page.tsx new file mode 100644 index 0000000..d2cff8c --- /dev/null +++ b/apps/fabrikanabytok/app/(auth)/login/page.tsx @@ -0,0 +1,38 @@ +import { LoginForm } from "@/components/auth/login-form" +import Link from "next/link" + +export default function LoginPage() { + return ( +
+
+
+ +
FABRIKA NABYTOK
+ +

Bejelentkezés

+

+ Jelentkezzen be fiókjába a folytatáshoz +

+

+ Ügyfelek, munkatársak és adminisztrátorok számára +

+
+ + + +
+

+ Még nincs fiókja?{" "} + + Regisztráljon itt + +

+ +
+

A bejelentkezés után automatikusan átirányítjuk Önt a megfelelő felületre.

+
+
+
+
+ ) +} diff --git a/apps/fabrikanabytok/app/(auth)/register/page.tsx b/apps/fabrikanabytok/app/(auth)/register/page.tsx new file mode 100644 index 0000000..d79622e --- /dev/null +++ b/apps/fabrikanabytok/app/(auth)/register/page.tsx @@ -0,0 +1,27 @@ +import { RegisterForm } from "@/components/auth/register-form" +import Link from "next/link" + +export default function RegisterPage() { + return ( +
+
+
+ +
FABRIKA NABYTOK
+ +

Regisztráció

+

Hozzon létre új fiókot a kezdéshez

+
+ + + +

+ Már van fiókja?{" "} + + Jelentkezzen be itt + +

+
+
+ ) +} diff --git a/apps/fabrikanabytok/app/(auth)/setup-wizard/page.tsx b/apps/fabrikanabytok/app/(auth)/setup-wizard/page.tsx new file mode 100644 index 0000000..eae5010 --- /dev/null +++ b/apps/fabrikanabytok/app/(auth)/setup-wizard/page.tsx @@ -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 ( +
+
+
+
+ + + +
+

Üdvözöljük a FABRIKA NABYTOK rendszerben

+

+ A kezdéshez konfigurálja a rendszert és hozza létre a superadmin fiókot +

+
+ + +
+
+ ) +} diff --git a/apps/fabrikanabytok/app/api/auth/[...nextauth]/route.ts b/apps/fabrikanabytok/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..c9ebf8e --- /dev/null +++ b/apps/fabrikanabytok/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/lib/auth/auth" + +export const { GET, POST } = handlers diff --git a/apps/fabrikanabytok/components/auth/login-form.tsx b/apps/fabrikanabytok/components/auth/login-form.tsx new file mode 100644 index 0000000..f23eff4 --- /dev/null +++ b/apps/fabrikanabytok/components/auth/login-form.tsx @@ -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(null) + const setupSuccess = searchParams.get("setup") === "success" + + const handleSubmit = async (e: React.FormEvent) => { + 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 ( + + + Üdvözöljük vissza + Adja meg bejelentkezési adatait + + + {setupSuccess && ( +
+ +

Rendszer sikeresen inicializálva! Jelentkezzen be.

+
+ )} + +
+
+ + +
+ +
+ + +
+ + {error && ( +
+ +

{error}

+
+ )} + + +
+
+
+ ) +} diff --git a/apps/fabrikanabytok/components/auth/register-form.tsx b/apps/fabrikanabytok/components/auth/register-form.tsx new file mode 100644 index 0000000..2f06ce8 --- /dev/null +++ b/apps/fabrikanabytok/components/auth/register-form.tsx @@ -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(null) + + const handleSubmit = async (e: React.FormEvent) => { + 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 ( + + + Új fiók létrehozása + Töltse ki az alábbi mezőket a regisztrációhoz + + +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + {error && ( +
+ +

{error}

+
+ )} + + +
+
+
+ ) +} diff --git a/apps/fabrikanabytok/components/auth/setup-wizard-form.tsx b/apps/fabrikanabytok/components/auth/setup-wizard-form.tsx new file mode 100644 index 0000000..2ea514e --- /dev/null +++ b/apps/fabrikanabytok/components/auth/setup-wizard-form.tsx @@ -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(null) + const [step, setStep] = useState(1) + const [formData, setFormData] = useState({ + email: "", + password: "", + confirmPassword: "", + firstName: "", + lastName: "", + companyName: "", + mongoUri: "", + }) + + const handleInputChange = (e: React.ChangeEvent) => { + 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 ( + + +
+
+ {[1, 2].map((s) => ( +
+ ))} +
+ {step}/2 lépés +
+ + {step === 1 ? "Superadmin fiók létrehozása" : "Vállalati információk"} + + + {step === 1 + ? "Hozza létre a fő adminisztrátori fiókot a rendszer kezeléséhez" + : "Adja meg a vállalat adatait (opcionális)"} + + + +
+ {step === 1 ? ( + <> +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + ) : ( + <> +
+ + +

Később módosítható a beállításokban

+
+ + {error && ( +
+ +

{error}

+
+ )} + +
+ + +
+ + )} +
+
+ + ) +} diff --git a/apps/fabrikanabytok/lib/auth/AUTHENTICATION_FLOW.md b/apps/fabrikanabytok/lib/auth/AUTHENTICATION_FLOW.md new file mode 100644 index 0000000..220ec80 --- /dev/null +++ b/apps/fabrikanabytok/lib/auth/AUTHENTICATION_FLOW.md @@ -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) │ +│ } │ +└─────────────────────────────────────────────────────────────────┘ +``` + diff --git a/apps/fabrikanabytok/lib/auth/MIGRATION_GUIDE.md b/apps/fabrikanabytok/lib/auth/MIGRATION_GUIDE.md new file mode 100644 index 0000000..1b5dce6 --- /dev/null +++ b/apps/fabrikanabytok/lib/auth/MIGRATION_GUIDE.md @@ -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. + diff --git a/apps/fabrikanabytok/lib/auth/README.md b/apps/fabrikanabytok/lib/auth/README.md new file mode 100644 index 0000000..89d63f5 --- /dev/null +++ b/apps/fabrikanabytok/lib/auth/README.md @@ -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 + diff --git a/apps/fabrikanabytok/lib/auth/auth-helpers.ts b/apps/fabrikanabytok/lib/auth/auth-helpers.ts new file mode 100644 index 0000000..71af949 --- /dev/null +++ b/apps/fabrikanabytok/lib/auth/auth-helpers.ts @@ -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 = { + 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 +} + diff --git a/apps/fabrikanabytok/lib/auth/auth.config.ts b/apps/fabrikanabytok/lib/auth/auth.config.ts new file mode 100644 index 0000000..aa49fcc --- /dev/null +++ b/apps/fabrikanabytok/lib/auth/auth.config.ts @@ -0,0 +1,8 @@ +import type { NextAuthConfig } from "next-auth" +import Credentials from "next-auth/providers/credentials" + +export default { + providers: [ + Credentials + ] +} satisfies NextAuthConfig \ No newline at end of file diff --git a/apps/fabrikanabytok/lib/auth/auth.ts b/apps/fabrikanabytok/lib/auth/auth.ts new file mode 100644 index 0000000..811beb2 --- /dev/null +++ b/apps/fabrikanabytok/lib/auth/auth.ts @@ -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, +}) diff --git a/apps/fabrikanabytok/lib/auth/migrate-employees.ts b/apps/fabrikanabytok/lib/auth/migrate-employees.ts new file mode 100644 index 0000000..7c14ca7 --- /dev/null +++ b/apps/fabrikanabytok/lib/auth/migrate-employees.ts @@ -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, + } +} +