# 画面設計書 9-ログイン画面

## 概要

本ドキュメントは、InvenTree Flutterアプリケーションのログイン画面（InvenTreeLoginWidget）の画面設計を定義するものである。

### 本画面の処理概要

ログイン画面はInvenTreeサーバーへのユーザー認証を行い、APIアクセス用のトークンを取得する機能を提供する。

**業務上の目的・背景**：InvenTreeサーバーのAPIにアクセスするためには認証トークンが必要である。本画面はユーザー名とパスワードを入力し、サーバーからトークンを取得する認証処理を提供する。取得したトークンはプロファイルに保存され、以降のAPIアクセスに使用される。

**画面へのアクセス方法**：サーバー選択画面でトークン未取得のプロファイルを選択した際に自動的に遷移する。

**主要な操作・処理内容**：
1. ユーザー名の入力と検証
2. パスワードの入力と検証（マスク表示）
3. ログインボタン押下による認証処理
4. 認証成功時のトークン保存と画面遷移

**画面遷移**：
- 遷移元：サーバー選択画面
- 遷移先：サーバー選択画面（ログイン成功後に戻る）

**権限による表示制御**：本画面は全ユーザーがアクセス可能。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 3 | ログイン機能 | 主機能 | ユーザー名とパスワードでサーバーにログインし認証トークン取得 |
| 1 | サーバー接続管理 | 補助機能 | ログイン成功時のサーバー接続確立 |

## 画面種別

入力フォーム画面（認証）

## URL/ルーティング

- Flutter Widget：`InvenTreeLoginWidget`（StatefulWidget）
- 状態クラス：`_InvenTreeLoginState`

## 入出力項目

| 項目名 | 入力/出力 | データ型 | 必須 | 説明 |
|--------|----------|----------|------|------|
| profile | 入力 | UserProfile | 必須 | ログイン対象のサーバープロファイル |
| username | 入力 | String | 必須 | ログインユーザー名 |
| password | 入力 | String | 必須 | ログインパスワード |

## 表示項目

| 項目名 | データ型 | 説明 | 取得元 |
|--------|----------|------|--------|
| AppBarタイトル | String | 「ログイン」 | L10().login |
| 案内テキスト | ListTile | ログイン案内のタイトルと説明 | L10().loginEnter / L10().loginEnterDetails |
| サーバー情報 | ListTile | 接続先サーバーURL | widget.profile.server |
| ユーザー名入力欄 | TextFormField | ユーザー名の入力フィールド | 初期値 "" |
| パスワード入力欄 | TextFormField | パスワードの入力フィールド（マスク可能） | 初期値 "" |
| エラー表示 | ListTile | 認証エラー時のエラーメッセージ | error変数 |

## イベント仕様

### 1-ログインボタン押下

AppBarのログインアイコン（矢印）をタップした際に認証処理を実行する。

**処理フロー**：
1. ユーザーがログインアイコンをタップ
2. `formKey.currentState?.save()`でフォームの値を保存
3. `formKey.currentState?.validate()`でバリデーションを実行
4. バリデーション成功の場合：
   - `FocusScope.of(context).unfocus()`でキーボードを閉じる
   - `showLoadingOverlay()`でローディングオーバーレイを表示
   - `InvenTreeAPI().fetchToken(profile, username, password)`を呼び出し
   - `hideLoadingOverlay()`でローディングオーバーレイを非表示
5. レスポンスが成功（`response.successful()`）の場合：
   - `Navigator.of(context).pop()`で前画面に戻る
6. レスポンスが失敗の場合：
   - レスポンスデータに"detail"キーがあれば、その値をエラーメッセージとして使用
   - なければ`statusCodeToString(response.statusCode)`でエラーメッセージを取得
   - `setState()`でerror変数を更新してエラーを表示

### 2-ユーザー名入力

ユーザー名入力欄にテキストを入力する。

**バリデーションルール**：
- 必須入力（空文字不可）
- 前後の空白は自動でトリム

**処理フロー**：
1. ユーザーがテキストを入力
2. `onSaved`で`username = value?.trim() ?? ""`として保存
3. `validator`で空文字チェック
4. 空の場合は`L10().usernameEmpty`を返す

### 3-パスワード入力

パスワード入力欄にテキストを入力する。

**バリデーションルール**：
- 必須入力（空文字不可）
- 前後の空白は自動でトリム

**処理フロー**：
1. ユーザーがテキストを入力
2. `onSaved`で`password = value?.trim() ?? ""`として保存
3. `validator`で空文字チェック
4. 空の場合は`L10().passwordEmpty`を返す

### 4-パスワード表示切替

パスワード入力欄の目アイコンをタップしてマスク表示を切り替える。

**処理フロー**：
1. ユーザーが目アイコンをタップ
2. `setState()`で`_obscured = !_obscured`を切り替え
3. `_obscured`がtrueの場合：パスワードはマスク表示（TablerIcons.eye）
4. `_obscured`がfalseの場合：パスワードは平文表示（TablerIcons.eye_off）

## API仕様

### 認証トークン取得 API

| 項目 | 値 |
|------|-----|
| エンドポイント | /api/user/token/ |
| HTTPメソッド | GET |
| 認証ヘッダー | Basic認証（username:passwordをBase64エンコード） |

**リクエストパラメータ**：
| パラメータ名 | 必須 | 説明 |
|-------------|------|------|
| name | 必須 | プラットフォーム名（inventree-mobile-app-{platform}-{device}-{model}-{version}） |

**レスポンス**：
| 項目 | 型 | 説明 |
|------|-----|------|
| token | String | APIアクセス用認証トークン |

**エラーコード**：
| ステータスコード | 説明 | エラーメッセージ |
|----------------|------|-----------------|
| 401 | 認証失敗 | L10().response401（無効な認証情報） |
| 403 | アクセス拒否 | L10().response403 |
| 404 | エンドポイント未検出 | L10().response404 |
| 500 | サーバーエラー | L10().response500 |

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

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| ログイン成功 | profiles（ローカル） | UPDATE | プロファイルのtokenフィールドを更新 |

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

#### profiles（ローカルDB - Sembast）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | token | サーバーから取得したトークン | ログイン成功時のみ |

## メッセージ仕様

| メッセージID | 種別 | メッセージ内容 | 表示条件 |
|-------------|------|--------------|---------|
| MSG-001 | タイトル | L10().login | AppBarタイトル |
| MSG-002 | 案内 | L10().loginEnter | ログイン案内タイトル |
| MSG-003 | 案内 | L10().loginEnterDetails | ログイン案内説明 |
| MSG-004 | ラベル | L10().server | サーバー情報タイトル |
| MSG-005 | ラベル | L10().username | ユーザー名入力欄ラベル |
| MSG-006 | ヒント | L10().enterUsername | ユーザー名入力欄ヒント |
| MSG-007 | ラベル | L10().password | パスワード入力欄ラベル |
| MSG-008 | ヒント | L10().enterPassword | パスワード入力欄ヒント |
| MSG-009 | エラー | L10().usernameEmpty | ユーザー名が空の場合 |
| MSG-010 | エラー | L10().passwordEmpty | パスワードが空の場合 |
| MSG-011 | エラー | L10().error | エラータイトル |
| MSG-012 | エラー | response.data["detail"] | サーバーからのエラー詳細 |
| MSG-013 | エラー | statusCodeToString(statusCode) | HTTPステータスコードに対応するエラー |

## 例外処理

| 例外種別 | 発生条件 | 対応処理 |
|---------|---------|---------|
| バリデーションエラー | 入力値が無効 | エラーメッセージを表示、認証処理をスキップ |
| 認証エラー (401/403) | ユーザー名またはパスワードが無効 | showServerError()でエラーダイアログ表示 + エラーListTile表示 |
| トークン取得失敗 | レスポンスにtokenキーがない | showServerError()でエラーダイアログ表示 |
| ネットワークエラー | サーバーに接続できない | statusCodeToString()でエラーメッセージ表示 |

## 備考

- ローディングオーバーレイはOneContextを使用してグローバルに表示される
- パスワードはデフォルトでマスク表示（_obscured = true）
- フォームはSingleChildScrollViewでラップされている
- fetchToken()内でデバイス情報を取得してプラットフォーム名を生成
- 認証成功時にUserProfileDBManager().updateProfile()でトークンをローカルDBに保存

---

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

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

### 推奨読解順序

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

まず、ユーザープロファイルとAPI通信の構造を理解することが重要である。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | user_profile.dart | `lib/user_profile.dart` | UserProfileクラスのtokenプロパティを理解する |
| 1-2 | api.dart | `lib/api.dart` | APIResponseクラスとfetchToken()メソッドを理解する |

**読解のコツ**: UserProfileのtokenプロパティはログイン成功時にfetchToken()で設定され、以降のAPI呼び出しに使用される。

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

処理の起点となるファイル・関数を特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | login.dart | `lib/settings/login.dart` | InvenTreeLoginWidgetと_InvenTreeLoginStateを理解する（行1-184） |

**主要処理フロー**:
1. **行11-18**: InvenTreeLoginWidgetクラスの定義、profileをコンストラクタで受け取る
2. **行20-28**: _InvenTreeLoginStateクラスの定義、formKey, username, password, _obscured, error変数の宣言
3. **行31-74**: `_doLogin()`メソッドで認証処理を実行
4. **行33-36**: フォームの保存とバリデーション
5. **行38-43**: キーボードの閉じ処理
6. **行45-54**: ローディングオーバーレイ表示とAPI呼び出し
7. **行56-72**: レスポンスに基づく成功/失敗処理
8. **行77-182**: `build()`メソッドでScaffoldとFormを構築
9. **行78-90**: beforeリスト（案内テキストとサーバー情報）
10. **行94-102**: afterリスト（エラー表示）
11. **行104-181**: Scaffoldの構築

#### Step 3: 認証ロジックを理解する

認証処理の詳細を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | api.dart | `lib/api.dart` | fetchToken()メソッドの実装（行547-635） |

**主要処理フロー**:
- **行554**: プロファイルをAPIインスタンスに設定
- **行557-583**: プラットフォーム名の生成（デバイス情報を含む）
- **行586-587**: Basic認証ヘッダーの構築
- **行590-594**: /api/user/token/へのGETリクエスト
- **行597-615**: エラーハンドリング
- **行619-625**: トークンの存在チェック
- **行628-632**: プロファイルへのトークン保存とDB更新

#### Step 4: バリデーションロジックを理解する

入力値検証の詳細を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | login.dart | `lib/settings/login.dart` | validator関数の実装（行137-143, 167-173） |

**主要処理フロー**:
- **行137-143**: ユーザー名のバリデーション（空文字チェック）
- **行167-173**: パスワードのバリデーション（空文字チェック）

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

```
InvenTreeLoginWidget (login.dart)
    |
    +-- _InvenTreeLoginState
            |
            +-- build()
            |       |
            |       +-- Scaffold
            |               |
            |               +-- AppBar
            |               |       +-- IconButton (ログイン)
            |               |               +-- _doLogin()
            |               |
            |               +-- Form
            |                       |
            |                       +-- SingleChildScrollView
            |                               |
            |                               +-- Column
            |                                       |
            |                                       +-- ListTile (案内テキスト)
            |                                       +-- ListTile (サーバー情報)
            |                                       +-- Divider
            |                                       +-- TextFormField (ユーザー名)
            |                                       |       +-- validator: 空文字チェック
            |                                       |       +-- onSaved: username = value
            |                                       |
            |                                       +-- TextFormField (パスワード)
            |                                       |       +-- validator: 空文字チェック
            |                                       |       +-- onSaved: password = value
            |                                       |       +-- suffixIcon: 表示切替
            |                                       |
            |                                       +-- [error.isNotEmpty]
            |                                               +-- Divider
            |                                               +-- ListTile (エラー表示)
            |
            +-- _doLogin()
                    |
                    +-- formKey.currentState?.save()
                    +-- formKey.currentState?.validate()
                    |
                    +-- [valid]
                            +-- FocusScope.of(context).unfocus()
                            +-- showLoadingOverlay()
                            +-- InvenTreeAPI().fetchToken(profile, username, password)
                            |       |
                            |       +-- get(_URL_TOKEN) with Basic Auth
                            |       +-- userProfile.token = data["token"]
                            |       +-- UserProfileDBManager().updateProfile()
                            |
                            +-- hideLoadingOverlay()
                            |
                            +-- [response.successful()]
                            |       +-- Navigator.of(context).pop()
                            |
                            +-- [!response.successful()]
                                    +-- setState(() => error = errorMessage)
```

### データフロー図

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

profile (UserProfile)    -->  build()                          -->  サーバー情報表示
                                |
                                v
ユーザー入力             -->  TextFormField.onChanged         -->  リアルタイム入力
(username, password)           |
                                v
[ログインボタン]         -->  _doLogin()                       -->  認証処理開始
                                |
                                v
                         validate()                            -->  バリデーション
                                |
        +-------------------+---+
        |                       |
        v                       v
[成功]                  [失敗]
save()                  エラーメッセージ表示
        |
        v
showLoadingOverlay()                                           -->  ローディング表示
        |
        v
InvenTreeAPI().fetchToken()
        |
        +-- Basic Auth Header構築
        +-- GET /api/user/token/
        |
        v
hideLoadingOverlay()                                           -->  ローディング非表示
        |
        v
[response.successful()]        [!response.successful()]
        |                               |
        v                               v
profile.token = token          error = errorMessage
        |                               |
        v                               v
updateProfile()                setState()                      -->  エラー表示
        |
        v
Navigator.pop()                                                -->  前画面へ戻る
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| login.dart | `lib/settings/login.dart` | ソース | InvenTreeLoginWidgetの定義（行1-184） |
| api.dart | `lib/api.dart` | ソース | InvenTreeAPI.fetchToken()メソッド（行547-635） |
| user_profile.dart | `lib/user_profile.dart` | ソース | UserProfile/UserProfileDBManagerクラス |
| progress.dart | `lib/widget/progress.dart` | ソース | showLoadingOverlay()/hideLoadingOverlay()関数（行46-70） |
| dialogs.dart | `lib/widget/dialogs.dart` | ソース | statusCodeToString()関数（行272-295） |
| app_colors.dart | `lib/app_colors.dart` | ソース | COLOR_APP_BAR, COLOR_SUCCESS, COLOR_DANGERの定義 |
| l10.dart | `lib/l10.dart` | ソース | 多言語対応 |
