Build and Push Docker Image / build-and-push (push) Failing after 19s
Drops viewport-fit=cover so the iPad/iPhone status bar is its own area and the system's top-edge blur no longer covers the app bar. Fixed-height study stages are position:fixed so they cannot scroll the document, and min-h-screen/100vh become dvh units. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
194 lines
8.9 KiB
React
194 lines
8.9 KiB
React
import { useState } from 'react';
|
|
import { useApp } from '../context/AppContext.js';
|
|
import { themeVars } from '../components/reader/readerThemes.js';
|
|
import { loginUser, registerUser, verifyMfaLogin, logoutUser } from '../syncService.js';
|
|
|
|
export default function AuthPage() {
|
|
const { signIn, readerTheme } = useApp();
|
|
const shell = { style: themeVars(readerTheme) };
|
|
const [authMode, setAuthMode] = useState('login'); // 'login' | 'register'
|
|
const [authForm, setAuthForm] = useState({ email: '', password: '' });
|
|
const [authError, setAuthError] = useState('');
|
|
const [authBusy, setAuthBusy] = useState(false);
|
|
const [authMfaPending, setAuthMfaPending] = useState(false); // password ok, waiting on TOTP/backup code
|
|
const [authMfaCode, setAuthMfaCode] = useState('');
|
|
const [authMfaUseBackup, setAuthMfaUseBackup] = useState(false);
|
|
|
|
const submitAuth = async (e) => {
|
|
e.preventDefault();
|
|
setAuthError('');
|
|
setAuthBusy(true);
|
|
const action = authMode === 'login' ? loginUser : registerUser;
|
|
const result = await action(authForm.email.trim(), authForm.password);
|
|
setAuthBusy(false);
|
|
if (!result.ok) {
|
|
setAuthError(result.error ?? 'Something went wrong.');
|
|
return;
|
|
}
|
|
if (result.data?.mfaRequired) {
|
|
setAuthMfaPending(true);
|
|
return;
|
|
}
|
|
signIn(result.data);
|
|
};
|
|
|
|
const submitMfa = async (e) => {
|
|
e.preventDefault();
|
|
setAuthError('');
|
|
setAuthBusy(true);
|
|
const result = await verifyMfaLogin(
|
|
authMfaUseBackup ? { backupCode: authMfaCode.trim() } : { token: authMfaCode.trim() },
|
|
);
|
|
setAuthBusy(false);
|
|
if (!result.ok) {
|
|
setAuthError(result.error ?? 'Invalid code.');
|
|
return;
|
|
}
|
|
setAuthMfaPending(false);
|
|
setAuthMfaCode('');
|
|
signIn(result.data);
|
|
};
|
|
|
|
if (authMfaPending) {
|
|
return (
|
|
<div {...shell} className="flex min-h-dvh items-center justify-center bg-[var(--r-bg)] px-4 text-[var(--r-text)]">
|
|
<div className="w-full max-w-sm rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-8">
|
|
<p className="text-sm uppercase tracking-[0.24em] text-[var(--r-sub)]">Bible Study Project</p>
|
|
<h1 className="mt-2 text-xl font-medium text-[var(--r-text)]">Two-factor verification</h1>
|
|
<p className="mt-2 text-sm text-[var(--r-sub)]">
|
|
{authMfaUseBackup
|
|
? 'Enter one of your saved backup codes.'
|
|
: 'Enter the 6-digit code from your authenticator app.'}
|
|
</p>
|
|
<form onSubmit={submitMfa} className="mt-6 space-y-4">
|
|
<input
|
|
type="text"
|
|
required
|
|
autoFocus
|
|
inputMode={authMfaUseBackup ? 'text' : 'numeric'}
|
|
placeholder={authMfaUseBackup ? 'xxxxxxxxxx' : '123456'}
|
|
value={authMfaCode}
|
|
onChange={(e) => setAuthMfaCode(e.target.value)}
|
|
className="block w-full rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2 text-[var(--r-text)] text-center text-lg tracking-widest focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]"
|
|
/>
|
|
{authError && <p className="text-sm text-[var(--r-danger,#b42318)]">{authError}</p>}
|
|
<button
|
|
type="submit"
|
|
disabled={authBusy}
|
|
className="w-full rounded-full bg-[var(--r-accent)] px-4 py-2.5 text-sm font-medium text-[var(--r-on)] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{authBusy ? 'Verifying…' : 'Verify'}
|
|
</button>
|
|
</form>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAuthMfaUseBackup((v) => !v);
|
|
setAuthMfaCode('');
|
|
setAuthError('');
|
|
}}
|
|
className="mt-4 w-full text-center text-sm text-[var(--r-sub)] underline hover:text-[var(--r-text)]"
|
|
>
|
|
{authMfaUseBackup ? 'Use your authenticator app instead' : "Lost your device? Use a backup code"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={async () => {
|
|
await logoutUser();
|
|
setAuthMfaPending(false);
|
|
setAuthMfaCode('');
|
|
setAuthError('');
|
|
}}
|
|
className="mt-2 w-full text-center text-sm text-[var(--r-sub)] underline hover:text-[var(--r-sub)]"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div {...shell} className="flex min-h-dvh items-center justify-center bg-[var(--r-bg)] px-4 py-12 text-[var(--r-text)]">
|
|
<div className="grid w-full max-w-5xl items-center gap-16 lg:grid-cols-2">
|
|
{/* Branding / feature panel — hidden on small screens to keep the form front and center there */}
|
|
<div className="hidden lg:block">
|
|
<p className="text-sm uppercase tracking-[0.24em] text-[var(--r-sub)]">Bible Study Project</p>
|
|
<h1 className="mt-4 text-3xl font-semibold leading-tight text-[var(--r-text)]">
|
|
Deep Bible study,<br />one chunk at a time.
|
|
</h1>
|
|
<p className="mt-4 max-w-sm text-[var(--r-sub)]">
|
|
Read Scripture in six translations, follow a reading plan into the text, and study a passage one chunk
|
|
at a time with commentary, word studies, and cross-references beside it.
|
|
</p>
|
|
<ul className="mt-8 space-y-3 text-sm text-[var(--r-sub)]">
|
|
<li>Read in BSB, ESV, NLT, CSB, NASB, or NET, with audio and search</li>
|
|
<li>Reading plans that open straight into the Reader</li>
|
|
<li>Observe, Interpret, Apply notes with Greek & Hebrew word studies</li>
|
|
<li>Turn your notes into a script, then export or share</li>
|
|
<li>Your studies stay private to your account, with optional 2FA</li>
|
|
</ul>
|
|
<blockquote className="mt-8 border-l-2 border-[var(--r-line)] pl-4 text-sm italic text-[var(--r-sub)]">
|
|
"Make every effort to present yourself approved to God, an unashamed workman who accurately
|
|
handles the word of truth."
|
|
<footer className="mt-1 not-italic text-[var(--r-sub)]">— 2 Timothy 2:15 (BSB)</footer>
|
|
</blockquote>
|
|
</div>
|
|
|
|
{/* Sign in / register form */}
|
|
<div className="flex justify-center lg:justify-start">
|
|
<div className="w-full max-w-sm rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-8">
|
|
<p className="text-sm uppercase tracking-[0.24em] text-[var(--r-sub)] lg:hidden">Bible Study Project</p>
|
|
<h1 className="mt-2 text-xl font-medium text-[var(--r-text)]">
|
|
{authMode === 'login' ? 'Sign in' : 'Create an account'}
|
|
</h1>
|
|
<form onSubmit={submitAuth} className="mt-6 space-y-4">
|
|
<label className="block text-sm">
|
|
Email
|
|
<input
|
|
type="email"
|
|
required
|
|
autoComplete="email"
|
|
value={authForm.email}
|
|
onChange={(e) => setAuthForm((f) => ({ ...f, email: e.target.value }))}
|
|
className="mt-1 block w-full rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2 text-[var(--r-text)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]"
|
|
/>
|
|
</label>
|
|
<label className="block text-sm font-medium text-[var(--r-text)]">
|
|
Password
|
|
<input
|
|
type="password"
|
|
required
|
|
minLength={8}
|
|
autoComplete={authMode === 'login' ? 'current-password' : 'new-password'}
|
|
value={authForm.password}
|
|
onChange={(e) => setAuthForm((f) => ({ ...f, password: e.target.value }))}
|
|
className="mt-1 block w-full rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2 text-[var(--r-text)] text-sm focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]"
|
|
/>
|
|
</label>
|
|
{authError && <p className="text-sm text-[var(--r-danger,#b42318)]">{authError}</p>}
|
|
<button
|
|
type="submit"
|
|
disabled={authBusy}
|
|
className="w-full rounded-full bg-[var(--r-accent)] px-4 py-2.5 text-sm font-medium text-[var(--r-on)] transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{authBusy ? 'Please wait…' : authMode === 'login' ? 'Sign in' : 'Create account'}
|
|
</button>
|
|
</form>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setAuthMode((m) => (m === 'login' ? 'register' : 'login'));
|
|
setAuthError('');
|
|
}}
|
|
className="mt-4 w-full text-center text-sm text-[var(--r-sub)] underline hover:text-[var(--r-text)]"
|
|
>
|
|
{authMode === 'login' ? "Need an account? Sign up" : 'Already have an account? Sign in'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|