mcpField
E
extension-email-verification

extension-email-verification

github.com/caffeinelabsセキュリティ228.6k 回インストール6 日前に更新
リポジトリ

extension-email-verification について

Caffeine AI アプリに、メール内のリンクをクリックして確認するメールアドレス認証を追加するスキルです。バックエンドのトークン処理とフロントエンドの認証ページを備えます。

主な機能

  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を入力して読み込まれたか確認します。

詳細

Email — Verification

Caffeine AI 用のメール認証拡張機能です。

概要

このスキルは、アプリ独自のフロントエンドに遷移するクリック認証リンクを使って、メールアドレスを認証できるようにします。バックエンドはリンクトークン(verificationTokens)を生成して保存し、MixinEmailVerification を通じて認証を確定します。認証済みアドレスは verifiedEmails で管理します。フロントエンドでは /verify-email ページを表示し、受信者がボタンをクリックして認証を確定します。

必須セットアップチェックリスト

以下の5つの手順はすべて必須です。

  1. mops の依存関係 — mops add caffeineai-email-verification と mops add caffeineai-email。
  2. 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 をインストールし、confirm() を呼び出すボタンを表示する、公開ルート /verify-email(サインインせずにアクセス可能)を用意します。

重要:メール内のリンクは 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 を使用します。このモジュールは、送信済みでまだクリックされていないリンクのトークンを保持します。トークンは1回限り有効で、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 に更新します。アプリのコードに変更はありません。ミックスインにはトランスポートキャニスターの _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 の匿名アクターで十分です。
  • フックでトークンを読み取ります(URL から ?token= を取得します)。confirm() を呼び出すボタンを表示します。ページ読み込み時やエフェクト内で confirm() を呼び出してはいけません。メールクライアント、リンクプレビュー機能、スキャナーは、人が操作していなくても URL にアクセスします。アクセスによって検証が行われてはなりません。
  • status の結果を表示します。検証済みの email とともに done、リンクがない、使用済み、または有効期限切れの場合は 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}} が必要であることを明記してください。

コメント