# 機能設計書 10-メンバープロフィール

## 概要

本ドキュメントは、Ghost CMSにおけるメンバープロフィール機能について、その設計仕様を記載する。メンバープロフィール機能は、認証済みメンバーの自己情報取得・更新を担う。

### 本機能の処理概要

メンバープロフィール機能は、ログイン中のメンバーが自身のプロフィール情報を閲覧・更新するための機能である。Portal経由でメンバーは名前、メールアドレス、ニュースレター購読設定、コメント通知設定などを管理できる。メールアドレスの変更にはマジックリンクによる確認が必要。

**業務上の目的・背景**：メンバーが自身の情報を管理できることは、会員制サービスの基本機能である。特にニュースレターの購読設定やコメント通知設定は、メンバー自身が制御できる必要がある。また、メールアドレスの変更要件にも対応する。

**機能の利用シーン**：
- メンバーがPortalで自分のプロフィール情報を確認する
- メンバーが名前やexpertise（専門分野）を更新する
- メンバーがニュースレターの購読設定を変更する
- メンバーがコメント通知のオン・オフを切り替える
- メンバーがメールアドレスを変更する（確認メール送信）
- メンバーがメール配信停止状態を解除する

**主要な処理内容**：
1. メンバーデータ取得（GET /members/api/member/）
2. メンバーデータ更新（PUT /members/api/member/）
3. ニュースレター設定取得（GET /members/api/member/newsletters/）
4. ニュースレター設定更新（PUT /members/api/member/newsletters/）
5. メールアドレス変更（POST /members/api/send-magic-link/ with type=updateEmail）
6. メール配信停止解除（DELETE /members/api/member/suppression/）

**関連システム・外部連携**：
- Members API（プロフィールデータのCRUD）
- メールサービス（メールアドレス変更確認）
- Email Suppression List（配信停止管理）

**権限による制御**：
- メンバー本人のセッションが必要（セッションCookieまたはuuid+key認証）
- 他のメンバーのプロフィールは閲覧・編集不可

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 15 | Portalウィジェット | 主機能 | プロフィール表示・編集 |
| 16 | 会員ページ | 補助機能 | プロフィール情報の表示 |

## 機能種別

CRUD操作

## 入力仕様

### 入力パラメータ

#### メンバーデータ更新（updateMemberData）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | No | 氏名 | maxlength: 191 |
| expertise | string | No | 専門分野 | maxlength: 50 |
| subscribed | boolean | No | 購読状態 | - |
| newsletters | array | No | 購読ニュースレター配列 | - |
| enable_comment_notifications | boolean | No | コメント通知有効化 | - |

#### ニュースレター設定更新（updateMemberNewsletters）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| newsletters | array | No | 購読ニュースレター配列 | - |
| enable_comment_notifications | boolean | No | コメント通知有効化 | - |

#### メールアドレス変更（updateEmailAddress）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| identity | string | Yes | 認証トークン（JWT） | - |
| email | string | Yes | 新しいメールアドレス | isEmail形式 |

#### uuid+key認証

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| uuid | string | Yes | メンバーUUID | UUID形式 |
| key | string | Yes | HMAC署名済みキー | SHA256-HMAC形式 |

### 入力データソース

- Portal（フロントエンドプロフィール画面）
- セッションCookie（members-ssr）
- uuid+keyパラメータ（メールからのリンク）

## 出力仕様

### 出力データ

#### メンバープロフィール

| 項目名 | 型 | 説明 |
|--------|-----|------|
| uuid | string | メンバーUUID |
| email | string | メールアドレス |
| name | string | 氏名 |
| expertise | string | 専門分野 |
| status | string | ステータス（free/paid/comped） |
| newsletters | array | 購読ニュースレター配列 |
| enable_comment_notifications | boolean | コメント通知有効化 |
| subscriptions | array | サブスクリプション配列 |
| avatar_image | string | Gravatarアバター画像URL |

### 出力先

- APIレスポンス（JSON形式）

## 処理フロー

### 処理シーケンス

```
【プロフィール取得】
1. セッションCookie確認
   └─ members-ssr Cookieからtransient_id取得
2. メンバーデータ取得
   └─ transient_idでメンバー検索
3. レスポンス返却
   └─ フォーマット済みメンバーデータをJSON返却

【プロフィール更新】
1. セッション確認
   └─ members-ssr Cookieからメンバー取得
2. 更新データ抽出
   └─ name, expertise, newsletters, enable_comment_notifications
3. メンバー更新
   └─ members.update()実行
4. 更新後データ取得
   └─ セッションから最新データ再取得
5. レスポンス返却

【メールアドレス変更】
1. 認証トークン検証
   └─ JWTからメンバー特定
2. 新メールアドレス確認
   └─ 重複チェック
3. 確認メール送信
   └─ updateEmailタイプでマジックリンク送信
4. マジックリンク経由で更新
   └─ トークン検証後にメールアドレス更新
```

### フローチャート

```mermaid
flowchart TD
    A[プロフィール取得] --> B{セッション確認}
    B -->|なし| C[204 No Content]
    B -->|あり| D[メンバーデータ取得]
    D --> E[JSON返却]

    F[プロフィール更新] --> G{セッション確認}
    G -->|なし| H[null返却]
    G -->|あり| I[更新データ抽出]
    I --> J[members.update]
    J --> K[更新後データ取得]
    K --> L[JSON返却]

    M[メールアドレス変更] --> N{トークン検証}
    N -->|失敗| O[401 Unauthorized]
    N -->|成功| P[新メールアドレス確認]
    P --> Q[確認メール送信]
    Q --> R[201 Created]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-001 | セッション必須 | プロフィール操作にはセッション認証が必要 | 全プロフィール操作時 |
| BR-002 | uuid+key認証 | メールリンク経由の場合はuuid+key認証も可能 | ニュースレター設定時 |
| BR-003 | 本人限定 | メンバーは自身のプロフィールのみ操作可能 | 全プロフィール操作時 |
| BR-004 | メールアドレス変更確認 | メールアドレス変更にはマジックリンク確認が必要 | メールアドレス変更時 |
| BR-005 | HMACキー検証 | uuid+key認証時はmembers_validation_keyでHMAC検証 | uuid+key認証時 |
| BR-006 | 更新可能フィールド制限 | 更新可能なフィールドは限定されている | プロフィール更新時 |

### 計算ロジック

- **HMACキー生成**: crypto.createHmac('sha256', members_validation_key).update(uuid).digest('hex')
- **アクセスCookie有効期間**: 3600秒（1時間）
- **ghost-access Cookie**: {tier_id}:{timestamp} 形式

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| プロフィール取得 | members | SELECT | transient_idでメンバー取得 |
| プロフィール更新 | members | UPDATE | 名前、expertise等の更新 |
| ニュースレター更新 | members_newsletters | INSERT/DELETE | 購読設定の変更 |
| メール変更 | members | UPDATE | メールアドレスの更新 |
| 配信停止解除 | email_suppression | DELETE | 抑制リストからの削除 |

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

#### members

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | name | リクエスト値 | オプション |
| UPDATE | expertise | リクエスト値 | オプション、maxlength: 50 |
| UPDATE | enable_comment_notifications | リクエスト値 | boolean |
| UPDATE | email | リクエスト値 | マジックリンク確認後のみ |
| UPDATE | email_disabled | false | 配信停止解除時 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| 204 | No Content | セッションが存在しない | ログイン後に再実行 |
| 400 | BadRequestError | プロフィール更新失敗 | 入力値を確認 |
| 401 | UnauthorizedError | uuid/keyが無効 | 正しいリンクを使用 |
| 404 | Not Found | メンバーが存在しない | - |
| 500 | Internal Server Error | メールアドレス変更失敗 | 再試行 |

### リトライ仕様

特別なリトライ処理は実装されていない。

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

プロフィール更新はmembers.update()内でトランザクション制御される。

## パフォーマンス要件

- セッション確認: 各リクエストで実行されるため高速である必要がある
- プロフィール更新: 即座に反映される必要がある

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

- **本人認証**: セッションCookieまたはuuid+key認証必須
- **HMACキー検証**: uuid認証時はmembers_validation_keyで署名検証
- **メール変更確認**: マジックリンクによる二重確認
- **更新フィールド制限**: _.pick()で更新可能フィールドを制限

## 備考

- ghost-access Cookieはティアベースのコンテンツキャッシュ最適化用
- メール配信停止解除はemail_suppression_listとemail_disabledの両方を更新

---

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

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

### 推奨読解順序

#### Step 1: メンバーミドルウェアを理解する

プロフィール操作のメインロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | middleware.js | `ghost/core/core/server/services/members/middleware.js` | メンバーミドルウェア |

**主要処理フロー**:
- **89-99行目**: loadMemberSession() - セッションからメンバー読み込み
- **105-147行目**: authMemberByUuid() - uuid+key認証
- **128行目**: HMACキー検証（members_validation_key使用）
- **149-158行目**: getIdentityToken() - 認証トークン取得
- **198-212行目**: deleteSession() - ログアウト
- **214-226行目**: getMemberData() - メンバーデータ取得
- **228-248行目**: deleteSuppression() - 配信停止解除
- **250-270行目**: getMemberNewsletters() - ニュースレター設定取得
- **272-298行目**: updateMemberNewsletters() - ニュースレター設定更新
- **300-325行目**: updateMemberData() - プロフィール更新

**updateMemberData()の詳細（300-325行目）**:
- **302行目**: _.pick(req.body, 'name', 'expertise', 'subscribed', 'newsletters', 'enable_comment_notifications') - 更新可能フィールド制限
- **305-308行目**: options設定（withRelated: stripeSubscriptions, newsletters）
- **309行目**: membersService.api.members.update()実行
- **310行目**: セッションから最新データ再取得

**authMemberByUuid()の詳細（105-147行目）**:
- **107-117行目**: uuid必須チェック（セッション認証時はスキップ可能）
- **119-124行目**: key必須チェック
- **128-133行目**: HMACキー検証
- **135-140行目**: uuidでメンバー取得

#### Step 2: アクセスCookie管理を理解する

ティアベースキャッシュ用のCookie管理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | middleware.js | `ghost/core/core/server/services/members/middleware.js` | アクセスCookie処理 |

**setAccessCookies()の詳細（40-77行目）**:
- **41-54行目**: メンバーがない場合はCookieをクリア
- **55-62行目**: HMACシークレット取得・検証
- **63-68行目**: ティアIDとタイムスタンプからCookie値生成
- **68行目**: HMAC署名生成
- **70-72行目**: Cookie設定（MaxAge: 3600秒）

**accessInfoSession()（79-85行目）**:
- onHeadersでレスポンス送信前にアクセスCookieを設定

#### Step 3: メールアドレス変更を理解する

メールアドレス変更のフローを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | member-controller.js | `ghost/core/core/server/services/members/members-api/controllers/member-controller.js` | メンバーコントローラー |
| 3-2 | members-api.js | `ghost/core/core/server/services/members/members-api/members-api.js` | Members API |

**member-controller.js updateEmailAddress()（40-75行目）**:
- **41-42行目**: identity（JWT）とemailを取得
- **48-56行目**: JWTを検証してメンバー情報取得
- **60-67行目**: メンバー存在確認
- **70行目**: updateEmailタイプでマジックリンク送信

**members-api.js getMemberDataFromMagicLinkToken()（264-267行目）**:
- **264行目**: type === 'updateEmail'の場合
- **266行目**: users.update({email}, {id: member.id})でメールアドレス更新

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

```
Portal / Frontend
    │
    ├─ プロフィール取得
    │      └─ GET /members/api/member/
    │             └─ middleware.getMemberData()
    │                    ├─ membersService.ssr.getMemberDataFromSession()
    │                    └─ formattedMemberResponse()
    │
    ├─ プロフィール更新
    │      └─ PUT /members/api/member/
    │             └─ middleware.updateMemberData()
    │                    ├─ membersService.ssr.getMemberDataFromSession()
    │                    ├─ _.pick() - フィールド制限
    │                    ├─ membersService.api.members.update()
    │                    └─ formattedMemberResponse()
    │
    ├─ ニュースレター設定
    │      ├─ GET /members/api/member/newsletters/
    │      │      └─ middleware.getMemberNewsletters()
    │      │             └─ formatNewsletterResponse()
    │      │
    │      └─ PUT /members/api/member/newsletters/
    │             └─ middleware.updateMemberNewsletters()
    │                    ├─ _.pick(req.body, 'newsletters', 'enable_comment_notifications')
    │                    └─ membersService.api.members.update()
    │
    ├─ メールアドレス変更
    │      └─ POST /members/api/email/
    │             └─ memberController.updateEmailAddress()
    │                    ├─ _tokenService.decodeToken() - JWT検証
    │                    ├─ _memberRepository.get() - メンバー確認
    │                    └─ _sendEmailWithMagicLink() - 確認メール送信
    │
    └─ 配信停止解除
           └─ DELETE /members/api/member/suppression/
                  └─ middleware.deleteSuppression()
                         ├─ emailSuppressionList.removeEmail()
                         └─ membersService.api.members.update({email_disabled: false})

uuid+key認証
    │
    └─ GET /members/api/member/newsletters/?uuid=xxx&key=xxx
           └─ middleware.authMemberByUuid()
                  ├─ crypto.createHmac() - HMAC検証
                  └─ membersService.api.memberBREADService.read({uuid})
```

### データフロー図

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

【プロフィール取得】

Browser (Portal)
       │
       ▼
┌─────────────────┐
│ Cookie:         │
│ members-ssr=xxx │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ getMemberData() │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ membersService  │───▶│ transient_idで  │
│ .ssr.getMember  │    │ メンバー検索    │
│ DataFromSession │    │                 │
└─────────────────┘    └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ formatted       │
                       │ MemberResponse  │
                       └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ JSON Response   │
                       └─────────────────┘

【プロフィール更新】

Portal (PUT Request)
       │
       ▼
┌─────────────────┐
│ {name,expertise,│
│  newsletters,   │
│  enable_comment │
│  _notifications}│
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ updateMemberData│───▶│ _.pick()        │
│ ()              │    │ フィールド制限  │
└─────────────────┘    └────────┬────────┘
                                │
                                ▼
                       ┌─────────────────┐
                       │ membersService  │
                       │ .api.members    │
                       │ .update()       │
                       └────────┬────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
           ┌─────────────────┐     ┌─────────────────┐
           │ members         │     │ members_        │
           │ table           │     │ newsletters     │
           └─────────────────┘     └─────────────────┘

【uuid+key認証】

Email Link
       │
       ▼
┌─────────────────┐
│ ?uuid=xxx       │
│ &key=xxx        │
└────────┬────────┘
         │
         ▼
┌─────────────────┐    ┌─────────────────┐
│ authMemberBy    │───▶│ HMAC検証        │
│ Uuid()          │    │ sha256(uuid,    │
│                 │    │ validation_key) │
└─────────────────┘    └────────┬────────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
           ┌─────────────────┐     ┌─────────────────┐
           │ 検証成功        │     │ 検証失敗        │
           │ メンバー取得    │     │ 401 Error       │
           └─────────────────┘     └─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| middleware.js | `ghost/core/core/server/services/members/middleware.js` | ミドルウェア | プロフィール操作メイン |
| members-ssr.js | `ghost/core/core/server/services/members/members-ssr.js` | サービス | SSRセッション管理 |
| member-controller.js | `ghost/core/core/server/services/members/members-api/controllers/member-controller.js` | コントローラー | メールアドレス変更 |
| members-api.js | `ghost/core/core/server/services/members/members-api/members-api.js` | API | Members API |
| utils.js | `ghost/core/core/server/services/members/utils.js` | ユーティリティ | レスポンスフォーマット |
| settings-helpers.js | `ghost/core/core/server/services/settings-helpers/index.js` | ヘルパー | getMembersValidationKey |
