Files
shared-memory/apps/web/app/(authed)/settings/page.tsx
T
shadowdaoandClaude Opus 4.7 ff6baab393 feat: Phase 3b — proper Web UI for memories, projects, settings
Replaces the debug /me + /connect pages with a real authed app shell.

Pages
- /                       — anonymous landing; redirects to /dashboard once signed in
- /dashboard              — recent memories + top projects, quick "new memory" action
- /memories               — searchable list with hybrid (vector+FTS+tags) scoring;
                            per-result rank breakdown shown inline
- /memories/[id]          — view + inline edit toggle + delete
- /memories/new           — create form with project autocomplete
- /projects               — list with memory counts and last-activity
- /projects/[key]         — that project's memories
- /settings               — read-only Authentik profile + link to tokens
- /settings/tokens        — list / create / revoke CLI tokens

Old URLs preserved as redirects:
- /me      → /dashboard
- /connect → /settings/tokens

Stack additions
- Tailwind v4 with CSS-first @theme tokens (dark only for now)
- App shell in app/(authed)/ — auth guard + top nav with global search box
- Lightweight UI primitives in app/_components/ui/ (Button, Input, Card,
  Badge, EmptyState, Container, PageHeader)
- Search logic extracted from MCP tool into lib/memories.ts so Web UI and
  MCP both call the same RRF code path
- Memory CRUD via Server Actions in lib/memory-actions.ts; audit_log
  rows are tagged actor='web' to distinguish from MCP writes

Per-token revoke
- New cli_tokens table (id, user_id, jti unique, name, created_at,
  last_used_at, expires_at, revoked_at) — migration 0001_cli_tokens.sql
- mintCliToken now records jti + name; verifyCliToken enforces revocation
  for tracked tokens. Legacy tokens minted before this change (no jti)
  are accepted on signature alone until they expire naturally.
- /settings/tokens lists active + revoked tokens with one-click revoke

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:57:17 -07:00

79 lines
2.5 KiB
TypeScript

import Link from "next/link";
import { eq } from "drizzle-orm";
import { auth } from "@/auth";
import { db } from "@/lib/db/client";
import { users } from "@/lib/db/schema";
import { Container, PageHeader } from "@/app/_components/ui/container";
import { Card, CardBody, CardHeader } from "@/app/_components/ui/card";
import { Button } from "@/app/_components/ui/button";
export const dynamic = "force-dynamic";
export default async function SettingsPage() {
const session = await auth();
const userId = session!.user.id;
const userRow = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
const user = userRow[0];
return (
<Container className="pt-6 max-w-3xl">
<PageHeader title="Settings" />
<div className="space-y-6">
<Card>
<CardHeader className="text-sm font-medium text-fg">Profile</CardHeader>
<CardBody className="space-y-2 text-sm">
<Field label="Name" value={user?.name} />
<Field label="Email" value={user?.email} />
<Field label="Internal user id" value={user?.id} mono />
<Field label="OIDC issuer" value={user?.oidcIss} mono />
<Field label="OIDC sub" value={user?.oidcSub} mono />
<Field
label="Joined"
value={user?.createdAt ? new Date(user.createdAt).toLocaleString() : null}
/>
</CardBody>
</Card>
<Card>
<CardHeader className="flex items-center">
<span className="text-sm font-medium text-fg flex-1">CLI tokens</span>
<Link href="/settings/tokens" className="no-underline">
<Button variant="secondary" size="sm">Manage tokens</Button>
</Link>
</CardHeader>
<CardBody className="text-sm text-fg-muted">
Bearer tokens for headless/automated MCP clients. Visit{" "}
<Link href="/settings/tokens">/settings/tokens</Link> to generate
and revoke them.
</CardBody>
</Card>
</div>
</Container>
);
}
function Field({
label,
value,
mono,
}: {
label: string;
value: string | null | undefined;
mono?: boolean;
}) {
return (
<div className="flex items-baseline gap-3">
<span className="text-fg-muted w-36 shrink-0">{label}</span>
<span className={`${mono ? "font-mono text-xs" : "text-sm"} text-fg break-all`}>
{value ?? <span className="text-fg-subtle"></span>}
</span>
</div>
);
}