feat: add auth to gatekeeper
This commit is contained in:
@@ -9,8 +9,32 @@ export async function GET(req: NextRequest) {
|
|||||||
|
|
||||||
const session = cookieStore.get(authCookieName);
|
const session = cookieStore.get(authCookieName);
|
||||||
|
|
||||||
if (session?.value === password) {
|
let isAuthenticated = false;
|
||||||
return new NextResponse("OK", { status: 200 });
|
let identity = "Guest";
|
||||||
|
|
||||||
|
if (session?.value) {
|
||||||
|
if (session.value === password) {
|
||||||
|
isAuthenticated = true;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(session.value);
|
||||||
|
if (payload.identity) {
|
||||||
|
isAuthenticated = true;
|
||||||
|
identity = payload.identity;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback or old format
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
return new NextResponse("OK", {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
"X-Auth-User": identity,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Traefik ForwardAuth headers
|
// Traefik ForwardAuth headers
|
||||||
|
|||||||
26
packages/gatekeeper/src/app/api/whoami/route.ts
Normal file
26
packages/gatekeeper/src/app/api/whoami/route.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const authCookieName =
|
||||||
|
process.env.AUTH_COOKIE_NAME || "mintel_gatekeeper_session";
|
||||||
|
const session = cookieStore.get(authCookieName);
|
||||||
|
|
||||||
|
if (!session?.value) {
|
||||||
|
return NextResponse.json({ authenticated: false }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let identity = "Guest";
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(session.value);
|
||||||
|
identity = payload.identity || "Guest";
|
||||||
|
} catch (e) {
|
||||||
|
// Old format probably just the password
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
authenticated: true,
|
||||||
|
identity: identity,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -17,16 +17,69 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|||||||
async function login(formData: FormData) {
|
async function login(formData: FormData) {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const password = formData.get("password");
|
const email = formData.get("email") as string;
|
||||||
const expectedPassword = process.env.GATEKEEPER_PASSWORD || "mintel";
|
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 =
|
const authCookieName =
|
||||||
process.env.AUTH_COOKIE_NAME || "mintel_gatekeeper_session";
|
process.env.AUTH_COOKIE_NAME || "mintel_gatekeeper_session";
|
||||||
const targetRedirect = formData.get("redirect") as string;
|
const targetRedirect = formData.get("redirect") as string;
|
||||||
const cookieDomain = process.env.COOKIE_DOMAIN;
|
const cookieDomain = process.env.COOKIE_DOMAIN;
|
||||||
|
|
||||||
if (password === expectedPassword) {
|
let userIdentity = "";
|
||||||
|
|
||||||
|
// 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 Directus if email is provided
|
||||||
|
if (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 to get a nice display name
|
||||||
|
const userRes = await fetch(`${process.env.DIRECTUS_URL}/users/me`, {
|
||||||
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userRes.ok) {
|
||||||
|
const { data: user } = await userRes.json();
|
||||||
|
userIdentity = user.first_name || user.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Directus Auth Error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIdentity) {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
cookieStore.set(authCookieName, expectedPassword, {
|
// Store identity in the cookie (simplified for now, ideally signed)
|
||||||
|
const sessionValue = JSON.stringify({
|
||||||
|
identity: userIdentity,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
cookieStore.set(authCookieName, sessionValue, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: true,
|
secure: true,
|
||||||
path: "/",
|
path: "/",
|
||||||
@@ -85,24 +138,35 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form action={login} className="space-y-6">
|
<form action={login} className="space-y-4">
|
||||||
<input type="hidden" name="redirect" value={redirectUrl} />
|
<input type="hidden" name="redirect" value={redirectUrl} />
|
||||||
|
|
||||||
<div className="relative group">
|
<div className="space-y-2">
|
||||||
<input
|
<div className="relative group">
|
||||||
type="password"
|
<input
|
||||||
name="password"
|
type="email"
|
||||||
required
|
name="email"
|
||||||
autoFocus
|
placeholder="EMAIL (OPTIONAL)"
|
||||||
autoComplete="current-password"
|
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"
|
||||||
placeholder="GATEKEEPER 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 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>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary w-full py-5 rounded-2xl text-[10px] shadow-lg shadow-slate-100"
|
className="btn btn-primary w-full py-5 rounded-2xl text-[10px] shadow-lg shadow-slate-100 flex items-center justify-center"
|
||||||
>
|
>
|
||||||
Unlock Access
|
Unlock Access
|
||||||
<ArrowRight className="ml-3 w-3 h-3 group-hover:translate-x-1 transition-transform" />
|
<ArrowRight className="ml-3 w-3 h-3 group-hover:translate-x-1 transition-transform" />
|
||||||
|
|||||||
Reference in New Issue
Block a user