@@ -28,3 +28,158 @@ GDK_BACKEND=x11 WEBKIT_DISABLE_DMABUF_RENDERER=1 cargo tauri dev
2828```
2929
3030You can prefix the same environment variables to the app launch command outside of dev as well.
31+
32+ ## Local Authentication (Career Mode)
33+
34+ This project includes a local/offline authentication system for the Career panel (SQLite + Argon2 password hashing).
35+
36+ ### Test user (dev)
37+
38+ - Email: ` admin@admin.de `
39+ - Password: ` admin123 `
40+ - Role: ` admin `
41+
42+ The admin user is created/seeded automatically on first auth DB access and the password is stored only as a hash (never plaintext).
43+
44+ ### Where login data is stored
45+
46+ ** Database file**
47+ - Path: ` %LOCALAPPDATA%\\SimNexusHub\\logbook.sqlite ` (Windows)
48+ - Created automatically if missing.
49+ - Fallback: if ` dirs::data_local_dir() ` is unavailable, the current working directory is used.
50+
51+ ** Session file (remember-me token)**
52+ - Path: ` %LOCALAPPDATA%\\SimNexusHub\\auth_session.json `
53+ - Contains only a persisted session token for “remember me” (no password, no email).
54+
55+ Source of truth for paths:
56+ - ` src-tauri/src/features/auth/db.rs `
57+
58+ ### SQLite schema (auth-related)
59+
60+ Tables are created/updated automatically via “ensure tables/columns” (lightweight migrations):
61+ - ` src-tauri/src/features/auth/db.rs `
62+
63+ ``` sql
64+ -- users
65+ CREATE TABLE IF NOT EXISTS users (
66+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67+ username TEXT NOT NULL ,
68+ email TEXT NOT NULL ,
69+ password_hash TEXT NOT NULL ,
70+ role TEXT NOT NULL DEFAULT ' user' ,
71+ company_id INTEGER ,
72+ created_at TEXT NOT NULL ,
73+ updated_at TEXT NOT NULL ,
74+ last_login_at TEXT ,
75+ consent_at TEXT NOT NULL ,
76+ is_active INTEGER NOT NULL DEFAULT 1 ,
77+ is_seed INTEGER NOT NULL DEFAULT 0
78+ );
79+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username);
80+ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email);
81+
82+ -- sessions (remember-me)
83+ CREATE TABLE IF NOT EXISTS sessions (
84+ id INTEGER PRIMARY KEY AUTOINCREMENT,
85+ user_id INTEGER NOT NULL ,
86+ token TEXT ,
87+ created_at TEXT NOT NULL ,
88+ expires_at TEXT ,
89+ last_used_at TEXT ,
90+ revoked_at TEXT
91+ );
92+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
93+ CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
94+
95+ -- recovery codes (hashed, one-time)
96+ CREATE TABLE IF NOT EXISTS recovery_codes (
97+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98+ user_id INTEGER NOT NULL ,
99+ code_hash TEXT NOT NULL ,
100+ created_at TEXT NOT NULL ,
101+ used_at TEXT
102+ );
103+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user_id ON recovery_codes(user_id);
104+
105+ -- login events (privacy-friendly local MAU per installation)
106+ CREATE TABLE IF NOT EXISTS login_events (
107+ id INTEGER PRIMARY KEY AUTOINCREMENT,
108+ user_id INTEGER ,
109+ at_utc TEXT NOT NULL ,
110+ year_month TEXT NOT NULL
111+ );
112+ CREATE INDEX IF NOT EXISTS idx_login_events_year_month ON login_events(year_month);
113+ CREATE INDEX IF NOT EXISTS idx_login_events_user_month ON login_events(user_id, year_month);
114+ ```
115+
116+ ### What is stored / what is NOT stored (privacy)
117+
118+ Stored (minimum needed for local auth):
119+ - Email + username (for login + display)
120+ - Role (admin/user)
121+ - Password hash (Argon2, salted) in ` users.password_hash `
122+ - Session token for “remember me” in ` sessions.token ` and ` auth_session.json `
123+ - Timestamps: ` created_at ` , ` updated_at ` , ` last_login_at ` , session timestamps
124+
125+ Not stored:
126+ - No plaintext passwords
127+ - No IP address, device fingerprint, geo location, or tracking identifiers
128+ - No telemetry / online user tracking (local-only)
129+
130+ This is a technical, data-minimizing structure and ** not legal advice** . For production, you likely want to add:
131+ - Server-side auth (if you need global MAU), email delivery, and secure token flows
132+ - Proper audit/event model, rate limiting, lockouts, and encrypted backups
133+ - Optional database encryption at rest (depending on threat model)
134+
135+ ### How login/logout works (technical)
136+
137+ Backend:
138+ - Login/register: ` src-tauri/src/features/auth/service.rs ` (` login_local ` , ` register_local ` )
139+ - Password hashing: Argon2 in ` src-tauri/src/features/auth/service.rs ` (` hash_password ` , ` verify_password ` )
140+ - Session persistence: remember-me token written to ` auth_session.json ` and stored in ` sessions `
141+ - Logout: clears in-memory state + removes ` auth_session.json ` + revokes the session row (sets ` revoked_at ` )
142+
143+ Frontend:
144+ - Header user menu + login/logout: ` src/main.js `
145+ - Career auth gate modal: ` src/index.html ` + ` src/styles.css `
146+ - State refresh: on startup ` auth_restore_session ` is called, then UI fetches ` auth_get_current_user `
147+
148+ ### Admin / Database view (local)
149+
150+ If you are logged in as ` admin ` , the header user menu shows ** Admin / DB** .
151+ - It displays: user id, email, role, created-at, last login, and whether an active session exists.
152+ - It does ** not** display any password or password hash.
153+
154+ ## ETS2 Dispatcher Post-Write Validation
155+
156+ The ETS2 dispatcher write flow validates the written ` game.sii ` immediately after the file is updated.
157+
158+ - Validator source: ` src-tauri/src/features/ets2save/post_write_validator.rs `
159+ - Trigger point: ` src-tauri/src/features/ets2save/injector.rs `
160+ - UI output: Career Mode dispatcher detail panel, ` Last Write Output `
161+
162+ The validator checks the full pointer chain:
163+
164+ 1 . ` company.volatile.<company>.<city> ` block exists
165+ 2 . expected ` job_offer[i] ` pointer still exists in that company block
166+ 3 . matching ` job_offer_data : _nameless.* ` block exists
167+ 4 . written fields match the expected dispatcher payload:
168+ ` cargo ` , ` target ` , ` shortest_distance_km ` , ` expiration_time `
169+
170+ Result interpretation:
171+
172+ - ` post_write_valid = true `
173+ The write is structurally valid. If ETS2 still does not show the job, the remaining cause is ETS2 load/cache state. Load the exact quicksave that was written.
174+ - ` post_write_valid = false `
175+ The write is not valid for the expected depot/job chain. Use ` validation.rootCause ` and ` validation.validationErrorCode ` from the write output.
176+
177+ Root-cause mapping:
178+
179+ - ` wrong_depot ` : expected company block was not found after write
180+ - ` wrong_slot ` : expected ` job_offer ` pointer is missing from the company block
181+ - ` write_corrupt ` : ` job_offer_data ` block is missing for the selected pointer
182+ - ` cargo_mismatch ` : written cargo token does not match the expected token
183+ - ` target_mismatch ` : written target company does not match the expected target
184+
185+ The write output also includes an offer-slot scan so the selected ` job_offer[i] ` can be inspected when a depot contains multiple offer pointers.
0 commit comments