implement better-auth auth with postgres and route protection

This commit is contained in:
2026-02-24 13:32:43 -05:00
parent fd168f607c
commit 52a4ab38d3
31 changed files with 1202 additions and 89 deletions

View File

@@ -1,8 +1,11 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Activity, BookOpenText, ChartCandlestick, Eye } from 'lucide-react';
import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react';
import { Activity, BookOpenText, ChartCandlestick, Eye, LogOut } from 'lucide-react';
import { authClient } from '@/lib/auth-client';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
type AppShellProps = {
@@ -21,6 +24,31 @@ const NAV_ITEMS = [
export function AppShell({ title, subtitle, actions, children }: AppShellProps) {
const pathname = usePathname();
const router = useRouter();
const [isSigningOut, setIsSigningOut] = useState(false);
const { data: session } = authClient.useSession();
const sessionUser = (session?.user ?? null) as { name?: string | null; email?: string | null; role?: unknown } | null;
const role = typeof sessionUser?.role === 'string'
? sessionUser.role
: Array.isArray(sessionUser?.role)
? sessionUser.role.filter((entry): entry is string => typeof entry === 'string').join(', ')
: null;
const displayName = sessionUser?.name || sessionUser?.email || 'Authenticated user';
const signOut = async () => {
if (isSigningOut) {
return;
}
setIsSigningOut(true);
try {
await authClient.signOut();
router.replace('/auth/signin');
} finally {
setIsSigningOut(false);
}
};
return (
<div className="app-surface">
@@ -62,10 +90,15 @@ export function AppShell({ title, subtitle, actions, children }: AppShellProps)
<div className="mt-auto rounded-xl border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] p-3">
<p className="text-xs uppercase tracking-[0.2em] text-[color:var(--terminal-muted)]">Runtime</p>
<p className="mt-1 truncate text-sm text-[color:var(--terminal-bright)]">local operator mode</p>
<p className="mt-1 truncate text-sm text-[color:var(--terminal-bright)]">{displayName}</p>
{role ? <p className="mt-1 text-xs uppercase tracking-[0.16em] text-[color:var(--terminal-muted)]">Role: {role}</p> : null}
<p className="mt-2 text-xs text-[color:var(--terminal-muted)]">
OpenClaw and market data are driven by environment configuration and live API tasks.
</p>
<Button className="mt-3 w-full" variant="ghost" onClick={() => void signOut()} disabled={isSigningOut}>
<LogOut className="size-4" />
{isSigningOut ? 'Signing out...' : 'Sign out'}
</Button>
</div>
</aside>
@@ -79,7 +112,13 @@ export function AppShell({ title, subtitle, actions, children }: AppShellProps)
<p className="mt-1 text-sm text-[color:var(--terminal-muted)]">{subtitle}</p>
) : null}
</div>
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
<div className="flex flex-wrap items-center gap-2">
{actions}
<Button variant="ghost" onClick={() => void signOut()} disabled={isSigningOut}>
<LogOut className="size-4" />
{isSigningOut ? 'Signing out...' : 'Sign out'}
</Button>
</div>
</div>
</header>