Add superuser authentication check to site-settings page

Enhanced security for site configuration admin panel to ensure only superusers can access and modify site settings.

Backend changes:
- Enhanced /verify-token endpoint to return user information including is_superuser flag
- Now returns user.id, email, is_active, is_superuser, and is_verified status
- Allows frontend to verify user permissions before loading sensitive pages

Frontend changes:
- Added superuser verification check to site-settings page
- Verifies user is authenticated AND is_superuser before loading page
- Shows loading screen with "Verifying Access" message during check
- Redirects non-superusers to dashboard with clear error toast
- Prevents unauthorized access to admin-only configuration

Security improvements:
- Layered protection: Dashboard auth + Page-level superuser check + Backend API validation
- Clear error messages for non-superusers
- Graceful UX with loading states and redirects
- Backend API endpoints already protected with superuser checks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ojārs Kapteinis 2025-11-18 15:07:39 +02:00
parent b8bb74ec3e
commit ae6455069b
2 changed files with 82 additions and 4 deletions

View file

@ -111,4 +111,13 @@ async def authenticated_route(
user: User = Depends(current_active_user), user: User = Depends(current_active_user),
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
return {"message": "Token is valid"} return {
"message": "Token is valid",
"user": {
"id": str(user.id),
"email": user.email,
"is_active": user.is_active,
"is_superuser": user.is_superuser,
"is_verified": user.is_verified,
}
}

View file

@ -4,6 +4,8 @@ import { useState, useEffect } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
import { useSiteConfig } from "@/contexts/SiteConfigContext"; import { useSiteConfig } from "@/contexts/SiteConfigContext";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Loader2 } from "lucide-react";
interface SiteConfigForm { interface SiteConfigForm {
// Header/Navbar toggles // Header/Navbar toggles
@ -37,6 +39,7 @@ interface SiteConfigForm {
export default function SiteSettingsPage() { export default function SiteSettingsPage() {
const { config, isLoading, refetch } = useSiteConfig(); const { config, isLoading, refetch } = useSiteConfig();
const router = useRouter();
const [formData, setFormData] = useState<SiteConfigForm>({ const [formData, setFormData] = useState<SiteConfigForm>({
show_pricing_link: false, show_pricing_link: false,
show_docs_link: false, show_docs_link: false,
@ -56,6 +59,54 @@ export default function SiteSettingsPage() {
custom_copyright: "SurfSense 2025", custom_copyright: "SurfSense 2025",
}); });
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [isSuperuser, setIsSuperuser] = useState(false);
// Check if user is a superuser
useEffect(() => {
const checkSuperuser = async () => {
try {
const backendUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL || "http://localhost:8000";
const token = localStorage.getItem("access_token");
if (!token) {
router.push("/login");
return;
}
const response = await fetch(`${backendUrl}/verify-token`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
router.push("/login");
return;
}
const data = await response.json();
if (!data.user?.is_superuser) {
toast.error("Access Denied", {
description: "You must be a superuser to access site settings.",
duration: 5000,
});
router.push("/dashboard");
return;
}
setIsSuperuser(true);
} catch (error) {
console.error("Error verifying superuser status:", error);
router.push("/login");
} finally {
setIsCheckingAuth(false);
}
};
checkSuperuser();
}, [router]);
// Load config into form when available // Load config into form when available
useEffect(() => { useEffect(() => {
@ -127,14 +178,32 @@ export default function SiteSettingsPage() {
} }
}; };
if (isLoading) { // Show loading screen while checking authentication or loading config
if (isCheckingAuth || isLoading) {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-white dark:bg-black"> <div className="flex flex-col items-center justify-center min-h-screen space-y-4">
<div className="text-neutral-600 dark:text-neutral-400">Loading configuration...</div> <Card className="w-[350px] bg-background/60 backdrop-blur-sm">
<CardHeader className="pb-2">
<CardTitle className="text-xl font-medium">
{isCheckingAuth ? "Verifying Access" : "Loading Configuration"}
</CardTitle>
<CardDescription>
{isCheckingAuth ? "Checking superuser permissions..." : "Loading site settings..."}
</CardDescription>
</CardHeader>
<CardContent className="flex justify-center py-6">
<Loader2 className="h-12 w-12 text-primary animate-spin" />
</CardContent>
</Card>
</div> </div>
); );
} }
// If not checking auth anymore and user is not superuser, they've been redirected
if (!isSuperuser) {
return null;
}
return ( return (
<div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-black dark:to-gray-900 py-12 px-4 sm:px-6 lg:px-8"> <div className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-black dark:to-gray-900 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">