fix(invites): redirect to invited space, improve invite emails

- Fix invite accept fetch URL in shell.ts (was missing /api/spaces prefix)
- After accepting invite, redirect to the invited space instead of reloading
- Notification actionUrls now point to the space subdomain (https://slug.rspace.online)
- Direct-add email includes inviter name, role, and space description
- Identity invite email includes space name/role context when inviting to a space

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-25 15:39:58 -07:00
parent 2f3a4a13dc
commit aa23108f5f
3 changed files with 37 additions and 22 deletions

View File

@ -413,12 +413,15 @@ export function renderShell(opts: ShellOptions): string {
if (!raw) return; if (!raw) return;
var session = JSON.parse(raw); var session = JSON.parse(raw);
if (!session || !session.accessToken) return; if (!session || !session.accessToken) return;
fetch('/' + '${escapeAttr(spaceSlug)}' + '/invite/accept', { fetch('/api/spaces/' + encodeURIComponent('${escapeAttr(spaceSlug)}') + '/invite/accept', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.accessToken }, headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.accessToken },
body: JSON.stringify({ inviteToken: inviteToken }), body: JSON.stringify({ inviteToken: inviteToken }),
}).then(function(res) { return res.json(); }).then(function(data) { }).then(function(res) { return res.json(); }).then(function(data) {
if (data.ok) { window.location.reload(); } if (data.ok) {
var slug = data.spaceSlug || '${escapeAttr(spaceSlug)}';
window.location.href = 'https://' + slug + '.rspace.online';
}
}); });
} catch(e) {} } catch(e) {}
} }

View File

@ -2194,7 +2194,7 @@ spaces.post("/:slug/invite", async (c) => {
spaceSlug: slug, spaceSlug: slug,
actorDid: claims.sub, actorDid: claims.sub,
actorUsername: claims.username, actorUsername: claims.username,
actionUrl: `/rspace`, actionUrl: `https://${slug}.rspace.online`,
metadata: { role }, metadata: { role },
}).catch(() => {}); }).catch(() => {});
@ -2209,15 +2209,21 @@ spaces.post("/:slug/invite", async (c) => {
if (inviteTransport) { if (inviteTransport) {
try { try {
const spaceUrl = `https://${slug}.rspace.online`; const spaceUrl = `https://${slug}.rspace.online`;
const inviterName = claims.username || "an admin";
await inviteTransport.sendMail({ await inviteTransport.sendMail({
from: process.env.SMTP_FROM || "rSpace <noreply@rmail.online>", from: process.env.SMTP_FROM || "rSpace <noreply@rmail.online>",
to: body.email, to: body.email,
subject: `You've been added to "${slug}" on rSpace`, subject: `${inviterName} added you to "${slug}" on rSpace`,
html: [ html: `
`<p>You've been added to <strong>${slug}</strong> as a <strong>${role}</strong>.</p>`, <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:520px;margin:0 auto;padding:2rem;">
`<p><a href="${spaceUrl}" style="display:inline-block;padding:12px 24px;background:#14b8a6;color:white;border-radius:8px;text-decoration:none;font-weight:600;">Open Space</a></p>`, <h2 style="color:#1a1a2e;margin-bottom:0.5rem;">You've been added to ${slug}</h2>
`<p style="color:#64748b;font-size:12px;">rSpace — collaborative knowledge work</p>`, <p style="color:#475569;line-height:1.6;"><strong>${inviterName}</strong> added you to the <strong>${slug}</strong> space as a <strong>${role}</strong>.</p>
].join("\n"), <p style="color:#475569;line-height:1.6;">You now have access to all the collaborative tools in this space notes, maps, voting, calendar, and more.</p>
<p style="text-align:center;margin:2rem 0;">
<a href="${spaceUrl}" style="display:inline-block;padding:12px 28px;background:linear-gradient(135deg,#14b8a6,#0d9488);color:white;border-radius:8px;text-decoration:none;font-weight:600;font-size:1rem;">Open ${slug}</a>
</p>
<p style="color:#94a3b8;font-size:0.85rem;">rSpace collaborative knowledge work</p>
</div>`,
}); });
} catch (emailErr: any) { } catch (emailErr: any) {
console.error("Direct-add email notification failed:", emailErr.message); console.error("Direct-add email notification failed:", emailErr.message);
@ -2309,7 +2315,7 @@ spaces.post("/:slug/members/add", async (c) => {
spaceSlug: slug, spaceSlug: slug,
actorDid: claims.sub, actorDid: claims.sub,
actorUsername: claims.username, actorUsername: claims.username,
actionUrl: `/rspace`, actionUrl: `https://${slug}.rspace.online`,
metadata: { role }, metadata: { role },
}).catch(() => {}); }).catch(() => {});

View File

@ -5407,22 +5407,28 @@ app.post('/api/invites/identity', async (c) => {
const joinLink = `https://auth.rspace.online/join?token=${encodeURIComponent(token)}`; const joinLink = `https://auth.rspace.online/join?token=${encodeURIComponent(token)}`;
if (smtpTransport) { if (smtpTransport) {
try { try {
const spaceInfo = spaceSlug
? `<p style="color:#475569;line-height:1.6;">You'll automatically join the <strong>${escapeHtml(spaceSlug)}</strong> space as a <strong>${escapeHtml(spaceRole || 'member')}</strong>, with access to collaborative notes, maps, voting, calendar, and more.</p>`
: `<p style="color:#475569;line-height:1.6;">rSpace is a suite of privacy-first collaborative tools — notes, maps, voting, calendar, wallet, and more — powered by passkey authentication (no passwords).</p>`;
const subjectLine = spaceSlug
? `${payload.username} invited you to join "${spaceSlug}" on rSpace`
: `${payload.username} invited you to join rSpace`;
await smtpTransport.sendMail({ await smtpTransport.sendMail({
from: CONFIG.smtp.from, from: CONFIG.smtp.from,
to: email, to: email,
subject: `${payload.username} invited you to join rSpace`, subject: subjectLine,
html: ` html: `
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 520px; margin: 0 auto; padding: 2rem;"> <div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:520px;margin:0 auto;padding:2rem;">
<h2 style="color: #1a1a2e;">You've been invited to rSpace</h2> <h2 style="color:#1a1a2e;margin-bottom:0.5rem;">${spaceSlug ? `You've been invited to ${escapeHtml(spaceSlug)}` : `You've been invited to rSpace`}</h2>
<p><strong>${escapeHtml(payload.username)}</strong> wants you to join the rSpace ecosystem a suite of privacy-first tools powered by passkey authentication.</p> <p style="color:#475569;line-height:1.6;"><strong>${escapeHtml(payload.username)}</strong> wants you to join${spaceSlug ? ` the <strong>${escapeHtml(spaceSlug)}</strong> space on` : ''} rSpace.</p>
${message ? `<blockquote style="border-left: 3px solid #7c3aed; padding: 0.5rem 1rem; margin: 1rem 0; color: #475569; background: #f8fafc; border-radius: 0 0.5rem 0.5rem 0;">"${escapeHtml(message)}"</blockquote>` : ''} ${message ? `<blockquote style="border-left:3px solid #7c3aed;padding:0.5rem 1rem;margin:1rem 0;color:#475569;background:#f8fafc;border-radius:0 0.5rem 0.5rem 0;">"${escapeHtml(message)}"</blockquote>` : ''}
<p>Click below to claim your identity and set up your passkey:</p> ${spaceInfo}
<p style="text-align: center; margin: 2rem 0;"> <p style="color:#475569;line-height:1.6;">Click below to create your account and set up your passkey:</p>
<a href="${joinLink}" style="display: inline-block; padding: 0.85rem 2rem; background: linear-gradient(90deg, #00d4ff, #7c3aed); color: #fff; text-decoration: none; border-radius: 0.5rem; font-weight: 600; font-size: 1rem;">Claim your rSpace</a> <p style="text-align:center;margin:2rem 0;">
</p> <a href="${joinLink}" style="display:inline-block;padding:0.85rem 2rem;background:linear-gradient(135deg,#14b8a6,#0d9488);color:#fff;text-decoration:none;border-radius:0.5rem;font-weight:600;font-size:1rem;">Accept Invitation</a>
<p style="color: #94a3b8; font-size: 0.85rem;">This invite expires in 7 days. If you didn't expect this, you can safely ignore it.</p> </p>
</div> <p style="color:#94a3b8;font-size:0.85rem;">This invite expires in 7 days. No passwords needed you'll use a passkey to sign in securely.</p>
`, </div>`,
}); });
} catch (err) { } catch (err) {
console.error('EncryptID: Failed to send invite email:', (err as Error).message); console.error('EncryptID: Failed to send invite email:', (err as Error).message);