关于 extension-email-verification
面向 Caffeine AI 应用的邮箱验证 Skill,通过点击邮件链接确认地址,并配套后端令牌处理和前端验证页面。
主要功能
- 01发送单独的验证邮件
- 02跟踪已验证地址和待处理令牌
- 03通过公开页面确认地址
- 04令牌单次使用且 24 小时后过期
适用场景
- 01注册时要求确认邮箱所有权
- 02允许收件人无需登录即可验证
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ caffeinelabs/ skills --skill extension-email-verification安装后在 Claude Code 中输入/extension-email-verification确认已加载。
详细介绍
电子邮件 — 验证
适用于 Caffeine AI 的电子邮件验证扩展。
概述
此技能通过点击验证链接验证电子邮件地址,链接会跳转到应用自身的前端。后端生成并存储链接令牌(verificationTokens),并通过 MixinEmailVerification 确认;verifiedEmails 用于跟踪已验证的地址。前端会呈现 /verify-email 页面,收件人可在此点击按钮进行确认。
必需设置清单
以下五个步骤均为必需。
- mops 依赖项 —
mops add caffeineai-email-verification和mops add caffeineai-email。 - 两个稳定状态字段 —
verifiedEmails : VerifiedEmails.State和verificationTokens : VerificationTokens.State,在迁移链头中使用VerifiedEmails.new()和VerificationTokens.new()初始化。 - Mixin 调用 — 在
main.mo中使用include MixinEmailVerification(verifiedEmails, verificationTokens)。 - 发送 —
EmailVerification.sendVerificationEmail(verificationTokens, fromUsername, recipients, subject, htmlBody);正文必须包含{{VERIFICATION_URL}}。 - 前端 npm 包和路由 — 安装
@caffeineai/email-verification,并设置一个公开路由/verify-email(无需登录即可访问),用于呈现一个调用confirm()的按钮。
关键提示:电子邮件中的链接为 https://<app domain>/verify-email?token=…。如果没有前端路由,收件人将进入缺失页面,地址也永远不会得到验证。
后端
此组件用于向用户发送包含验证链接的电子邮件,用户可点击链接以证明自己拥有该电子邮件地址。
检查电子邮件地址是否已验证
使用预制模块 mo:caffeineai-email-verification/verifiedEmails.mo,此模块不可修改。
module {
public type State = {
verifiedEmails : Set.Set<Text>;
};
public func new() : State;
public func contains(state : State, email : Text) : Bool;
public func iter(state : State) : Iter.Iter<Text>;
public func size(state : State) : Nat;
};
若要检查电子邮件是否已验证,请使用 contains 函数。切勿尝试在用户个人资料中独立存储电子邮件验证状态来进行跟踪。
待验证链接
使用预制模块 mo:caffeineai-email-verification/verificationTokens.mo,此模块不可修改。它保存已发送但尚未点击的链接令牌;令牌只能使用一次,并会在 24 小时后过期。应用只需声明状态并传递该状态。
module {
public type State = {
tokens : Map.Map<Text, { email : Text; createdAt : Time.Time }>;
};
public func new() : State;
public func size(state : State) : Nat;
};
处理验证链接
使用预制模块 mo:caffeineai-email-verification/verificationMixin.mo,此模块不可修改。
MixinEmailVerification 接收这两个状态。它会添加 _caffeineEmailConfirmVerification(token),应用前端会在验证页面调用此函数:该函数将令牌发送至的地址标记为已验证并返回该地址。任何持有有效令牌的人都可以调用它,包括匿名主体,因此收件人无需拥有账户。
import MixinEmailVerification "mo:caffeineai-email-verification/verificationMixin";
mixin (verifiedEmails : VerifiedEmails.State, verificationTokens : VerificationTokens.State) {
public shared func _caffeineEmailConfirmVerification(token : Text) : async { #ok : Text; #err : Text };
};
向用户发送验证电子邮件
- 使用预制模块
mo:caffeineai-email-verification/verification.mo中的sendVerificationEmail函数,此模块不可修改。切勿从应用代码调用EmailClient.broadcast:此模块会准备令牌和所发送的链接模板。 - 它会返回一个 SendResult;若电子邮件发送成功,则为 #ok,否则为带有错误文本的 #err(error)。
- 每位收件人都会收到一封单独的电子邮件,其中包含专属验证链接,链接指向应用的
/verify-email页面。 - htmlBody 必须包含占位文本 {{VERIFICATION_URL}};如果缺少此占位文本,函数会返回 #err。
- 邮件会通过邮件网关发送,使用平台在每次部署时注入的设置(
INTEGRATIONS_GATEWAY_URL/INTEGRATIONS_GATEWAY_API_KEY);若缺少这些设置,函数会返回#err。
module {
public func sendVerificationEmail(
tokens : VerificationTokens.State,
fromUsername : Text,
recipients : [Text],
subject : Text,
htmlBody : Text,
) : async { #ok; #err : Text };
};
注册用户及检查用户是否已验证的端点示例。
import Map "mo:core/Map";
import Runtime "mo:core/Runtime";
import Principal "mo:core/Principal";
import Text "mo:core/Text";
import EmailVerification "mo:caffeineai-email-verification/verification";
import MixinEmailVerification "mo:caffeineai-email-verification/verificationMixin";
import VerificationTokens "mo:caffeineai-email-verification/verificationTokens";
import VerifiedEmails "mo:caffeineai-email-verification/verifiedEmails";
actor {
// Stores which emails are verified
let verifiedEmails : VerifiedEmails.State;
// Pending verification links, until clicked or expired
let verificationTokens : VerificationTokens.State;
// User profiles storage
let users : Map.Map<Principal, User>;
// Email to principal mapping for uniqueness check
let emailToPrincipal : Map.Map<Text, Principal>;
// Handles the verification link and updates the verifiedEmails store
include MixinEmailVerification(verifiedEmails, verificationTokens);
type User = {
name : Text;
email : Text;
};
public shared ({ caller }) func registerUser(email : Text, name : Text) : async () {
if (users.containsKey(caller)) {
Runtime.trap("User already registered");
};
if (emailToPrincipal.containsKey(email)) {
Runtime.trap("Email already registered");
};
let user : User = {
name;
email;
};
users.add(caller, user);
emailToPrincipal.add(email, caller);
let result = await EmailVerification.sendVerificationEmail(
verificationTokens,
"no-reply",
[email],
"Welcome to Our Service",
"Hello " # name # ",<br><br>Thank you for registering with our service. Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address<br><br>Best regards,<br>The Team",
);
switch (result) {
case (#ok) {};
case (#err(error)) {
Runtime.trap("Couldn't send verification email: " # error);
};
};
};
public shared ({ caller }) func isEmailVerified() : async Bool {
let user = users.get(caller) ?? Runtime.trap("User not registered");
VerifiedEmails.contains(verifiedEmails, user.email);
};
};
迁移链头:
import Map "mo:core/Map";
import VerificationTokens "mo:caffeineai-email-verification/verificationTokens";
import VerifiedEmails "mo:caffeineai-email-verification/verifiedEmails";
module {
type User = {
name : Text;
email : Text;
};
type NewActor = {
verifiedEmails : VerifiedEmails.State;
verificationTokens : VerificationTokens.State;
users : Map.Map<Principal, User>;
emailToPrincipal : Map.Map<Text, Principal>;
};
public func migration(_old : {}) : NewActor {
{
verifiedEmails = VerifiedEmails.new();
verificationTokens = VerificationTokens.new();
users = Map.empty<Principal, User>();
emailToPrincipal = Map.empty<Text, Principal>();
};
};
};
从上一版本升级
使用 caffeineai-email-verification 0.2.x 构建的应用需要重新固定依赖版本:运行 mops add [email protected] 和 mops add [email protected],并将 @caffeineai/email-verification 升级到 0.3.0。应用代码无需更改。该混入不再包含传输 canister 的 _caffeineEmailVerify 回调:验证邮件现在始终通过邮件网关发送,而传输在 0.2.0 之前发送的邮件中的链接将不再有效。
使用 caffeineai-email-verification 0.1.x 和 caffeineai-email 0.2.x 构建的应用需要完成以下所有步骤,然后再执行上述重新固定依赖版本的操作:
- 重新固定依赖版本:运行
mops add [email protected]和mops add [email protected]。 - 声明新的稳定字段
verificationTokens : VerificationTokens.State,并将其添加到迁移链中(新建一个迁移文件,用VerificationTokens.new()初始化该字段;verifiedEmails的结构和数据保持不变)。 - 将
include MixinEmailVerification(verifiedEmails)改为include MixinEmailVerification(verifiedEmails, verificationTokens)。 - 将
EmailClient.sendVerificationEmail(fromUsername, recipients, subject, htmlBody)替换为EmailVerification.sendVerificationEmail(verificationTokens, fromUsername, recipients, subject, htmlBody)(导入mo:caffeineai-email-verification/verification)。 - 添加下方所述的前端包和
/verify-email路由。
前端
安装 @caffeineai/email-verification。它导出了预制的钩子 useEmailVerification,不能修改:
export interface EmailVerificationActor {
_caffeineEmailConfirmVerification(token: string): Promise<{ ok: string } | { err: string }>;
}
// ready: a token is in the URL and nothing was sent yet
// pending: the backend call is in flight
// done: the address is verified; `email` holds it
// invalid: the URL has no token, or the backend rejected it
// error: the backend call failed; `error` holds the message
export type EmailVerificationStatus = 'ready' | 'pending' | 'done' | 'invalid' | 'error';
export declare function useEmailVerification(actor: EmailVerificationActor | null | undefined): {
status: EmailVerificationStatus;
email: string | null;
error: string | null;
hasToken: boolean;
// True once the actor is available, a token is present and nothing was sent yet
isReady: boolean;
// Confirms the address through the backend. Call it from a button click only.
confirm: () => Promise<void>;
};
验证页面规则:
- 添加
/verify-email路由,并确保无需登录即可访问:收件人会从邮件客户端打开该页面,且没有会话。后端方法仅将令牌作为凭证,因此使用useActor提供的匿名 actor 即可。 - 使用钩子读取令牌(它会自行从 URL 中获取
?token=),并渲染一个调用confirm()的按钮。切勿在页面加载时或在 effect 中调用confirm():邮件客户端、链接预览器和扫描器可能会在没有用户操作的情况下请求该 URL,而请求本身不能验证任何内容。 - 根据
status显示结果:验证成功时显示done和已验证的email;对于缺失、已使用或过期的链接,显示invalid(并提供重新请求邮件的选项);出错时显示error和错误消息。
页面示例:
import { useEmailVerification } from '@caffeineai/email-verification';
import { useActor } from '@caffeineai/core-infrastructure';
import { createActor } from '@/backend';
export function VerifyEmailPage() {
const { actor } = useActor(createActor);
const { status, email, error, isReady, confirm } = useEmailVerification(actor);
if (status === 'done') return <p>{email} is verified. You can close this page.</p>;
if (status === 'invalid') return <p>{error ?? 'This verification link is invalid or has expired.'}</p>;
if (status === 'error') return <p>Something went wrong: {error}</p>;
return (
<button onClick={() => void confirm()} disabled={!isReady}>
{status === 'pending' ? 'Verifying…' : 'Verify my email address'}
</button>
);
}
在应用的 TanStack Router 中,将该路由与其他路由一同注册为根路由的子路由,设置 path: '/verify-email',并放在任何已验证身份的布局之外。
如果管理员可以通过 UI 输入验证邮件的内容,请注明邮件正文中必须包含占位文本 {{VERIFICATION_URL}}。