fix: graph 3d-force-graph resolution, DID/username display, space role checks
- Switch 3d-force-graph CDN from jsdelivr to esm.sh with bundle-deps
to resolve missing "three-forcegraph" bare specifier error
- Fix storeCredential() to pass displayName and DID to createUser()
(prevents NULL did column for credential-first user creation)
- Fix invite acceptance to use claims.did instead of claims.sub for
space_members.user_did (DID format consistency)
- Fix session refresh to look up username from DB when missing from
old token (prevents empty username after token refresh)
- Fix resolveCallerRole() in spaces.ts to check both claims.sub and
claims.did against ownerDID and member keys (auto-provisioned spaces
store ownerDID as did🔑, API-created as raw userId)
- Refactor CRM route to use URL subpath tabs with renderCrm helper
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b60c0f565e
commit
0c3d8728a0
|
|
@ -19,7 +19,7 @@ const GRAPH3D_IMPORTMAP = `<script type="importmap">
|
|||
"three": "https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/",
|
||||
"three/examples/jsm/": "https://cdn.jsdelivr.net/npm/three@0.169.0/examples/jsm/",
|
||||
"3d-force-graph": "https://cdn.jsdelivr.net/npm/3d-force-graph@1/dist/3d-force-graph.mjs"
|
||||
"3d-force-graph": "https://esm.sh/3d-force-graph@1?bundle-deps&external=three"
|
||||
}
|
||||
}
|
||||
</script>`;
|
||||
|
|
@ -383,9 +383,18 @@ routes.get("/api/opportunities", async (c) => {
|
|||
});
|
||||
|
||||
// ── CRM sub-route — API-driven CRM view ──
|
||||
routes.get("/crm", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.html(renderShell({
|
||||
const CRM_TABS = [
|
||||
{ id: "pipeline", label: "Pipeline" },
|
||||
{ id: "contacts", label: "Contacts" },
|
||||
{ id: "companies", label: "Companies" },
|
||||
{ id: "graph", label: "Graph" },
|
||||
{ id: "delegations", label: "Delegations" },
|
||||
] as const;
|
||||
|
||||
const CRM_TAB_IDS = new Set(CRM_TABS.map(t => t.id));
|
||||
|
||||
function renderCrm(space: string, activeTab: string) {
|
||||
return renderShell({
|
||||
title: `${space} — CRM | rSpace`,
|
||||
moduleId: "rnetwork",
|
||||
spaceSlug: space,
|
||||
|
|
@ -395,14 +404,25 @@ routes.get("/crm", (c) => {
|
|||
<script type="module" src="/modules/rnetwork/folk-delegation-manager.js"></script>
|
||||
<script type="module" src="/modules/rnetwork/folk-trust-sankey.js"></script>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rnetwork/network.css">`,
|
||||
tabs: [
|
||||
{ id: "pipeline", label: "Pipeline" },
|
||||
{ id: "contacts", label: "Contacts" },
|
||||
{ id: "companies", label: "Companies" },
|
||||
{ id: "graph", label: "Graph" },
|
||||
{ id: "delegations", label: "Delegations" },
|
||||
],
|
||||
}));
|
||||
tabs: [...CRM_TABS],
|
||||
activeTab,
|
||||
tabBasePath: `/${space}/rnetwork/crm`,
|
||||
});
|
||||
}
|
||||
|
||||
routes.get("/crm", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
return c.html(renderCrm(space, "pipeline"));
|
||||
});
|
||||
|
||||
// Tab subpath routes: /crm/:tabId
|
||||
routes.get("/crm/:tabId", (c) => {
|
||||
const space = c.req.param("space") || "demo";
|
||||
const tabId = c.req.param("tabId");
|
||||
if (!CRM_TAB_IDS.has(tabId as any)) {
|
||||
return c.redirect(`/${space}/rnetwork/crm`, 302);
|
||||
}
|
||||
return c.html(renderCrm(space, tabId));
|
||||
});
|
||||
|
||||
// ── Page route ──
|
||||
|
|
|
|||
|
|
@ -95,10 +95,14 @@ export async function resolveCallerRole(
|
|||
|
||||
if (!claims) return { role: 'viewer', isOwner: false };
|
||||
|
||||
const isOwner = data.meta.ownerDID === claims.sub;
|
||||
// Check both claims.sub (raw userId) and claims.did (did:key:...) for
|
||||
// compatibility — auto-provisioned spaces store ownerDID as did:key:,
|
||||
// while API-created spaces store it as raw userId.
|
||||
const callerDid = (claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`;
|
||||
const isOwner = data.meta.ownerDID === claims.sub || data.meta.ownerDID === callerDid;
|
||||
if (isOwner) return { role: 'admin', isOwner: true };
|
||||
|
||||
const member = data.members?.[claims.sub];
|
||||
const member = data.members?.[claims.sub] || data.members?.[callerDid];
|
||||
if (member) return { role: member.role, isOwner: false };
|
||||
|
||||
// Non-member defaults
|
||||
|
|
|
|||
|
|
@ -349,10 +349,11 @@ export class RStackIdentity extends HTMLElement {
|
|||
if (!session) { _removeSessionCookie(); }
|
||||
return;
|
||||
}
|
||||
const { token: newToken } = await res.json();
|
||||
const refreshData = await res.json();
|
||||
const newToken = refreshData.token;
|
||||
if (newToken) {
|
||||
const payload = parseJWT(newToken);
|
||||
storeSession(newToken, (payload.username as string) || username, (payload.did as string) || did);
|
||||
storeSession(newToken, (payload.username as string) || refreshData.username || username, (payload.did as string) || did);
|
||||
this.#render();
|
||||
this.dispatchEvent(new CustomEvent("auth-change", { bubbles: true, composed: true }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,8 +83,9 @@ export async function getUserByUsername(username: string) {
|
|||
// ============================================================================
|
||||
|
||||
export async function storeCredential(cred: StoredCredential): Promise<void> {
|
||||
// Ensure user exists first
|
||||
await createUser(cred.userId, cred.username);
|
||||
// Ensure user exists first (with display name + DID so they're never NULL)
|
||||
const did = `did:key:${cred.userId.slice(0, 32)}`;
|
||||
await createUser(cred.userId, cred.username, cred.username, did);
|
||||
|
||||
await sql`
|
||||
INSERT INTO credentials (credential_id, user_id, public_key, counter, transports, created_at, rp_id)
|
||||
|
|
|
|||
|
|
@ -806,13 +806,20 @@ app.post('/api/session/refresh', async (c) => {
|
|||
try {
|
||||
const payload = await verify(token, CONFIG.jwtSecret, { alg: 'HS256', exp: false }); // Allow expired tokens for refresh
|
||||
|
||||
// Ensure username is present — look up from DB if missing from old token
|
||||
let username = payload.username as string | undefined;
|
||||
if (!username) {
|
||||
const user = await getUserById(payload.sub as string);
|
||||
username = user?.username || '';
|
||||
}
|
||||
|
||||
// Issue new token
|
||||
const newToken = await generateSessionToken(
|
||||
payload.sub as string,
|
||||
payload.username as string
|
||||
username
|
||||
);
|
||||
|
||||
return c.json({ token: newToken });
|
||||
return c.json({ token: newToken, username });
|
||||
} catch {
|
||||
return c.json({ error: 'Invalid token' }, 401);
|
||||
}
|
||||
|
|
@ -3346,7 +3353,8 @@ app.post('/api/spaces/:slug/members', async (c) => {
|
|||
return c.json({ error: `Invalid role. Must be one of: ${VALID_ROLES.join(', ')}` }, 400);
|
||||
}
|
||||
|
||||
const member = await upsertSpaceMember(slug, body.userDID, body.role, claims.sub);
|
||||
const grantedByDid = (claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`;
|
||||
const member = await upsertSpaceMember(slug, body.userDID, body.role, grantedByDid);
|
||||
return c.json({
|
||||
userDID: member.userDID,
|
||||
spaceSlug: member.spaceSlug,
|
||||
|
|
@ -3488,8 +3496,9 @@ app.post('/api/invites/:token/accept', async (c) => {
|
|||
const accepted = await acceptSpaceInvite(token, claims.sub);
|
||||
if (!accepted) return c.json({ error: 'Failed to accept invite' }, 500);
|
||||
|
||||
// Add to space_members with the invite's role
|
||||
await upsertSpaceMember(accepted.spaceSlug, claims.sub, accepted.role, accepted.invitedBy);
|
||||
// Add to space_members with the invite's role (use DID, not raw userId)
|
||||
const userDid = (claims.did as string) || `did:key:${(claims.sub as string).slice(0, 32)}`;
|
||||
await upsertSpaceMember(accepted.spaceSlug, userDid, accepted.role, accepted.invitedBy);
|
||||
|
||||
return c.json({
|
||||
ok: true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue