diff --git a/App/pelagia-portal/app/(auth)/login/page.tsx b/App/pelagia-portal/app/(auth)/login/page.tsx
new file mode 100644
index 0000000..fe5c80a
--- /dev/null
+++ b/App/pelagia-portal/app/(auth)/login/page.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { useState } from "react";
+import { signIn } from "next-auth/react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Anchor } from "lucide-react";
+
+export default function LoginPage() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(false);
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard";
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setLoading(true);
+ setError("");
+
+ const result = await signIn("credentials", {
+ email,
+ password,
+ redirect: false,
+ });
+
+ if (result?.error) {
+ setError("Invalid email or password. Please try again.");
+ setLoading(false);
+ } else {
+ router.push(callbackUrl);
+ router.refresh();
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
Pelagia Portal
+
Purchase Order Management
+
+
+
+
Sign in
+
+
+
+
+
+ Contact your administrator if you need access.
+
+
+
+ );
+}
diff --git a/App/pelagia-portal/app/api/auth/[...nextauth]/route.ts b/App/pelagia-portal/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..86c9f3d
--- /dev/null
+++ b/App/pelagia-portal/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,3 @@
+import { handlers } from "@/auth";
+
+export const { GET, POST } = handlers;
diff --git a/App/pelagia-portal/auth.ts b/App/pelagia-portal/auth.ts
new file mode 100644
index 0000000..7170e34
--- /dev/null
+++ b/App/pelagia-portal/auth.ts
@@ -0,0 +1,61 @@
+import NextAuth from "next-auth";
+import Credentials from "next-auth/providers/credentials";
+import bcrypt from "bcryptjs";
+import { db } from "@/lib/db";
+import { loginSchema } from "@/lib/validations/user";
+import type { Role } from "@prisma/client";
+
+export const { handlers, auth, signIn, signOut } = NextAuth({
+ session: { strategy: "jwt" },
+ pages: {
+ signIn: "/login",
+ error: "/login",
+ },
+ providers: [
+ Credentials({
+ credentials: {
+ email: { label: "Email", type: "email" },
+ password: { label: "Password", type: "password" },
+ },
+ async authorize(credentials) {
+ const parsed = loginSchema.safeParse(credentials);
+ if (!parsed.success) return null;
+
+ const user = await db.user.findUnique({
+ where: { email: parsed.data.email },
+ });
+ if (!user || !user.isActive) return null;
+
+ const valid = await bcrypt.compare(parsed.data.password, user.passwordHash);
+ if (!valid) return null;
+
+ return { id: user.id, email: user.email, name: user.name, role: user.role };
+ },
+ }),
+ ],
+ callbacks: {
+ jwt({ token, user }) {
+ if (user) {
+ token.id = user.id;
+ token.role = (user as unknown as { role: Role }).role;
+ }
+ return token;
+ },
+ session({ session, token }) {
+ session.user.id = token.id as string;
+ session.user.role = token.role as Role;
+ return session;
+ },
+ },
+});
+
+declare module "next-auth" {
+ interface Session {
+ user: {
+ id: string;
+ name: string;
+ email: string;
+ role: Role;
+ };
+ }
+}
diff --git a/App/pelagia-portal/lib/permissions.ts b/App/pelagia-portal/lib/permissions.ts
new file mode 100644
index 0000000..11e91c7
--- /dev/null
+++ b/App/pelagia-portal/lib/permissions.ts
@@ -0,0 +1,75 @@
+import type { Role } from "@prisma/client";
+
+export type Permission =
+ | "create_po"
+ | "submit_po"
+ | "edit_own_draft_po"
+ | "view_own_pos"
+ | "view_all_pos"
+ | "approve_po"
+ | "reject_po"
+ | "request_edits"
+ | "request_vendor_id"
+ | "process_payment"
+ | "confirm_receipt"
+ | "view_analytics"
+ | "export_reports"
+ | "manage_users"
+ | "manage_vendors"
+ | "manage_vessels_accounts"
+ | "manage_products";
+
+const ROLE_PERMISSIONS: Record = {
+ TECHNICAL: ["create_po", "submit_po", "edit_own_draft_po", "view_own_pos", "confirm_receipt"],
+ MANNING: ["create_po", "submit_po", "edit_own_draft_po", "view_own_pos", "confirm_receipt"],
+ ACCOUNTS: ["view_all_pos", "process_payment"],
+ MANAGER: [
+ "view_all_pos",
+ "approve_po",
+ "reject_po",
+ "request_edits",
+ "request_vendor_id",
+ "view_analytics",
+ "export_reports",
+ ],
+ SUPERUSER: [
+ "create_po",
+ "submit_po",
+ "edit_own_draft_po",
+ "view_own_pos",
+ "view_all_pos",
+ "approve_po",
+ "reject_po",
+ "request_edits",
+ "request_vendor_id",
+ "process_payment",
+ "confirm_receipt",
+ "view_analytics",
+ "export_reports",
+ ],
+ AUDITOR: ["view_own_pos", "view_all_pos", "view_analytics", "export_reports"],
+ ADMIN: [
+ "view_own_pos",
+ "view_all_pos",
+ "view_analytics",
+ "export_reports",
+ "manage_users",
+ "manage_vendors",
+ "manage_vessels_accounts",
+ "manage_products",
+ ],
+};
+
+export function hasPermission(role: Role, permission: Permission): boolean {
+ return ROLE_PERMISSIONS[role]?.includes(permission) ?? false;
+}
+
+export function requirePermission(role: Role, permission: Permission): void {
+ if (!hasPermission(role, permission)) {
+ throw new Error(`Forbidden: role ${role} lacks permission ${permission}`);
+ }
+}
+
+export function getPermissions(role: Role): Permission[] {
+ return ROLE_PERMISSIONS[role] ?? [];
+}
diff --git a/App/pelagia-portal/lib/validations/user.ts b/App/pelagia-portal/lib/validations/user.ts
new file mode 100644
index 0000000..b9cedd4
--- /dev/null
+++ b/App/pelagia-portal/lib/validations/user.ts
@@ -0,0 +1,21 @@
+import { z } from "zod";
+
+export const loginSchema = z.object({
+ email: z.string().email("Invalid email address"),
+ password: z.string().min(1, "Password is required"),
+});
+
+export const createUserSchema = z.object({
+ employeeId: z.string().min(1, "Employee ID is required"),
+ email: z.string().email("Invalid email address"),
+ name: z.string().min(1, "Name is required"),
+ password: z.string().min(8, "Password must be at least 8 characters"),
+ role: z.enum(["TECHNICAL", "MANNING", "ACCOUNTS", "MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"]),
+});
+
+export const updateUserSchema = createUserSchema
+ .omit({ password: true })
+ .extend({ password: z.string().min(8).optional() });
+
+export type LoginInput = z.infer;
+export type CreateUserInput = z.infer;
diff --git a/App/pelagia-portal/middleware.ts b/App/pelagia-portal/middleware.ts
new file mode 100644
index 0000000..fa42626
--- /dev/null
+++ b/App/pelagia-portal/middleware.ts
@@ -0,0 +1,24 @@
+import { auth } from "@/auth";
+import { NextResponse } from "next/server";
+
+export default auth((req) => {
+ const isAuthenticated = !!req.auth;
+ const pathname = req.nextUrl.pathname;
+ const isLoginPage = pathname === "/login";
+
+ if (!isAuthenticated && !isLoginPage) {
+ const loginUrl = new URL("/login", req.url);
+ loginUrl.searchParams.set("callbackUrl", pathname);
+ return NextResponse.redirect(loginUrl);
+ }
+
+ if (isAuthenticated && isLoginPage) {
+ return NextResponse.redirect(new URL("/dashboard", req.url));
+ }
+});
+
+export const config = {
+ matcher: [
+ "/((?!api/auth|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
+ ],
+};