# 通知設計書 9-マジックリンクメール

## 概要

本ドキュメントは、Fizzyアプリケーションにおける「マジックリンクメール」機能について、その設計仕様を記載するものである。

### 本通知の処理概要

ユーザーがサインインまたはサインアップする際に、パスワードの代わりにメールで送信される6桁のワンタイムコード（マジックリンク）を使用して認証を行う機能を提供する。

**業務上の目的・背景**：パスワードレス認証により、ユーザーはパスワードを記憶する必要がなく、セキュリティリスク（パスワードの使い回し、弱いパスワード等）を軽減できる。また、パスワードリセット機能を別途実装する必要がなく、シンプルな認証フローを実現できる。15分間有効なワンタイムコードにより、セキュリティと利便性のバランスを取っている。

**通知の送信タイミング**：ユーザーがサインインまたはサインアップ画面でメールアドレスを入力し、認証リクエストを行った際に、MagicLinkレコードが作成され、MagicLinkMailerによりメールが送信される。

**通知の受信者**：認証を要求したメールアドレスの所有者が受信者となる。既存ユーザーの場合はサインイン用、新規ユーザーの場合はサインアップ用のメッセージが表示される。

**通知内容の概要**：メールには、6桁の認証コード、コードの有効期限（15分間）、サポート連絡先が含まれる。件名にもコードが表示され、メールを開かずにコードを確認できる。

**期待されるアクション**：受信者はメールに記載された6桁のコードをFizzyの認証画面に入力し、サインイン/サインアップを完了することが期待される。

## 通知種別

メール通知

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（即座に送信） |
| 優先度 | 高 |
| リトライ | Action Mailerのデフォルト設定に準拠 |
| コード長 | 6桁 |
| 有効期限 | 15分間 |

### 送信先決定ロジック

1. ユーザーが入力したメールアドレスに送信
2. メールアドレスの存在確認はMagicLink作成時に行われる

```ruby
mail to: @identity.email_address, subject: "Your Fizzy code is #{ @magic_link.code }"
```

## 通知テンプレート

### メール通知

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | support@fizzy.do（環境変数で設定可能） |
| 送信元名称 | Fizzy |
| 件名 | "Your Fizzy code is {6桁コード}" |
| 形式 | HTML/テキスト両対応 |

### 本文テンプレート

サインイン時（for_sign_in?）:
```html
<h1>Fizzy verification code</h1>
<p>Please enter this 6-character verification code on the Fizzy sign-in page:</p>
<strong>{6桁コード}</strong>
<p>This code will work for 15 minutes.</p>
<footer>Need help? Send us an email: support@fizzy.do</footer>
```

サインアップ時（for_sign_up?）:
```html
<h1>Welcome to Fizzy!</h1>
<p>Please enter this 6-character verification code on the Fizzy sign-up page to create your new account:</p>
<strong>{6桁コード}</strong>
<p>This code will work for 15 minutes.</p>
<footer>Need help? Send us an email: support@fizzy.do</footer>
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| - | - | - | 添付ファイルなし |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| @magic_link.code | 認証コード | magic_links.code | Yes |
| @magic_link.for_sign_in? | サインイン用か | magic_links.purpose | Yes |
| @identity.email_address | 送信先アドレス | identities.email_address | Yes |
| MagicLink::EXPIRATION_TIME | 有効期限 | 定数（15.minutes） | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | サインイン要求 | MagicLink作成 | 既存ユーザーがサインイン要求した際 |
| 画面操作 | サインアップ要求 | MagicLink作成 | 新規ユーザーがサインアップ要求した際 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| - | 特に抑止条件なし（認証フローのため） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[ユーザーがメールアドレス入力] --> B[MagicLink作成]
    B --> C[コード生成（6桁）]
    C --> D[有効期限設定（15分後）]
    D --> E[MagicLinkMailer.sign_in_instructions送信]
    E --> F[ユーザーがコードを入力]
    F --> G{MagicLink.consume code}
    G -->|有効| H[MagicLink削除、認証成功]
    G -->|無効/期限切れ| I[認証失敗]
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| magic_links | コード情報取得 | 認証コード検証 |
| identities | ユーザー識別情報取得 | メールアドレス |

### テーブル別参照項目詳細

#### magic_links

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | レコードID | 主キー |
| code | 認証コード | ユニーク、6桁 |
| identity_id | ユーザー識別子 | 送信先特定 |
| purpose | 用途 | sign_in/sign_up |
| expires_at | 有効期限 | 15分後 |

#### identities

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| id | 識別子ID | magic_link.identity_id |
| email_address | メールアドレス | 送信先 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| magic_links | INSERT | コード生成時に作成 |
| magic_links | DELETE | 認証成功時に削除（consume） |

#### magic_linksテーブル

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| INSERT | identity_id | IdentityのID | 認証対象ユーザー |
| INSERT | code | 6桁コード | generate_codeで生成 |
| INSERT | purpose | 0 (sign_in) or 1 (sign_up) | 用途 |
| INSERT | expires_at | 現在時刻 + 15分 | 有効期限 |
| DELETE | - | - | consume時に削除 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| Net::SMTPError | SMTP接続エラー | リトライまたはエラー表示 |
| ActionMailer::DeliveryError | メール送信エラー | リトライまたはエラー表示 |
| ActiveRecord::RecordNotUnique | コード重複 | 再生成してリトライ |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | コード生成はループで重複回避 |
| リトライ間隔 | 即時 |
| リトライ対象エラー | コード重複のみ |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | アプリケーション側で制限推奨 |
| 1日あたり上限 | 制限なし（要検討） |

### 配信時間帯

制限なし（認証フローのため24時間対応必須）

## セキュリティ考慮事項

- コードは6桁の英数字でCode.generateにより生成
- コードはユニーク制約で重複防止
- 有効期限は15分間と短く設定
- コードは使用後即座に削除（ワンタイム）
- 期限切れコードはMagicLink.cleanupで定期削除
- 件名にコードを含めることで、メール本文を開かずに確認可能（利便性）

## 備考

- purposeはenum（sign_in: 0, sign_up: 1）で管理
- コード生成ロジックは重複を避けるためループで確認
- active scopeで有効なコードのみ検索可能
- stale scopeで期限切れコードを特定し、cleanupで削除
- Code.sanitizeで入力コードの正規化を実施

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、マジックリンクで使用されるデータモデルの構造を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | magic_link.rb | `app/models/magic_link.rb` | マジックリンクモデル、定数、スコープ（1-43行目） |
| 1-2 | schema.rb | `db/schema.rb` | magic_linksテーブル定義（347-357行目） |

**読解のコツ**: CODE_LENGTH=6とEXPIRATION_TIME=15.minutesが重要な定数。purposeはenumでsign_in/sign_upを区別。

#### Step 2: コード生成ロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | magic_link.rb | `app/models/magic_link.rb` | generate_code メソッド（32-37行目） |

**主要処理フロー**:
- **32-37行目**: コード生成（重複回避ループ）
  ```ruby
  def generate_code
    self.code ||= loop do
      candidate = Code.generate(CODE_LENGTH)
      break candidate unless self.class.exists?(code: candidate)
    end
  end
  ```

#### Step 3: メーラーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | magic_link_mailer.rb | `app/mailers/magic_link_mailer.rb` | メール送信ロジック（1-8行目） |
| 3-2 | sign_in_instructions.html.erb | `app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb` | HTMLテンプレート（1-17行目） |

**主要処理フロー**:
- **2-7行目（mailer）**: sign_in_instructions メソッド
  ```ruby
  def sign_in_instructions(magic_link)
    @magic_link = magic_link
    @identity = @magic_link.identity
    mail to: @identity.email_address, subject: "Your Fizzy code is #{ @magic_link.code }"
  end
  ```

#### Step 4: コード検証と消費を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | magic_link.rb | `app/models/magic_link.rb` | consume メソッド、スコープ（17-30行目） |

**主要処理フロー**:
- **17-20行目**: consumeクラスメソッド
  ```ruby
  def self.consume(code)
    active.find_by(code: Code.sanitize(code))&.consume
  end
  ```
- **26-29行目**: consumeインスタンスメソッド（削除して自身を返す）
  ```ruby
  def consume
    destroy
    self
  end
  ```

### プログラム呼び出し階層図

```
SessionsController または SignupsController
    │
    ├─ MagicLink.create(identity: identity, purpose: :sign_in/:sign_up)
    │      │
    │      ├─ before_validation :generate_code
    │      │     └─ loop do Code.generate(6) end（重複回避）
    │      │
    │      ├─ before_validation :set_expiration
    │      │     └─ expires_at = 15.minutes.from_now
    │      │
    │      └─ validates :code, uniqueness: true
    │
    └─ MagicLinkMailer.sign_in_instructions(magic_link).deliver_later
           │
           ├─ @magic_link = magic_link
           ├─ @identity = magic_link.identity
           └─ mail to: email, subject: "Your Fizzy code is {code}"

=== ユーザーがコードを入力 ===

SessionsController
    │
    └─ MagicLink.consume(code)
           │
           ├─ active.find_by(code: Code.sanitize(code))
           │     └─ active = where(expires_at: Time.current...)
           │
           └─ consume
                  └─ destroy → self
```

### データフロー図

```
[入力]                    [処理]                              [出力]

メールアドレス       ───▶ MagicLink.create               ───▶ magic_linksテーブルINSERT
                           │                                     │
                           ├─ generate_code                      ├─ code: 6桁
                           ├─ set_expiration                     ├─ expires_at: +15分
                           └─ purpose設定                        └─ purpose: sign_in/sign_up
                                                                      │
                                                                      ▼
                                                               MagicLinkMailer
                                                                      │
                                                                      ├─ subject: "Your Fizzy code is {code}"
                                                                      ├─ body: 認証コード + 説明
                                                                      │
                                                                      ▼
                                                               ユーザーのメールボックス

=== 認証時 ===

入力コード           ───▶ MagicLink.consume(code)        ───▶ 認証結果
                           │                                     │
                           ├─ Code.sanitize                      ├─ 成功: MagicLink削除、セッション作成
                           ├─ active.find_by                     └─ 失敗: エラー表示
                           └─ destroy
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| magic_link.rb | `app/models/magic_link.rb` | モデル | マジックリンクモデル定義 |
| magic_link_mailer.rb | `app/mailers/magic_link_mailer.rb` | メーラー | メール送信 |
| sign_in_instructions.html.erb | `app/views/mailers/magic_link_mailer/sign_in_instructions.html.erb` | ビュー | HTMLテンプレート |
| sign_in_instructions.text.erb | `app/views/mailers/magic_link_mailer/sign_in_instructions.text.erb` | ビュー | テキストテンプレート |
| magic_links_controller.rb | `app/controllers/sessions/magic_links_controller.rb` | コントローラ | 認証処理 |
| schema.rb | `db/schema.rb` | スキーマ | テーブル定義（347-357行目） |
