Compare commits
2 Commits
c6ae38d0d7
...
48c86c6c1f
| Author | SHA1 | Date |
|---|---|---|
|
|
48c86c6c1f | |
|
|
cfe060dc61 |
|
|
@ -2479,20 +2479,21 @@ spaces.get("/:slug/invites", async (c) => {
|
|||
const data = getDocumentData(slug);
|
||||
if (!data) return c.json({ error: "Space not found" }, 404);
|
||||
|
||||
const isOwner = data.meta.ownerDID === claims.sub;
|
||||
const callerMember = data.members?.[claims.sub];
|
||||
// ownerDID may be stored as raw userId or did:key:... — check both.
|
||||
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;
|
||||
const callerMember = data.members?.[claims.sub] || data.members?.[callerDid];
|
||||
if (!isOwner && callerMember?.role !== "admin") {
|
||||
return c.json({ error: "Admin access required" }, 403);
|
||||
}
|
||||
|
||||
// Fetch from EncryptID
|
||||
|
||||
// Use the internal (no-auth) endpoint — rspace has already verified admin via the
|
||||
// Automerge space doc. The public encryptid endpoint gates on the encryptid
|
||||
// space_members table, which can be out of sync with Automerge ownership.
|
||||
try {
|
||||
const res = await fetch(`${ENCRYPTID_URL}/api/spaces/${slug}/invites`, {
|
||||
headers: { "Authorization": `Bearer ${token}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
return c.json(data as any);
|
||||
const res = await fetch(`${ENCRYPTID_URL}/api/internal/spaces/${slug}/invites`);
|
||||
const payload = await res.json();
|
||||
return c.json(payload as any);
|
||||
} catch {
|
||||
return c.json({ invites: [], total: 0 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -296,44 +296,7 @@ export class RStackAppSwitcher extends HTMLElement {
|
|||
}
|
||||
}
|
||||
|
||||
// "Manage rApps" catalog section
|
||||
const disabledModules = this.#allModules.filter(
|
||||
m => m.enabled === false && m.id !== 'rspace'
|
||||
);
|
||||
// Only show the Manage section when there are disabled modules to add,
|
||||
// or when enabledModules is actively configured (not null/all-enabled)
|
||||
const hasRestrictions = disabledModules.length > 0;
|
||||
if (hasRestrictions || this.#allModules.length > this.#modules.length) {
|
||||
html += `
|
||||
<div class="catalog-divider">
|
||||
<button class="catalog-toggle" id="catalog-toggle">
|
||||
${this.#catalogOpen ? '▾' : '▸'} Manage rApps
|
||||
${disabledModules.length > 0 ? `<span class="catalog-count">${disabledModules.length} more</span>` : ''}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
if (this.#catalogOpen) {
|
||||
html += `<div class="catalog-panel" id="catalog-panel">`;
|
||||
// Show disabled modules with add option
|
||||
if (disabledModules.length > 0) {
|
||||
html += `<div class="catalog-section-label">Available to Add</div>`;
|
||||
for (const m of disabledModules) {
|
||||
html += this.#renderCatalogItem(m, false);
|
||||
}
|
||||
}
|
||||
// Show enabled modules with remove option (compact section below)
|
||||
const enabledNonCore = this.#allModules.filter(
|
||||
m => m.enabled !== false && m.id !== 'rspace'
|
||||
);
|
||||
if (enabledNonCore.length > 0) {
|
||||
html += `<div class="catalog-section-label">Remove from Space</div>`;
|
||||
for (const m of enabledNonCore) {
|
||||
html += this.#renderCatalogItem(m, true);
|
||||
}
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
}
|
||||
// Module enable/disable is managed via Edit Space → Modules tab, not from this dropdown.
|
||||
|
||||
// Sort toggle + Footer
|
||||
html += `
|
||||
|
|
|
|||
|
|
@ -270,6 +270,22 @@ export async function getUserById(userId: string) {
|
|||
return user || null;
|
||||
}
|
||||
|
||||
/** Resolve a user by internal id (UUID), real did (did:key:z6Mk...), or
|
||||
* legacy synthetic did (did:key: + first 32 chars of userId). */
|
||||
export async function getUserByIdOrDid(idOrDid: string) {
|
||||
const [direct] = await sql`SELECT * FROM users WHERE id = ${idOrDid} OR did = ${idOrDid} LIMIT 1`;
|
||||
if (direct) return direct;
|
||||
// Legacy synthetic DID: did:key:<first 32 chars of userId>
|
||||
if (idOrDid.startsWith('did:key:')) {
|
||||
const idPrefix = idOrDid.slice('did:key:'.length);
|
||||
if (idPrefix.length >= 16) {
|
||||
const [legacy] = await sql`SELECT * FROM users WHERE id LIKE ${idPrefix + '%'} LIMIT 1`;
|
||||
return legacy || null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Record a global logout — all JWTs issued before this timestamp are revoked */
|
||||
export async function setUserLoggedOutAt(userId: string): Promise<void> {
|
||||
await sql`UPDATE users SET logged_out_at = NOW() WHERE id = ${userId}`;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
setUserEmail,
|
||||
getUserByEmail,
|
||||
getUserById,
|
||||
getUserByIdOrDid,
|
||||
getUserByUsername,
|
||||
storeRecoveryToken,
|
||||
getRecoveryToken,
|
||||
|
|
@ -4629,9 +4630,12 @@ app.delete('/api/spaces/:slug/members/:did', async (c) => {
|
|||
|
||||
// ── Internal: email lookup by userId (no auth, internal network only) ──
|
||||
app.get('/api/internal/user-email/:userId', async (c) => {
|
||||
const userId = c.req.param('userId');
|
||||
if (!userId) return c.json({ error: 'userId required' }, 400);
|
||||
const [user, profile] = await Promise.all([getUserById(userId), getUserProfile(userId)]);
|
||||
const idOrDid = c.req.param('userId');
|
||||
if (!idOrDid) return c.json({ error: 'userId required' }, 400);
|
||||
// Members dicts store either raw userId (UUID) or did:key:... — accept both.
|
||||
const user = await getUserByIdOrDid(idOrDid);
|
||||
const resolvedId = user?.id || idOrDid;
|
||||
const profile = await getUserProfile(resolvedId);
|
||||
if (!user && !profile) return c.json({ error: 'User not found' }, 404);
|
||||
return c.json({
|
||||
recoveryEmail: user?.email || null,
|
||||
|
|
@ -4829,6 +4833,15 @@ app.get('/api/spaces/:slug/invites', async (c) => {
|
|||
return c.json({ invites, total: invites.length });
|
||||
});
|
||||
|
||||
// GET /api/internal/spaces/:slug/invites — internal (no auth) list for rspace proxy.
|
||||
// Authority check lives on the rspace side against the Automerge space doc, so this
|
||||
// route is safe to expose on the internal network only.
|
||||
app.get('/api/internal/spaces/:slug/invites', async (c) => {
|
||||
const { slug } = c.req.param();
|
||||
const invites = await listSpaceInvites(slug);
|
||||
return c.json({ invites, total: invites.length });
|
||||
});
|
||||
|
||||
// POST /api/spaces/:slug/invites/:id/revoke — revoke an invite
|
||||
app.post('/api/spaces/:slug/invites/:id/revoke', async (c) => {
|
||||
const { slug, id } = c.req.param();
|
||||
|
|
@ -6362,6 +6375,7 @@ function joinPage(token: string): string {
|
|||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: startData.userId,
|
||||
username,
|
||||
challenge: options.challenge,
|
||||
deviceName: detectDeviceName(),
|
||||
|
|
@ -6703,6 +6717,7 @@ function oidcAcceptPage(token: string): string {
|
|||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userId: startData.userId,
|
||||
username,
|
||||
challenge: options.challenge,
|
||||
deviceName: detectDeviceName(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue