From ede11d85682f007f257fae2a6476851c1df97de8 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 25 Feb 2026 14:16:56 -0800 Subject: [PATCH] feat: standardize header, categories, cross-app spaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppSwitcher: rTube/rSwag → Creating, rSocials → Sharing, rData → Observing - EcosystemFooter: updated link order to match new categories - UserMenu: 🔑 Sign In button, 🔐 lock when logged in - SpaceSwitcher: reads EncryptID token, sends Bearer header - /api/spaces proxy: forwards to rspace.online (canonical spaces) - /api/me: verifies EncryptID token for auth status - Header.tsx: standardized bg-slate-900/85 across all apps Co-Authored-By: Claude Opus 4.6 --- src/app/api/me/route.ts | 59 ++++++++++++++++++++++---------- src/app/api/spaces/route.ts | 55 +++++++++++++++++------------ src/components/SpaceSwitcher.tsx | 37 +++++++++++++++----- 3 files changed, 103 insertions(+), 48 deletions(-) diff --git a/src/app/api/me/route.ts b/src/app/api/me/route.ts index 969ab85..7ec020e 100644 --- a/src/app/api/me/route.ts +++ b/src/app/api/me/route.ts @@ -1,29 +1,52 @@ +/** + * /api/me — Returns current user's auth status. + * + * Checks for EncryptID token in Authorization header or cookie, + * then verifies it against the EncryptID server. + */ + import { NextRequest, NextResponse } from 'next/server'; -import { getAuthUser } from '@/lib/auth'; -import { getWorkspaceSlug } from '@/lib/workspace'; -export async function GET(request: NextRequest) { +const ENCRYPTID_URL = process.env.ENCRYPTID_URL || 'https://auth.ridentity.online'; + +export async function GET(req: NextRequest) { + // Extract token from Authorization header or cookie + const auth = req.headers.get('Authorization'); + let token: string | null = null; + + if (auth?.startsWith('Bearer ')) { + token = auth.slice(7); + } else { + const tokenCookie = req.cookies.get('encryptid_token'); + if (tokenCookie) token = tokenCookie.value; + } + + if (!token) { + return NextResponse.json({ authenticated: false }); + } + try { - const auth = await getAuthUser(request); - const workspaceSlug = getWorkspaceSlug(); + const res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, { + headers: { Authorization: `Bearer ${token}` }, + }); - if (!auth) { + if (!res.ok) { + return NextResponse.json({ authenticated: false }); + } + + const data = await res.json(); + if (data.valid) { return NextResponse.json({ - authenticated: false, - workspace: workspaceSlug || null, + authenticated: true, + user: { + username: data.username || null, + did: data.did || data.userId || null, + }, }); } - return NextResponse.json({ - authenticated: true, - user: { - id: auth.user.id, - username: auth.user.username, - did: auth.did, - }, - workspace: workspaceSlug || null, - }); + return NextResponse.json({ authenticated: false }); } catch { - return NextResponse.json({ authenticated: false, workspace: null }); + return NextResponse.json({ authenticated: false }); } } diff --git a/src/app/api/spaces/route.ts b/src/app/api/spaces/route.ts index aff72df..f5cf371 100644 --- a/src/app/api/spaces/route.ts +++ b/src/app/api/spaces/route.ts @@ -1,34 +1,45 @@ -import { NextResponse } from 'next/server'; - /** - * GET /api/spaces — List spaces available to the current user. - * Proxies to rSpace API when available, otherwise returns empty list. + * Spaces API proxy — forwards to rSpace (the canonical spaces authority). + * + * Every r*App proxies /api/spaces to rSpace so the SpaceSwitcher dropdown + * shows the same spaces everywhere. The EncryptID token is forwarded so + * rSpace can return user-specific spaces (owned/member). */ -export async function GET(request: Request) { - const rspaceUrl = process.env.RSPACE_INTERNAL_URL || process.env.NEXT_PUBLIC_RSPACE_URL; - if (!rspaceUrl) { - return NextResponse.json({ spaces: [] }); +import { NextRequest, NextResponse } from 'next/server'; + +const RSPACE_API = process.env.RSPACE_API_URL || 'https://rspace.online'; + +export async function GET(req: NextRequest) { + const headers: Record = {}; + + // Forward the EncryptID token (from Authorization header or cookie) + const auth = req.headers.get('Authorization'); + if (auth) { + headers['Authorization'] = auth; + } else { + // Fallback: check for encryptid_token cookie + const tokenCookie = req.cookies.get('encryptid_token'); + if (tokenCookie) { + headers['Authorization'] = `Bearer ${tokenCookie.value}`; + } } try { - // Forward auth header to rSpace - const authHeader = request.headers.get('Authorization'); - const headers: Record = {}; - if (authHeader) headers['Authorization'] = authHeader; - - const res = await fetch(`${rspaceUrl}/api/spaces`, { + const res = await fetch(`${RSPACE_API}/api/spaces`, { headers, - next: { revalidate: 60 }, + next: { revalidate: 30 }, // cache for 30s to avoid hammering rSpace }); - if (res.ok) { - const data = await res.json(); - return NextResponse.json(data); + if (!res.ok) { + // If rSpace is down, return empty spaces (graceful degradation) + return NextResponse.json({ spaces: [] }); } - } catch { - // rSpace not reachable - } - return NextResponse.json({ spaces: [] }); + const data = await res.json(); + return NextResponse.json(data); + } catch { + // rSpace unreachable — return empty list + return NextResponse.json({ spaces: [] }); + } } diff --git a/src/components/SpaceSwitcher.tsx b/src/components/SpaceSwitcher.tsx index f5a9450..b23e813 100644 --- a/src/components/SpaceSwitcher.tsx +++ b/src/components/SpaceSwitcher.tsx @@ -10,10 +10,20 @@ interface SpaceInfo { } interface SpaceSwitcherProps { - /** Current app domain, e.g. 'rfunds.online'. Space links become . */ + /** Current app domain, e.g. 'rchats.online'. Space links become . */ domain?: string; } +/** Read the EncryptID token from localStorage (set by token-relay across r*.online) */ +function getEncryptIDToken(): string | null { + if (typeof window === 'undefined') return null; + try { + return localStorage.getItem('encryptid_token'); + } catch { + return null; + } +} + export function SpaceSwitcher({ domain }: SpaceSwitcherProps) { const [open, setOpen] = useState(false); const [spaces, setSpaces] = useState([]); @@ -38,18 +48,29 @@ export function SpaceSwitcher({ domain }: SpaceSwitcherProps) { // Check auth status on mount useEffect(() => { - fetch('/api/me') - .then((r) => r.json()) - .then((data) => { - if (data.authenticated) setIsAuthenticated(true); - }) - .catch(() => {}); + const token = getEncryptIDToken(); + if (token) { + setIsAuthenticated(true); + } else { + // Fallback: check /api/me + fetch('/api/me') + .then((r) => r.json()) + .then((data) => { + if (data.authenticated) setIsAuthenticated(true); + }) + .catch(() => {}); + } }, []); const loadSpaces = async () => { if (loaded) return; try { - const res = await fetch('/api/spaces'); + // Pass EncryptID token so the proxy can forward it to rSpace + const token = getEncryptIDToken(); + const headers: Record = {}; + if (token) headers['Authorization'] = `Bearer ${token}`; + + const res = await fetch('/api/spaces', { headers }); if (res.ok) { const data = await res.json(); setSpaces(data.spaces || []);