c5ca3ca992
Implement local user authentication with password hashing, add admin roles for user management and audit log viewing, and introduce audit logging for critical actions. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 776963d0-f75d-42e2-a57b-cc36bdff8495 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 832a44ff-12ae-4096-8a0d-666ec083d536 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0683fb79-a27c-485c-9333-5f4b288c4567/776963d0-f75d-42e2-a57b-cc36bdff8495/1p7jhzu Replit-Helium-Checkpoint-Created: true
118 lines
4.6 KiB
TypeScript
118 lines
4.6 KiB
TypeScript
import { Link, useLocation } from "wouter";
|
|
import { LayoutDashboard, Wrench, PlusCircle, BarChart3, LogIn, LogOut, User, ShieldCheck } from "lucide-react";
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
const [location] = useLocation();
|
|
const { user, isLoading, isAuthenticated, isAdmin, isLocalMode, login, logout } = useAuth();
|
|
|
|
const links = [
|
|
{ href: "/", label: "Dashboard", icon: LayoutDashboard },
|
|
{ href: "/tools", label: "Browse Tools", icon: Wrench },
|
|
{ href: "/tools/new", label: "Add Tool", icon: PlusCircle },
|
|
{ href: "/analytics", label: "Analytics", icon: BarChart3 },
|
|
...(isAdmin ? [{ href: "/admin", label: "Admin", icon: ShieldCheck }] : []),
|
|
];
|
|
|
|
return (
|
|
<div className="flex min-h-screen bg-background text-foreground">
|
|
<aside className="w-64 border-r bg-card flex flex-col hidden md:flex">
|
|
<div className="p-6 border-b">
|
|
<div className="flex items-center gap-2 text-primary font-bold text-xl">
|
|
<Wrench className="w-6 h-6" />
|
|
<span>ToolRate</span>
|
|
</div>
|
|
</div>
|
|
<nav className="flex-1 p-4 space-y-2">
|
|
{links.map((link) => {
|
|
const isActive = location === link.href || (link.href !== "/" && location.startsWith(link.href));
|
|
const Icon = link.icon;
|
|
return (
|
|
<Link
|
|
key={link.href}
|
|
href={link.href}
|
|
className={`flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${isActive ? "bg-primary text-primary-foreground font-medium" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`}
|
|
>
|
|
<Icon className="w-5 h-5" />
|
|
{link.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
<div className="p-4 border-t">
|
|
{isLoading ? (
|
|
<div className="flex items-center gap-3 px-3 py-2">
|
|
<Skeleton className="w-8 h-8 rounded-full" />
|
|
<Skeleton className="h-4 w-24" />
|
|
</div>
|
|
) : isAuthenticated && user ? (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-3 px-3 py-2 rounded-md bg-muted/50">
|
|
<div className="w-8 h-8 rounded-full bg-primary/15 flex items-center justify-center shrink-0">
|
|
<User className="w-4 h-4 text-primary" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-medium truncate text-foreground">
|
|
{user.name || user.preferredUsername || "User"}
|
|
</p>
|
|
{user.email && (
|
|
<p className="text-xs text-muted-foreground truncate">{user.email}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-start gap-2 text-muted-foreground hover:text-destructive"
|
|
onClick={logout}
|
|
data-testid="button-logout"
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
Sign out
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full gap-2"
|
|
onClick={() => login(location)}
|
|
data-testid="button-login"
|
|
>
|
|
<LogIn className="w-4 h-4" />
|
|
{isLocalMode ? "Sign in" : "Sign in with Keycloak"}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
|
|
<main className="flex-1 flex flex-col min-w-0">
|
|
<header className="md:hidden border-b p-4 flex items-center justify-between bg-card">
|
|
<div className="flex items-center gap-2 text-primary font-bold text-lg">
|
|
<Wrench className="w-5 h-5" />
|
|
<span>ToolRate</span>
|
|
</div>
|
|
{!isLoading && (
|
|
isAuthenticated ? (
|
|
<Button variant="ghost" size="sm" onClick={logout} data-testid="button-logout-mobile">
|
|
<LogOut className="w-4 h-4" />
|
|
</Button>
|
|
) : (
|
|
<Button variant="outline" size="sm" onClick={() => login(location)} data-testid="button-login-mobile">
|
|
<LogIn className="w-4 h-4 mr-1" />
|
|
Sign in
|
|
</Button>
|
|
)
|
|
)}
|
|
</header>
|
|
<div className="flex-1 p-6 md:p-8 overflow-auto">
|
|
{children}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|