作成日: 2025-11-27 分析者: Claude (Sonnet 4.5) 目的: 論理的矛盾の特定と修正方針の文書化
KakeiBonByRustアプリケーションの設計を徹底的に検証した結果、1つの重大な論理的矛盾と2つの軽微な懸念事項が発見されました。
- ✅ セキュリティ実装: 優秀 (Argon2id, 適切なバリデーション)
- ✅ データベース設計: 優秀 (正規化、制約、インデックス)
- ✅ 多言語化: 優秀 (一貫したi18nアーキテクチャ)
- ❌ セッション管理: 未完了 (実装済みだが使用されていない)
⚠️ ロール管理: 軽微な改善余地あり
場所: src/lib.rs のトランザクション管理コマンド群
発見された箇所 (8箇所):
- Line 1396:
get_transaction_header - Line 1427:
select_transaction_headers - Line 1456:
update_transaction_header - Line 1485:
get_transaction_header_with_info - Line 1498:
get_transaction_details - Line 1520:
add_transaction_detail - Line 1555:
update_transaction_detail - Line 1581:
delete_transaction_detail
問題のコード例:
#[tauri::command]
async fn get_transaction_header(
transaction_id: i64,
state: tauri::State<'_, AppState>
) -> Result<serde_json::Value, String> {
let transaction = state.transaction.lock().await;
// TODO: Get user_id from session/auth
// For now, use user_id = 2 to match frontend currentUserId
let user_id = 2; // ← ハードコード!
let (header, memo_text) = transaction.get_transaction_header_with_memo(user_id, transaction_id).await
.map_err(|e| e.to_string())?;
...
}- セキュリティリスク: 認証なしでデータにアクセス可能
- 機能不全: 複数ユーザーのログイン切り替えが正しく機能しない
- データ整合性: 常に
user_id = 2のデータを操作してしまう - 矛盾: セッション管理機能が完全に実装されているのに使われていない
✅ 実装済み:
src/services/session.rs: SessionState完全実装src/lib.rs:154-228: セッション管理用Tauriコマンド (9個)src/lib.rs:60-88:login_userでセッションに保存- テストカバレッジ: 10個のテストケース
❌ 未使用:
- トランザクション管理コマンド群 (8箇所)
- その他のユーザー依存コマンド
- トランザクションデータ: 常に
user_id = 2として扱われる - ユーザー切り替え: 正しく機能しない
- 認証: バイパスされている
- データベース整合性: 他のユーザーのデータにアクセス可能
- セキュリティ: 認証機構が形骸化
- テスト: ユーザー依存のテストが不可能
ファイル: src/lib.rs
場所: ファイル先頭 (use文の後、最初のTauriコマンドの前)
/// セッションから認証済みユーザーを取得
///
/// # Returns
/// * `Ok(User)` - セッションに保存されているユーザー情報
/// * `Err(String)` - 未認証の場合のエラーメッセージ
fn get_session_user(state: &tauri::State<'_, AppState>) -> Result<services::session::User, String> {
state.session.get_user()
.ok_or_else(|| "Not authenticated. Please login first.".to_string())
}パターン:
// ❌ 修正前
#[tauri::command]
async fn some_command(
param1: Type1,
state: tauri::State<'_, AppState>
) -> Result<ReturnType, String> {
// TODO: Get user_id from session/auth
// For now, use user_id = 2 to match frontend currentUserId
let user_id = 2;
// ... 処理 ...
}
// ✅ 修正後
#[tauri::command]
async fn some_command(
param1: Type1,
state: tauri::State<'_, AppState>
) -> Result<ReturnType, String> {
let user = get_session_user(&state)?; // セッションから取得
// user.user_id を使用
// ... 処理 ...
}変更箇所:
// Line 1394-1396 を削除
// TODO: Get user_id from session/auth
// For now, use user_id = 2 to match frontend currentUserId
let user_id = 2;
// 以下を追加
let user = get_session_user(&state)?;
// Line 1398 を変更
// 変更前: transaction.get_transaction_header_with_memo(user_id, transaction_id)
// 変更後: transaction.get_transaction_header_with_memo(user.user_id, transaction_id)変更箇所:
// Line 1425-1427 を削除
// TODO: Get user_id from session/auth
// For now, use user_id = 2 to match frontend currentUserId
let user_id = 2;
// 以下を追加
let user = get_session_user(&state)?;
// Line 1431 を変更
// 変更前: transaction.get_transaction_header(user_id, transaction_id)
// 変更後: transaction.get_transaction_header(user.user_id, transaction_id)変更箇所:
// Line 1454-1456 を削除
// TODO: Get user_id from session/auth
// For now, use user_id = 2 to match frontend currentUserId
let user_id = 2;
// 以下を追加
let user = get_session_user(&state)?;
// Line 1470 を変更
// 変更前: transaction.update_transaction_header(user_id, transaction_id, request)
// 変更後: transaction.update_transaction_header(user.user_id, transaction_id, request)変更箇所:
// Line 1484-1485 を削除
// TODO: Get user_id from session/auth
let user_id = 2;
// 以下を追加
let user = get_session_user(&state)?;
// Line 1487 を変更
// 変更前: transaction.get_transaction_header_with_info(user_id, transaction_id)
// 変更後: transaction.get_transaction_header_with_info(user.user_id, transaction_id)変更箇所:
// Line 1497-1498 を削除
// TODO: Get user_id from session/auth
let user_id = 2;
// 以下を追加
let user = get_session_user(&state)?;
// Line 1500 を変更
// 変更前: transaction.get_transaction_details(user_id, transaction_id)
// 変更後: transaction.get_transaction_details(user.user_id, transaction_id)変更箇所:
// Line 1519-1520 を削除
// TODO: Get user_id from session/auth
let user_id = 2;
// 以下を追加
let user = get_session_user(&state)?;
// Line 1535 を変更
// 変更前: transaction.add_transaction_detail(user_id, transaction_id, request)
// 変更後: transaction.add_transaction_detail(user.user_id, transaction_id, request)変更箇所:
// Line 1554-1555 を削除
// TODO: Get user_id from session/auth
let user_id = 2;
// 以下を追加
let user = get_session_user(&state)?;
// Line 1570 を変更
// 変更前: transaction.update_transaction_detail(user_id, detail_id, request)
// 変更後: transaction.update_transaction_detail(user.user_id, detail_id, request)変更箇所:
// Line 1580-1581 を削除
// TODO: Get user_id from session/auth
let user_id = 2;
// 以下を追加
let user = get_session_user(&state)?;
// Line 1583 を変更
// 変更前: transaction.delete_transaction_detail(user_id, detail_id)
// 変更後: transaction.delete_transaction_detail(user.user_id, detail_id)確認項目:
- ✅ ヘルパー関数が追加されている
- ✅ 全8箇所のTODOコメントが削除されている
- ✅ 全8箇所で
user_id = 2のハードコードが削除されている - ✅ 全8箇所で
get_session_user(&state)?が使用されている - ✅ コンパイルエラーがない
- ✅ 既存のテストが通る
- ✅ ログイン前にコマンドを呼ぶと "Not authenticated" エラーが返る
- ✅ ログイン後にコマンドが正しく動作する
- ✅ 複数ユーザーの切り替えが正しく機能する
テストシナリオ:
1. アプリ起動
2. 管理者でログイン (user_id = 1)
3. トランザクション作成 → user_id = 1 で保存されることを確認
4. ログアウト
5. 一般ユーザーでログイン (user_id = 2)
6. トランザクション作成 → user_id = 2 で保存されることを確認
7. 管理者のトランザクションは見えないことを確認
8. ログアウト
9. ログインせずにトランザクション操作 → エラーになることを確認
場所:
src/lib.rs:113(register_admin)src/services/auth.rs:113(register_admin_user)
問題のコード:
// src/lib.rs:113
sqlx::query(sql_queries::AUTH_INSERT_USER)
.bind(1) // USER_ID = 1 for admin ← ハードコード
.bind(username)
.bind(password_hash)
.bind(ROLE_ADMIN)
.bind(now)
.execute(&mut *tx)
.await?;- 拡張性の制限: 複数の管理者を作成できない
- 設計の不整合: 一般ユーザーは自動採番、管理者は固定
- 将来のリスク: 管理者機能の拡張時に制約となる
- 管理者ユーザーは1人のみ (USER_ID = 1 固定)
- 2人目の管理者を作成しようとするとPRIMARY KEY違反
- 複数管理者が必要になった場合、大規模な変更が必要
- テスト環境で複数の管理者を作成できない
方針: USER_ID = 1 のハードコードを自動採番に変更
変更箇所:
// ❌ 修正前
#[tauri::command]
async fn register_admin(
username: String,
password: String,
state: tauri::State<'_, AppState>
) -> Result<String, String> {
validate_password(&password)?;
let auth = state.auth.lock().await;
match auth.register_admin_user(&username, &password).await {
Ok(_) => Ok("Admin user registered successfully".to_string()),
Err(e) => Err(format!("Registration failed: {}", e)),
}
}
// ✅ 修正後 (変更なし - auth.rsの修正のみで対応)// ❌ 修正前
pub async fn register_admin_user(&self, username: &str, password: &str) -> Result<(), AuthError> {
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let password_hash = hash_password(password)?;
let mut tx = self.pool.begin().await?;
sqlx::query(sql_queries::AUTH_INSERT_USER)
.bind(1) // USER_ID = 1 for admin ← これを削除
.bind(username)
.bind(password_hash)
.bind(ROLE_ADMIN)
.bind(now)
.execute(&mut *tx)
.await?;
tx.commit().await?;
// ... 以下省略
}
// ✅ 修正後
pub async fn register_admin_user(&self, username: &str, password: &str) -> Result<(), AuthError> {
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();
let password_hash = hash_password(password)?;
// USER_IDを自動採番
let result = sqlx::query(sql_queries::AUTH_GET_NEXT_USER_ID)
.fetch_one(&self.pool)
.await?;
let next_id: i64 = result.get(0);
let mut tx = self.pool.begin().await?;
sqlx::query(sql_queries::AUTH_INSERT_USER)
.bind(next_id) // 自動採番されたIDを使用
.bind(username)
.bind(password_hash)
.bind(ROLE_ADMIN)
.bind(now)
.execute(&mut *tx)
.await?;
tx.commit().await?;
// 以降は next_id を使用
let category_service = category::CategoryService::new(self.pool.clone());
category_service.populate_default_categories(next_id).await
.map_err(|e| AuthError::DatabaseError(sqlx::Error::Configuration(
format!("Failed to populate default categories for admin: {}", e).into()
)))?;
crate::services::account::initialize_none_account(&self.pool, next_id).await
.map_err(|e| AuthError::DatabaseError(sqlx::Error::Configuration(
format!("Failed to initialize NONE account for admin: {}", e).into()
)))?;
Ok(())
}方針: 最初のユーザーを自動的に管理者にする
pub async fn register_first_user(&self, username: &str, password: &str) -> Result<(), AuthError> {
// ユーザー数を確認
let count: i64 = sqlx::query_scalar(sql_queries::AUTH_COUNT_USERS)
.fetch_one(&self.pool)
.await?;
// 最初のユーザーは管理者、それ以外は一般ユーザー
let role = if count == 0 { ROLE_ADMIN } else { ROLE_USER };
// USER_IDは常に自動採番
let next_id: i64 = sqlx::query_scalar(sql_queries::AUTH_GET_NEXT_USER_ID)
.fetch_one(&self.pool)
.await?;
// ... 登録処理
}メリット:
- 最初のユーザーが自動的に管理者になる
- 2人目以降の管理者は別の方法で昇格可能
- 柔軟性が高い
デメリット:
- 既存の動作との互換性確認が必要
理由:
- 最小限の変更
- 既存の動作を維持
- 複数管理者の作成が可能になる
- リスクが低い
場所: src/consts.rs:4
定義:
pub const ROLE_ADMIN: i64 = 0;
pub const ROLE_USER: i64 = 1;
pub const ROLE_VISIT: i64 = 999; // ← どこでも使われていない検索結果:
$ grep -r "ROLE_VISIT" src/ res/
src/consts.rs:pub const ROLE_VISIT: i64 = 999;
# → 定義以外に使用箇所なし- 意図不明: 将来の実装のための予約か、削除し忘れか不明
- 保守性: 使われていない定数はコードの理解を妨げる
- 混乱: 開発者が「これは何に使うのか?」と疑問を持つ
将来の実装予定がある場合:
pub const ROLE_ADMIN: i64 = 0;
pub const ROLE_USER: i64 = 1;
// 将来の実装予定: 読み取り専用の訪問者ロール
// TODO: ゲストアクセス機能実装時に使用
pub const ROLE_VISIT: i64 = 999;使用予定がない場合:
pub const ROLE_ADMIN: i64 = 0;
pub const ROLE_USER: i64 = 1;
// ROLE_VISIT は削除理由:
- コメントで意図を明確化
- 将来の拡張性を保持
- リスクなし
検証項目:
- ✅ Argon2id使用 (業界標準)
- ✅ ランダムソルト生成
- ✅ 最小16文字の要求
- ✅ フロントエンド・バックエンドの一貫性
結論: 問題なし
検証項目:
- ✅ 適切な正規化
- ✅ 外部キー制約
- ✅ インデックス最適化
- ✅ CASCADE動作
結論: 問題なし
検証項目:
- ✅ 一貫したリソース管理
- ✅ 言語切り替え機能
- ✅ カテゴリ名の多言語対応
結論: 問題なし
検証項目:
- ✅ ヘッダー・ディテール分離
- ✅ マイグレーション実装
- ✅ テストカバレッジ
結論: 問題なし (軽微な懸念はあるが実用上は問題なし)
目標: セッション管理の統合
タスク:
- ✅
get_session_user()ヘルパー関数を追加 - ✅ 8つのトランザクション管理コマンドを修正
- ✅ 全TODOコメントを削除
- ✅ テストと検証
所要時間: 1-2時間 リスク: 低 (既存のセッション実装を使うだけ)
目標: 管理者ユーザーの柔軟化
タスク:
- ✅
auth.rs:register_admin_userを修正 - ✅ USER_ID自動採番に変更
- ✅ テストと検証
所要時間: 30分-1時間 リスク: 低 (既存のロジックとほぼ同じ)
目標: コードの明確化
タスク:
- ✅
ROLE_VISITにコメント追加 - ✅ または削除
所要時間: 5分 リスク: なし
新規テスト:
#[cfg(test)]
mod session_tests {
use super::*;
#[test]
fn test_get_session_user_not_authenticated() {
// セッションが空の場合、エラーが返ることを確認
}
#[test]
fn test_get_session_user_authenticated() {
// セッションにユーザーが保存されている場合、正しく取得できることを確認
}
}テストシナリオ:
- 複数ユーザーのログイン切り替え
- 未認証でのAPI呼び出し
- ユーザー毎のデータ分離
実装時に使用するチェックリスト:
-
src/lib.rsにget_session_user()関数を追加 -
get_transaction_header(line 1389) を修正 -
select_transaction_headers(line 1420) を修正 -
update_transaction_header(line 1440) を修正 -
get_transaction_header_with_info(line 1479) を修正 -
get_transaction_details(line 1492) を修正 -
add_transaction_detail(line 1505) を修正 -
update_transaction_detail(line 1540) を修正 -
delete_transaction_detail(line 1575) を修正 - 全TODOコメントが削除されていることを確認
-
cargo buildが成功することを確認 -
cargo testが成功することを確認 - 手動テスト: ログイン → トランザクション操作
- 手動テスト: 未ログイン → トランザクション操作 (エラー確認)
- 手動テスト: ユーザー切り替え → データ分離確認
-
src/services/auth.rs:register_admin_userを修正 -
cargo buildが成功することを確認 -
cargo testが成功することを確認 - 手動テスト: 管理者ユーザー作成 (複数回)
-
src/consts.rsのROLE_VISITにコメント追加 or 削除 -
cargo buildが成功することを確認
- ✅ 全てのトランザクション操作が認証必須
- ✅ ユーザー毎のデータ分離が保証される
- ✅ セッション管理が完全に機能する
- ✅ 複数ユーザーのログイン切り替えが正しく動作
- ✅ 複数の管理者ユーザーを作成可能
- ✅ データの整合性が保たれる
- ✅ TODOコメントが全て解消される
- ✅ コードの意図が明確になる
- ✅ 将来の拡張が容易になる
セッション管理:
src/services/session.rs- SessionState実装src/lib.rs:154-228- セッション管理Tauriコマンドsrc/lib.rs:60-88- login_user実装
トランザクション管理:
src/services/transaction.rs- TransactionService実装src/lib.rs:1357-1851- トランザクション管理Tauriコマンド
認証管理:
src/services/auth.rs- AuthService実装src/security.rs- パスワードハッシュ化
.ai-context/CONVENTIONS.md を参照:
- SQL queries centralization
- Constants externalization
- Error handling patterns
最終更新: 2025-11-27 次回レビュー: Phase 1実装完了後