# 画面設計書 42-ユーザーアクセス管理画面

## 概要

本ドキュメントは、Actual Budgetアプリケーションの「ユーザーアクセス管理画面」の画面設計書です。マルチユーザー環境において、特定の予算ファイルへのアクセス権限を管理するための管理者専用画面を提供します。

### 本画面の処理概要

ユーザーアクセス管理画面は、マルチユーザーモードが有効な環境において、管理者が現在開いている予算ファイルに対するユーザーのアクセス権限を管理するための画面です。どのユーザーがこの予算を閲覧・編集できるかを制御し、予算ファイルの所有権を他のユーザーに移譲することができます。

**業務上の目的・背景**：マルチユーザー環境では、チームや家族で予算を共有することが一般的です。この画面は、予算ファイルの所有者または管理者が、誰がその予算にアクセスできるかを細かく制御するために必要です。例えば、家計簿を配偶者と共有したり、チーム予算を特定のメンバーにのみ公開したりする際に使用します。また、担当者変更時には所有権を移譲することで、責任の明確化とセキュリティの維持を実現します。

**画面へのアクセス方法**：サイドバーまたは設定画面から「User Access」リンクをクリックしてアクセスします。URL直接アクセスは `/user-access` です。この画面はマルチユーザーモードが有効かつ管理者権限（ADMINISTRATOR）を持ち、さらに予算ファイルのオーナーであるユーザーのみがアクセスできます。

**主要な操作・処理内容**：
1. アクセス可能ユーザー一覧の表示：システムに登録された全ユーザーを一覧表示し、現在の予算へのアクセス権限の有無を表示します
2. ユーザーの検索・フィルタリング：検索ボックスで表示名によるフィルタリングが可能です
3. アクセス権限の付与：チェックボックスをONにすることで、ユーザーに予算へのアクセス権限を付与します
4. アクセス権限の削除：チェックボックスをOFFにすることで、ユーザーのアクセス権限を削除します
5. 所有権の移譲：「Transfer ownership」ボタンから所有権移譲モーダルを開き、予算の所有者を変更できます

**画面遷移**：
- 遷移元：設定画面、サイドバー、ログインユーザーメニュー
- 遷移先：所有権移譲モーダル（transfer-ownership）

**権限による表示制御**：
- マルチユーザーモードが無効の場合、この画面自体が存在しません（ルートが登録されません）
- 管理者権限（ADMINISTRATOR）を持たないユーザーはアクセスが拒否されます
- 予算ファイルのオーナーでないユーザーはアクセスが拒否されます（validateOwner）
- オーナーのアクセス権限チェックボックスは無効化されており変更できません

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 66 | ユーザー管理 | 主機能 | ユーザーアクセス権限の設定・管理 |

## 画面種別

一覧 / 管理

## URL/ルーティング

- **パス**: `/user-access`
- **ルーティング定義**: `packages/desktop-client/src/components/FinancesApp.tsx` 356-367行目
- **アクセス制御**: `ProtectedRoute` コンポーネントで `Permissions.ADMINISTRATOR` 権限チェック + `validateOwner`

## 入出力項目

### 入力項目

| 項目名 | 項目ID | 型 | 必須 | 説明 |
|--------|--------|-----|------|------|
| 検索フィルター | filter | string | - | 表示名でフィルタリング |
| アクセス権限チェックボックス | marked | boolean | - | ユーザーへのアクセス付与/削除 |

### 出力項目（ユーザー一覧表示）

| 項目名 | 型 | 説明 |
|--------|-----|------|
| ユーザーID | string | 内部ID（非表示） |
| 表示名 | string | ユーザーの表示名（displayName or userName） |
| アクセス権限 | boolean | 予算へのアクセス権限の有無 |
| オーナー | boolean | 予算ファイルの所有者かどうか |

## 表示項目

### ヘッダー部

| 項目 | 幅 | 説明 |
|------|-----|------|
| Access | 100px | アクセス権限 |
| User | flex | ユーザー表示名 |
| Owner | 100px | オーナーフラグ |

### ユーザー行

| 項目 | 幅 | 説明 |
|------|-----|------|
| アクセスチェックボックス | 100px | アクセス権限のON/OFF（オーナーは無効） |
| displayName | flex | 表示名（未設定時はuserName） |
| ownerチェックボックス | 100px | オーナーフラグ（読み取り専用） |

### フッター部

| 項目 | 説明 |
|------|------|
| Transfer ownershipボタン | 所有権移譲モーダルを開く |

## イベント仕様

### 1-検索フィルター入力

ユーザーが検索ボックスに文字を入力すると、リアルタイムでユーザー一覧をフィルタリングします。

- **処理**: `onSearchChange` 関数（UserAccess.tsx 65-71行目）
- **フィルタリング対象**: displayName（大文字小文字を区別しない）
- **ページネーション**: フィルター変更時にページをリセット

### 2-アクセス権限チェックボックス変更

チェックボックスのON/OFFでアクセス権限を付与/削除します。

- **処理**: `handleAccessToggle` 関数（UserAccessRow.tsx 36-69行目）
- **ON時**: `access-add` APIを呼び出してアクセス権限を付与
- **OFF時**: `access-delete-all` APIを呼び出してアクセス権限を削除
- **オーナー**: チェックボックスは無効化されており変更不可

### 3-Transfer ownershipボタン押下

「Transfer ownership」ボタン押下時、所有権移譲モーダルを表示します。

- **処理**: `pushModal` による遷移（UserAccess.tsx 259-269行目）
- **遷移先モーダル**: `transfer-ownership`
- **モーダル終了後**: アクセス一覧を再読み込み

### 4-無限スクロール

一覧のスクロールにより追加データを読み込みます。

- **処理**: `loadMore` 関数（UserAccess.tsx 126-128行目）
- **表示件数**: 初期100件、以降50件ずつ追加

## データベース更新仕様

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| アクセス権限付与 | user_access（サーバーDB） | INSERT | アクセス権限レコード作成 |
| アクセス権限削除 | user_access（サーバーDB） | DELETE | アクセス権限レコード削除 |
| アクセス一覧取得 | user_access/users（サーバーDB） | SELECT | 利用可能ユーザーとアクセス状態の取得 |
| 所有権移譲 | files（サーバーDB） | UPDATE | ファイルの所有者変更 |

### テーブル別更新項目詳細

#### user_access（サーバー側データベース）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | fileId | 現在の予算ファイルID | cloudFileIdから取得 |
| INSERT | userId | 対象ユーザーID | - |
| DELETE | - | WHERE fileId=... AND userId=... | アクセス権限削除 |

#### files（サーバー側データベース - 所有権移譲時）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | owner | 新しい所有者ID | transfer-ownership API |

## メッセージ仕様

| メッセージID | 種別 | タイトル | メッセージ内容 | 発生条件 |
|-------------|------|---------|---------------|---------|
| error | エラー | Error getting available users | (エラー内容) | ユーザー一覧取得失敗時 |
| error | エラー | Access Revocation Incomplete | Some access permissions were not revoked successfully. | アクセス削除が一部失敗時 |
| login-expired | エラー | Login expired | Login expired, please log in again. | トークン期限切れ |
| error | エラー | Something happened while editing access | (エラー内容) | アクセス権限変更失敗時 |

## 例外処理

| 例外状況 | 対応処理 |
|---------|---------|
| 未認証（unauthorized） | エラー通知表示 |
| トークン期限切れ（token-expired） | エラー通知表示、「Go to login」ボタンでサインアウト処理 |
| ファイルID不正（invalid-file-id） | エラー通知表示 |
| 権限不足（file-denied） | エラー通知表示 |
| 既にアクセス権限あり（user-already-have-access） | エラー通知表示 |
| 権限不足/非オーナー | ProtectedRouteにより画面アクセス自体が拒否される |
| アクセス削除一部失敗 | 警告通知表示、部分的に削除完了 |

## 備考

- この画面はマルチユーザーモード（`multiuserEnabled`）が有効な場合のみルーティングに登録されます
- 予算ファイルのオーナーのみがこの画面にアクセスできます（validateOwner: true）
- オーナーユーザーは常にアクセス権限があり、自身の権限を削除することはできません
- 所有権移譲は不可逆操作であり、元のオーナーは管理権限を失います
- cloudFileIdメタデータプリファレンスから現在の予算ファイルIDを取得します
- ユーザー一覧はオーナーを先頭にソートされ、その後は表示名でアルファベット順にソートされます

---

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

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

### 推奨読解順序

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

まず、アクセス権限に関連する型定義を理解することが重要です。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | user-access.ts | `packages/loot-core/src/types/models/user-access.ts` | UserAccessEntity、NewUserAccessEntity型の定義 |
| 1-2 | user.ts | `packages/loot-core/src/types/models/user.ts` | UserAvailable型の定義。userId、displayName、userName、haveAccess、ownerフィールド |

**読解のコツ**: UserAvailableはアクセス可能ユーザーを表し、haveAccessとownerはnumber型（0/1）で真偽値を表現しています。

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

処理の起点となるページコンポーネントを確認します。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | UserAccessPage.tsx | `packages/desktop-client/src/components/admin/UserAccess/UserAccessPage.tsx` | ページ全体の構造、Pageコンポーネントでラップ |
| 2-2 | FinancesApp.tsx | `packages/desktop-client/src/components/FinancesApp.tsx` | ルーティング定義（356-367行目）、ProtectedRouteによる権限・オーナーチェック |

**主要処理フロー**:
1. **8-22行目**: UserAccessPageコンポーネント定義
2. **11-13行目**: Pageコンポーネントでヘッダー「User Access」を設定
3. **19行目**: UserAccessコンポーネントを呼び出し

#### Step 3: メインコンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | UserAccess.tsx | `packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx` | メインロジック、状態管理、APIコール |

**主要処理フロー**:
- **42-200行目**: UserAccessContentコンポーネント
- **45-48行目**: useState定義（allAccess、page、filter、cloudFileId）
- **73-108行目**: loadAccess関数 - APIからアクセス可能ユーザー一覧を取得
- **77-82行目**: sortUsers関数 - オーナー優先、表示名アルファベット順でソート
- **110-124行目**: useEffectでコンポーネントマウント時にアクセスデータをロード
- **191-197行目**: LockToggleボタン - 所有権移譲モーダルを開く

#### Step 4: 行コンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | UserAccessHeader.tsx | `packages/desktop-client/src/components/admin/UserAccess/UserAccessHeader.tsx` | テーブルヘッダーの定義 |
| 4-2 | UserAccessRow.tsx | `packages/desktop-client/src/components/admin/UserAccess/UserAccessRow.tsx` | 各ユーザー行のレンダリング、アクセス権限の変更ロジック |

**主要処理フロー（UserAccessRow）**:
- **25-161行目**: memoizedコンポーネント定義
- **31-33行目**: markedステート - アクセス権限のローカル状態
- **36-69行目**: handleAccessToggle関数 - チェックボックス変更時の処理
- **39-46行目**: アクセス権限付与（access-add API呼び出し）
- **47-66行目**: アクセス権限削除（access-delete-all API呼び出し）
- **122-127行目**: アクセスチェックボックス（オーナーは無効化）

#### Step 5: 所有権移譲モーダルを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | TransferOwnership.tsx | `packages/desktop-client/src/components/modals/TransferOwnership.tsx` | 所有権移譲モーダルのUI・ロジック |

**主要処理フロー**:
- **38-228行目**: TransferOwnershipコンポーネント
- **56-88行目**: useEffectでユーザー一覧を取得
- **90-105行目**: onSave関数 - transfer-ownership API呼び出し
- **196-219行目**: 移譲実行ボタン - 移譲後に予算を再読み込み

#### Step 6: API層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 6-1 | app.ts | `packages/loot-core/src/server/admin/app.ts` | サーバーAPIハンドラーの定義 |

**主要処理フロー**:
- **129-147行目**: addAccess関数 - POST /admin/access/
- **149-178行目**: deleteAllAccess関数 - DELETE /admin/access
- **180-205行目**: accessGetAvailableUsers関数 - GET /admin/access/users
- **207-231行目**: transferOwnership関数 - POST /admin/access/transfer-ownership/

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

```
FinancesApp（ルーティング）
    │
    ├─ ProtectedRoute（権限チェック + オーナー検証）
    │      └─ UserAccessPage
    │             └─ UserAccess
    │                    ├─ UserAccessHeader
    │                    ├─ UserAccessList
    │                    │      └─ UserAccessRow（複数）
    │                    │             └─ handleAccessToggle
    │                    │                    ├─ send('access-add')
    │                    │                    └─ send('access-delete-all')
    │                    └─ LockToggle
    │                           └─ pushModal('transfer-ownership')
    │
    └─ TransferOwnership（モーダル）
           └─ send('transfer-ownership')
```

### データフロー図

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

ページ読み込み ───▶ loadAccess() ───▶ send('access-get-available-users') ───▶ allAccess状態更新 ───▶ 一覧表示
                          │
                          ▼
                    サーバーAPI
               GET /admin/access/users
                          │
                          ▼
                UserAvailable[]JSON
                    （ソート済み）

アクセスON ───▶ handleAccessToggle() ───▶ send('access-add') ───▶ ローカル状態更新
                                                │
                                                ▼
                                      POST /admin/access/

アクセスOFF ───▶ handleAccessToggle() ───▶ send('access-delete-all') ───▶ ローカル状態更新
                                                │
                                                ▼
                                      DELETE /admin/access

Transfer押下 ───▶ pushModal() ───▶ TransferOwnership
                                         │
                                  ユーザー選択
                                         │
                                  send('transfer-ownership')
                                         │
                                         ▼
                              POST /admin/access/transfer-ownership/
                                         │
                                         ▼
                              closeAndLoadBudget（予算再読み込み）
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| UserAccessPage.tsx | `packages/desktop-client/src/components/admin/UserAccess/UserAccessPage.tsx` | ソース | ページラッパー |
| UserAccess.tsx | `packages/desktop-client/src/components/admin/UserAccess/UserAccess.tsx` | ソース | メインロジック |
| UserAccessHeader.tsx | `packages/desktop-client/src/components/admin/UserAccess/UserAccessHeader.tsx` | ソース | テーブルヘッダー |
| UserAccessRow.tsx | `packages/desktop-client/src/components/admin/UserAccess/UserAccessRow.tsx` | ソース | ユーザー行コンポーネント |
| TransferOwnership.tsx | `packages/desktop-client/src/components/modals/TransferOwnership.tsx` | ソース | 所有権移譲モーダル |
| app.ts | `packages/loot-core/src/server/admin/app.ts` | ソース | サーバーAPI定義 |
| user-access.ts | `packages/loot-core/src/types/models/user-access.ts` | 型定義 | アクセス権限エンティティ型 |
| user.ts | `packages/loot-core/src/types/models/user.ts` | 型定義 | UserAvailable型 |
| errors.ts | `packages/loot-core/src/shared/errors.ts` | ソース | getUserAccessErrors関数 |
| FinancesApp.tsx | `packages/desktop-client/src/components/FinancesApp.tsx` | ソース | ルーティング定義 |
| ProtectedRoute.tsx | `packages/desktop-client/src/auth/ProtectedRoute.tsx` | ソース | 権限チェックHOC |
