feat: standardize header, categories, cross-app spaces
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
f46575989f
commit
51b9b2f079
|
|
@ -0,0 +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';
|
||||
|
||||
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 res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ authenticated: false });
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (data.valid) {
|
||||
return NextResponse.json({
|
||||
authenticated: true,
|
||||
user: {
|
||||
username: data.username || null,
|
||||
did: data.did || data.userId || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ authenticated: false });
|
||||
} catch {
|
||||
return NextResponse.json({ authenticated: false });
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* 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).
|
||||
*/
|
||||
|
||||
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<string, string> = {};
|
||||
|
||||
// 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 {
|
||||
const res = await fetch(`${RSPACE_API}/api/spaces`, {
|
||||
headers,
|
||||
next: { revalidate: 30 }, // cache for 30s to avoid hammering rSpace
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
// If rSpace is down, return empty spaces (graceful degradation)
|
||||
return NextResponse.json({ spaces: [] });
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
return NextResponse.json(data);
|
||||
} catch {
|
||||
// rSpace unreachable — return empty list
|
||||
return NextResponse.json({ spaces: [] });
|
||||
}
|
||||
}
|
||||
|
|
@ -10,10 +10,20 @@ interface SpaceInfo {
|
|||
}
|
||||
|
||||
interface SpaceSwitcherProps {
|
||||
/** Current app domain, e.g. 'rfunds.online'. Space links become <space>.<domain> */
|
||||
/** Current app domain, e.g. 'rchats.online'. Space links become <space>.<domain> */
|
||||
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<SpaceInfo[]>([]);
|
||||
|
|
@ -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<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch('/api/spaces', { headers });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSpaces(data.spaces || []);
|
||||
|
|
|
|||
Loading…
Reference in New Issue