Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ COPY --from=builder /src/dist /app

RUN rm -rf /usr/share/nginx/html \
&& ln -s /app /usr/share/nginx/html

# Bit of a hack to insert support for SPA sites without comitting a whole config file
RUN sed -i '/^\s*index.*;/s/$/\n\ttry_files $uri \/index.html;/' /etc/nginx/conf.d/default.conf
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@types/node": "^25.3.0",
"@types/papaparse": "^5.3.14",
"@types/react": "^19.2.9",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.53.1",
"@typescript-eslint/parser": "^8.56.0",
"@vitejs/plugin-react": "^5.1.2",
Expand Down Expand Up @@ -52,6 +53,7 @@
"@mui/icons-material": "^7.1.0",
"@mui/material": "^7.1.0",
"@tanstack/react-query": "^5.90.20",
"@vitejs/plugin-basic-ssl": "^2.1.4",
"history": "^5.3.0",
"lodash": "^4.17.23",
"papaparse": "^5.4.1",
Expand Down
18 changes: 14 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { merge } from "lodash";
import polyglotI18nProvider from "ra-i18n-polyglot";

import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
import { Route } from "react-router-dom";
import { createBrowserRouter, Route, RouterProvider } from "react-router-dom";

import { ImportFeature } from "./components/ImportFeature";
import germanMessages from "./i18n/de";
Expand Down Expand Up @@ -48,8 +48,8 @@ const i18nProvider = polyglotI18nProvider(

const queryClient = new QueryClient();

const App = () => (
<QueryClientProvider client={queryClient}>
const Main = () => {
return <QueryClientProvider client={queryClient}>
<Admin
disableTelemetry
requireAuth
Expand Down Expand Up @@ -80,6 +80,16 @@ const App = () => (
<Resource name="destination_rooms" />
</Admin>
</QueryClientProvider>
);
};

// Matrix doesn't like hash routing
// Ref: https://spec.matrix.org/v1.17/client-server-api/#redirect-uri-validation
const App = () => {
const router = createBrowserRouter([{
path: '*',
element: <Main />
}]);
return <RouterProvider router={router} />
}

export default App;
1 change: 1 addition & 0 deletions src/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createContext, useContext } from "react";

interface AppContextType {
restrictBaseUrl: string | string[];
allowInsecure?: boolean
}

export const AppContext = createContext({});
Expand Down
19 changes: 18 additions & 1 deletion src/components/AvatarField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,28 @@ import { get } from "lodash";

import { Avatar } from "@mui/material";
import { useRecordContext } from "react-admin";
import { useEffect, useRef, useState } from "react";

const AvatarField = ({ source, ...rest }) => {
const record = useRecordContext(rest);
const src = get(record, source)?.toString();
const _src = get(record, source);
const { alt, classes, sizes, sx, variant } = rest;

const [src, setSrc] = useState('');
useEffect(() => {
(async () => {
const raw = await _src;
if(!raw)
return;

if(typeof raw === 'string'){
setSrc(raw);
} else { // Blob
const url = window.URL.createObjectURL(raw);
setSrc(url);
}
})();
}, [_src]);
return <Avatar alt={alt} classes={classes} sizes={sizes} src={src} sx={sx} variant={variant} />;
};

Expand Down
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ const configJSON = "config.json";
// load config.json from relative path if import.meta.env.BASE_URL is None or empty
const configJSONUrl = baseUrl ? `${baseUrl.replace(/\/$/, "")}/${configJSON}` : configJSON;

const root = document.getElementById("root");
if(!root)
throw new Error('Failed to find document root');
fetch(configJSONUrl)
.then(res => res.json())
.then(props =>
createRoot(document.getElementById("root")).render(
createRoot(root).render(
<React.StrictMode>
<AppContext.Provider value={props}>
<App />
Expand Down
58 changes: 47 additions & 11 deletions src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ import { useFormContext } from "react-hook-form";

import { useAppContext } from "../AppContext";
import {
authoriseClient,
getAuthMetadata,
getAuthSession,
getServerVersion,
getSupportedFeatures,
getSupportedLoginFlows,
getWellKnownUrl,
homeserverUrl,
isValidBaseUrl,
registerClient,
splitMxid,
} from "../synapse/synapse";
import storage from "../storage";
Expand Down Expand Up @@ -86,7 +91,7 @@ const FormBox = styled(Box)(({ theme }) => ({
const LoginPage = () => {
const login = useLogin();
const notify = useNotify();
const { restrictBaseUrl } = useAppContext();
const { restrictBaseUrl, allowInsecure } = useAppContext();
const allowSingleBaseUrl = typeof restrictBaseUrl === "string";
const allowMultipleBaseUrls = Array.isArray(restrictBaseUrl);
const allowAnyBaseUrl = !(allowSingleBaseUrl || allowMultipleBaseUrls);
Expand All @@ -95,10 +100,17 @@ const LoginPage = () => {
const [locale, setLocale] = useLocaleState();
const locales = useLocales();
const translate = useTranslate();
const base_url = allowSingleBaseUrl ? restrictBaseUrl : storage.getItem("base_url");
const base_url = allowSingleBaseUrl ? restrictBaseUrl : homeserverUrl();
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
const [refresh, setRefresh] = useState(false);

const search = new URLSearchParams(new URL(window.location.href).search);
const code = search.get('code');
const state = search.get('state');
const authSession = getAuthSession();

// Can probably be removed
if (loginToken) {
const ssoToken = loginToken[1];
console.log("SSO token is", ssoToken);
Expand All @@ -108,6 +120,7 @@ const LoginPage = () => {
storage.removeItem("sso_base_url");
if (baseUrl) {
const auth = {
type: 'loginToken',
base_url: baseUrl,
username: null,
password: null,
Expand All @@ -129,7 +142,15 @@ const LoginPage = () => {
}
}

const validateBaseUrl = value => {
// TODO: Refine this for error handling
if(code && state && authSession)
{
if(authSession.state !== state)
throw new Error('Session state mismatch');
login({type: 'keyExchange', code, verifier: authSession.verifier});
}

const validateBaseUrl = (value: string) => {
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]*$/)) {
Expand All @@ -139,8 +160,9 @@ const LoginPage = () => {
}
};

const handleSubmit = auth => {
const handleSubmit = (auth) => {
setLoading(true);
auth.type= 'loginToken';
login(auth).catch(error => {
setLoading(false);
notify(
Expand All @@ -154,12 +176,10 @@ const LoginPage = () => {
});
};

const handleSSO = () => {
storage.setItem("sso_base_url", ssoBaseUrl);
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
window.location.href
)}`;
window.location.href = ssoFullUrl;
const handleSSO = async () => {
const success = await authoriseClient();
if(success === null)
notify('Something went wrong when attempting SSO: see console for more information', {type: 'error'});
};

const UserData = ({ formData }) => {
Expand All @@ -184,6 +204,7 @@ const LoginPage = () => {
form.setValue("base_url", restrictBaseUrl[0]);
}
if (!isValidBaseUrl(formData.base_url)) return;
homeserverUrl(formData.base_url);

getServerVersion(formData.base_url)
.then(serverVersion => setServerVersion(`${translate("synapseadmin.auth.server_version")} ${serverVersion}`))
Expand All @@ -204,7 +225,21 @@ const LoginPage = () => {
setSSOBaseUrl(supportSSO ? formData.base_url : "");
})
.catch(() => setSSOBaseUrl(""));
}, [formData.base_url, form]);

getAuthMetadata(formData.base_url).then(async metadata => {
// By default MAS doesn't allow insecure or localhost to register a client
if(!allowInsecure && (location.protocol === 'http' || location.host.includes('localhost')))
return console.error('Cannot register a client on an insecure domain: you can bypass this by setting "allowInsecure": true in the config file.');
if(!metadata)
return console.log("Missing client metadata");
await registerClient({
endpoint: metadata.registration_endpoint,
clientUri: location.origin,
redirectUri: location.href,
});
});
// Use refresh instead of the form_url to avoid pointless requests that lag the form
}, [refresh, form]);

return (
<>
Expand Down Expand Up @@ -241,6 +276,7 @@ const LoginPage = () => {
readOnly={allowSingleBaseUrl}
resettable={allowAnyBaseUrl}
validate={[required(), validateBaseUrl]}
onBlur={() => setRefresh(val => !val)}
>
{allowMultipleBaseUrls &&
restrictBaseUrl.map(url => (
Expand Down
2 changes: 1 addition & 1 deletion src/resources/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const UserList = (props: ListProps) => (
<List
{...props}
filters={userFilters}
filterDefaultValues={{ guests: true, deactivated: false, locked: false }}
filterDefaultValues={{ guests: false, deactivated: false, locked: false }}
sort={{ field: "name", order: "ASC" }}
actions={<UserListActions />}
pagination={<UserPagination />}
Expand Down
Loading
Loading