Files
BanGUI/frontend/src/pages/LoginPage.tsx
Lukas 7f81f0614b Stage 7: configuration view — backend service, routers, tests, and frontend
- config_service.py: read/write jail config via asyncio.gather, global
  settings, in-process regex validation, log preview via _read_tail_lines
- server_service.py: read/write server settings, flush logs
- config router: 9 endpoints for jail/global config, regex-test,
  logpath management, log preview
- server router: GET/PUT settings, POST flush-logs
- models/config.py expanded with JailConfig, GlobalConfigUpdate,
  LogPreview* models
- 285 tests pass (68 new), ruff clean, mypy clean (44 files)
- Frontend: types/config.ts, api/config.ts, hooks/useConfig.ts,
  ConfigPage.tsx full implementation (Jails accordion editor,
  Global config, Server settings, Regex Tester with preview)
- Fixed pre-existing frontend lint: JSX.Element → React.JSX.Element
  (10 files), void/promise patterns in useServerStatus + useJails,
  no-misused-spread in client.ts, eslint.config.ts self-excluded
2026-03-01 14:37:55 +01:00

159 lines
4.2 KiB
TypeScript

/**
* Login page.
*
* A single password field and submit button. On success the user is
* redirected to the originally requested page (via the `?next=` query
* parameter) or the dashboard.
*/
import { useState } from "react";
import {
Button,
Field,
Input,
makeStyles,
MessageBar,
MessageBarBody,
Spinner,
Text,
tokens,
} from "@fluentui/react-components";
import { useNavigate, useSearchParams } from "react-router-dom";
import type { ChangeEvent, FormEvent } from "react";
import { ApiError } from "../api/client";
import { useAuth } from "../providers/AuthProvider";
// ---------------------------------------------------------------------------
// Styles
// ---------------------------------------------------------------------------
const useStyles = makeStyles({
root: {
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
backgroundColor: tokens.colorNeutralBackground2,
padding: tokens.spacingHorizontalM,
},
card: {
width: "100%",
maxWidth: "360px",
backgroundColor: tokens.colorNeutralBackground1,
borderRadius: tokens.borderRadiusXLarge,
padding: tokens.spacingVerticalXXL,
boxShadow: tokens.shadow8,
},
heading: {
marginBottom: tokens.spacingVerticalXS,
display: "block",
},
subtitle: {
marginBottom: tokens.spacingVerticalXXL,
color: tokens.colorNeutralForeground2,
display: "block",
},
field: {
marginBottom: tokens.spacingVerticalM,
},
submitRow: {
marginTop: tokens.spacingVerticalL,
},
error: {
marginBottom: tokens.spacingVerticalM,
},
});
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
/**
* Login page — single password input, no username.
*/
export function LoginPage(): React.JSX.Element {
const styles = useStyles();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { login } = useAuth();
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const nextPath = searchParams.get("next") ?? "/";
function handlePasswordChange(ev: ChangeEvent<HTMLInputElement>): void {
setPassword(ev.target.value);
setError(null);
}
async function handleSubmit(ev: FormEvent<HTMLFormElement>): Promise<void> {
ev.preventDefault();
if (!password) {
setError("Please enter a password.");
return;
}
setSubmitting(true);
setError(null);
try {
await login(password);
navigate(nextPath, { replace: true });
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
setError("Incorrect password. Please try again.");
} else {
setError("An unexpected error occurred. Please try again.");
}
} finally {
setSubmitting(false);
}
}
return (
<div className={styles.root}>
<div className={styles.card}>
<Text as="h1" size={700} weight="semibold" className={styles.heading}>
BanGUI
</Text>
<Text size={300} className={styles.subtitle}>
Enter your master password to continue.
</Text>
{error !== null && (
<MessageBar intent="error" className={styles.error}>
<MessageBarBody>{error}</MessageBarBody>
</MessageBar>
)}
<form onSubmit={(ev) => void handleSubmit(ev)}>
<div className={styles.field}>
<Field label="Password" required>
<Input
type="password"
value={password}
onChange={handlePasswordChange}
autoComplete="current-password"
autoFocus
/>
</Field>
</div>
<div className={styles.submitRow}>
<Button
type="submit"
appearance="primary"
disabled={submitting || !password}
icon={submitting ? <Spinner size="tiny" /> : undefined}
>
{submitting ? "Signing in…" : "Sign in"}
</Button>
</div>
</form>
</div>
</div>
);
}