# 機能設計書 31-招待機能

## 概要

本ドキュメントは、Liam DBアプリケーションにおける組織へのメンバー招待機能について記述する。本機能は、組織管理者が新規メンバーをメールアドレスで招待し、招待されたユーザーが組織に参加できるようにする一連の処理を提供する。

### 本機能の処理概要

**業務上の目的・背景**：マルチテナント型SaaSアプリケーションにおいて、組織管理者がチームメンバーを効率的に追加できる仕組みが必要である。本機能は、メールアドレスベースの招待フローを提供し、既存ユーザーだけでなく新規ユーザーも組織に参加できるようにする。これにより、組織内でのスキーマ設計やデータベース管理の共同作業が可能になる。

**機能の利用シーン**：
- 組織管理者が設定画面のメンバー管理セクションから新規メンバーを招待する場面
- 招待されたユーザーがメールリンクをクリックして招待を承認する場面
- 管理者が保留中の招待を確認・削除する場面

**主要な処理内容**：
1. メンバー招待処理：メールアドレスを入力し、招待トークンを生成してデータベースに保存、招待メールを送信
2. 招待データ取得処理：招待トークンから組織情報を取得し、招待承認画面を表示
3. 招待承認処理：招待トークンを検証し、ユーザーを組織メンバーとして追加、招待レコードを削除
4. 招待削除処理：保留中の招待を管理者が取り消す

**関連システム・外部連携**：
- Resend API：招待メール送信に使用
- Supabase Auth：ユーザー認証とメール確認状態の検証に使用

**権限による制御**：
- 招待送信：組織のメンバーのみ実行可能（RLSにより制御）
- 招待承認：招待メールの受信者（メールアドレス一致）かつ認証済みユーザーのみ
- 招待削除：組織のメンバーのみ実行可能

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 4 | 招待承認画面 | 主画面 | 招待トークンを検証し、招待承認フォームを表示 |
| 18 | 設定-メンバー画面 | 主画面 | 新規メンバーの招待処理、招待一覧表示 |

## 機能種別

CRUD操作 / データ連携（メール送信）

## 入力仕様

### 入力パラメータ

#### 招待送信

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| email | string | Yes | 招待先メールアドレス | 有効なメールアドレス形式、小文字に正規化 |
| organizationId | string | Yes | 招待元組織ID | UUID形式 |

#### 招待承認

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| token | string | Yes | 招待トークン | UUID形式 |

#### 招待削除

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| invitationId | string | Yes | 招待ID | UUID形式 |
| organizationId | string | Yes | 組織ID | UUID形式 |

### 入力データソース

- 画面入力（InviteMemberModalフォーム、招待承認ボタン）
- URLパラメータ（招待トークン）

## 出力仕様

### 出力データ

#### 招待送信結果

| 項目名 | 型 | 説明 |
|--------|-----|------|
| success | boolean | 処理成功フラグ |
| error | string \| null | エラーメッセージ |
| invitation_token | string | 生成された招待トークン（成功時） |

#### 招待データ取得結果

| 項目名 | 型 | 説明 |
|--------|-----|------|
| organizationName | string \| null | 組織名（招待が有効な場合） |

#### 招待承認結果

| 項目名 | 型 | 説明 |
|--------|-----|------|
| success | boolean | 処理成功フラグ |
| organizationId | string \| null | 参加した組織ID |
| error | string \| null | エラーメッセージ |

### 出力先

- 画面表示（Toast通知、エラーメッセージ）
- データベース（invitationsテーブル、organization_membersテーブル）
- メール送信（Resend API経由）
- クッキー（organizationId）

## 処理フロー

### 処理シーケンス

#### 招待送信フロー

```
1. フォームバリデーション
   └─ メールアドレス形式チェック、必須項目確認
2. Server Action実行（inviteMember）
   └─ Valibotによるスキーマ検証
3. RPC関数呼び出し（invite_organization_member）
   └─ トークン生成、招待レコード作成
4. 招待メール送信（sendInvitationEmail）
   └─ Resend APIを使用して招待リンクを送信
5. パスの再検証
   └─ メンバー一覧ページをrevalidate
```

#### 招待承認フロー

```
1. 招待ページアクセス（トークン付きURL）
   └─ /invitations/tokens/[token]
2. 招待データ取得（getInvitationData）
   └─ RPC関数で組織名を取得
3. 招待カード表示
   └─ 組織名とユーザー情報を表示
4. 承認ボタンクリック
   └─ acceptInvitation Server Action実行
5. RPC関数呼び出し（accept_invitation）
   └─ メンバー追加、招待削除をトランザクション内で実行
6. リダイレクト
   └─ プロジェクト一覧ページへ遷移
```

### フローチャート

```mermaid
flowchart TD
    A[招待送信開始] --> B{メール形式有効?}
    B -->|No| C[エラー表示]
    B -->|Yes| D[invite_organization_member RPC呼び出し]
    D --> E{招待作成成功?}
    E -->|No| F[エラーToast表示]
    E -->|Yes| G[招待メール送信]
    G --> H{メール送信成功?}
    H -->|No| I[エラーToast表示]
    H -->|Yes| J[成功Toast表示]
    J --> K[モーダルを閉じる]

    L[招待リンククリック] --> M[招待ページ表示]
    M --> N[get_invitation_data RPC呼び出し]
    N --> O{招待有効?}
    O -->|No| P[無効/期限切れメッセージ表示]
    O -->|Yes| Q[招待カード表示]
    Q --> R[承認ボタンクリック]
    R --> S[accept_invitation RPC呼び出し]
    S --> T{承認成功?}
    T -->|No| U[エラー表示]
    T -->|Yes| V[組織IDクッキー設定]
    V --> W[プロジェクト一覧へリダイレクト]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-31-01 | 招待有効期限 | 招待は一定期間後に期限切れとなる | current_timestamp < expired_at |
| BR-31-02 | メール確認必須 | 招待承認にはメール確認済みアカウントが必要 | email_confirmed_at IS NOT NULL |
| BR-31-03 | メールアドレス一致 | 招待は招待先メールアドレスと一致するアカウントのみ承認可能 | lower(invitation.email) = lower(user.email) |
| BR-31-04 | 一回限りの承認 | 招待は承認後に削除され、再利用不可 | 承認時に招待レコードを削除 |
| BR-31-05 | 重複メンバー防止 | 既に組織メンバーの場合は招待不可 | RPC関数内でチェック |

### 計算ロジック

招待トークンはUUID v4形式で自動生成される（PostgreSQLのgen_random_uuid()関数）。

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 招待送信 | invitations | INSERT | 新規招待レコードの作成 |
| 招待データ取得 | invitations, organizations | SELECT | 招待情報と組織名の取得 |
| 招待承認 | organization_members | INSERT | メンバーレコードの作成 |
| 招待承認 | invitations | DELETE | 使用済み招待の削除 |
| 招待削除 | invitations | DELETE | 保留中招待の削除 |

### テーブル別操作詳細

#### invitations

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | gen_random_uuid() | 自動生成 |
| INSERT | email | 入力値（小文字化） | 招待先メールアドレス |
| INSERT | organization_id | 入力値 | 招待元組織 |
| INSERT | invite_by_user_id | auth.uid() | 招待者 |
| INSERT | token | gen_random_uuid() | 招待トークン |
| INSERT | expired_at | デフォルト値 | 有効期限 |
| SELECT | token = p_token | 招待トークンで検索 | |
| DELETE | id = v_invitation_id | 承認時に削除 | |

#### organization_members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | user_id | auth.uid() | 承認ユーザー |
| INSERT | organization_id | v_organization_id | 招待元組織 |
| INSERT | joined_at | current_timestamp | 参加日時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | バリデーションエラー | メールアドレス形式不正 | フォームでエラーメッセージ表示 |
| - | 認証エラー | 未ログイン状態 | ログインページへリダイレクト |
| - | 招待不存在 | トークンが無効または期限切れ | 「招待が見つからないか期限切れ」メッセージ表示 |
| - | メール送信失敗 | Resend API エラー | Toastでエラー通知 |
| - | データベースエラー | RPC関数内でのエラー | SQLERRMを含むJSONBオブジェクトを返却 |

### リトライ仕様

メール送信失敗時のリトライ機構は実装されていない。ユーザーは再度招待操作を行う必要がある。

## トランザクション仕様

- `accept_invitation` RPC関数内で、メンバー追加と招待削除をトランザクション内で実行
- `invite_organization_member` RPC関数内で、招待レコード作成をアトミックに実行
- エラー発生時は自動的にロールバックされ、エラー情報を含むJSONBオブジェクトを返却

## パフォーマンス要件

- 招待送信：メール送信を含むため、Resend APIのレスポンス時間に依存（通常数秒以内）
- 招待承認：データベース操作のみのため、サブセカンドでの応答が期待される

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

- 招待トークンはUUID v4形式で、推測困難な形式を採用
- RLS（Row Level Security）により、組織メンバーのみが招待操作を実行可能
- `accept_invitation`および`get_invitation_data`関数は`SECURITY DEFINER`で実行され、匿名アクセスは`REVOKE`で禁止
- メールアドレスの大文字小文字を正規化して比較（lower関数使用）
- メール確認済みアカウントのみが招待を承認可能

## 備考

- Resend APIキーが設定されていない場合、メール送信はスキップされる（開発環境向け）
- 招待リンクのベースURLは環境変数（NEXT_PUBLIC_ENV_NAME）に応じて動的に決定される

---

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

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

### 推奨読解順序

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

招待機能で使用されるデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | database.types.ts | `frontend/internal-packages/db/supabase/database.types.ts` | invitationsテーブルの型定義（371-415行目）を確認 |

**読解のコツ**: Supabaseの型定義は`Row`（読み取り時の型）、`Insert`（挿入時の型）、`Update`（更新時の型）の3種類があり、オプショナルな項目が異なる。

#### Step 2: エントリーポイントを理解する

招待承認ページと招待モーダルの2つのエントリーポイントがある。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | page.tsx | `frontend/apps/app/app/invitations/tokens/[token]/page.tsx` | 招待承認ページのルーティング |
| 2-2 | InvitationPage.tsx | `frontend/apps/app/components/InvitationPage/InvitationPage.tsx` | 招待ページコンポーネント |
| 2-3 | InviteMemberModal.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/InviteMemberModal.tsx` | 招待モーダルコンポーネント |

**主要処理フロー**:
1. **9-18行目（InvitationPage.tsx）**: 招待データを取得し、InvitationCardに渡す
2. **56-109行目（InviteMemberModal.tsx）**: フォーム送信時にinviteMemberを呼び出し、成功/失敗時のUI処理

#### Step 3: Server Actionsを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | inviteMember.ts | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/actions/inviteMember.ts` | 招待送信のServer Action |
| 3-2 | sendInvitationEmail.ts | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/actions/sendInvitationEmail.ts` | メール送信処理 |
| 3-3 | getInvitationData.ts | `frontend/apps/app/components/InvitationPage/services/getInvitationData.ts` | 招待データ取得 |
| 3-4 | acceptInvitation.ts | `frontend/apps/app/components/InvitationPage/components/InvitationCard/actions/acceptInvitation.ts` | 招待承認処理 |
| 3-5 | removeInvitation.ts | `frontend/apps/app/components/OrganizationMembersPage/components/DeleteInvitationModal/actions/removeInvitation.ts` | 招待削除処理 |

**主要処理フロー**:
- **34-96行目（inviteMember.ts）**: Valibotでバリデーション後、RPC呼び出し、メール送信
- **38-102行目（sendInvitationEmail.ts）**: 環境に応じたURLを構築し、Resendでメール送信
- **33-97行目（acceptInvitation.ts）**: トークン検証、RPC呼び出し、クッキー設定、リダイレクト

#### Step 4: データベース関数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | accept_invitation.sql | `frontend/internal-packages/db/supabase/migrations/20250424163200_accept_invitation.sql` | 招待承認のアトミック処理 |
| 4-2 | get_invitation_data.sql | `frontend/internal-packages/db/supabase/migrations/20250424163100_get_invitation_data.sql` | 招待データ取得 |

**主要処理フロー**:
- **2-69行目（accept_invitation.sql）**: トランザクション内でメンバー追加と招待削除を実行
- **14-27行目**: 招待の有効性チェック（トークン、メールアドレス一致、メール確認済み、有効期限）

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

```
InviteMemberModal（招待送信UI）
    │
    ├─ inviteMember（Server Action）
    │      ├─ Valibot検証
    │      ├─ supabase.rpc('invite_organization_member')
    │      └─ sendInvitationEmail
    │             ├─ supabase.from('organizations').select
    │             └─ resend.emails.send
    │
InvitationPage（招待承認UI）
    │
    ├─ getInvitationData（Server Action）
    │      └─ supabase.rpc('get_invitation_data')
    │
    └─ InvitationCard
           └─ acceptInvitation（Server Action）
                  ├─ supabase.rpc('accept_invitation')
                  ├─ setOrganizationIdCookie
                  └─ redirect
```

### データフロー図

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

招待送信:
メールアドレス ───▶ inviteMember ───▶ invite_organization_member RPC
organizationId                         │
                                       ├───▶ invitationsテーブルINSERT
                                       │
                  sendInvitationEmail ◀─┘
                         │
                         └───▶ Resend API ───▶ 招待メール送信

招待承認:
招待トークン ───▶ getInvitationData ───▶ get_invitation_data RPC
(URLパラメータ)                              │
                                            └───▶ 組織名表示

承認ボタン ───▶ acceptInvitation ───▶ accept_invitation RPC
                                          │
                                          ├───▶ organization_membersテーブルINSERT
                                          └───▶ invitationsテーブルDELETE
                                                      │
                                    クッキー設定 ◀────┘
                                          │
                                          └───▶ プロジェクト一覧へリダイレクト
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| InvitationPage.tsx | `frontend/apps/app/components/InvitationPage/InvitationPage.tsx` | ソース | 招待承認ページコンポーネント |
| InvitationCard.tsx | `frontend/apps/app/components/InvitationPage/components/InvitationCard/InvitationCard.tsx` | ソース | 招待カードUI |
| acceptInvitation.ts | `frontend/apps/app/components/InvitationPage/components/InvitationCard/actions/acceptInvitation.ts` | ソース | 招待承認Server Action |
| getInvitationData.ts | `frontend/apps/app/components/InvitationPage/services/getInvitationData.ts` | ソース | 招待データ取得 |
| InviteMemberModal.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/InviteMemberModal.tsx` | ソース | 招待モーダルUI |
| inviteMember.ts | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/actions/inviteMember.ts` | ソース | 招待送信Server Action |
| sendInvitationEmail.ts | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/actions/sendInvitationEmail.ts` | ソース | メール送信処理 |
| removeInvitation.ts | `frontend/apps/app/components/OrganizationMembersPage/components/DeleteInvitationModal/actions/removeInvitation.ts` | ソース | 招待削除Server Action |
| InvitationItem.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/InvitationItem/InvitationItem.tsx` | ソース | 招待一覧アイテムUI |
| DeleteInvitationModal.tsx | `frontend/apps/app/components/OrganizationMembersPage/components/DeleteInvitationModal/DeleteInvitationModal.tsx` | ソース | 招待削除確認モーダル |
| accept_invitation.sql | `frontend/internal-packages/db/supabase/migrations/20250424163200_accept_invitation.sql` | マイグレーション | 招待承認RPC関数 |
| get_invitation_data.sql | `frontend/internal-packages/db/supabase/migrations/20250424163100_get_invitation_data.sql` | マイグレーション | 招待データ取得RPC関数 |
| database.types.ts | `frontend/internal-packages/db/supabase/database.types.ts` | 型定義 | データベーステーブル型 |
| page.tsx | `frontend/apps/app/app/invitations/tokens/[token]/page.tsx` | ソース | 招待ページルーティング |
| InvitationCard.module.css | `frontend/apps/app/components/InvitationPage/components/InvitationCard/InvitationCard.module.css` | スタイル | 招待カードスタイル |
| InviteMemberModal.module.css | `frontend/apps/app/components/OrganizationMembersPage/components/InviteMemberModal/InviteMemberModal.module.css` | スタイル | 招待モーダルスタイル |
