From 79448a230aae9be988d7a6c152774e4f185ede40 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 12 Mar 2026 18:28:07 -0700 Subject: [PATCH 1/2] fix(encryptid): persist verified email and fix OIDC re-prompt bug Normalize emails to lowercase at all setUserEmail() call sites so case mismatches no longer break the OIDC allowedEmails check. Split the authorize error into email_required (shows verification form) vs access_denied (shows error message) so users with a verified email are never re-prompted unnecessarily. Co-Authored-By: Claude Opus 4.6 --- src/encryptid/server.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 76f05bb..57f4930 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -581,7 +581,7 @@ app.post('/api/register/complete', async (c) => { // Set recovery email if provided during registration if (email && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { - await setUserEmail(userId, email); + await setUserEmail(userId, email.trim().toLowerCase()); } // Resolve the RP ID from the caller's origin @@ -1203,7 +1203,7 @@ app.post('/api/account/email/verify', async (c) => { } await markRecoveryTokenUsed(tokenKey); - await setUserEmail(claims.sub as string, email); + await setUserEmail(claims.sub as string, email.trim().toLowerCase()); return c.json({ success: true, email }); }); @@ -1433,8 +1433,9 @@ app.post('/api/recovery/email/set', async (c) => { return c.json({ error: 'Valid email required' }, 400); } - await setUserEmail(payload.sub as string, email); - return c.json({ success: true, email }); + const normalizedEmail = email.trim().toLowerCase(); + await setUserEmail(payload.sub as string, normalizedEmail); + return c.json({ success: true, email: normalizedEmail }); } catch { return c.json({ error: 'Unauthorized' }, 401); } @@ -5441,10 +5442,14 @@ app.post('/oidc/authorize', async (c) => { if (!user) { return c.json({ error: 'User not found' }, 404); } - const userEmail = user.email || user.profile_email; + const userEmail = (user.email || user.profile_email || '').toLowerCase(); if (client.allowedEmails.length > 0) { - if (!userEmail || !client.allowedEmails.includes(userEmail)) { - return c.json({ error: 'access_denied', message: 'You do not have access to this application.' }, 403); + const allowedLower = client.allowedEmails.map((e: string) => e.toLowerCase()); + if (!userEmail) { + return c.json({ error: 'email_required', message: 'Email verification is required for this application.' }, 403); + } + if (!allowedLower.includes(userEmail)) { + return c.json({ error: 'access_denied', message: 'Your email is not authorized for this application.' }, 403); } } @@ -5906,8 +5911,8 @@ function oidcAuthorizePage(appName: string, clientId: string, redirectUri: strin const authorizeResult = await authorizeRes.json(); if (authorizeResult.error) { - if (authorizeResult.error === 'access_denied') { - // Show email verification flow instead of dead-end error + if (authorizeResult.error === 'email_required') { + // User has no verified email — show email verification form loginBtn.style.display = 'none'; statusEl.style.display = 'none'; verifySection.style.display = 'block'; From 3b3a642813c1a21351c613941b7e401e29e3015d Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 12 Mar 2026 18:48:20 -0700 Subject: [PATCH 2/2] fix(routing): remove hardcoded /demo/ path prefix from URLs demo.rspace.online subdomain already identifies the space, so paths should not redundantly include /demo/. Replaced 7 occurrences across rcart, rswag, rpubs, rschedule, and space-switcher with either relative paths or full demo.rspace.online URLs. Co-Authored-By: Claude Opus 4.6 --- modules/rcart/components/folk-cart-shop.ts | 2 +- modules/rpubs/landing.ts | 2 +- modules/rschedule/landing.ts | 2 +- modules/rschedule/mod.ts | 2 +- modules/rswag/components/folk-swag-designer.ts | 4 ++-- shared/components/rstack-space-switcher.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/rcart/components/folk-cart-shop.ts b/modules/rcart/components/folk-cart-shop.ts index 8adbcac..2761b6e 100644 --- a/modules/rcart/components/folk-cart-shop.ts +++ b/modules/rcart/components/folk-cart-shop.ts @@ -950,7 +950,7 @@ class FolkCartShop extends HTMLElement { if (this.space === 'demo') { const demoId = `demo-gb-${Date.now()}`; const host = window.location.host; - const shareUrl = `https://${host}/demo/rcart/buy/${demoId}`; + const shareUrl = `https://${host}/rcart/buy/${demoId}`; try { await navigator.clipboard.writeText(shareUrl); } catch {} alert(`Group buy created! Link copied.\nView all group buys in the Group Buys tab.`); this._history.push(this.view); diff --git a/modules/rpubs/landing.ts b/modules/rpubs/landing.ts index ce91b94..f33808e 100644 --- a/modules/rpubs/landing.ts +++ b/modules/rpubs/landing.ts @@ -118,7 +118,7 @@ export function renderLanding(): string {
  • Print-ready — download and fold, or send straight to rPubs for binding
  • - + Generate a Zine diff --git a/modules/rschedule/landing.ts b/modules/rschedule/landing.ts index fdea940..95a13de 100644 --- a/modules/rschedule/landing.ts +++ b/modules/rschedule/landing.ts @@ -26,7 +26,7 @@ export function renderLanding(): string { + onclick="document.querySelector('.rl-hero').closest('[data-space]')?.getAttribute('data-space') ? window.location.href='/' + document.querySelector('.rl-hero').closest('[data-space]').getAttribute('data-space') + '/rschedule/reminders' : window.location.href='https://demo.rspace.online/rschedule/reminders'; return false;"> Automation Canvas Learn More diff --git a/modules/rschedule/mod.ts b/modules/rschedule/mod.ts index edc8720..2e6c098 100644 --- a/modules/rschedule/mod.ts +++ b/modules/rschedule/mod.ts @@ -434,7 +434,7 @@ async function executeBacklogBriefing( ${taskRows}

    - Sent by rSchedule • Manage Schedules + Sent by rSchedule • Manage Schedules

    `; diff --git a/modules/rswag/components/folk-swag-designer.ts b/modules/rswag/components/folk-swag-designer.ts index 370f1c8..098586d 100644 --- a/modules/rswag/components/folk-swag-designer.ts +++ b/modules/rswag/components/folk-swag-designer.ts @@ -289,7 +289,7 @@ class FolkSwagDesigner extends HTMLElement { }, pricing: { creator_share: "35%", community_share: "15%", provider_share: "50%" }, next_actions: [ - { action: "ingest_to_rcart", label: "Send to rCart", url: "/demo/rcart?tab=catalog" }, + { action: "ingest_to_rcart", label: "Send to rCart", url: "/rcart?tab=catalog" }, { action: "edit_design", label: "Edit Design" }, { action: "save_to_rfiles", label: "Save to rFiles" }, ], @@ -537,7 +537,7 @@ class FolkSwagDesigner extends HTMLElement {
    Local Print
    - Send to rCart + Send to rCart
    ${this.esc(JSON.stringify(this.artifact, null, 2))}
    diff --git a/shared/components/rstack-space-switcher.ts b/shared/components/rstack-space-switcher.ts index 4c0d7e9..3f00cd1 100644 --- a/shared/components/rstack-space-switcher.ts +++ b/shared/components/rstack-space-switcher.ts @@ -703,7 +703,7 @@ export class RStackSpaceSwitcher extends HTMLElement { overlay.remove(); // Redirect to demo space - window.location.href = "/demo/canvas"; + window.location.href = rspaceNavUrl("demo", "rspace"); } catch (err: any) { statusEl.textContent = err.message || "Failed to delete"; statusEl.className = "status error";