Merge branch 'dev'
CI/CD / deploy (push) Failing after 2m40s
Details
CI/CD / deploy (push) Failing after 2m40s
Details
This commit is contained in:
commit
48c86c6c1f
|
|
@ -2479,20 +2479,21 @@ spaces.get("/:slug/invites", async (c) => {
|
||||||
const data = getDocumentData(slug);
|
const data = getDocumentData(slug);
|
||||||
if (!data) return c.json({ error: "Space not found" }, 404);
|
if (!data) return c.json({ error: "Space not found" }, 404);
|
||||||
|
|
||||||
const isOwner = data.meta.ownerDID === claims.sub;
|
// ownerDID may be stored as raw userId or did:key:... — check both.
|
||||||
const callerMember = data.members?.[claims.sub];
|
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") {
|
if (!isOwner && callerMember?.role !== "admin") {
|
||||||
return c.json({ error: "Admin access required" }, 403);
|
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 {
|
try {
|
||||||
const res = await fetch(`${ENCRYPTID_URL}/api/spaces/${slug}/invites`, {
|
const res = await fetch(`${ENCRYPTID_URL}/api/internal/spaces/${slug}/invites`);
|
||||||
headers: { "Authorization": `Bearer ${token}` },
|
const payload = await res.json();
|
||||||
});
|
return c.json(payload as any);
|
||||||
const data = await res.json();
|
|
||||||
return c.json(data as any);
|
|
||||||
} catch {
|
} catch {
|
||||||
return c.json({ invites: [], total: 0 });
|
return c.json({ invites: [], total: 0 });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -296,44 +296,7 @@ export class RStackAppSwitcher extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Manage rApps" catalog section
|
// Module enable/disable is managed via Edit Space → Modules tab, not from this dropdown.
|
||||||
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>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort toggle + Footer
|
// Sort toggle + Footer
|
||||||
html += `
|
html += `
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,22 @@ export async function getUserById(userId: string) {
|
||||||
return user || null;
|
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 */
|
/** Record a global logout — all JWTs issued before this timestamp are revoked */
|
||||||
export async function setUserLoggedOutAt(userId: string): Promise<void> {
|
export async function setUserLoggedOutAt(userId: string): Promise<void> {
|
||||||
await sql`UPDATE users SET logged_out_at = NOW() WHERE id = ${userId}`;
|
await sql`UPDATE users SET logged_out_at = NOW() WHERE id = ${userId}`;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
setUserEmail,
|
setUserEmail,
|
||||||
getUserByEmail,
|
getUserByEmail,
|
||||||
getUserById,
|
getUserById,
|
||||||
|
getUserByIdOrDid,
|
||||||
getUserByUsername,
|
getUserByUsername,
|
||||||
storeRecoveryToken,
|
storeRecoveryToken,
|
||||||
getRecoveryToken,
|
getRecoveryToken,
|
||||||
|
|
@ -4629,9 +4630,12 @@ app.delete('/api/spaces/:slug/members/:did', async (c) => {
|
||||||
|
|
||||||
// ── Internal: email lookup by userId (no auth, internal network only) ──
|
// ── Internal: email lookup by userId (no auth, internal network only) ──
|
||||||
app.get('/api/internal/user-email/:userId', async (c) => {
|
app.get('/api/internal/user-email/:userId', async (c) => {
|
||||||
const userId = c.req.param('userId');
|
const idOrDid = c.req.param('userId');
|
||||||
if (!userId) return c.json({ error: 'userId required' }, 400);
|
if (!idOrDid) return c.json({ error: 'userId required' }, 400);
|
||||||
const [user, profile] = await Promise.all([getUserById(userId), getUserProfile(userId)]);
|
// 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);
|
if (!user && !profile) return c.json({ error: 'User not found' }, 404);
|
||||||
return c.json({
|
return c.json({
|
||||||
recoveryEmail: user?.email || null,
|
recoveryEmail: user?.email || null,
|
||||||
|
|
@ -4829,6 +4833,15 @@ app.get('/api/spaces/:slug/invites', async (c) => {
|
||||||
return c.json({ invites, total: invites.length });
|
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
|
// POST /api/spaces/:slug/invites/:id/revoke — revoke an invite
|
||||||
app.post('/api/spaces/:slug/invites/:id/revoke', async (c) => {
|
app.post('/api/spaces/:slug/invites/:id/revoke', async (c) => {
|
||||||
const { slug, id } = c.req.param();
|
const { slug, id } = c.req.param();
|
||||||
|
|
@ -6362,6 +6375,7 @@ function joinPage(token: string): string {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
userId: startData.userId,
|
||||||
username,
|
username,
|
||||||
challenge: options.challenge,
|
challenge: options.challenge,
|
||||||
deviceName: detectDeviceName(),
|
deviceName: detectDeviceName(),
|
||||||
|
|
@ -6703,6 +6717,7 @@ function oidcAcceptPage(token: string): string {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
userId: startData.userId,
|
||||||
username,
|
username,
|
||||||
challenge: options.challenge,
|
challenge: options.challenge,
|
||||||
deviceName: detectDeviceName(),
|
deviceName: detectDeviceName(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue