Files
at-mintel/packages/gatekeeper/src/app/login/page.tsx
Marc Mintel 36ed26ad79
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m10s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m15s
Monorepo Pipeline / 🏗️ Build (push) Successful in 1m53s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s
feat(gatekeeper): major UI upgrade - high-fidelity light theme, iridescent mouse-reactive form, and enhanced background animation
2026-02-28 21:48:03 +01:00

237 lines
8.6 KiB
TypeScript

import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { ShieldCheck } from "lucide-react";
import Image from "next/image";
import { GateScene } from "../../components/gate-scene";
import { AnimatedLoginForm } from "../../components/animated-login-form";
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) || "").trim();
const password = ((formData.get("password") as string) || "").trim();
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 Generic Code (Guest) - High Priority to prevent autofill traps
if (password === expectedCode) {
userIdentity = "Guest";
}
// 2. Check Global Admin (from ENV)
else if (
adminEmail &&
adminPassword &&
email === adminEmail.trim() &&
password === adminPassword.trim()
) {
userIdentity = "Admin";
}
// 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) {
console.log(
`[Login] Success: ${userIdentity} | Redirect: ${targetRedirect}`,
);
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";
console.log(
`[Login] Setting Cookie: ${authCookieName} | Domain: ${cookieDomain || "Default"}`,
);
cookieStore.set(authCookieName, sessionValue, {
httpOnly: true,
secure: !isDev,
path: "/",
maxAge: 30 * 24 * 60 * 60, // 30 days
sameSite: "lax",
...(cookieDomain ? { domain: cookieDomain } : {}),
});
redirect(targetRedirect);
} else {
console.log(`[Login] Failed for inputs. Redirecting back with error.`);
redirect(`/login?error=1&redirect=${encodeURIComponent(targetRedirect)}`);
}
}
return (
<div className="min-h-screen flex items-center justify-center relative bg-[#f5f5f7] font-serif antialiased overflow-hidden selection:bg-black/10 selection:text-black">
{/* 3D Digital Gate Background */}
<GateScene />
<main className="relative z-10 w-full max-w-[380px] px-6 sm:px-4 pb-24 sm:pb-32 pointer-events-auto">
<div className="space-y-10 animate-fade-in">
{/* Top Icon Box */}
<div className="flex justify-center">
<a
href="https://mintel.me"
target="_blank"
rel="noopener noreferrer"
className="w-14 h-14 bg-white rounded-2xl flex items-center justify-center shadow-lg shadow-black/[0.06] hover:scale-105 hover:shadow-black/10 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] border border-black/[0.06] hover:border-black/10"
>
<Image
src="/gatekeeper/icon-white.svg"
alt="Mintel"
style={{ filter: "invert(1)" }}
width={28}
height={28}
className="w-7 h-7 opacity-80"
unoptimized
/>
</a>
</div>
<div className="space-y-8 animate-slide-up">
<div className="text-center space-y-3">
<h1 className="text-[11px] font-sans font-bold uppercase tracking-[0.5em] text-black/80 pb-3 inline-block mx-auto min-w-[220px]">
{projectName} <span className="text-black/30">Gatekeeper</span>
</h1>
<div className="flex items-center justify-center gap-4">
<div className="h-px w-8 bg-gradient-to-r from-transparent to-black/10" />
<p className="text-[8px] text-black/30 font-sans uppercase tracking-[0.35em] font-semibold">
Infrastructure Protection
</p>
<div className="h-px w-8 bg-gradient-to-l from-transparent to-black/10" />
</div>
</div>
{error && (
<div className="bg-red-50 backdrop-blur-md text-red-600 px-5 py-4 rounded-2xl text-[9px] font-sans font-bold uppercase tracking-widest flex items-center gap-3 border border-red-200 animate-shake">
<ShieldCheck className="w-4 h-4 text-red-500/70" />
<span>Access Denied. Try Again.</span>
</div>
)}
{/* The Animated Framer Motion Form */}
<AnimatedLoginForm
redirectUrl={redirectUrl}
loginAction={login}
projectName={projectName}
/>
{/* Bottom Section */}
<div className="pt-4 sm:pt-6 flex flex-col items-center gap-5">
<div className="h-px w-16 bg-gradient-to-r from-transparent via-black/10 to-transparent" />
<a
href="https://mintel.me"
target="_blank"
rel="noopener noreferrer"
className="opacity-30 transition-opacity hover:opacity-60"
>
<Image
src="/gatekeeper/logo-white.svg"
alt={projectName}
width={120}
height={36}
className="h-5 sm:h-6 w-auto"
style={{ filter: "invert(1)" }}
unoptimized
/>
</a>
<p className="text-[7px] font-sans font-semibold text-black/25 uppercase tracking-[0.5em] text-center">
&copy; {new Date().getFullYear()} MINTEL
</p>
</div>
</div>
</div>
</main>
</div>
);
}