93 lines
4.4 KiB
JavaScript
93 lines
4.4 KiB
JavaScript
import VisibilityOffOutlined from "@mui/icons-material/VisibilityOffOutlined";
|
|
import VisibilityOutlined from "@mui/icons-material/VisibilityOutlined";
|
|
import Alert from "@mui/material/Alert";
|
|
import Box from "@mui/material/Box";
|
|
import Button from "@mui/material/Button";
|
|
import Card from "@mui/material/Card";
|
|
import CardContent from "@mui/material/CardContent";
|
|
import CircularProgress from "@mui/material/CircularProgress";
|
|
import IconButton from "@mui/material/IconButton";
|
|
import InputAdornment from "@mui/material/InputAdornment";
|
|
import Stack from "@mui/material/Stack";
|
|
import TextField from "@mui/material/TextField";
|
|
import Typography from "@mui/material/Typography";
|
|
import { useEffect, useState } from "react";
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
import { useExternalAuth } from "../auth/ExternalAuthProvider.jsx";
|
|
import { LanguageSwitcher } from "../i18n/LanguageSwitcher.jsx";
|
|
import { useExternalI18n } from "../i18n/ExternalI18nProvider.jsx";
|
|
import { translateExternalError } from "../i18n/errors.js";
|
|
|
|
export default function LoginPage() {
|
|
const { initializing, login, session } = useExternalAuth();
|
|
const { t } = useExternalI18n();
|
|
const [form, setForm] = useState({ account: "", password: "" });
|
|
const [error, setError] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
useEffect(() => {
|
|
// 登录成功前没有可信 App 上下文;使用通用标题并清掉上一个会话的图标,避免泄露或误导用户所属 App。
|
|
globalThis.document.title = t("app.title");
|
|
globalThis.document.head.querySelector('link[data-external-app-favicon="true"]')?.remove();
|
|
}, [t]);
|
|
|
|
useEffect(() => {
|
|
if (!initializing && session) {
|
|
const destination = session.passwordChangeRequired ? "/change-password" : location.state?.from?.pathname || "/overview";
|
|
navigate(destination, { replace: true });
|
|
}
|
|
}, [initializing, location.state, navigate, session]);
|
|
|
|
const handleSubmit = async (event) => {
|
|
event.preventDefault();
|
|
if (!form.account.trim() || !form.password) {
|
|
setError(t("auth.fillAccountPassword"));
|
|
return;
|
|
}
|
|
setSubmitting(true);
|
|
setError("");
|
|
try {
|
|
const nextSession = await login(form);
|
|
navigate(nextSession.passwordChangeRequired ? "/change-password" : "/overview", { replace: true });
|
|
} catch (requestError) {
|
|
// Every login 401 uses one generic credential message so unknown, disabled and locked accounts cannot be distinguished.
|
|
setError(requestError?.status === 401 ? t("auth.invalidCredentials") : translateExternalError(requestError, t, "auth.loginFailed"));
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Box className="external-login-page">
|
|
<Card className="external-login-card" elevation={0}>
|
|
<CardContent>
|
|
<Stack sx={{ alignItems: "flex-end" }}><LanguageSwitcher /></Stack>
|
|
<Stack spacing={1.5} sx={{ alignItems: "center" }}>
|
|
<Typography component="h1" variant="h5">{t("app.title")}</Typography>
|
|
</Stack>
|
|
<Stack component="form" spacing={2.25} onSubmit={handleSubmit}>
|
|
{error ? <Alert severity="error">{error}</Alert> : null}
|
|
<TextField autoComplete="username" disabled={submitting} label={t("auth.account")} required value={form.account} onChange={(event) => setForm({ ...form, account: event.target.value })} />
|
|
<TextField
|
|
autoComplete="current-password"
|
|
disabled={submitting}
|
|
label={t("auth.password")}
|
|
required
|
|
type={showPassword ? "text" : "password"}
|
|
value={form.password}
|
|
onChange={(event) => setForm({ ...form, password: event.target.value })}
|
|
slotProps={{ input: { endAdornment: <InputAdornment position="end"><IconButton aria-label={showPassword ? t("auth.hidePassword") : t("auth.showPassword")} edge="end" onClick={() => setShowPassword((current) => !current)}>{showPassword ? <VisibilityOffOutlined /> : <VisibilityOutlined />}</IconButton></InputAdornment> } }}
|
|
/>
|
|
<Button disabled={submitting} size="large" type="submit" variant="contained">
|
|
{submitting ? <CircularProgress color="inherit" size={20} /> : t("auth.login")}
|
|
</Button>
|
|
</Stack>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
);
|
|
}
|