diff --git a/modules/rnetwork/mod.ts b/modules/rnetwork/mod.ts
index 52cf273..3c8687d 100644
--- a/modules/rnetwork/mod.ts
+++ b/modules/rnetwork/mod.ts
@@ -19,7 +19,7 @@ const GRAPH3D_IMPORTMAP = ``;
@@ -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) => {
`,
styles: ``,
- 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 ──
diff --git a/server/spaces.ts b/server/spaces.ts
index 0d2e8f4..8c5b0c3 100644
--- a/server/spaces.ts
+++ b/server/spaces.ts
@@ -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
diff --git a/shared/components/rstack-identity.ts b/shared/components/rstack-identity.ts
index 5a5c162..793d631 100644
--- a/shared/components/rstack-identity.ts
+++ b/shared/components/rstack-identity.ts
@@ -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 }));
}
diff --git a/src/encryptid/db.ts b/src/encryptid/db.ts
index cfc4a7e..11abe52 100644
--- a/src/encryptid/db.ts
+++ b/src/encryptid/db.ts
@@ -83,8 +83,9 @@ export async function getUserByUsername(username: string) {
// ============================================================================
export async function storeCredential(cred: StoredCredential): Promise {
- // 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)
diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts
index 50c529b..1e2c5ed 100644
--- a/src/encryptid/server.ts
+++ b/src/encryptid/server.ts
@@ -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,