2026-06-14 18:08:01 -05:00
|
|
|
import { useState } from 'react';
|
feat(mobile): shared api/config layer, server-mode CSRF fix, robustness + update-safe DB (batch 10)
Unify the native shell and harden it:
- src/config.ts: single source for local host/port, preference keys, secure-storage
key, timeouts, and CSRF constants (dedupes LOCAL_URL etc. across App/LoadingScreen).
- src/api.ts: one HTTP client — credentials mode, AbortController timeouts, and error
normalization into user-facing messages (network/timeout/401/403/429/5xx), with typed
auth responses.
- Batch 10 CSRF fix: /api/auth/totp/challenge is not CSRF-exempt, so the native TOTP
step now fetches GET /api/auth/csrf-token and echoes it as x-csrf-token (mirrors the
web client); TOTP login no longer breaks for 2FA users.
- Error-handling / boot robustness: LoadingScreen gets a health-poll timeout and
surfaces main.js's {type:'serverError'} channel messages instead of spinning forever;
App.tsx bootstrap and the encryption-key fetch now .catch to a visible state; crypto.ts
reports secure-storage failures.
- Data safety: move the SQLite DB, backups, and tmp OUT of nodejs-project into the app's
filesDir (bt-data). nodejs-mobile re-copies nodejs-project on every app update, which was
silently wiping local-mode data. Verified on the emulator: create data -> bump versionCode
-> reinstall -> migrations skipped, no re-seed, data persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:48:23 -05:00
|
|
|
import { login, totpChallenge } from './api';
|
2026-06-14 18:08:01 -05:00
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
serverUrl: string;
|
|
|
|
|
/** Called once the backend has confirmed the credentials and set a session cookie. */
|
|
|
|
|
onSuccess: () => void;
|
|
|
|
|
onBack: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Stage = 'credentials' | 'totp' | 'webauthn';
|
|
|
|
|
|
|
|
|
|
export default function LoginScreen({ serverUrl, onSuccess, onBack }: Props) {
|
|
|
|
|
const [stage, setStage] = useState<Stage>('credentials');
|
|
|
|
|
const [username, setUsername] = useState('');
|
|
|
|
|
const [password, setPassword] = useState('');
|
|
|
|
|
const [code, setCode] = useState('');
|
|
|
|
|
const [challengeToken, setChallengeToken] = useState('');
|
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
function handleKey(e: React.KeyboardEvent) {
|
|
|
|
|
if (e.key !== 'Enter') return;
|
|
|
|
|
if (stage === 'credentials') handleLogin();
|
|
|
|
|
if (stage === 'totp') handleTotp();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleLogin() {
|
|
|
|
|
if (!username.trim() || !password) {
|
|
|
|
|
setError('Enter your username and password.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setError('');
|
|
|
|
|
setLoading(true);
|
feat(mobile): shared api/config layer, server-mode CSRF fix, robustness + update-safe DB (batch 10)
Unify the native shell and harden it:
- src/config.ts: single source for local host/port, preference keys, secure-storage
key, timeouts, and CSRF constants (dedupes LOCAL_URL etc. across App/LoadingScreen).
- src/api.ts: one HTTP client — credentials mode, AbortController timeouts, and error
normalization into user-facing messages (network/timeout/401/403/429/5xx), with typed
auth responses.
- Batch 10 CSRF fix: /api/auth/totp/challenge is not CSRF-exempt, so the native TOTP
step now fetches GET /api/auth/csrf-token and echoes it as x-csrf-token (mirrors the
web client); TOTP login no longer breaks for 2FA users.
- Error-handling / boot robustness: LoadingScreen gets a health-poll timeout and
surfaces main.js's {type:'serverError'} channel messages instead of spinning forever;
App.tsx bootstrap and the encryption-key fetch now .catch to a visible state; crypto.ts
reports secure-storage failures.
- Data safety: move the SQLite DB, backups, and tmp OUT of nodejs-project into the app's
filesDir (bt-data). nodejs-mobile re-copies nodejs-project on every app update, which was
silently wiping local-mode data. Verified on the emulator: create data -> bump versionCode
-> reinstall -> migrations skipped, no re-seed, data persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:48:23 -05:00
|
|
|
// api.login normalizes network/timeout/429/5xx into res.error.
|
|
|
|
|
const res = await login(serverUrl, username.trim(), password);
|
|
|
|
|
setLoading(false);
|
2026-06-14 18:08:01 -05:00
|
|
|
|
feat(mobile): shared api/config layer, server-mode CSRF fix, robustness + update-safe DB (batch 10)
Unify the native shell and harden it:
- src/config.ts: single source for local host/port, preference keys, secure-storage
key, timeouts, and CSRF constants (dedupes LOCAL_URL etc. across App/LoadingScreen).
- src/api.ts: one HTTP client — credentials mode, AbortController timeouts, and error
normalization into user-facing messages (network/timeout/401/403/429/5xx), with typed
auth responses.
- Batch 10 CSRF fix: /api/auth/totp/challenge is not CSRF-exempt, so the native TOTP
step now fetches GET /api/auth/csrf-token and echoes it as x-csrf-token (mirrors the
web client); TOTP login no longer breaks for 2FA users.
- Error-handling / boot robustness: LoadingScreen gets a health-poll timeout and
surfaces main.js's {type:'serverError'} channel messages instead of spinning forever;
App.tsx bootstrap and the encryption-key fetch now .catch to a visible state; crypto.ts
reports secure-storage failures.
- Data safety: move the SQLite DB, backups, and tmp OUT of nodejs-project into the app's
filesDir (bt-data). nodejs-mobile re-copies nodejs-project on every app update, which was
silently wiping local-mode data. Verified on the emulator: create data -> bump versionCode
-> reinstall -> migrations skipped, no re-seed, data persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:48:23 -05:00
|
|
|
if (!res.ok) {
|
|
|
|
|
setError(res.error || 'Invalid username or password.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const data = res.data;
|
|
|
|
|
if (data?.requires_totp) {
|
|
|
|
|
setChallengeToken(data.challenge_token || '');
|
|
|
|
|
setStage('totp');
|
|
|
|
|
return;
|
2026-06-14 18:08:01 -05:00
|
|
|
}
|
feat(mobile): shared api/config layer, server-mode CSRF fix, robustness + update-safe DB (batch 10)
Unify the native shell and harden it:
- src/config.ts: single source for local host/port, preference keys, secure-storage
key, timeouts, and CSRF constants (dedupes LOCAL_URL etc. across App/LoadingScreen).
- src/api.ts: one HTTP client — credentials mode, AbortController timeouts, and error
normalization into user-facing messages (network/timeout/401/403/429/5xx), with typed
auth responses.
- Batch 10 CSRF fix: /api/auth/totp/challenge is not CSRF-exempt, so the native TOTP
step now fetches GET /api/auth/csrf-token and echoes it as x-csrf-token (mirrors the
web client); TOTP login no longer breaks for 2FA users.
- Error-handling / boot robustness: LoadingScreen gets a health-poll timeout and
surfaces main.js's {type:'serverError'} channel messages instead of spinning forever;
App.tsx bootstrap and the encryption-key fetch now .catch to a visible state; crypto.ts
reports secure-storage failures.
- Data safety: move the SQLite DB, backups, and tmp OUT of nodejs-project into the app's
filesDir (bt-data). nodejs-mobile re-copies nodejs-project on every app update, which was
silently wiping local-mode data. Verified on the emulator: create data -> bump versionCode
-> reinstall -> migrations skipped, no re-seed, data persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:48:23 -05:00
|
|
|
if (data?.requires_webauthn) {
|
|
|
|
|
setStage('webauthn');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
onSuccess();
|
2026-06-14 18:08:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleTotp() {
|
|
|
|
|
if (!code.trim()) {
|
|
|
|
|
setError('Enter your authenticator code.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setError('');
|
|
|
|
|
setLoading(true);
|
feat(mobile): shared api/config layer, server-mode CSRF fix, robustness + update-safe DB (batch 10)
Unify the native shell and harden it:
- src/config.ts: single source for local host/port, preference keys, secure-storage
key, timeouts, and CSRF constants (dedupes LOCAL_URL etc. across App/LoadingScreen).
- src/api.ts: one HTTP client — credentials mode, AbortController timeouts, and error
normalization into user-facing messages (network/timeout/401/403/429/5xx), with typed
auth responses.
- Batch 10 CSRF fix: /api/auth/totp/challenge is not CSRF-exempt, so the native TOTP
step now fetches GET /api/auth/csrf-token and echoes it as x-csrf-token (mirrors the
web client); TOTP login no longer breaks for 2FA users.
- Error-handling / boot robustness: LoadingScreen gets a health-poll timeout and
surfaces main.js's {type:'serverError'} channel messages instead of spinning forever;
App.tsx bootstrap and the encryption-key fetch now .catch to a visible state; crypto.ts
reports secure-storage failures.
- Data safety: move the SQLite DB, backups, and tmp OUT of nodejs-project into the app's
filesDir (bt-data). nodejs-mobile re-copies nodejs-project on every app update, which was
silently wiping local-mode data. Verified on the emulator: create data -> bump versionCode
-> reinstall -> migrations skipped, no re-seed, data persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:48:23 -05:00
|
|
|
// totpChallenge fetches + echoes the CSRF token (the challenge endpoint,
|
|
|
|
|
// unlike /login, is not CSRF-exempt).
|
|
|
|
|
const res = await totpChallenge(serverUrl, challengeToken, code.trim());
|
|
|
|
|
setLoading(false);
|
2026-06-14 18:08:01 -05:00
|
|
|
|
feat(mobile): shared api/config layer, server-mode CSRF fix, robustness + update-safe DB (batch 10)
Unify the native shell and harden it:
- src/config.ts: single source for local host/port, preference keys, secure-storage
key, timeouts, and CSRF constants (dedupes LOCAL_URL etc. across App/LoadingScreen).
- src/api.ts: one HTTP client — credentials mode, AbortController timeouts, and error
normalization into user-facing messages (network/timeout/401/403/429/5xx), with typed
auth responses.
- Batch 10 CSRF fix: /api/auth/totp/challenge is not CSRF-exempt, so the native TOTP
step now fetches GET /api/auth/csrf-token and echoes it as x-csrf-token (mirrors the
web client); TOTP login no longer breaks for 2FA users.
- Error-handling / boot robustness: LoadingScreen gets a health-poll timeout and
surfaces main.js's {type:'serverError'} channel messages instead of spinning forever;
App.tsx bootstrap and the encryption-key fetch now .catch to a visible state; crypto.ts
reports secure-storage failures.
- Data safety: move the SQLite DB, backups, and tmp OUT of nodejs-project into the app's
filesDir (bt-data). nodejs-mobile re-copies nodejs-project on every app update, which was
silently wiping local-mode data. Verified on the emulator: create data -> bump versionCode
-> reinstall -> migrations skipped, no re-seed, data persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:48:23 -05:00
|
|
|
if (!res.ok) {
|
|
|
|
|
setError(res.error || 'Invalid authenticator code.');
|
|
|
|
|
return;
|
2026-06-14 18:08:01 -05:00
|
|
|
}
|
feat(mobile): shared api/config layer, server-mode CSRF fix, robustness + update-safe DB (batch 10)
Unify the native shell and harden it:
- src/config.ts: single source for local host/port, preference keys, secure-storage
key, timeouts, and CSRF constants (dedupes LOCAL_URL etc. across App/LoadingScreen).
- src/api.ts: one HTTP client — credentials mode, AbortController timeouts, and error
normalization into user-facing messages (network/timeout/401/403/429/5xx), with typed
auth responses.
- Batch 10 CSRF fix: /api/auth/totp/challenge is not CSRF-exempt, so the native TOTP
step now fetches GET /api/auth/csrf-token and echoes it as x-csrf-token (mirrors the
web client); TOTP login no longer breaks for 2FA users.
- Error-handling / boot robustness: LoadingScreen gets a health-poll timeout and
surfaces main.js's {type:'serverError'} channel messages instead of spinning forever;
App.tsx bootstrap and the encryption-key fetch now .catch to a visible state; crypto.ts
reports secure-storage failures.
- Data safety: move the SQLite DB, backups, and tmp OUT of nodejs-project into the app's
filesDir (bt-data). nodejs-mobile re-copies nodejs-project on every app update, which was
silently wiping local-mode data. Verified on the emulator: create data -> bump versionCode
-> reinstall -> migrations skipped, no re-seed, data persists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:48:23 -05:00
|
|
|
onSuccess();
|
2026-06-14 18:08:01 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (stage === 'webauthn') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="setup-root">
|
|
|
|
|
<div className="setup-card">
|
|
|
|
|
<h1 className="setup-title">Security key required</h1>
|
|
|
|
|
<p className="setup-subtitle">
|
|
|
|
|
This account signs in with a security key. Continue to sign in through the server's
|
|
|
|
|
web interface.
|
|
|
|
|
</p>
|
|
|
|
|
<button className="btn-primary" onClick={onSuccess}>
|
|
|
|
|
Continue
|
|
|
|
|
</button>
|
|
|
|
|
<button className="btn-secondary" onClick={onBack}>
|
|
|
|
|
Back
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (stage === 'totp') {
|
|
|
|
|
return (
|
|
|
|
|
<div className="setup-root">
|
|
|
|
|
<div className="setup-card">
|
|
|
|
|
<h1 className="setup-title">Two-factor code</h1>
|
|
|
|
|
<p className="setup-subtitle">Enter the code from your authenticator app.</p>
|
|
|
|
|
|
|
|
|
|
<div className="form-group">
|
|
|
|
|
<label className="form-label" htmlFor="totp-code">Code</label>
|
|
|
|
|
<input
|
|
|
|
|
id="totp-code"
|
|
|
|
|
className="form-input"
|
|
|
|
|
type="text"
|
|
|
|
|
inputMode="numeric"
|
|
|
|
|
autoCapitalize="none"
|
|
|
|
|
autoCorrect="off"
|
|
|
|
|
spellCheck={false}
|
|
|
|
|
placeholder="123456"
|
|
|
|
|
value={code}
|
|
|
|
|
onChange={e => { setCode(e.target.value); setError(''); }}
|
|
|
|
|
onKeyDown={handleKey}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{error && <p className="form-error">{error}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button className="btn-primary" onClick={handleTotp} disabled={loading || !code.trim()}>
|
|
|
|
|
{loading ? 'Verifying…' : 'Verify'}
|
|
|
|
|
</button>
|
|
|
|
|
<button className="btn-secondary" onClick={onBack} disabled={loading}>
|
|
|
|
|
Back
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="setup-root">
|
|
|
|
|
<div className="setup-card">
|
|
|
|
|
<h1 className="setup-title">Sign in</h1>
|
2026-07-11 14:41:04 -05:00
|
|
|
<p className="setup-subtitle">Sign in to {serverUrl.includes('127.0.0.1') ? 'this device' : serverUrl}</p>
|
2026-06-14 18:08:01 -05:00
|
|
|
|
|
|
|
|
<div className="form-group">
|
|
|
|
|
<label className="form-label" htmlFor="login-username">Username</label>
|
|
|
|
|
<input
|
|
|
|
|
id="login-username"
|
|
|
|
|
className="form-input"
|
|
|
|
|
type="text"
|
|
|
|
|
inputMode="text"
|
|
|
|
|
autoCapitalize="none"
|
|
|
|
|
autoCorrect="off"
|
|
|
|
|
spellCheck={false}
|
|
|
|
|
placeholder="Username"
|
|
|
|
|
value={username}
|
|
|
|
|
onChange={e => { setUsername(e.target.value); setError(''); }}
|
|
|
|
|
onKeyDown={handleKey}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="form-group">
|
|
|
|
|
<label className="form-label" htmlFor="login-password">Password</label>
|
|
|
|
|
<input
|
|
|
|
|
id="login-password"
|
|
|
|
|
className="form-input"
|
|
|
|
|
type="password"
|
|
|
|
|
autoCapitalize="none"
|
|
|
|
|
autoCorrect="off"
|
|
|
|
|
spellCheck={false}
|
|
|
|
|
placeholder="Password"
|
|
|
|
|
value={password}
|
|
|
|
|
onChange={e => { setPassword(e.target.value); setError(''); }}
|
|
|
|
|
onKeyDown={handleKey}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
{error && <p className="form-error">{error}</p>}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button className="btn-primary" onClick={handleLogin} disabled={loading || !username.trim() || !password}>
|
|
|
|
|
{loading ? 'Signing in…' : 'Sign In'}
|
|
|
|
|
</button>
|
|
|
|
|
<button className="btn-secondary" onClick={onBack} disabled={loading}>
|
|
|
|
|
Back
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|