mcpField
E
extension-email-verification

extension-email-verification

github.com/caffeinelabs安全228.6k 次安装6 天前更新
代码仓库

关于 extension-email-verification

面向 Caffeine AI 应用的邮箱验证 Skill,通过点击邮件链接确认地址,并配套后端令牌处理和前端验证页面。

主要功能

  1. 01发送单独的验证邮件
  2. 02跟踪已验证地址和待处理令牌
  3. 03通过公开页面确认地址
  4. 04令牌单次使用且 24 小时后过期

适用场景

  1. 01注册时要求确认邮箱所有权
  2. 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 页面,收件人可在此点击按钮进行确认。

必需设置清单

以下五个步骤均为必需。

  1. mops 依赖项 — mops add caffeineai-email-verification 和 mops add caffeineai-email。
  2. 两个稳定状态字段 — verifiedEmails : VerifiedEmails.State 和 verificationTokens : VerificationTokens.State,在迁移链头中使用 VerifiedEmails.new() 和 VerificationTokens.new() 初始化。
  3. Mixin 调用 — 在 main.mo 中使用 include MixinEmailVerification(verifiedEmails, verificationTokens)。
  4. 发送 — EmailVerification.sendVerificationEmail(verificationTokens, fromUsername, recipients, subject, htmlBody);正文必须包含 {{VERIFICATION_URL}}。
  5. 前端 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 构建的应用需要完成以下所有步骤,然后再执行上述重新固定依赖版本的操作:

  1. 重新固定依赖版本:运行 mops add [email protected] 和 mops add [email protected]。
  2. 声明新的稳定字段 verificationTokens : VerificationTokens.State,并将其添加到迁移链中(新建一个迁移文件,用 VerificationTokens.new() 初始化该字段;verifiedEmails 的结构和数据保持不变)。
  3. 将 include MixinEmailVerification(verifiedEmails) 改为 include MixinEmailVerification(verifiedEmails, verificationTokens)。
  4. 将 EmailClient.sendVerificationEmail(fromUsername, recipients, subject, htmlBody) 替换为 EmailVerification.sendVerificationEmail(verificationTokens, fromUsername, recipients, subject, htmlBody)(导入 mo:caffeineai-email-verification/verification)。
  5. 添加下方所述的前端包和 /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}}。

评论

支持 Markdown