245 lines
9.3 KiB
TypeScript
245 lines
9.3 KiB
TypeScript
import { cookies } from "next/headers";
|
|
import { redirect } from "next/navigation";
|
|
import { ArrowRight, ShieldCheck } from "lucide-react";
|
|
import Image from "next/image";
|
|
|
|
interface LoginPageProps {
|
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
|
}
|
|
|
|
export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|
const params = await searchParams;
|
|
const redirectUrl = (params.redirect as string) || "/";
|
|
const error = params.error === "1";
|
|
|
|
const projectName = process.env.PROJECT_NAME || "Mintel";
|
|
|
|
async function login(formData: FormData) {
|
|
"use server";
|
|
|
|
const email = formData.get("email") as string;
|
|
const password = formData.get("password") as string;
|
|
|
|
const expectedCode = process.env.GATEKEEPER_PASSWORD || "mintel";
|
|
const adminEmail = process.env.DIRECTUS_ADMIN_EMAIL;
|
|
const adminPassword = process.env.DIRECTUS_ADMIN_PASSWORD;
|
|
const authCookieName =
|
|
process.env.AUTH_COOKIE_NAME || "mintel_gatekeeper_session";
|
|
const targetRedirect = formData.get("redirect") as string;
|
|
const cookieDomain = process.env.COOKIE_DOMAIN;
|
|
|
|
let userIdentity = "";
|
|
let userCompany: any = null;
|
|
|
|
// 1. Check Global Admin (from ENV)
|
|
if (
|
|
adminEmail &&
|
|
adminPassword &&
|
|
email === adminEmail &&
|
|
password === adminPassword
|
|
) {
|
|
userIdentity = "Admin";
|
|
}
|
|
// 2. Check Generic Code (Guest)
|
|
else if (!email && password === expectedCode) {
|
|
userIdentity = "Guest";
|
|
}
|
|
// 3. Check Lightweight Client Users (dedicated collection)
|
|
if (email && password && process.env.INFRA_DIRECTUS_URL) {
|
|
try {
|
|
const clientUsersRes = await fetch(
|
|
`${process.env.INFRA_DIRECTUS_URL}/items/client_users?filter[email][_eq]=${encodeURIComponent(
|
|
email,
|
|
)}&fields=*,company.*`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.INFRA_DIRECTUS_TOKEN}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
if (clientUsersRes.ok) {
|
|
const { data: users } = await clientUsersRes.json();
|
|
const clientUser = users[0];
|
|
|
|
// ⚠️ NOTE: Plain text check for demo/dev, should use argon2 in production
|
|
if (
|
|
clientUser &&
|
|
(clientUser.password === password ||
|
|
clientUser.temporary_password === password)
|
|
) {
|
|
userIdentity = clientUser.first_name || clientUser.email;
|
|
userCompany = {
|
|
id: clientUser.company?.id,
|
|
name: clientUser.company?.name,
|
|
};
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Client User Auth Error:", e);
|
|
}
|
|
}
|
|
|
|
// 4. Fallback to Directus Staff Auth if still not identified
|
|
if (!userIdentity && email && password && process.env.DIRECTUS_URL) {
|
|
try {
|
|
const loginRes = await fetch(`${process.env.DIRECTUS_URL}/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
|
|
if (loginRes.ok) {
|
|
const { data } = await loginRes.json();
|
|
const accessToken = data.access_token;
|
|
|
|
// Fetch user info with company depth
|
|
const userRes = await fetch(
|
|
`${process.env.DIRECTUS_URL}/users/me?fields=*,company.*`,
|
|
{
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
},
|
|
);
|
|
|
|
if (userRes.ok) {
|
|
const { data: user } = await userRes.json();
|
|
userIdentity = user.first_name || user.email;
|
|
userCompany = {
|
|
id: user.company?.id,
|
|
name: user.company?.name,
|
|
};
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Directus Auth Error:", e);
|
|
}
|
|
}
|
|
|
|
if (userIdentity) {
|
|
const cookieStore = await cookies();
|
|
// Store identity in the cookie (simplified for now, ideally signed)
|
|
const sessionValue = JSON.stringify({
|
|
identity: userIdentity,
|
|
company: userCompany,
|
|
timestamp: Date.now(),
|
|
});
|
|
|
|
const isDev = process.env.NODE_ENV === "development";
|
|
|
|
cookieStore.set(authCookieName, sessionValue, {
|
|
httpOnly: true,
|
|
secure: !isDev,
|
|
path: "/",
|
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
|
sameSite: "lax",
|
|
...(cookieDomain ? { domain: cookieDomain } : {}),
|
|
});
|
|
redirect(targetRedirect);
|
|
} else {
|
|
redirect(`/login?error=1&redirect=${encodeURIComponent(targetRedirect)}`);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center relative bg-white font-serif antialiased overflow-hidden">
|
|
{/* Background Decor - Signature mintel.me style */}
|
|
<div
|
|
className="absolute inset-0 pointer-events-none opacity-[0.03] scale-[1.01]"
|
|
style={{
|
|
backgroundImage: `linear-gradient(to right, #000 1px, transparent 1px), linear-gradient(to bottom, #000 1px, transparent 1px)`,
|
|
backgroundSize: "clamp(30px, 8vw, 40px) clamp(30px, 8vw, 40px)",
|
|
}}
|
|
/>
|
|
|
|
<main className="relative z-10 w-full max-w-sm px-8 sm:px-6">
|
|
<div className="space-y-12 sm:space-y-16 animate-fade-in">
|
|
{/* Top Icon Box - Signature mintel.me Black Square */}
|
|
<div className="flex justify-center">
|
|
<div className="w-16 h-16 bg-black rounded-xl flex items-center justify-center shadow-xl shadow-slate-100 hover:scale-105 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] rotate-2 hover:rotate-0">
|
|
<Image
|
|
src="/icon-white.svg"
|
|
alt="Mintel"
|
|
width={32}
|
|
height={32}
|
|
className="w-8 h-8"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-12 animate-slide-up">
|
|
<div className="text-center space-y-4">
|
|
<h1 className="text-xs font-sans font-bold uppercase tracking-[0.4em] text-slate-900 border-b border-slate-50 pb-4 inline-block mx-auto min-w-[200px]">
|
|
{projectName} <span className="text-slate-300">Gatekeeper</span>
|
|
</h1>
|
|
<p className="text-[10px] text-slate-400 font-sans uppercase tracking-widest italic flex items-center justify-center gap-2">
|
|
<span className="w-1 h-1 bg-slate-200 rounded-full" />
|
|
Infrastructure Protection
|
|
<span className="w-1 h-1 bg-slate-200 rounded-full" />
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 text-red-600 px-5 py-3 rounded-2xl text-[9px] font-sans font-bold uppercase tracking-widest flex items-center gap-3 border border-red-100 animate-shake">
|
|
<ShieldCheck className="w-4 h-4" />
|
|
<span>Access Denied. Try Again.</span>
|
|
</div>
|
|
)}
|
|
|
|
<form action={login} className="space-y-4">
|
|
<input type="hidden" name="redirect" value={redirectUrl} />
|
|
|
|
<div className="space-y-2">
|
|
<div className="relative group">
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
placeholder="EMAIL (OPTIONAL)"
|
|
className="w-full bg-slate-50/50 border border-slate-200 rounded-2xl px-6 py-4 focus:outline-none focus:border-slate-900 focus:bg-white transition-all text-[10px] font-sans font-bold tracking-[0.2em] uppercase placeholder:text-slate-300 shadow-sm shadow-slate-50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="relative group">
|
|
<input
|
|
type="password"
|
|
name="password"
|
|
required
|
|
autoFocus
|
|
autoComplete="current-password"
|
|
placeholder="ACCESS CODE"
|
|
className="w-full bg-slate-50/50 border border-slate-200 rounded-2xl px-6 py-4 focus:outline-none focus:border-slate-900 focus:bg-white transition-all text-sm font-sans font-bold tracking-[0.3em] uppercase placeholder:text-slate-300 placeholder:tracking-widest shadow-sm shadow-slate-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary w-full py-5 rounded-2xl text-[10px] shadow-lg shadow-slate-100 flex items-center justify-center"
|
|
>
|
|
Unlock Access
|
|
<ArrowRight className="ml-3 w-3 h-3 group-hover:translate-x-1 transition-transform" />
|
|
</button>
|
|
</form>
|
|
|
|
{/* Bottom Section - Full Branding Parity */}
|
|
<div className="pt-12 sm:pt-20 flex flex-col items-center gap-6 sm:gap-8">
|
|
<div className="h-px w-8 bg-slate-100" />
|
|
<div className="opacity-80 transition-opacity hover:opacity-100">
|
|
<Image
|
|
src="/logo-black.svg"
|
|
alt={projectName}
|
|
width={140}
|
|
height={40}
|
|
className="h-7 sm:h-auto grayscale contrast-125 w-auto"
|
|
/>
|
|
</div>
|
|
<p className="text-[8px] font-sans font-bold text-slate-300 uppercase tracking-[0.4em] sm:tracking-[0.5em] text-center">
|
|
© 2026 MINTEL
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|