RM / src /components /auth /AuthGuard.tsx
trretretret's picture
Initial commit: Add research assistant application
b708f13
"use client";
import * as React from "react";
import { useRouter, usePathname } from "next/navigation";
import { Crown, Lock, ShieldAlert } from "lucide-react";
import { Button } from "@/components/atoms/Button";
import { Icon } from "@/components/atoms/Icon";
import { Spinner } from "@/components/atoms/Spinner";
/**
* Unified Auth & Tier Guard
* - Validates JWT existence & expiration.
* - Gates 'Pro' routes (Editor/WriteSage) based on is_premium claim.
*/
export function AuthGuard({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const [authState, setAuthState] = React.useState<{
isAuthorized: boolean;
isPremium: boolean;
}>({ isAuthorized: false, isPremium: false });
React.useEffect(() => {
const token = localStorage.getItem("token");
// 1. Check Existence
if (!token) {
router.push(`/login?callbackUrl=${encodeURIComponent(pathname)}`);
return;
}
try {
// 2. Decode & Validate Expiration
const payload = JSON.parse(window.atob(token.split(".")[1]));
const isExpired = Math.floor(Date.now() / 1000) >= payload.exp;
if (isExpired) {
localStorage.removeItem("token");
router.push(`/login?error=session_expired`);
return;
}
// 3. Set Global State
setAuthState({
isAuthorized: true,
isPremium: payload.is_premium === true,
});
} catch (err) {
localStorage.removeItem("token");
router.push("/login?error=invalid_session");
}
}, [router, pathname]);
// 4. Loading state while verifying credentials
if (!authState.isAuthorized) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-background">
<div className="flex flex-col items-center gap-4">
<Spinner size={32} />
<p className="text-[10px] font-bold uppercase tracking-[0.2em] text-muted-foreground animate-pulse">
Secure Session Handshake
</p>
</div>
</div>
);
}
// 5. Tier Gating: Block 'Standard' users from Pro-only routes (Editor)
const isProRoute = pathname.startsWith("/editor");
if (isProRoute && !authState.isPremium) {
return (
<div className="flex h-screen w-screen flex-col items-center justify-center p-6 text-center">
<div className="mb-6 flex h-20 w-20 items-center justify-center rounded-2xl bg-amber-500/10 text-amber-600 border border-amber-500/20 shadow-inner">
<Icon icon={Crown} size={40} />
</div>
<h2 className="text-2xl font-bold tracking-tight">Pro Access Required</h2>
<p className="mt-2 max-w-sm text-sm text-muted-foreground leading-relaxed">
The **WriteSage** manuscript engine requires a Pro subscription.
Upgrade your tier to unlock AI-assisted drafting.
</p>
<div className="mt-8 flex gap-3">
<Button variant="outline" onClick={() => router.push("/explore")}>
Return to Discovery
</Button>
<Button onClick={() => router.push("/settings")}>
View Tiers
</Button>
</div>
</div>
);
}
return <>{children}</>;
}