feat(spaces): resolve member DIDs to usernames in space settings

Add POST /api/users/resolve-dids batch endpoint in EncryptID, proxy
/api/users/* through rspace server, and batch-resolve missing displayNames
in the space settings panel so owners and members show usernames not DIDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-21 16:39:48 -07:00
parent a415d6c308
commit acafe15c4b
3 changed files with 58 additions and 1 deletions

View File

@ -452,7 +452,7 @@ app.all("/encryptid/*", async (c) => {
}
});
// ── User API proxy (forward /api/user/* to EncryptID for tab state, prefs) ──
// ── User API proxy (forward /api/user/* and /api/users/* to EncryptID) ──
app.all("/api/user/*", async (c) => {
const targetUrl = `${ENCRYPTID_INTERNAL}${c.req.path}${new URL(c.req.url).search}`;
const headers = new Headers(c.req.raw.headers);
@ -470,6 +470,23 @@ app.all("/api/user/*", async (c) => {
return c.json({ error: "EncryptID service unavailable" }, 502);
}
});
app.all("/api/users/*", async (c) => {
const targetUrl = `${ENCRYPTID_INTERNAL}${c.req.path}${new URL(c.req.url).search}`;
const headers = new Headers(c.req.raw.headers);
headers.delete("host");
try {
const res = await fetch(targetUrl, {
method: c.req.method,
headers,
body: c.req.method !== "GET" && c.req.method !== "HEAD" ? c.req.raw.body : undefined,
// @ts-ignore duplex needed for streaming request bodies
duplex: "half",
});
return new Response(res.body, { status: res.status, headers: res.headers });
} catch (e: any) {
return c.json({ error: "EncryptID service unavailable" }, 502);
}
});
// ── Existing /api/communities/* routes (backward compatible) ──

View File

@ -142,6 +142,26 @@ export class RStackSpaceSettings extends HTMLElement {
} catch {}
}
// Resolve missing displayNames from EncryptID
const unresolvedDids = this._members.filter(m => !m.displayName).map(m => m.did);
if (unresolvedDids.length && token) {
try {
const res = await fetch("/api/users/resolve-dids", {
method: "POST",
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ dids: unresolvedDids }),
});
if (res.ok) {
const resolved = await res.json() as Record<string, { username: string; displayName: string }>;
for (const m of this._members) {
if (!m.displayName && resolved[m.did]) {
m.displayName = resolved[m.did].displayName || resolved[m.did].username;
}
}
}
} catch {}
}
// Determine my role
if (session?.claims?.sub) {
this._isOwner = session.claims.sub === this._ownerDID;

View File

@ -3817,6 +3817,26 @@ app.get('/api/users/lookup', async (c) => {
});
});
// POST /api/users/resolve-dids — batch-resolve DIDs to usernames
app.post('/api/users/resolve-dids', async (c) => {
const claims = await verifyTokenFromRequest(c.req.header('Authorization'));
if (!claims) return c.json({ error: 'Authentication required' }, 401);
const body = await c.req.json();
const dids: string[] = Array.isArray(body.dids) ? body.dids.slice(0, 100) : [];
if (!dids.length) return c.json({});
// Query all matching users in one go
const rows = await sql`SELECT id, did, username, display_name FROM users WHERE did = ANY(${dids}) OR id = ANY(${dids})`;
const result: Record<string, { username: string; displayName: string }> = {};
for (const row of rows) {
const entry = { username: row.username, displayName: row.display_name || row.username };
if (row.did) result[row.did] = entry;
result[row.id] = entry;
}
return c.json(result);
});
// ============================================================================
// SPACE INVITE ROUTES
// ============================================================================