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:
parent
2f3a4a13dc
commit
aa23108f5f
|
|
@ -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) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(() => {});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;">
|
||||||
|
<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>
|
</p>
|
||||||
<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 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>
|
</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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue