团队邀请邮件链接(2026):用 Rovela 拉人进协作
在应用侧生成一次性邀请令牌并存储哈希,再用 Rovela POST /emails 发出接受链接——模式与密码重置相同;安全邮件建议关闭点击追踪,正文需含完整 URL。
为什么多用户应用需要邀请链接
如果你在做 AI 产品、协作空间,或任何需要多人共用一个项目的 SaaS,迟早会碰到团队邀请:管理员输入邮箱,应用发出一次性接受链接,受邀人落到你自己的域名,核销后写入成员关系(以及角色)。
Rovela 没有单独的「团队邀请邮件」接口或产品功能。文档里提到的「接受邀请」,指的是 Rovela 控制台组织成员——把同事拉进你的 Rovela 组织,而不是替你的终端用户发产品内邀请。产品邀请由你保管 token 与接受路由;Rovela 只通过 POST /emails 负责投递。
最接近的官方示例是 密码重置链接:32 字节随机数、存哈希、短过期、一次性核销,同时发 html 与带完整 URL 的 text。魔法登录邮件链接 是同一套发送形态、不同落地页。团队邀请只是多存了 team id、role 和可选的 inviter——仍然一次 HTTP 发送。
你负责什么,Rovela 负责什么
应用侧必须自己做:
- 邀请记录:邮箱、团队/工作区 id、角色、邀请人、状态(
pending/accepted/revoked) - 密码学安全的 token;只存哈希;过期时间;在你自己的源站一次性核销
- 绝对接受 URL(如
https://app.example.com/invites/accept?token=…) - 接受时的登录/注册,以及成员关系写入
- 重发 / 撤销 / 限流;只在服务端调用发信 API
Rovela 负责:
- 接受带 Bearer
re_…的POST https://api.rovela.dev/emails(生产发信用sending_access即可) - 入队、已验证域名的 DKIM、重试、退信/投诉处理与消息日志
- 可选模板、Webhook、抑制列表——与其他事务邮件同一套基础设施
不要把 API 密钥放进浏览器或移动客户端。套餐与额度见 定价页;本文不杜撰任何价格数字。
Token 与存储(哈希、团队/角色、过期)
沿用 密码重置示例 的熵要求:
- 规范化受邀邮箱(
trim+ 小写)。 - 校验邀请人权限(必须有权向该团队加人)。
- 生成 32 字节随机数,编码为
base64url。 - 只存 token 的 SHA-256 十六进制哈希,绝不存明文。32 字节无法从哈希暴力还原,因此普通 SHA-256 足够(六位 OTP 不同:短数字需要带 pepper 的 HMAC——见 验证码)。
- 持久化
token_hash、team_id、role、email、invited_by、expires_at(邀请常见 7 天,按产品自定)、status=pending。 - 拼出
https://app.acme.com/invites/accept?token=+ 明文 token。token 只出现在邮件里。
同一邮箱+团队的新邀请应作废或覆盖上一条 pending,避免两条有效链接对角色说法不一致。
与密码重置或魔法登录不同,邀请流程通常会暴露「已为该地址创建邀请」(管理员刚输入过)。但仍避免在受邀人点击后的错误页里泄露「该邮箱是否已有 Acme 账号」。
用 POST /emails 发出
用已验证的子域发件地址隔离事务/认证信誉(例如 invites@mail.acme.com 或 security@mail.acme.com)。同时发 html 和 text,并在纯文本里写上完整 URL——部分企业过滤器会剥掉 HTML 链接。
为每次逻辑发送设置一次 Idempotency-Key(≤256 字符,24 小时窗口,按组织生效)。可用 token 哈希的稳定前缀派生,避免超时重试发出第二封。管理员点「重发邀请」是新的逻辑发送:换新 token、新 key。
以下草图改编自密码重置文档(仅占位):
import crypto from "node:crypto"
export async function createTeamInvite({ teamId, email, role, invitedBy }) {
const normalized = email.trim().toLowerCase()
const token = crypto.randomBytes(32).toString("base64url")
const tokenHash = crypto.createHash("sha256").update(token).digest("hex")
await db.teamInvites.insert({
teamId,
email: normalized,
role,
invitedBy,
tokenHash,
status: "pending",
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
})
const link = "https://app.acme.com/invites/accept?token=" + token
const res = await fetch("https://api.rovela.dev/emails", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.ROVELA_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "invite-" + tokenHash.slice(0, 32),
},
body: JSON.stringify({
from: "Acme <invites@mail.acme.com>",
to: normalized,
subject: "邀请你加入 Acme 团队",
html:
"<p>你被邀请在 Acme 上协作。</p>" +
'<p><a href="' + link + '">接受邀请</a></p>' +
"<p>链接仅可使用一次,7 天后失效。" +
"若并非你本人,可忽略本邮件。</p>",
text:
"你被邀请在 Acme 上协作。\n\n" +
"接受邀请: " + link + "\n\n" +
"链接仅可使用一次,7 天后失效。",
}),
})
// 200 = 已接受并入队,不等于已送达收件箱
return res.json()
}
返回 200 且 body 为 {"object":"email","id":"…"} 表示 Rovela 已入队。需要退信/投诉信号时接 Webhook。字段说明见 发信 API。也可以用 模板 的 {{invite_url}}——非 URL 的用户字段要自行转义;模板变量不会自动做 HTML 转义。
一次性接受(会话 + 成员关系)
在一步数据库操作里花掉 token,避免并行点击都成功:
export async function acceptTeamInvite(token, currentUserId) {
const tokenHash = crypto.createHash("sha256").update(token).digest("hex")
const row = await db.query(
`UPDATE team_invites
SET status = 'accepted', accepted_at = NOW()
WHERE token_hash = $1
AND status = 'pending'
AND expires_at > NOW()
RETURNING team_id, role, email`,
[tokenHash]
)
if (row.rowCount === 0) return null
const invite = row.rows[0]
// 把成员关系绑到已登录用户;如需可强制邮箱一致
await db.memberships.upsert({
teamId: invite.team_id,
userId: currentUserId,
role: invite.role,
})
return invite
}
接受页常见产品规则:
- 未登录先走注册 / 魔法登录 / 密码登录,再核销。
- 可选:要求
currentUser.email与invite.email一致。 - 成功后跳进团队;尽快去掉地址栏里的原始 token(核销后无 query 重定向)。
这是你的成员模型,与把某人拉进 Rovela 控制台组织无关。
关闭追踪 + 纯文本兜底
打开/点击追踪是按域名配置的。开启点击追踪时,Rovela 会把链接改写到追踪主机。邀请 URL 经跳转后仍可用,但 token 多了一跳。
安全类邮件——邀请、密码重置、魔法登录——建议用单独发信域名或子域并关闭点击追踪,让 token 直达你的应用。该域名上不会产生打开/点击类互动 Webhook,这是预期行为。
务必保留带完整 URL 的纯文本。只有 HTML 时,剥链接的过滤器会让用户无路可点。
重发、撤销与抑制
- 重发: 作废上一条 pending(或标记 superseded),重新生成 32 字节 token、新的
Idempotency-Key再发。按团队+邮箱限流。 - 撤销: 设
status=revoked(或删除哈希)。核销必须检查状态,使转发的旧链接失败关闭。 - 接受前改角色: 撤销后重邀,或更新 pending 行并重发,保证邮件文案与将授予的角色一致。
- 收件人被抑制: 单次
POST /emails时,验证码文档 写明消息以recipient is suppressed开头的400。按不可投递处理——让管理员换地址,不要对受邀人说「请查收件箱」。 - 未验证的
from域名:400并指出域名——先完成 域名验证。 429: 退避;同一逻辑发送用同一Idempotency-Key重试。有意重发 = 新 token + 新 key。
邀请邮件避免 cc/bcc(泄露 token)。优先每次只发一个收件人。
相关模式
| 流程 | 谁保管密钥 | 文档 / 文章 |
|---|---|---|
| 团队邀请链接 | 你的应用(token + 团队/角色) | 本文;经 POST /emails 发送 |
| 魔法登录 | 你的应用(token → 会话) | 魔法登录邮件链接 |
| 密码重置 | 你的应用(token → 新密码) | 密码重置链接 |
| 邮箱 OTP | 你的应用(验证码哈希) | 验证码 |
| Rovela 组织邀请 | Rovela 控制台会话 | Authentication —— 不是终端用户邀请 API |
自己维护邀请表,一次 HTTP 发出邮件,在安全域名上关闭追踪——就能拉人进协作,而不把邮件 API 当成身份或目录产品。