Remove PostgreSQL and move to SQLite3

This commit is contained in:
2026-08-01 18:02:37 +02:00
parent 97ad77f2a4
commit 873627c757
12 changed files with 444 additions and 64 deletions
+54 -1
View File
@@ -30,6 +30,13 @@ export default function App() {
// Auth/Login UI state
const [me, setMe] = useState(null);
const [setupRequired, setSetupRequired] = useState(null);
const [setupEmail, setSetupEmail] = useState("");
const [setupDisplayName, setSetupDisplayName] = useState("");
const [setupPassword, setSetupPassword] = useState("");
const [setupPasswordConfirm, setSetupPasswordConfirm] = useState("");
const [setupError, setSetupError] = useState("");
const [setupSaving, setSetupSaving] = useState(false);
const [loginEmail, setLoginEmail] = useState("");
const [loginPassword, setLoginPassword] = useState("");
const [showPw, setShowPw] = useState(false);
@@ -182,8 +189,12 @@ export default function App() {
useEffect(() => {
(async () => {
try {
const status = await api("/setup/status");
setSetupRequired(!!status.setup_required);
await load();
} catch {}
} catch {
// The login/setup screen handles unauthenticated sessions.
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -278,6 +289,36 @@ export default function App() {
await load();
};
const doSetup = async () => {
setSetupError("");
if (setupPassword.length < 8) {
setSetupError("Password must be at least 8 characters long.");
return;
}
if (setupPassword !== setupPasswordConfirm) {
setSetupError("Passwords do not match.");
return;
}
setSetupSaving(true);
try {
await api("/setup/admin", {
method: "POST",
body: JSON.stringify({
email: setupEmail,
display_name: setupDisplayName,
password: setupPassword,
}),
});
setSetupRequired(false);
await load();
} catch (e) {
setSetupError(e?.message || "Setup failed.");
} finally {
setSetupSaving(false);
}
};
const doLogout = async () => {
await api("/auth/logout", { method: "POST" });
setMe(null);
@@ -540,6 +581,18 @@ export default function App() {
showPw={showPw}
setShowPw={setShowPw}
doLogin={doLogin}
setupRequired={setupRequired}
setupEmail={setupEmail}
setSetupEmail={setSetupEmail}
setupDisplayName={setupDisplayName}
setSetupDisplayName={setSetupDisplayName}
setupPassword={setupPassword}
setSetupPassword={setSetupPassword}
setupPasswordConfirm={setupPasswordConfirm}
setSetupPasswordConfirm={setSetupPasswordConfirm}
setupError={setupError}
setupSaving={setupSaving}
doSetup={doSetup}
/>
);
}
+77 -4
View File
@@ -9,7 +9,21 @@ export default function LoginPage({
showPw,
setShowPw,
doLogin,
setupRequired,
setupEmail,
setSetupEmail,
setupDisplayName,
setSetupDisplayName,
setupPassword,
setSetupPassword,
setupPasswordConfirm,
setSetupPasswordConfirm,
setupError,
setupSaving,
doSetup,
}) {
const isSetup = setupRequired === true;
return (
<div style={styles.loginPage}>
<div style={styles.bgFixed} aria-hidden="true">
@@ -21,9 +35,67 @@ export default function LoginPage({
<div style={styles.loginCard}>
<div style={styles.loginTitle}>Zauber-Detektiv Notizbogen</div>
<div style={styles.loginSubtitle}>Melde dich an, um dein Cluedo-Magie-Sheet zu öffnen</div>
<div style={styles.loginSubtitle}>
{setupRequired === null
? "Initialisiere Anwendung …"
: isSetup
? "Richte den ersten Administrator ein"
: "Melde dich an, um dein Cluedo-Magie-Sheet zu öffnen"}
</div>
<div style={{ marginTop: 18, display: "grid", gap: 12 }}>
{isSetup ? (
<div style={{ marginTop: 18, display: "grid", gap: 12 }}>
<div style={styles.loginFieldWrap}>
<input
value={setupDisplayName}
onChange={(e) => setSetupDisplayName(e.target.value)}
placeholder="Display name"
style={styles.loginInput}
autoComplete="name"
/>
</div>
<div style={styles.loginFieldWrap}>
<input
value={setupEmail}
onChange={(e) => setSetupEmail(e.target.value)}
placeholder="Admin email"
style={styles.loginInput}
inputMode="email"
autoComplete="username"
/>
</div>
<div style={styles.loginFieldWrap}>
<input
value={setupPassword}
onChange={(e) => setSetupPassword(e.target.value)}
placeholder="Password (min. 8 characters)"
type="password"
style={styles.loginInput}
autoComplete="new-password"
/>
</div>
<div style={styles.loginFieldWrap}>
<input
value={setupPasswordConfirm}
onChange={(e) => setSetupPasswordConfirm(e.target.value)}
placeholder="Confirm password"
type="password"
style={styles.loginInput}
autoComplete="new-password"
/>
</div>
{setupError && <div style={{ color: "#ffb3b3", fontSize: 13 }}>{setupError}</div>}
<button onClick={doSetup} style={styles.loginBtn} disabled={setupSaving}>
{setupSaving ? "Setting up …" : "✦ Create administrator"}
</button>
</div>
) : (
<div style={{ marginTop: 18, display: "grid", gap: 12 }}>
<div style={styles.loginFieldWrap}>
<input
value={loginEmail}
@@ -60,10 +132,11 @@ export default function LoginPage({
<button onClick={doLogin} style={styles.loginBtn}>
Anmelden
</button>
</div>
</div>
)}
<div style={styles.loginHint}>
Deine Notizen bleiben privat jeder Spieler sieht nur seinen eigenen Zettel.
Your notes remain private every player only sees their own sheet.
</div>
</div>
</div>