343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
|
|
import { Avatar, Box, Button, Card, CardActions, CircularProgress, MenuItem, Select, Tab, Tabs, Typography } from "@mui/material";
|
|
import {
|
|
Form,
|
|
FormDataConsumer,
|
|
Notification,
|
|
required,
|
|
useLogin,
|
|
useNotify,
|
|
useLocaleState,
|
|
useTranslate,
|
|
PasswordInput,
|
|
TextInput,
|
|
useLocales,
|
|
} from "react-admin";
|
|
import { useFormContext } from "react-hook-form";
|
|
import LoginFormBox from "../components/LoginFormBox";
|
|
import { useAppContext } from "../App";
|
|
import {
|
|
getServerVersion,
|
|
getSupportedFeatures,
|
|
getSupportedLoginFlows,
|
|
getWellKnownUrl,
|
|
isValidBaseUrl,
|
|
splitMxid,
|
|
} from "../synapse/matrix";
|
|
import Footer from "../components/Footer";
|
|
|
|
export type LoginMethod = "credentials" | "accessToken";
|
|
|
|
const LoginPage = () => {
|
|
const login = useLogin();
|
|
const notify = useNotify();
|
|
const { restrictBaseUrl } = useAppContext();
|
|
const allowSingleBaseUrl = typeof restrictBaseUrl === "string" && restrictBaseUrl !== "";
|
|
const allowMultipleBaseUrls =
|
|
Array.isArray(restrictBaseUrl) &&
|
|
restrictBaseUrl.length > 0 &&
|
|
restrictBaseUrl[0] !== "" &&
|
|
restrictBaseUrl[0] !== null;
|
|
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
|
|
const [loading, setLoading] = useState(false);
|
|
const [supportPassAuth, setSupportPassAuth] = useState(true);
|
|
const [locale, setLocale] = useLocaleState();
|
|
const locales = useLocales();
|
|
const translate = useTranslate();
|
|
const base_url = allowSingleBaseUrl ? restrictBaseUrl : localStorage.getItem("base_url");
|
|
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
|
|
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
|
|
const [loginMethod, setLoginMethod] = useState<LoginMethod>("credentials");
|
|
|
|
useEffect(() => {
|
|
if (!loginToken) {
|
|
return;
|
|
}
|
|
|
|
const ssoToken = loginToken[1];
|
|
console.log("SSO token is", ssoToken);
|
|
// Prevent further requests
|
|
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
|
|
const baseUrl = localStorage.getItem("sso_base_url");
|
|
localStorage.removeItem("sso_base_url");
|
|
if (baseUrl) {
|
|
const auth = {
|
|
base_url: baseUrl,
|
|
username: null,
|
|
password: null,
|
|
loginToken: ssoToken,
|
|
};
|
|
console.log("Base URL is:", baseUrl);
|
|
console.log("SSO Token is:", ssoToken);
|
|
console.log("Let's try token login...");
|
|
login(auth).catch(error => {
|
|
alert(
|
|
typeof error === "string"
|
|
? error
|
|
: typeof error === "undefined" || !error.message
|
|
? "ra.auth.sign_in_error"
|
|
: error.message
|
|
);
|
|
console.error(error);
|
|
});
|
|
}
|
|
}, [loginToken]);
|
|
|
|
const validateBaseUrl = value => {
|
|
if (!value.match(/^(http|https):\/\//)) {
|
|
return translate("synapseadmin.auth.protocol_error");
|
|
} else if (!value.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?[^?&\s]*$/)) {
|
|
return translate("synapseadmin.auth.url_error");
|
|
} else {
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const handleSubmit = auth => {
|
|
setLoading(true);
|
|
const cleanUrl = window.location.href.replace(window.location.search, "");
|
|
window.history.replaceState({}, "", cleanUrl);
|
|
|
|
login(auth).catch(error => {
|
|
setLoading(false);
|
|
notify(
|
|
typeof error === "string"
|
|
? error
|
|
: typeof error === "undefined" || !error.message
|
|
? "ra.auth.sign_in_error"
|
|
: error.message,
|
|
{ type: "warning" }
|
|
);
|
|
});
|
|
};
|
|
|
|
const handleSSO = () => {
|
|
localStorage.setItem("sso_base_url", ssoBaseUrl);
|
|
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
|
|
window.location.href
|
|
)}`;
|
|
window.location.href = ssoFullUrl;
|
|
};
|
|
|
|
const UserData = ({ formData }) => {
|
|
const form = useFormContext();
|
|
const [serverVersion, setServerVersion] = useState("");
|
|
const [matrixVersions, setMatrixVersions] = useState("");
|
|
|
|
const handleUsernameChange = () => {
|
|
if (formData.base_url || allowSingleBaseUrl) {
|
|
return;
|
|
}
|
|
// check if username is a full qualified userId then set base_url accordingly
|
|
const domain = splitMxid(formData.username)?.domain;
|
|
if (domain) {
|
|
getWellKnownUrl(domain).then(url => {
|
|
if (allowAnyBaseUrl || (allowMultipleBaseUrls && restrictBaseUrl.includes(url)))
|
|
form.setValue("base_url", url);
|
|
});
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!formData.base_url) {
|
|
form.setValue("base_url", "");
|
|
}
|
|
if (formData.base_url === "" && allowMultipleBaseUrls) {
|
|
form.setValue("base_url", restrictBaseUrl[0]);
|
|
}
|
|
if (!isValidBaseUrl(formData.base_url)) return;
|
|
|
|
getServerVersion(formData.base_url)
|
|
.then(serverVersion => setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`))
|
|
.catch(() => setServerVersion(""));
|
|
|
|
getSupportedFeatures(formData.base_url)
|
|
.then(features =>
|
|
setMatrixVersions(`${translate("synapseadmin.auth.supports_specs")} ${features.versions.join(", ")}`)
|
|
)
|
|
.catch(() => setMatrixVersions(""));
|
|
|
|
// Set SSO Url
|
|
getSupportedLoginFlows(formData.base_url)
|
|
.then(loginFlows => {
|
|
const supportPass = loginFlows.find(f => f.type === "m.login.password") !== undefined;
|
|
const supportSSO = loginFlows.find(f => f.type === "m.login.sso") !== undefined;
|
|
setSupportPassAuth(supportPass);
|
|
setSSOBaseUrl(supportSSO ? formData.base_url : "");
|
|
})
|
|
.catch(() => setSSOBaseUrl(""));
|
|
}, [formData.base_url, form]);
|
|
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const hostname = window.location.hostname;
|
|
const username = params.get("username");
|
|
const password = params.get("password");
|
|
const accessToken = params.get("accessToken");
|
|
let serverURL = params.get("server");
|
|
if (username) {
|
|
form.setValue("username", username);
|
|
}
|
|
|
|
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
if (password) {
|
|
form.setValue("password", password);
|
|
}
|
|
if (accessToken) {
|
|
setLoginMethod("accessToken");
|
|
form.setValue("accessToken", accessToken);
|
|
}
|
|
}
|
|
if (serverURL) {
|
|
const isFullUrl = serverURL.match(/^(http|https):\/\//);
|
|
if (!isFullUrl) {
|
|
serverURL = `https://${serverURL}`;
|
|
}
|
|
form.setValue("base_url", serverURL);
|
|
}
|
|
}, [window.location.search]);
|
|
|
|
return (
|
|
<>
|
|
<Tabs
|
|
value={loginMethod}
|
|
onChange={(_, newValue) => setLoginMethod(newValue as LoginMethod)}
|
|
indicatorColor="primary"
|
|
textColor="primary"
|
|
centered
|
|
>
|
|
<Tab label={translate("synapseadmin.auth.credentials")} value="credentials" />
|
|
<Tab label={translate("synapseadmin.auth.access_token")} value="accessToken" />
|
|
</Tabs>
|
|
{loginMethod === "credentials" ? (
|
|
<>
|
|
<Box>
|
|
<TextInput
|
|
autoFocus
|
|
source="username"
|
|
label="ra.auth.username"
|
|
autoComplete="username"
|
|
onBlur={handleUsernameChange}
|
|
resettable
|
|
validate={required()}
|
|
{...(loading || !supportPassAuth ? { disabled: true } : {})}
|
|
/>
|
|
</Box>
|
|
<Box>
|
|
<PasswordInput
|
|
source="password"
|
|
label="ra.auth.password"
|
|
type="password"
|
|
autoComplete="current-password"
|
|
{...(loading || !supportPassAuth ? { disabled: true } : {})}
|
|
resettable
|
|
validate={required()}
|
|
/>
|
|
</Box>
|
|
</>
|
|
) : (
|
|
<Box>
|
|
<TextInput
|
|
source="accessToken"
|
|
label="synapseadmin.auth.access_token"
|
|
{...(loading ? { disabled: true } : {})}
|
|
resettable
|
|
validate={required()}
|
|
/>
|
|
</Box>
|
|
)}
|
|
<Box>
|
|
<TextInput
|
|
source="base_url"
|
|
label="synapseadmin.auth.base_url"
|
|
select={allowMultipleBaseUrls}
|
|
autoComplete="url"
|
|
{...(loading ? { disabled: true } : {})}
|
|
readOnly={allowSingleBaseUrl}
|
|
resettable={allowAnyBaseUrl}
|
|
validate={[required(), validateBaseUrl]}
|
|
>
|
|
{allowMultipleBaseUrls &&
|
|
restrictBaseUrl.map(url => (
|
|
<MenuItem key={url} value={url}>
|
|
{url}
|
|
</MenuItem>
|
|
))}
|
|
</TextInput>
|
|
</Box>
|
|
<Typography className="serverVersion">{serverVersion}</Typography>
|
|
<Typography className="matrixVersions">{matrixVersions}</Typography>
|
|
</>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Form defaultValues={{ base_url: base_url }} onSubmit={handleSubmit} mode="onTouched">
|
|
<LoginFormBox>
|
|
<Card className="card">
|
|
<Box className="avatar">
|
|
{loading ? (
|
|
<CircularProgress size={25} thickness={2} />
|
|
) : (
|
|
<Avatar sx={{ width: "120px", height: "120px" }} src="./images/logo.webp"/>
|
|
)}
|
|
</Box>
|
|
<Box className="hint">{translate("synapseadmin.auth.welcome")}</Box>
|
|
<Box className="form">
|
|
<Select
|
|
fullWidth
|
|
value={locale}
|
|
onChange={e => setLocale(e.target.value)}
|
|
disabled={loading}
|
|
className="select"
|
|
>
|
|
{locales.map(l => (
|
|
<MenuItem key={l.locale} value={l.locale}>
|
|
{l.name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
<FormDataConsumer>{formDataProps => <UserData {...formDataProps} />}</FormDataConsumer>
|
|
{loginMethod === "credentials" && <CardActions className="actions">
|
|
<Button
|
|
variant="contained"
|
|
type="submit"
|
|
color="primary"
|
|
disabled={loading || !supportPassAuth}
|
|
fullWidth
|
|
>
|
|
{translate("ra.auth.sign_in")}
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
color="secondary"
|
|
onClick={handleSSO}
|
|
disabled={loading || ssoBaseUrl === ""}
|
|
fullWidth
|
|
>
|
|
{translate("synapseadmin.auth.sso_sign_in")}
|
|
</Button>
|
|
</CardActions>}
|
|
{loginMethod === "accessToken" && <CardActions className="actions">
|
|
<Button
|
|
variant="contained"
|
|
type="submit"
|
|
color="primary"
|
|
disabled={loading}
|
|
fullWidth
|
|
>
|
|
{translate("ra.auth.sign_in")}
|
|
</Button>
|
|
</CardActions>}
|
|
</Box>
|
|
</Card>
|
|
</LoginFormBox>
|
|
<Notification />
|
|
<Footer />
|
|
</Form>
|
|
);
|
|
};
|
|
|
|
export default LoginPage;
|