From 392fec3186077ca23efe522646edb07acea34d0d Mon Sep 17 00:00:00 2001
From: Aine <97398200+aine-etke@users.noreply.github.com>
Date: Mon, 25 Nov 2024 12:51:05 +0200
Subject: [PATCH] refactoring (#178)
* unify components import
* refactor config and app context
* refactor icons
* refactor date, error, mxid and storage
* refactor synapse utils
---
docs/config.md | 2 +-
docs/custom-menu.md | 2 +-
src/App.tsx | 14 +++--
src/AppContext.tsx | 6 --
src/components/AdminLayout.tsx | 6 +-
src/components/AvatarField.tsx | 5 +-
.../{devices.tsx => DeviceRemoveButton.tsx} | 4 +-
src/components/ExperimentalFeatures.tsx | 2 +
.../{ImportFeature.tsx => UserImport.tsx} | 10 +--
src/components/UserRateLimits.tsx | 4 +-
src/components/icons.ts | 12 ----
src/components/media.tsx | 5 +-
src/components/mxid.tsx | 20 ------
src/index.tsx | 6 +-
src/pages/LoginPage.test.tsx | 2 +-
src/pages/LoginPage.tsx | 13 ++--
src/resources/destinations.tsx | 2 +-
src/resources/registration_tokens.tsx | 2 +-
src/resources/reports.tsx | 2 +-
src/resources/rooms.tsx | 2 +-
src/resources/users.tsx | 12 ++--
src/storage.ts | 3 -
src/synapse/authProvider.test.ts | 27 ++++----
src/synapse/authProvider.ts | 33 +++++-----
src/synapse/dataProvider.test.ts | 5 +-
src/synapse/dataProvider.ts | 61 +++++++++----------
.../{synapse.test.ts => matrix.test.ts} | 2 +-
src/synapse/{synapse.ts => matrix.ts} | 49 +--------------
src/{components => utils}/config.ts | 6 +-
src/{components => utils}/date.ts | 0
src/{components => utils}/error.ts | 0
src/utils/fetchMedia.ts | 9 +--
src/utils/icons.ts | 16 +++++
src/utils/mxid.ts | 52 ++++++++++++++++
src/utils/password.ts | 10 +++
35 files changed, 200 insertions(+), 206 deletions(-)
delete mode 100644 src/AppContext.tsx
rename src/components/{devices.tsx => DeviceRemoveButton.tsx} (90%)
rename src/components/{ImportFeature.tsx => UserImport.tsx} (98%)
delete mode 100644 src/components/icons.ts
delete mode 100644 src/components/mxid.tsx
delete mode 100644 src/storage.ts
rename src/synapse/{synapse.test.ts => matrix.test.ts} (95%)
rename src/synapse/{synapse.ts => matrix.ts} (51%)
rename src/{components => utils}/config.ts (94%)
rename src/{components => utils}/date.ts (100%)
rename src/{components => utils}/error.ts (100%)
create mode 100644 src/utils/icons.ts
create mode 100644 src/utils/mxid.ts
create mode 100644 src/utils/password.ts
diff --git a/docs/config.md b/docs/config.md
index 01f75ac..67e05c3 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -35,7 +35,7 @@ In this case, you could provide the configuration in the `/.well-known/matrix/cl
* `menu` - add custom menu items to the main menu (sidebar) by providing a `menu` array in the config.
Each `menu` item can contain the following fields:
* `label` (required): The text to display in the menu.
- * `icon` (optional): The icon to display next to the label, one of the [../src/components/icons.ts] icons, otherwise a default icon will be used.
+ * `icon` (optional): The icon to display next to the label, one of the [src/utils/icons.ts](../src/utils/icons.ts) icons, otherwise a default icon will be used.
* `url` (required): The URL to navigate to when the menu item is clicked.
[More details](custom-menu.md)
diff --git a/docs/custom-menu.md b/docs/custom-menu.md
index 6422ba3..cb1ca14 100644
--- a/docs/custom-menu.md
+++ b/docs/custom-menu.md
@@ -10,7 +10,7 @@ The examples below contain the configuration settings to add a link to the [Syna
Each `menu` item can contain the following fields:
* `label` (required): The text to display in the menu.
-* `icon` (optional): The icon to display next to the label, one of the [../src/components/icons.ts] icons, otherwise a
+* `icon` (optional): The icon to display next to the label, one of the [src/utils/icons.ts](../src/utils/icons.ts) icons, otherwise a
default icon will be used.
* `url` (required): The URL to navigate to when the menu item is clicked.
diff --git a/src/App.tsx b/src/App.tsx
index 3c4162c..d1d6468 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -2,10 +2,11 @@ import { merge } from "lodash";
import polyglotI18nProvider from "ra-i18n-polyglot";
import { Admin, CustomRoutes, Resource, resolveBrowserLocale } from "react-admin";
+import { createContext, useContext } from "react";
import { Route } from "react-router-dom";
-import { AdminLayout } from "./components/AdminLayout";
-import { ImportFeature } from "./components/ImportFeature";
+import AdminLayout from "./components/AdminLayout";
+import UserImport from "./components/UserImport";
import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en";
import frenchMessages from "./i18n/fr";
@@ -23,6 +24,7 @@ import users from "./resources/users";
import authProvider from "./synapse/authProvider";
import dataProvider from "./synapse/dataProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { Config } from "./utils/config";
// TODO: Can we use lazy loading together with browser locale?
const messages = {
@@ -49,7 +51,7 @@ const i18nProvider = polyglotI18nProvider(
const queryClient = new QueryClient();
-const App = () => (
+export const App = () => (
(
i18nProvider={i18nProvider}
>
- } />
+ } />
@@ -84,4 +86,8 @@ const App = () => (
);
+export const AppContext = createContext({});
+
+export const useAppContext = () => useContext(AppContext) as Config;
+
export default App;
diff --git a/src/AppContext.tsx b/src/AppContext.tsx
deleted file mode 100644
index 72b8bc3..0000000
--- a/src/AppContext.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createContext, useContext } from "react";
-import { Config } from "./components/config";
-
-export const AppContext = createContext({});
-
-export const useAppContext = () => useContext(AppContext) as Config;
diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx
index dd1cc93..8d134fa 100644
--- a/src/components/AdminLayout.tsx
+++ b/src/components/AdminLayout.tsx
@@ -1,8 +1,8 @@
import { CheckForApplicationUpdate, AppBar, TitlePortal, InspectorButton, Confirm, Layout, Logout, Menu, useLogout, UserMenu } from "react-admin";
import { LoginMethod } from "../pages/LoginPage";
import { useEffect, useState, Suspense } from "react";
-import { Icons, DefaultIcon } from "./icons";
-import { MenuItem, GetConfig, ClearConfig } from "./config";
+import { Icons, DefaultIcon } from "../utils/icons";
+import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
import Footer from "./Footer";
const AdminUserMenu = () => {
@@ -96,3 +96,5 @@ export const AdminLayout = ({ children }) => {
>
};
+
+export default AdminLayout;
diff --git a/src/components/AvatarField.tsx b/src/components/AvatarField.tsx
index a7988b1..bfa4244 100644
--- a/src/components/AvatarField.tsx
+++ b/src/components/AvatarField.tsx
@@ -3,8 +3,7 @@ import { Avatar, AvatarProps, Badge, Tooltip } from "@mui/material";
import { FieldProps, useRecordContext, useTranslate } from "react-admin";
import { useState, useEffect, useCallback } from "react";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
-import { isMXID, isASManaged } from "./mxid";
-import storage from "../storage";
+import { isMXID, isASManaged } from "../utils/mxid";
const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
const { alt, classes, sizes, sx, variant } = rest;
@@ -74,7 +73,7 @@ const AvatarField = ({ source, ...rest }: AvatarProps & FieldProps) => {
badge = "🛡️";
tooltip = `${translate("resources.users.badge.system_managed")} (${tooltip})`;
}
- if (storage.getItem("user_id") === record?.id) {
+ if (localStorage.getItem("user_id") === record?.id) {
badge = "🧙";
tooltip = `${translate("resources.users.badge.you")} (${tooltip})`;
}
diff --git a/src/components/devices.tsx b/src/components/DeviceRemoveButton.tsx
similarity index 90%
rename from src/components/devices.tsx
rename to src/components/DeviceRemoveButton.tsx
index 4661856..70f3305 100644
--- a/src/components/devices.tsx
+++ b/src/components/DeviceRemoveButton.tsx
@@ -1,5 +1,5 @@
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
-import { isASManaged } from "./mxid";
+import { isASManaged } from "../utils/mxid";
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
const record = useRecordContext();
@@ -26,3 +26,5 @@ export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
/>
);
};
+
+export default DeviceRemoveButton;
diff --git a/src/components/ExperimentalFeatures.tsx b/src/components/ExperimentalFeatures.tsx
index 19d9d7a..7914aa0 100644
--- a/src/components/ExperimentalFeatures.tsx
+++ b/src/components/ExperimentalFeatures.tsx
@@ -93,3 +93,5 @@ export const ExperimentalFeaturesList = () => {
>
}
+
+export default ExperimentalFeaturesList;
diff --git a/src/components/ImportFeature.tsx b/src/components/UserImport.tsx
similarity index 98%
rename from src/components/ImportFeature.tsx
rename to src/components/UserImport.tsx
index ce51fa4..2bcf418 100644
--- a/src/components/ImportFeature.tsx
+++ b/src/components/UserImport.tsx
@@ -15,7 +15,8 @@ import {
import { DataProvider, useTranslate } from "ra-core";
import { useDataProvider, useNotify, RaRecord, Title } from "react-admin";
-import { generateRandomMxId, generateRandomPassword, returnMXID } from "../synapse/synapse";
+import { generateRandomMXID, returnMXID } from "../utils/mxid";
+import { generateRandomPassword } from "../utils/password";
const LOGGING = true;
@@ -274,7 +275,7 @@ const FilePicker = () => {
// No need to do a bunch of cryptographic random number getting if
// we are using neither a generated password nor a generated user id.
if (useridMode === "ignore" || userRecord.id === undefined || userRecord.id === "") {
- userRecord.id = generateRandomMxId();
+ userRecord.id = generateRandomMXID();
}
if (passwordMode === false || entry.password === undefined || entry.password === "") {
userRecord.password = generateRandomPassword();
@@ -325,7 +326,7 @@ const FilePicker = () => {
);
} else {
const newRecordData = Object.assign({}, recordData, {
- id: generateRandomMxId(),
+ id: generateRandomMXID(),
});
retries++;
if (retries > 512) {
@@ -570,4 +571,5 @@ const FilePicker = () => {
return [
, cardContainer];
};
-export const ImportFeature = FilePicker;
+export const UserImport = FilePicker;
+export default UserImport;
diff --git a/src/components/UserRateLimits.tsx b/src/components/UserRateLimits.tsx
index 8d5b0cf..7ae92c4 100644
--- a/src/components/UserRateLimits.tsx
+++ b/src/components/UserRateLimits.tsx
@@ -46,7 +46,7 @@ const RateLimitRow = ({ limit, value, updateRateLimit }: { limit: string, value:
}
-export const UserRateLimits = () => {
+const UserRateLimits = () => {
const translate = useTranslate();
const notify = useNotify();
const record = useRecordContext();
@@ -93,3 +93,5 @@ export const UserRateLimits = () => {
>
};
+
+export default UserRateLimits;
diff --git a/src/components/icons.ts b/src/components/icons.ts
deleted file mode 100644
index 16e22cc..0000000
--- a/src/components/icons.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { lazy } from "react";
-
-export const Icons = {
- Announcement: lazy(() => import('@mui/icons-material/Announcement')),
- Engineering: lazy(() => import('@mui/icons-material/Engineering')),
- HelpCenter: lazy(() => import('@mui/icons-material/HelpCenter')),
- SupportAgent: lazy(() => import('@mui/icons-material/SupportAgent')),
- Default: lazy(() => import('@mui/icons-material/OpenInNew')),
- // Add more icons as needed
-};
-
-export const DefaultIcon = Icons.Default;
diff --git a/src/components/media.tsx b/src/components/media.tsx
index d1e73f4..27943e5 100644
--- a/src/components/media.tsx
+++ b/src/components/media.tsx
@@ -32,9 +32,8 @@ import {
} from "react-admin";
import { useMutation } from "@tanstack/react-query";
-import { dateParser } from "./date";
+import { dateParser } from "../utils/date";
import { DeleteMediaParams, SynapseDataProvider } from "../synapse/dataProvider";
-import storage from "../storage";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
const DeleteMediaDialog = ({ open, onClose, onSubmit }) => {
@@ -385,7 +384,7 @@ export const MediaIDField = ({ source }) => {
if (!record) {
return null;
}
- const homeserver = storage.getItem("home_server");
+ const homeserver = localStorage.getItem("home_server");
const mediaID = get(record, source)?.toString();
if (!mediaID) {
diff --git a/src/components/mxid.tsx b/src/components/mxid.tsx
deleted file mode 100644
index ba850ea..0000000
--- a/src/components/mxid.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Identifier } from "ra-core";
-import { GetConfig } from "./config";
-
-const mxidPattern = /^@[^@:]+:[^@:]+$/;
-
-/*
- * Check if id is a valid Matrix ID (user)
- * @param id The ID to check
- * @returns Whether the ID is a valid Matrix ID
- */
-export const isMXID = (id: string | Identifier): boolean => mxidPattern.test(id as string);
-
-/**
- * Check if a user is managed by an application service
- * @param id The user ID to check
- * @returns Whether the user is managed by an application service
- */
-export const isASManaged = (id: string | Identifier): boolean => {
- return GetConfig().asManagedUsers.some(regex => regex.test(id as string));
-};
diff --git a/src/index.tsx b/src/index.tsx
index 929621e..b620cd4 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -2,10 +2,8 @@ import React from "react";
import { createRoot } from "react-dom/client";
-import App from "./App";
-import { FetchConfig, GetConfig } from "./components/config";
-import { AppContext } from "./AppContext";
-import storage from "./storage";
+import {App, AppContext } from "./App";
+import { FetchConfig, GetConfig } from "./utils/config";
await FetchConfig();
diff --git a/src/pages/LoginPage.test.tsx b/src/pages/LoginPage.test.tsx
index 225d13c..0adca70 100644
--- a/src/pages/LoginPage.test.tsx
+++ b/src/pages/LoginPage.test.tsx
@@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react";
import { AdminContext } from "react-admin";
import LoginPage from "./LoginPage";
-import { AppContext } from "../AppContext";
+import { AppContext } from "../App";
import englishMessages from "../i18n/en";
const i18nProvider = polyglotI18nProvider(() => englishMessages, "en", [{ locale: "en", name: "English" }]);
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
index 0691f7b..c013352 100644
--- a/src/pages/LoginPage.tsx
+++ b/src/pages/LoginPage.tsx
@@ -16,7 +16,7 @@ import {
} from "react-admin";
import { useFormContext } from "react-hook-form";
import LoginFormBox from "../components/LoginFormBox";
-import { useAppContext } from "../AppContext";
+import { useAppContext } from "../App";
import {
getServerVersion,
getSupportedFeatures,
@@ -24,8 +24,7 @@ import {
getWellKnownUrl,
isValidBaseUrl,
splitMxid,
-} from "../synapse/synapse";
-import storage from "../storage";
+} from "../synapse/matrix";
import Footer from "../components/Footer";
export type LoginMethod = "credentials" | "accessToken";
@@ -46,7 +45,7 @@ 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 : localStorage.getItem("base_url");
const [ssoBaseUrl, setSSOBaseUrl] = useState("");
const loginToken = /\?loginToken=([a-zA-Z0-9_-]+)/.exec(window.location.href);
const [loginMethod, setLoginMethod] = useState("credentials");
@@ -60,8 +59,8 @@ const LoginPage = () => {
console.log("SSO token is", ssoToken);
// Prevent further requests
window.history.replaceState({}, "", window.location.href.replace(loginToken[0], "#").split("#")[0]);
- const baseUrl = storage.getItem("sso_base_url");
- storage.removeItem("sso_base_url");
+ const baseUrl = localStorage.getItem("sso_base_url");
+ localStorage.removeItem("sso_base_url");
if (baseUrl) {
const auth = {
base_url: baseUrl,
@@ -114,7 +113,7 @@ const LoginPage = () => {
};
const handleSSO = () => {
- storage.setItem("sso_base_url", ssoBaseUrl);
+ localStorage.setItem("sso_base_url", ssoBaseUrl);
const ssoFullUrl = `${ssoBaseUrl}/_matrix/client/r0/login/sso/redirect?redirectUrl=${encodeURIComponent(
window.location.href
)}`;
diff --git a/src/resources/destinations.tsx b/src/resources/destinations.tsx
index e17f1f3..6abc0bc 100644
--- a/src/resources/destinations.tsx
+++ b/src/resources/destinations.tsx
@@ -33,7 +33,7 @@ import {
DateFieldProps,
} from "react-admin";
-import { DATE_FORMAT } from "../components/date";
+import { DATE_FORMAT } from "../utils/date";
import { get } from "lodash";
const DestinationPagination = () => ;
diff --git a/src/resources/registration_tokens.tsx b/src/resources/registration_tokens.tsx
index b3553bc..9fed7cb 100644
--- a/src/resources/registration_tokens.tsx
+++ b/src/resources/registration_tokens.tsx
@@ -24,7 +24,7 @@ import {
Toolbar,
} from "react-admin";
-import { DATE_FORMAT, dateFormatter, dateParser } from "../components/date";
+import { DATE_FORMAT, dateFormatter, dateParser } from "../utils/date";
const validateToken = [regex(/^[A-Za-z0-9._~-]{0,64}$/)];
const validateUsesAllowed = [number()];
diff --git a/src/resources/reports.tsx b/src/resources/reports.tsx
index 4c683a0..03488fb 100644
--- a/src/resources/reports.tsx
+++ b/src/resources/reports.tsx
@@ -22,7 +22,7 @@ import {
useTranslate,
} from "react-admin";
-import { DATE_FORMAT } from "../components/date";
+import { DATE_FORMAT } from "../utils/date";
import { ReportMediaContent } from "../components/media";
const ReportPagination = () => ;
diff --git a/src/resources/rooms.tsx b/src/resources/rooms.tsx
index c2341c2..24f4eab 100644
--- a/src/resources/rooms.tsx
+++ b/src/resources/rooms.tsx
@@ -47,7 +47,7 @@ import {
RoomDirectoryUnpublishButton,
RoomDirectoryPublishButton,
} from "./room_directory";
-import { DATE_FORMAT } from "../components/date";
+import { DATE_FORMAT } from "../utils/date";
import DeleteRoomButton from "../components/DeleteRoomButton";
import AvatarField from "../components/AvatarField";
import { Room } from "../synapse/dataProvider";
diff --git a/src/resources/users.tsx b/src/resources/users.tsx
index 19c6316..1b6e2c7 100644
--- a/src/resources/users.tsx
+++ b/src/resources/users.tsx
@@ -69,15 +69,15 @@ import { Link } from "react-router-dom";
import AvatarField from "../components/AvatarField";
import DeleteUserButton from "../components/DeleteUserButton";
-import { isASManaged } from "../components/mxid";
+import { isASManaged } from "../utils/mxid";
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
-import { DATE_FORMAT } from "../components/date";
-import { DeviceRemoveButton } from "../components/devices";
+import { DATE_FORMAT } from "../utils/date";
+import DeviceRemoveButton from "../components/DeviceRemoveButton";
import { MediaIDField, ProtectMediaButton, QuarantineMediaButton } from "../components/media";
-import { generateRandomPassword } from "../synapse/synapse";
+import { generateRandomPassword } from "../utils/password";
import { useFormContext } from "react-hook-form";
-import { ExperimentalFeaturesList } from "../components/ExperimentalFeatures";
-import { UserRateLimits } from "../components/UserRateLimits";
+import ExperimentalFeaturesList from "../components/ExperimentalFeatures";
+import UserRateLimits from "../components/UserRateLimits";
import { User, UsernameAvailabilityResult } from "../synapse/dataProvider";
import { MakeAdminBtn } from "./rooms";
diff --git a/src/storage.ts b/src/storage.ts
deleted file mode 100644
index a6f1adf..0000000
--- a/src/storage.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-const storage = localStorage;
-
-export default storage;
diff --git a/src/synapse/authProvider.test.ts b/src/synapse/authProvider.test.ts
index b2e89d6..6317a6d 100644
--- a/src/synapse/authProvider.test.ts
+++ b/src/synapse/authProvider.test.ts
@@ -1,7 +1,6 @@
import fetchMock from "jest-fetch-mock";
import authProvider from "./authProvider";
-import storage from "../storage";
import { HttpError } from "ra-core";
fetchMock.enableMocks();
@@ -9,7 +8,7 @@ fetchMock.enableMocks();
describe("authProvider", () => {
beforeEach(() => {
fetchMock.resetMocks();
- storage.clear();
+ localStorage.clear();
});
describe("login", () => {
@@ -38,10 +37,10 @@ describe("authProvider", () => {
}),
method: "POST",
});
- expect(storage.getItem("base_url")).toEqual("http://example.com");
- expect(storage.getItem("user_id")).toEqual("@user:example.com");
- expect(storage.getItem("access_token")).toEqual("foobar");
- expect(storage.getItem("device_id")).toEqual("some_device");
+ expect(localStorage.getItem("base_url")).toEqual("http://example.com");
+ expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
+ expect(localStorage.getItem("access_token")).toEqual("foobar");
+ expect(localStorage.getItem("device_id")).toEqual("some_device");
});
});
@@ -69,16 +68,16 @@ describe("authProvider", () => {
}),
method: "POST",
});
- expect(storage.getItem("base_url")).toEqual("https://example.com");
- expect(storage.getItem("user_id")).toEqual("@user:example.com");
- expect(storage.getItem("access_token")).toEqual("foobar");
- expect(storage.getItem("device_id")).toEqual("some_device");
+ expect(localStorage.getItem("base_url")).toEqual("https://example.com");
+ expect(localStorage.getItem("user_id")).toEqual("@user:example.com");
+ expect(localStorage.getItem("access_token")).toEqual("foobar");
+ expect(localStorage.getItem("device_id")).toEqual("some_device");
});
describe("logout", () => {
it("should remove the access_token from storage", async () => {
- storage.setItem("base_url", "example.com");
- storage.setItem("access_token", "foo");
+ localStorage.setItem("base_url", "example.com");
+ localStorage.setItem("access_token", "foo");
fetchMock.mockResponse(JSON.stringify({}));
await authProvider.logout(null);
@@ -91,7 +90,7 @@ describe("authProvider", () => {
method: "POST",
user: { authenticated: true, token: "Bearer foo" },
});
- expect(storage.getItem("access_token")).toBeNull();
+ expect(localStorage.getItem("access_token")).toBeNull();
});
});
@@ -115,7 +114,7 @@ describe("authProvider", () => {
});
it("should resolve when logged in", async () => {
- storage.setItem("access_token", "foobar");
+ localStorage.setItem("access_token", "foobar");
await expect(authProvider.checkAuth({})).resolves.toBeUndefined();
});
diff --git a/src/synapse/authProvider.ts b/src/synapse/authProvider.ts
index 26358f8..d88c83c 100644
--- a/src/synapse/authProvider.ts
+++ b/src/synapse/authProvider.ts
@@ -1,9 +1,8 @@
import { AuthProvider, HttpError, Options, fetchUtils } from "react-admin";
-import storage from "../storage";
-import { MatrixError, displayError } from "../components/error";
+import { MatrixError, displayError } from "../utils/error";
import { fetchAuthenticatedMedia } from "../utils/fetchMedia";
-import { FetchConfig, ClearConfig } from "../components/config";
+import { FetchConfig, ClearConfig } from "../utils/config";
const authProvider: AuthProvider = {
// called when the user attempts to log in
@@ -26,7 +25,7 @@ const authProvider: AuthProvider = {
body: JSON.stringify(
Object.assign(
{
- device_id: storage.getItem("device_id"),
+ device_id: localStorage.getItem("device_id"),
initial_device_display_name: "Synapse Admin",
},
loginToken
@@ -52,11 +51,11 @@ const authProvider: AuthProvider = {
if (!base_url) {
// there is some kind of bug with base_url being present in the form, but not submitted
// ref: https://github.com/etkecc/synapse-admin/issues/14
- storage.removeItem("base_url")
+ localStorage.removeItem("base_url")
throw new Error("Homeserver URL is required.");
}
base_url = base_url.replace(/\/+$/g, "");
- storage.setItem("base_url", base_url);
+ localStorage.setItem("base_url", base_url);
const decoded_base_url = window.decodeURIComponent(base_url);
let login_api_url = decoded_base_url + (accessToken ? "/_matrix/client/v3/account/whoami" : "/_matrix/client/v3/login");
@@ -76,11 +75,11 @@ const authProvider: AuthProvider = {
response = await fetchUtils.fetchJson(login_api_url, options);
const json = response.json;
- storage.setItem("home_server", accessToken ? json.user_id.split(":")[1] : json.home_server);
- storage.setItem("user_id", json.user_id);
- storage.setItem("access_token", accessToken ? accessToken : json.access_token);
- storage.setItem("device_id", json.device_id);
- storage.setItem("login_type", accessToken ? "accessToken" : "credentials");
+ localStorage.setItem("home_server", accessToken ? json.user_id.split(":")[1] : json.home_server);
+ localStorage.setItem("user_id", json.user_id);
+ localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
+ localStorage.setItem("device_id", json.device_id);
+ localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
// when doing access token auth, config is not fetched, so we need to do it here
if (accessToken) {
@@ -103,9 +102,9 @@ const authProvider: AuthProvider = {
}
},
getIdentity: async () => {
- const access_token = storage.getItem("access_token");
- const user_id = storage.getItem("user_id");
- const base_url = storage.getItem("base_url");
+ const access_token = localStorage.getItem("access_token");
+ const user_id = localStorage.getItem("user_id");
+ const base_url = localStorage.getItem("base_url");
if (typeof access_token !== "string" || typeof user_id !== "string" || typeof base_url !== "string") {
return Promise.reject();
@@ -143,8 +142,8 @@ const authProvider: AuthProvider = {
logout: async () => {
console.log("logout");
- const logout_api_url = storage.getItem("base_url") + "/_matrix/client/v3/logout";
- const access_token = storage.getItem("access_token");
+ const logout_api_url = localStorage.getItem("base_url") + "/_matrix/client/v3/logout";
+ const access_token = localStorage.getItem("access_token");
const options: Options = {
method: "POST",
@@ -176,7 +175,7 @@ const authProvider: AuthProvider = {
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
- const access_token = storage.getItem("access_token");
+ const access_token = localStorage.getItem("access_token");
return typeof access_token === "string" ? Promise.resolve() : Promise.reject();
},
// called when the user navigates to a new location, to check for permissions / roles
diff --git a/src/synapse/dataProvider.test.ts b/src/synapse/dataProvider.test.ts
index dbf9b45..9e8cb1f 100644
--- a/src/synapse/dataProvider.test.ts
+++ b/src/synapse/dataProvider.test.ts
@@ -1,7 +1,6 @@
import fetchMock from "jest-fetch-mock";
import dataProvider from "./dataProvider";
-import storage from "../storage";
fetchMock.enableMocks();
@@ -10,8 +9,8 @@ beforeEach(() => {
});
describe("dataProvider", () => {
- storage.setItem("base_url", "http://localhost");
- storage.setItem("access_token", "access_token");
+ localStorage.setItem("base_url", "http://localhost");
+ localStorage.setItem("access_token", "access_token");
it("fetches all users", async () => {
fetchMock.mockResponseOnce(
diff --git a/src/synapse/dataProvider.ts b/src/synapse/dataProvider.ts
index 2feee38..270b6c5 100644
--- a/src/synapse/dataProvider.ts
+++ b/src/synapse/dataProvider.ts
@@ -13,13 +13,12 @@ import {
withLifecycleCallbacks,
} from "react-admin";
-import storage from "../storage";
-import { returnMXID } from "./synapse";
-import { MatrixError, displayError } from "../components/error";
+import { returnMXID } from "../utils/mxid";
+import { MatrixError, displayError } from "../utils/error";
// Adds the access token to all requests
const jsonClient = async (url: string, options: Options = {}) => {
- const token = storage.getItem("access_token");
+ const token = localStorage.getItem("access_token");
console.log("httpClient " + url);
if (token !== null) {
options.user = {
@@ -401,7 +400,7 @@ const resourceMap = {
data: "media",
total: json => json.total,
delete: (params: DeleteParams) => ({
- endpoint: `/_synapse/admin/v1/media/${storage.getItem("home_server")}/${params.id}`,
+ endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`,
}),
},
protect_media: {
@@ -418,11 +417,11 @@ const resourceMap = {
quarantine_media: {
map: (qm: UserMedia) => ({ id: qm.media_id }),
create: (params: UserMedia) => ({
- endpoint: `/_synapse/admin/v1/media/quarantine/${storage.getItem("home_server")}/${params.media_id}`,
+ endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem("home_server")}/${params.media_id}`,
method: "POST",
}),
delete: (params: DeleteParams) => ({
- endpoint: `/_synapse/admin/v1/media/unquarantine/${storage.getItem("home_server")}/${params.id}`,
+ endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem("home_server")}/${params.id}`,
method: "POST",
}),
},
@@ -567,7 +566,7 @@ const baseDataProvider: SynapseDataProvider = {
order_by: field,
dir: getSearchOrder(order),
};
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -586,7 +585,7 @@ const baseDataProvider: SynapseDataProvider = {
getOne: async (resource, params) => {
console.log("getOne " + resource);
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -598,8 +597,8 @@ const baseDataProvider: SynapseDataProvider = {
getMany: async (resource, params) => {
console.log("getMany " + resource);
- const base_url = storage.getItem("base_url");
- const homeserver = storage.getItem("home_server");
+ const base_url = localStorage.getItem("base_url");
+ const homeserver = localStorage.getItem("home_server");
if (!base_url || !(resource in resourceMap)) throw Error("base_url not set");
const res = resourceMap[resource];
@@ -638,7 +637,7 @@ const baseDataProvider: SynapseDataProvider = {
dir: getSearchOrder(order),
};
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -655,7 +654,7 @@ const baseDataProvider: SynapseDataProvider = {
update: async (resource, params) => {
console.log("update " + resource);
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -670,7 +669,7 @@ const baseDataProvider: SynapseDataProvider = {
updateMany: async (resource, params) => {
console.log("updateMany " + resource);
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -687,7 +686,7 @@ const baseDataProvider: SynapseDataProvider = {
create: async (resource, params) => {
console.log("create " + resource);
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -704,7 +703,7 @@ const baseDataProvider: SynapseDataProvider = {
createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => {
console.log("createMany " + resource);
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -726,7 +725,7 @@ const baseDataProvider: SynapseDataProvider = {
delete: async (resource, params) => {
console.log("delete " + resource);
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -751,7 +750,7 @@ const baseDataProvider: SynapseDataProvider = {
deleteMany: async (resource, params) => {
console.log("deleteMany " + resource, "params", params);
- const homeserver = storage.getItem("base_url");
+ const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");
const res = resourceMap[resource];
@@ -798,17 +797,17 @@ const baseDataProvider: SynapseDataProvider = {
* @returns
*/
deleteMedia: async ({ before_ts, size_gt = 0, keep_profiles = true }) => {
- const homeserver = storage.getItem("home_server"); // TODO only required for synapse < 1.78.0
+ const homeserver = localStorage.getItem("home_server"); // TODO only required for synapse < 1.78.0
const endpoint = `/_synapse/admin/v1/media/${homeserver}/delete?before_ts=${before_ts}&size_gt=${size_gt}&keep_profiles=${keep_profiles}`;
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = base_url + endpoint;
const { json } = await jsonClient(endpoint_url, { method: "POST" });
return json as DeleteMediaResult;
},
uploadMedia: async ({ file, filename, content_type }: UploadMediaParams) => {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const uploadMediaURL = `${base_url}/_matrix/media/v3/upload`;
const { json } = await jsonClient(`${uploadMediaURL}?filename=${filename}`, {
@@ -822,18 +821,18 @@ const baseDataProvider: SynapseDataProvider = {
return json as UploadMediaResult;
},
getFeatures: async (id: Identifier) => {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/experimental_features/${encodeURIComponent(returnMXID(id))}`;
const { json } = await jsonClient(endpoint_url);
return json.features as ExperimentalFeaturesModel;
},
updateFeatures: async (id: Identifier, features: ExperimentalFeaturesModel) => {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/experimental_features/${encodeURIComponent(returnMXID(id))}`;
await jsonClient(endpoint_url, { method: "PUT", body: JSON.stringify({ features }) });
},
getRateLimits: async (id: Identifier) => {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`;
const { json } = await jsonClient(endpoint_url);
return json as RateLimitsModel;
@@ -846,7 +845,7 @@ const baseDataProvider: SynapseDataProvider = {
return obj;
}, {});
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/override_ratelimit`;
if (Object.keys(filtered).length === 0) {
await jsonClient(endpoint_url, { method: "DELETE" });
@@ -856,7 +855,7 @@ const baseDataProvider: SynapseDataProvider = {
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify(filtered) });
},
checkUsernameAvailability: async (username: string) => {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/username_available?username=${encodeURIComponent(username)}`;
try {
const { json } = await jsonClient(endpoint_url);
@@ -869,7 +868,7 @@ const baseDataProvider: SynapseDataProvider = {
}
},
makeRoomAdmin: async (room_id: string, user_id: string) => {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/rooms/${encodeURIComponent(room_id)}/make_room_admin`;
try {
@@ -914,13 +913,13 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
},
beforeDelete: async (params: DeleteParams, dataProvider: DataProvider) => {
if (params.meta?.deleteMedia) {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/media`;
await jsonClient(endpoint_url, { method: "DELETE" });
}
if (params.meta?.redactEvents) {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/user/${encodeURIComponent(returnMXID(params.id))}/redact`;
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ rooms: [] }) });
}
@@ -931,13 +930,13 @@ const dataProvider = withLifecycleCallbacks(baseDataProvider, [
await Promise.all(
params.ids.map(async id => {
if (params.meta?.deleteMedia) {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(id))}/media`;
await jsonClient(endpoint_url, { method: "DELETE" });
}
if (params.meta?.redactEvents) {
- const base_url = storage.getItem("base_url");
+ const base_url = localStorage.getItem("base_url");
const endpoint_url = `${base_url}/_synapse/admin/v1/user/${encodeURIComponent(returnMXID(id))}/redact`;
await jsonClient(endpoint_url, { method: "POST", body: JSON.stringify({ rooms: [] }) });
}
diff --git a/src/synapse/synapse.test.ts b/src/synapse/matrix.test.ts
similarity index 95%
rename from src/synapse/synapse.test.ts
rename to src/synapse/matrix.test.ts
index 0242670..1b8ea35 100644
--- a/src/synapse/synapse.test.ts
+++ b/src/synapse/matrix.test.ts
@@ -1,4 +1,4 @@
-import { isValidBaseUrl, splitMxid } from "./synapse";
+import { isValidBaseUrl, splitMxid } from "./matrix";
describe("splitMxid", () => {
it("splits valid MXIDs", () =>
diff --git a/src/synapse/synapse.ts b/src/synapse/matrix.ts
similarity index 51%
rename from src/synapse/synapse.ts
rename to src/synapse/matrix.ts
index 63e07f3..2b51e0f 100644
--- a/src/synapse/synapse.ts
+++ b/src/synapse/matrix.ts
@@ -1,7 +1,6 @@
import { Identifier, fetchUtils } from "react-admin";
-import storage from "../storage";
-import { isMXID } from "../components/mxid";
+import { isMXID } from "../utils/mxid";
export const splitMxid = mxid => {
const re = /^@(?[a-zA-Z0-9._=\-/]+):(?[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/;
@@ -50,51 +49,7 @@ export const getSupportedFeatures = async baseUrl => {
* @returns array of supported login flows
*/
export const getSupportedLoginFlows = async baseUrl => {
- const loginFlowsUrl = `${baseUrl}/_matrix/client/r0/login`;
+ const loginFlowsUrl = `${baseUrl}/_matrix/client/v3/login`;
const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" });
return response.json.flows;
};
-
-/**
- * Generate a random MXID for current homeserver
- * @returns full MXID as string
- */
-export function generateRandomMxId(): string {
- const homeserver = storage.getItem("home_server");
- const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
- const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
- .map(x => characters[x % characters.length])
- .join("");
- return `@${localpart}:${homeserver}`;
-}
-
-/**
- * Return the full MXID from an arbitrary input
- * @param input the input string
- * @returns full MXID as string
- */
-export function returnMXID(input: string | Identifier): string {
- const homeserver = storage.getItem("home_server");
-
- // Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
- const mxidPattern = /^@[^@:]+:[^@:]+$/;
- if (isMXID(input)) {
- return input as string; // Already a valid MXID
- }
-
- // If input is not a valid MXID, assume it's a localpart and construct the MXID
- const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
- return `@${localpart}:${homeserver}`;
-}
-
-
-/**
- * Generate a random user password
- * @returns a new random password as string
- */
-export function generateRandomPassword(length = 64): string {
- const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~`!@#$%^&*()_-+={[}]|:;'.?/<>,";
- return Array.from(crypto.getRandomValues(new Uint32Array(length)))
- .map(x => characters[x % characters.length])
- .join("");
-}
diff --git a/src/components/config.ts b/src/utils/config.ts
similarity index 94%
rename from src/components/config.ts
rename to src/utils/config.ts
index f680d2b..23e56fa 100644
--- a/src/components/config.ts
+++ b/src/utils/config.ts
@@ -1,5 +1,3 @@
-import storage from "../storage";
-
export interface Config {
restrictBaseUrl: string | string[];
asManagedUsers: RegExp[];
@@ -32,7 +30,7 @@ export const FetchConfig = async () => {
}
// if home_server is set, try to load https://home_server/.well-known/matrix/client
- const homeserver = storage.getItem("home_server");
+ const homeserver = localStorage.getItem("home_server");
if (homeserver) {
try {
const resp = await fetch(`https://${homeserver}/.well-known/matrix/client`);
@@ -80,5 +78,5 @@ export const ClearConfig = () => {
// config.json
config = {} as Config;
// session
- storage.clear();
+ localStorage.clear();
}
diff --git a/src/components/date.ts b/src/utils/date.ts
similarity index 100%
rename from src/components/date.ts
rename to src/utils/date.ts
diff --git a/src/components/error.ts b/src/utils/error.ts
similarity index 100%
rename from src/components/error.ts
rename to src/utils/error.ts
diff --git a/src/utils/fetchMedia.ts b/src/utils/fetchMedia.ts
index 87972e2..70be3ed 100644
--- a/src/utils/fetchMedia.ts
+++ b/src/utils/fetchMedia.ts
@@ -1,9 +1,6 @@
-import storage from "../storage";
-
export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: string, mediaId: string } => {
const re = /^mxc:\/\/([^/]+)\/(\w+)/;
const ret = re.exec(mxcUrl);
- console.log("mxcClient " + ret);
if (ret == null) {
throw new Error("Invalid mxcUrl");
}
@@ -15,8 +12,8 @@ export const getServerAndMediaIdFromMxcUrl = (mxcUrl: string): { serverName: str
export type MediaType = "thumbnail" | "original";
export const fetchAuthenticatedMedia = async (mxcUrl: string, type: MediaType): Promise => {
- const homeserver = storage.getItem("base_url");
- const accessToken = storage.getItem("access_token");
+ const homeserver = localStorage.getItem("base_url");
+ const accessToken = localStorage.getItem("access_token");
const { serverName, mediaId } = getServerAndMediaIdFromMxcUrl(mxcUrl);
if (!serverName || !mediaId) {
@@ -40,4 +37,4 @@ export const fetchAuthenticatedMedia = async (mxcUrl: string, type: MediaType):
});
return response;
-};
\ No newline at end of file
+};
diff --git a/src/utils/icons.ts b/src/utils/icons.ts
new file mode 100644
index 0000000..133dfe5
--- /dev/null
+++ b/src/utils/icons.ts
@@ -0,0 +1,16 @@
+import AnnouncementIcon from '@mui/icons-material/Announcement';
+import EngineeringIcon from '@mui/icons-material/Engineering';
+import HelpCenterIcon from '@mui/icons-material/HelpCenter';
+import SupportAgentIcon from '@mui/icons-material/SupportAgent';
+import OpenInNewIcon from '@mui/icons-material/OpenInNew';
+
+export const Icons = {
+ Announcement: AnnouncementIcon,
+ Engineering: EngineeringIcon,
+ HelpCenter: HelpCenterIcon,
+ SupportAgent: SupportAgentIcon,
+ Default: OpenInNewIcon,
+ // Add more icons as needed
+};
+
+export const DefaultIcon = Icons.Default;
diff --git a/src/utils/mxid.ts b/src/utils/mxid.ts
new file mode 100644
index 0000000..f5651b8
--- /dev/null
+++ b/src/utils/mxid.ts
@@ -0,0 +1,52 @@
+import { Identifier } from "ra-core";
+import { GetConfig } from "../utils/config";
+
+const mxidPattern = /^@[^@:]+:[^@:]+$/;
+
+/*
+ * Check if id is a valid Matrix ID (user)
+ * @param id The ID to check
+ * @returns Whether the ID is a valid Matrix ID
+ */
+export const isMXID = (id: string | Identifier): boolean => mxidPattern.test(id as string);
+
+/**
+ * Check if a user is managed by an application service
+ * @param id The user ID to check
+ * @returns Whether the user is managed by an application service
+ */
+export const isASManaged = (id: string | Identifier): boolean => {
+ return GetConfig().asManagedUsers.some(regex => regex.test(id as string));
+};
+
+/**
+ * Generate a random MXID for current homeserver
+ * @returns full MXID as string
+ */
+export function generateRandomMXID(): string {
+ const homeserver = localStorage.getItem("home_server");
+ const characters = "0123456789abcdefghijklmnopqrstuvwxyz";
+ const localpart = Array.from(crypto.getRandomValues(new Uint32Array(8)))
+ .map(x => characters[x % characters.length])
+ .join("");
+ return `@${localpart}:${homeserver}`;
+}
+
+/**
+ * Return the full MXID from an arbitrary input
+ * @param input the input string
+ * @returns full MXID as string
+ */
+export function returnMXID(input: string | Identifier): string {
+ const homeserver = localStorage.getItem("home_server");
+
+ // Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
+ const mxidPattern = /^@[^@:]+:[^@:]+$/;
+ if (isMXID(input)) {
+ return input as string; // Already a valid MXID
+ }
+
+ // If input is not a valid MXID, assume it's a localpart and construct the MXID
+ const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input;
+ return `@${localpart}:${homeserver}`;
+}
diff --git a/src/utils/password.ts b/src/utils/password.ts
new file mode 100644
index 0000000..408099e
--- /dev/null
+++ b/src/utils/password.ts
@@ -0,0 +1,10 @@
+/**
+ * Generate a random user password
+ * @returns a new random password as string
+ */
+export function generateRandomPassword(length = 64): string {
+ const characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~`!@#$%^&*()_-+={[}]|:;'.?/<>,";
+ return Array.from(crypto.getRandomValues(new Uint32Array(length)))
+ .map(x => characters[x % characters.length])
+ .join("");
+}