Compare commits
27 Commits
0.2.1
..
AMP/2020.05
| Author | SHA1 | Date | |
|---|---|---|---|
| ab649fbf70 | |||
| c41b8ab846 | |||
| 7f16f784f9 | |||
| 1002b6464a | |||
| 50b770a312 | |||
| 2d0ce50444 | |||
| 1fb89c9e58 | |||
| 8a4c0fe0fe | |||
| dd022eab04 | |||
| 880223e5de | |||
| 76fdc80e3e | |||
| 375649756f | |||
| 662735a91f | |||
| 437fd70d6d | |||
| 0823976edd | |||
| d3cd2e9e33 | |||
| 24abcd4e4a | |||
| c1c32e3268 | |||
| ca15435625 | |||
| e9c3901b68 | |||
| 7aec6f9369 | |||
| d2a3f07a59 | |||
| bf7867f106 | |||
| f0e32abc4f | |||
| 61b1580735 | |||
| 0f7e4c1909 | |||
| c9bce409d2 |
@@ -4,6 +4,7 @@
|
|||||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"homepage": ".",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
"url": "https://github.com/Awesome-Technologies/synapse-admin"
|
||||||
@@ -20,7 +21,10 @@
|
|||||||
"prettier": "^2.0.0"
|
"prettier": "^2.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@progress/kendo-drawing": "^1.6.0",
|
||||||
|
"@progress/kendo-react-pdf": "^3.10.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
|
"qrcode.react": "^1.0.0",
|
||||||
"ra-language-german": "^2.1.2",
|
"ra-language-german": "^2.1.2",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-admin": "^3.4.0",
|
"react-admin": "^3.4.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 358 KiB |
+14
-2
@@ -4,12 +4,14 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
|
|||||||
import authProvider from "./synapse/authProvider";
|
import authProvider from "./synapse/authProvider";
|
||||||
import dataProvider from "./synapse/dataProvider";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
import { UserList, UserCreate, UserEdit } from "./components/users";
|
import { UserList, UserCreate, UserEdit } from "./components/users";
|
||||||
import { RoomList } from "./components/rooms";
|
import { RoomList, RoomCreate, RoomShow } from "./components/rooms";
|
||||||
import LoginPage from "./components/LoginPage";
|
import LoginPage from "./components/LoginPage";
|
||||||
import UserIcon from "@material-ui/icons/Group";
|
import UserIcon from "@material-ui/icons/Group";
|
||||||
import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList";
|
import { ViewListIcon as RoomIcon } from "@material-ui/icons/ViewList";
|
||||||
import germanMessages from "./i18n/de";
|
import germanMessages from "./i18n/de";
|
||||||
import englishMessages from "./i18n/en";
|
import englishMessages from "./i18n/en";
|
||||||
|
import ShowUserPdf from "./components/ShowUserPdf";
|
||||||
|
import { Route } from "react-router-dom";
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
@@ -27,6 +29,9 @@ const App = () => (
|
|||||||
authProvider={authProvider}
|
authProvider={authProvider}
|
||||||
dataProvider={dataProvider}
|
dataProvider={dataProvider}
|
||||||
i18nProvider={i18nProvider}
|
i18nProvider={i18nProvider}
|
||||||
|
customRoutes={[
|
||||||
|
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Resource
|
<Resource
|
||||||
name="users"
|
name="users"
|
||||||
@@ -35,8 +40,15 @@ const App = () => (
|
|||||||
edit={UserEdit}
|
edit={UserEdit}
|
||||||
icon={UserIcon}
|
icon={UserIcon}
|
||||||
/>
|
/>
|
||||||
<Resource name="rooms" list={RoomList} icon={RoomIcon} />
|
<Resource
|
||||||
|
name="rooms"
|
||||||
|
list={RoomList}
|
||||||
|
create={RoomCreate}
|
||||||
|
show={RoomShow}
|
||||||
|
icon={RoomIcon}
|
||||||
|
/>
|
||||||
<Resource name="connections" />
|
<Resource name="connections" />
|
||||||
|
<Resource name="servernotices" />
|
||||||
</Admin>
|
</Admin>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+88
-33
@@ -1,13 +1,17 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
fetchUtils,
|
||||||
|
FormDataConsumer,
|
||||||
Notification,
|
Notification,
|
||||||
useLogin,
|
useLogin,
|
||||||
useNotify,
|
useNotify,
|
||||||
useLocale,
|
useLocale,
|
||||||
useSetLocale,
|
useSetLocale,
|
||||||
useTranslate,
|
useTranslate,
|
||||||
|
PasswordInput,
|
||||||
|
TextInput,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
import { Field, Form } from "react-final-form";
|
import { Form, useForm } from "react-final-form";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
@@ -34,7 +38,7 @@ const useStyles = makeStyles(theme => ({
|
|||||||
backgroundSize: "cover",
|
backgroundSize: "cover",
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
minWidth: 300,
|
minWidth: "30em",
|
||||||
marginTop: "6em",
|
marginTop: "6em",
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
@@ -70,7 +74,7 @@ const LoginPage = ({ theme }) => {
|
|||||||
var locale = useLocale();
|
var locale = useLocale();
|
||||||
const setLocale = useSetLocale();
|
const setLocale = useSetLocale();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const homeserver = localStorage.getItem("base_url");
|
const base_url = localStorage.getItem("base_url");
|
||||||
|
|
||||||
const renderInput = ({
|
const renderInput = ({
|
||||||
meta: { touched, error } = {},
|
meta: { touched, error } = {},
|
||||||
@@ -88,15 +92,23 @@ const LoginPage = ({ theme }) => {
|
|||||||
|
|
||||||
const validate = values => {
|
const validate = values => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
if (!values.homeserver) {
|
|
||||||
errors.homeserver = translate("ra.validation.required");
|
|
||||||
}
|
|
||||||
if (!values.username) {
|
if (!values.username) {
|
||||||
errors.username = translate("ra.validation.required");
|
errors.username = translate("ra.validation.required");
|
||||||
}
|
}
|
||||||
if (!values.password) {
|
if (!values.password) {
|
||||||
errors.password = translate("ra.validation.required");
|
errors.password = translate("ra.validation.required");
|
||||||
}
|
}
|
||||||
|
if (!values.base_url) {
|
||||||
|
errors.base_url = translate("ra.validation.required");
|
||||||
|
} else {
|
||||||
|
if (!values.base_url.match(/^(http|https):\/\//)) {
|
||||||
|
errors.base_url = translate("synapseadmin.auth.protocol_error");
|
||||||
|
} else if (
|
||||||
|
!values.base_url.match(/^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/)
|
||||||
|
) {
|
||||||
|
errors.base_url = translate("synapseadmin.auth.url_error");
|
||||||
|
}
|
||||||
|
}
|
||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,9 +127,75 @@ const LoginPage = ({ theme }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractHomeServer = username => {
|
||||||
|
const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/;
|
||||||
|
if (!username) return null;
|
||||||
|
const res = username.match(usernameRegex);
|
||||||
|
if (res) return res[1];
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserData = ({ formData }) => {
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
|
const handleUsernameChange = _ => {
|
||||||
|
if (formData.base_url) return;
|
||||||
|
// check if username is a full qualified userId then set base_url accordially
|
||||||
|
const home_server = extractHomeServer(formData.username);
|
||||||
|
const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`;
|
||||||
|
if (home_server) {
|
||||||
|
// fetch .well-known entry to get base_url
|
||||||
|
fetchUtils
|
||||||
|
.fetchJson(wellKnownUrl, { method: "GET" })
|
||||||
|
.then(({ json }) => {
|
||||||
|
form.change("base_url", json["m.homeserver"].base_url);
|
||||||
|
})
|
||||||
|
.catch(_ => {
|
||||||
|
// if there is no .well-known entry, try the home server name
|
||||||
|
form.change("base_url", `https://${home_server}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={classes.input}>
|
||||||
|
<TextInput
|
||||||
|
autoFocus
|
||||||
|
name="username"
|
||||||
|
component={renderInput}
|
||||||
|
label={translate("ra.auth.username")}
|
||||||
|
disabled={loading}
|
||||||
|
onBlur={handleUsernameChange}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.input}>
|
||||||
|
<PasswordInput
|
||||||
|
name="password"
|
||||||
|
component={renderInput}
|
||||||
|
label={translate("ra.auth.password")}
|
||||||
|
type="password"
|
||||||
|
disabled={loading}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={classes.input}>
|
||||||
|
<TextInput
|
||||||
|
name="base_url"
|
||||||
|
component={renderInput}
|
||||||
|
label={translate("synapseadmin.auth.base_url")}
|
||||||
|
disabled={loading}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
initialValues={{ homeserver: homeserver }}
|
initialValues={{ base_url: base_url }}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
render={({ handleSubmit }) => (
|
render={({ handleSubmit }) => (
|
||||||
@@ -146,32 +224,9 @@ const LoginPage = ({ theme }) => {
|
|||||||
<MenuItem value="en">English</MenuItem>
|
<MenuItem value="en">English</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.input}>
|
<FormDataConsumer>
|
||||||
<Field
|
{formDataProps => <UserData {...formDataProps} />}
|
||||||
autoFocus
|
</FormDataConsumer>
|
||||||
name="homeserver"
|
|
||||||
component={renderInput}
|
|
||||||
label={translate("synapseadmin.auth.homeserver")}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={classes.input}>
|
|
||||||
<Field
|
|
||||||
name="username"
|
|
||||||
component={renderInput}
|
|
||||||
label={translate("ra.auth.username")}
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={classes.input}>
|
|
||||||
<Field
|
|
||||||
name="password"
|
|
||||||
component={renderInput}
|
|
||||||
label={translate("ra.auth.password")}
|
|
||||||
type="password"
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<CardActions className={classes.actions}>
|
<CardActions className={classes.actions}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { SaveButton, useCreate, useRedirect, useNotify } from "react-admin";
|
||||||
|
|
||||||
|
const SaveQrButton = props => {
|
||||||
|
const [create] = useCreate("users");
|
||||||
|
const redirectTo = useRedirect();
|
||||||
|
const notify = useNotify();
|
||||||
|
const { basePath } = props;
|
||||||
|
|
||||||
|
const handleSave = useCallback(
|
||||||
|
(values, redirect) => {
|
||||||
|
create(
|
||||||
|
{
|
||||||
|
payload: { data: { ...values } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: ({ data: newRecord }) => {
|
||||||
|
notify("ra.notification.created", "info", {
|
||||||
|
smart_count: 1,
|
||||||
|
});
|
||||||
|
redirectTo(redirect, basePath, newRecord.id, {
|
||||||
|
password: values.password,
|
||||||
|
...newRecord,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[create, notify, redirectTo, basePath]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <SaveButton {...props} onSave={handleSave} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SaveQrButton;
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { Fragment, useState } from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
SaveButton,
|
||||||
|
SimpleForm,
|
||||||
|
TextInput,
|
||||||
|
Toolbar,
|
||||||
|
required,
|
||||||
|
useCreate,
|
||||||
|
useNotify,
|
||||||
|
useTranslate,
|
||||||
|
} from "react-admin";
|
||||||
|
import MessageIcon from "@material-ui/icons/Message";
|
||||||
|
import IconCancel from "@material-ui/icons/Cancel";
|
||||||
|
import Dialog from "@material-ui/core/Dialog";
|
||||||
|
import DialogContent from "@material-ui/core/DialogContent";
|
||||||
|
import DialogContentText from "@material-ui/core/DialogContentText";
|
||||||
|
import DialogTitle from "@material-ui/core/DialogTitle";
|
||||||
|
|
||||||
|
const ServerNoticeDialog = ({ open, loading, onClose, onSend }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
const ServerNoticeToolbar = props => (
|
||||||
|
<Toolbar {...props}>
|
||||||
|
<SaveButton label="resources.servernotices.action.send" />
|
||||||
|
<Button label="ra.action.cancel" onClick={onClose}>
|
||||||
|
<IconCancel />
|
||||||
|
</Button>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} loading={loading}>
|
||||||
|
<DialogTitle>
|
||||||
|
{translate("resources.servernotices.action.send")}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
{translate("resources.servernotices.helper.send")}
|
||||||
|
</DialogContentText>
|
||||||
|
<SimpleForm
|
||||||
|
toolbar={<ServerNoticeToolbar />}
|
||||||
|
submitOnEnter={false}
|
||||||
|
redirect={false}
|
||||||
|
save={onSend}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
source="body"
|
||||||
|
label="resources.servernotices.fields.body"
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows="4"
|
||||||
|
resettable
|
||||||
|
validate={required()}
|
||||||
|
/>
|
||||||
|
</SimpleForm>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServerNoticeButton = ({ record }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const notify = useNotify();
|
||||||
|
const [create, { loading }] = useCreate("servernotices");
|
||||||
|
|
||||||
|
const handleDialogOpen = () => setOpen(true);
|
||||||
|
const handleDialogClose = () => setOpen(false);
|
||||||
|
|
||||||
|
const handleSend = values => {
|
||||||
|
create(
|
||||||
|
{ payload: { data: { id: record.id, ...values } } },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notify("resources.servernotices.action.send_success");
|
||||||
|
handleDialogClose();
|
||||||
|
},
|
||||||
|
onFailure: () =>
|
||||||
|
notify("resources.servernotices.action.send_failure", "error"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Button
|
||||||
|
label="resources.servernotices.send"
|
||||||
|
onClick={handleDialogOpen}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<MessageIcon />
|
||||||
|
</Button>
|
||||||
|
<ServerNoticeDialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleDialogClose}
|
||||||
|
onSend={handleSend}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Title, Button } from "react-admin";
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import { PDFExport } from "@progress/kendo-react-pdf";
|
||||||
|
import QRCode from "qrcode.react";
|
||||||
|
|
||||||
|
function xor(a, b) {
|
||||||
|
var res = "";
|
||||||
|
for (var i = 0; i < a.length; i++) {
|
||||||
|
res += String.fromCharCode(a.charCodeAt(i) ^ b.charCodeAt(i % b.length));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateQrString(serverUrl, username, password) {
|
||||||
|
const magicString = "wo9k5tep252qxsa5yde7366kugy6c01w7oeeya9hrmpf0t7ii7";
|
||||||
|
var urlString = "user=" + username + "&password=" + password;
|
||||||
|
|
||||||
|
urlString = xor(urlString, magicString); // xor with magic string
|
||||||
|
urlString = btoa(urlString); // to base64
|
||||||
|
|
||||||
|
return serverUrl + "/#" + urlString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShowUserPdf = props => {
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
page: {
|
||||||
|
height: 800,
|
||||||
|
width: 566,
|
||||||
|
padding: "none",
|
||||||
|
backgroundColor: "white",
|
||||||
|
boxShadow: "5px 5px 5px black",
|
||||||
|
margin: "auto",
|
||||||
|
overflowX: "hidden",
|
||||||
|
overflowY: "hidden",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
height: 144,
|
||||||
|
width: 534,
|
||||||
|
marginLeft: 32,
|
||||||
|
marginTop: 15,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
width: 233,
|
||||||
|
fontSize: 40,
|
||||||
|
float: "left",
|
||||||
|
marginTop: 15,
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
width: 90,
|
||||||
|
marginTop: 20,
|
||||||
|
marginRight: 32,
|
||||||
|
float: "left",
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
marginLeft: 330,
|
||||||
|
marginTop: 86,
|
||||||
|
},
|
||||||
|
qr: {
|
||||||
|
marginRight: 40,
|
||||||
|
float: "right",
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
fontSize: 18,
|
||||||
|
marginTop: 100,
|
||||||
|
marginLeft: 32,
|
||||||
|
marginRight: 32,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
var resume;
|
||||||
|
|
||||||
|
const exportPDF = () => {
|
||||||
|
resume.save();
|
||||||
|
};
|
||||||
|
|
||||||
|
var qrCode = "";
|
||||||
|
var displayname = "";
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.location.state &&
|
||||||
|
props.location.state.id &&
|
||||||
|
props.location.state.password
|
||||||
|
) {
|
||||||
|
const { id, password } = props.location.state;
|
||||||
|
|
||||||
|
const username = id.substring(1, id.indexOf(":"));
|
||||||
|
const serverUrl = "https://" + id.substring(id.indexOf(":") + 1);
|
||||||
|
|
||||||
|
const qrString = calculateQrString(serverUrl, username, password);
|
||||||
|
|
||||||
|
qrCode = <QRCode value={qrString} size={128} />;
|
||||||
|
displayname = props.location.state.displayname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title title="PDF" />
|
||||||
|
<Button label="synapseadmin.action.download_pdf" onClick={exportPDF} />
|
||||||
|
|
||||||
|
<PDFExport
|
||||||
|
paperSize={"A4"}
|
||||||
|
fileName="User.pdf"
|
||||||
|
title=""
|
||||||
|
subject=""
|
||||||
|
keywords=""
|
||||||
|
ref={r => (resume = r)}
|
||||||
|
>
|
||||||
|
<div className={classes.page}>
|
||||||
|
<div className={classes.code}>Ihr persönlicher Anmeldecode:</div>
|
||||||
|
<div className={classes.header}>
|
||||||
|
<div className={classes.name}>{displayname}</div>
|
||||||
|
<img className={classes.logo} alt="Logo" src="images/logo.png" />
|
||||||
|
<div className={classes.qr}>{qrCode}</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.note}>
|
||||||
|
Hier können Sie Ihre selbst gewählte Schlüsselsicherungs-Passphrase
|
||||||
|
notieren:
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PDFExport>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowUserPdf;
|
||||||
+152
-2
@@ -1,5 +1,24 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Datagrid, List, TextField, Pagination } from "react-admin";
|
import {
|
||||||
|
AutocompleteArrayInput,
|
||||||
|
BooleanInput,
|
||||||
|
Create,
|
||||||
|
Datagrid,
|
||||||
|
FormTab,
|
||||||
|
List,
|
||||||
|
Pagination,
|
||||||
|
ReferenceArrayField,
|
||||||
|
ReferenceArrayInput,
|
||||||
|
Show,
|
||||||
|
Tab,
|
||||||
|
TabbedForm,
|
||||||
|
TabbedShowLayout,
|
||||||
|
TextField,
|
||||||
|
TextInput,
|
||||||
|
useTranslate,
|
||||||
|
} from "react-admin";
|
||||||
|
import ViewListIcon from "@material-ui/icons/ViewList";
|
||||||
|
import UserIcon from "@material-ui/icons/Group";
|
||||||
|
|
||||||
const RoomPagination = props => (
|
const RoomPagination = props => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
@@ -7,7 +26,7 @@ const RoomPagination = props => (
|
|||||||
|
|
||||||
export const RoomList = props => (
|
export const RoomList = props => (
|
||||||
<List {...props} pagination={<RoomPagination />}>
|
<List {...props} pagination={<RoomPagination />}>
|
||||||
<Datagrid>
|
<Datagrid rowClick="show">
|
||||||
<TextField source="room_id" />
|
<TextField source="room_id" />
|
||||||
<TextField source="name" />
|
<TextField source="name" />
|
||||||
<TextField source="canonical_alias" />
|
<TextField source="canonical_alias" />
|
||||||
@@ -15,3 +34,134 @@ export const RoomList = props => (
|
|||||||
</Datagrid>
|
</Datagrid>
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const validateDisplayName = fieldval =>
|
||||||
|
fieldval === undefined
|
||||||
|
? "synapseadmin.rooms.room_name_required"
|
||||||
|
: fieldval.length === 0
|
||||||
|
? "synapseadmin.rooms.room_name_required"
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
function approximateAliasLength(alias, homeserver) {
|
||||||
|
/* TODO maybe handle punycode in homeserver name */
|
||||||
|
|
||||||
|
var te;
|
||||||
|
|
||||||
|
// Support for TextEncoder is quite widespread, but the polyfill is
|
||||||
|
// pretty large; We will only underestimate the size with the regular
|
||||||
|
// length attribute of String, so we never prevent the user from using
|
||||||
|
// an alias that is short enough for the server, but too long for our
|
||||||
|
// heuristic.
|
||||||
|
try {
|
||||||
|
te = new TextEncoder();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ReferenceError) {
|
||||||
|
te = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasLength = te === undefined ? alias.length : te.encode(alias).length;
|
||||||
|
|
||||||
|
return "#".length + aliasLength + ":".length + homeserver.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateAlias = fieldval => {
|
||||||
|
if (fieldval === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const homeserver = localStorage.getItem("home_server");
|
||||||
|
|
||||||
|
if (approximateAliasLength(fieldval, homeserver) > 255) {
|
||||||
|
return "synapseadmin.rooms.alias_too_long";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLeadingWhitespace = fieldVal =>
|
||||||
|
fieldVal === undefined ? undefined : fieldVal.trimStart();
|
||||||
|
const replaceAllWhitespace = fieldVal =>
|
||||||
|
fieldVal === undefined ? undefined : fieldVal.replace(/\s/, "_");
|
||||||
|
const removeLeadingSigil = fieldVal =>
|
||||||
|
fieldVal === undefined
|
||||||
|
? undefined
|
||||||
|
: fieldVal.startsWith("#")
|
||||||
|
? fieldVal.substr(1)
|
||||||
|
: fieldVal;
|
||||||
|
|
||||||
|
const validateHasAliasIfPublic = formdata => {
|
||||||
|
let errors = {};
|
||||||
|
if (formdata.public) {
|
||||||
|
if (
|
||||||
|
formdata.canonical_alias === undefined ||
|
||||||
|
formdata.canonical_alias.trim().length === 0
|
||||||
|
) {
|
||||||
|
errors.canonical_alias = "synapseadmin.rooms.alias_required_if_public";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoomCreate = props => (
|
||||||
|
<Create {...props}>
|
||||||
|
<TabbedForm validate={validateHasAliasIfPublic}>
|
||||||
|
<FormTab label="synapseadmin.rooms.details" icon={<ViewListIcon />}>
|
||||||
|
<TextInput
|
||||||
|
source="name"
|
||||||
|
parse={removeLeadingWhitespace}
|
||||||
|
validate={validateDisplayName}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
source="canonical_alias"
|
||||||
|
parse={fv => replaceAllWhitespace(removeLeadingSigil(fv))}
|
||||||
|
validate={validateAlias}
|
||||||
|
placeholder="#"
|
||||||
|
/>
|
||||||
|
<BooleanInput source="public" label="synapseadmin.rooms.make_public" />
|
||||||
|
</FormTab>
|
||||||
|
<FormTab
|
||||||
|
label="resources.rooms.fields.invite_members"
|
||||||
|
icon={<UserIcon />}
|
||||||
|
>
|
||||||
|
<ReferenceArrayInput
|
||||||
|
reference="users"
|
||||||
|
source="invitees"
|
||||||
|
filterToQuery={searchText => ({ user_id: searchText })}
|
||||||
|
>
|
||||||
|
<AutocompleteArrayInput
|
||||||
|
optionText="displayname"
|
||||||
|
suggestionText="displayname"
|
||||||
|
/>
|
||||||
|
</ReferenceArrayInput>
|
||||||
|
</FormTab>
|
||||||
|
</TabbedForm>
|
||||||
|
</Create>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RoomTitle = ({ record }) => {
|
||||||
|
const translate = useTranslate();
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{translate("resources.rooms.name", 1)} {record ? `"${record.name}"` : ""}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const RoomShow = props => (
|
||||||
|
<Show {...props} title={<RoomTitle />}>
|
||||||
|
<TabbedShowLayout>
|
||||||
|
<Tab label="synapseadmin.rooms.details" icon={<ViewListIcon />}>
|
||||||
|
<TextField source="id" disabled />
|
||||||
|
<TextField source="name" />
|
||||||
|
<TextField source="canonical_alias" />
|
||||||
|
<TextField source="join_rules" />
|
||||||
|
<TextField source="guest_access" />
|
||||||
|
</Tab>
|
||||||
|
<Tab label="resources.rooms.fields.joined_members" icon={<UserIcon />}>
|
||||||
|
<ReferenceArrayField reference="users" source="members">
|
||||||
|
<Datagrid>
|
||||||
|
<TextField source="id" />
|
||||||
|
<TextField source="displayname" />
|
||||||
|
</Datagrid>
|
||||||
|
</ReferenceArrayField>
|
||||||
|
</Tab>
|
||||||
|
</TabbedShowLayout>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
|||||||
+90
-21
@@ -21,6 +21,7 @@ import {
|
|||||||
PasswordInput,
|
PasswordInput,
|
||||||
TextField,
|
TextField,
|
||||||
TextInput,
|
TextInput,
|
||||||
|
SearchInput,
|
||||||
ReferenceField,
|
ReferenceField,
|
||||||
SelectInput,
|
SelectInput,
|
||||||
BulkDeleteButton,
|
BulkDeleteButton,
|
||||||
@@ -30,6 +31,8 @@ import {
|
|||||||
useTranslate,
|
useTranslate,
|
||||||
Pagination,
|
Pagination,
|
||||||
} from "react-admin";
|
} from "react-admin";
|
||||||
|
import SaveQrButton from "./SaveQrButton";
|
||||||
|
import { ServerNoticeButton } from "./ServerNotices";
|
||||||
|
|
||||||
const UserPagination = props => (
|
const UserPagination = props => (
|
||||||
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
<Pagination {...props} rowsPerPageOptions={[10, 25, 50, 100, 500, 1000]} />
|
||||||
@@ -37,6 +40,7 @@ const UserPagination = props => (
|
|||||||
|
|
||||||
const UserFilter = props => (
|
const UserFilter = props => (
|
||||||
<Filter {...props}>
|
<Filter {...props}>
|
||||||
|
<SearchInput source="user_id" alwaysOn />
|
||||||
<BooleanInput source="guests" alwaysOn />
|
<BooleanInput source="guests" alwaysOn />
|
||||||
<BooleanInput
|
<BooleanInput
|
||||||
label="resources.users.fields.show_deactivated"
|
label="resources.users.fields.show_deactivated"
|
||||||
@@ -68,24 +72,9 @@ export const UserList = props => (
|
|||||||
pagination={<UserPagination />}
|
pagination={<UserPagination />}
|
||||||
>
|
>
|
||||||
<Datagrid rowClick="edit">
|
<Datagrid rowClick="edit">
|
||||||
<ReferenceField
|
<ImageField source="avatar_url" title="displayname" />
|
||||||
source="Avatar"
|
<TextField source="id" sortable={false} />
|
||||||
reference="users"
|
<TextField source="displayname" />
|
||||||
link={false}
|
|
||||||
sortable={false}
|
|
||||||
>
|
|
||||||
<ImageField source="avatar_url" title="displayname" />
|
|
||||||
</ReferenceField>
|
|
||||||
<TextField source="id" />
|
|
||||||
{/* Hack since the users endpoint does not give displaynames in the list*/}
|
|
||||||
<ReferenceField
|
|
||||||
source="name"
|
|
||||||
reference="users"
|
|
||||||
link={false}
|
|
||||||
sortable={false}
|
|
||||||
>
|
|
||||||
<TextField source="displayname" />
|
|
||||||
</ReferenceField>
|
|
||||||
<BooleanField source="is_guest" sortable={false} />
|
<BooleanField source="is_guest" sortable={false} />
|
||||||
<BooleanField source="admin" sortable={false} />
|
<BooleanField source="admin" sortable={false} />
|
||||||
<BooleanField source="deactivated" sortable={false} />
|
<BooleanField source="deactivated" sortable={false} />
|
||||||
@@ -93,6 +82,75 @@ export const UserList = props => (
|
|||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function generateRandomUser() {
|
||||||
|
const homeserver = localStorage.getItem("home_server");
|
||||||
|
const user_id =
|
||||||
|
"@" +
|
||||||
|
Array(8)
|
||||||
|
.fill("0123456789abcdefghijklmnopqrstuvwxyz")
|
||||||
|
.map(
|
||||||
|
x =>
|
||||||
|
x[
|
||||||
|
Math.floor(
|
||||||
|
(crypto.getRandomValues(new Uint32Array(1))[0] /
|
||||||
|
(0xffffffff + 1)) *
|
||||||
|
x.length
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.join("") +
|
||||||
|
":" +
|
||||||
|
homeserver;
|
||||||
|
|
||||||
|
const password = Array(20)
|
||||||
|
.fill(
|
||||||
|
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~!@-#$"
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
x =>
|
||||||
|
x[
|
||||||
|
Math.floor(
|
||||||
|
(crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)) *
|
||||||
|
x.length
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user_id,
|
||||||
|
password: password,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect to the related Author show page
|
||||||
|
const redirect = (basePath, id, data) => {
|
||||||
|
return {
|
||||||
|
pathname: "/showpdf",
|
||||||
|
state: {
|
||||||
|
id: data.id,
|
||||||
|
displayname: data.displayname,
|
||||||
|
password: data.password,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserCreateToolbar = props => (
|
||||||
|
<Toolbar {...props}>
|
||||||
|
<SaveQrButton
|
||||||
|
label="synapseadmin.action.save_and_show"
|
||||||
|
redirect={redirect}
|
||||||
|
submitOnEnter={true}
|
||||||
|
/>
|
||||||
|
<SaveButton
|
||||||
|
label="synapseadmin.action.save_only"
|
||||||
|
redirect="list"
|
||||||
|
submitOnEnter={false}
|
||||||
|
variant="text"
|
||||||
|
/>
|
||||||
|
</Toolbar>
|
||||||
|
);
|
||||||
|
|
||||||
// https://matrix.org/docs/spec/appendices#user-identifiers
|
// https://matrix.org/docs/spec/appendices#user-identifiers
|
||||||
const validateUser = regex(
|
const validateUser = regex(
|
||||||
/^@[a-z0-9._=\-/]+:.*/,
|
/^@[a-z0-9._=\-/]+:.*/,
|
||||||
@@ -103,18 +161,29 @@ const UserEditToolbar = props => {
|
|||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
return (
|
return (
|
||||||
<Toolbar {...props}>
|
<Toolbar {...props}>
|
||||||
<SaveButton submitOnEnter={true} />
|
<SaveQrButton
|
||||||
|
label="synapseadmin.action.save_and_show"
|
||||||
|
redirect={redirect}
|
||||||
|
submitOnEnter={true}
|
||||||
|
/>
|
||||||
|
<SaveButton
|
||||||
|
label="synapseadmin.action.save_only"
|
||||||
|
redirect="list"
|
||||||
|
submitOnEnter={false}
|
||||||
|
variant="text"
|
||||||
|
/>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
title={translate("resources.users.helper.erase")}
|
title={translate("resources.users.helper.erase")}
|
||||||
/>
|
/>
|
||||||
|
<ServerNoticeButton />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserCreate = props => (
|
export const UserCreate = props => (
|
||||||
<Create {...props}>
|
<Create record={generateRandomUser()} {...props}>
|
||||||
<SimpleForm>
|
<SimpleForm toolbar={<UserCreateToolbar />}>
|
||||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
<PasswordInput source="password" autoComplete="new-password" />
|
<PasswordInput source="password" autoComplete="new-password" />
|
||||||
|
|||||||
+67
-1
@@ -4,13 +4,31 @@ export default {
|
|||||||
...germanMessages,
|
...germanMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
homeserver: "Heimserver",
|
base_url: "Heimserver URL",
|
||||||
welcome: "Willkommen bei Synapse-admin",
|
welcome: "Willkommen bei Synapse-admin",
|
||||||
|
username_error: "Bitte vollständigen Nutzernamen angeben: '@user:domain'",
|
||||||
|
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
|
||||||
|
url_error: "Keine gültige Matrix Server URL",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
save_and_show: "Speichern und QR Code erzeugen",
|
||||||
|
save_only: "Nur speichern",
|
||||||
|
download_pdf: "PDF speichern",
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id:
|
invalid_user_id:
|
||||||
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver",
|
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver",
|
||||||
},
|
},
|
||||||
|
rooms: {
|
||||||
|
details: "Raumdetails",
|
||||||
|
room_name: "Raumname",
|
||||||
|
make_public: "Öffentlicher Raum",
|
||||||
|
room_name_required: "Muss angegeben werden",
|
||||||
|
alias_required_if_public: "Muss für öffentliche Räume angegeben werden.",
|
||||||
|
alias: "Alias",
|
||||||
|
alias_too_long:
|
||||||
|
"Darf zusammen mit der Domain des Homeservers 255 bytes nicht überschreiten",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
@@ -50,6 +68,8 @@ export default {
|
|||||||
name: "Name",
|
name: "Name",
|
||||||
canonical_alias: "Alias",
|
canonical_alias: "Alias",
|
||||||
joined_members: "Mitglieder",
|
joined_members: "Mitglieder",
|
||||||
|
invite_members: "Mitglieder einladen",
|
||||||
|
invitees: "Einladungen",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
@@ -60,5 +80,51 @@ export default {
|
|||||||
user_agent: "User Agent",
|
user_agent: "User Agent",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
servernotices: {
|
||||||
|
name: "Serverbenachrichtigungen",
|
||||||
|
send: "Servernachricht versenden",
|
||||||
|
fields: {
|
||||||
|
body: "Nachricht",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Sende Nachricht",
|
||||||
|
send_success: "Nachricht erfolgreich versendet.",
|
||||||
|
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send:
|
||||||
|
'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ra: {
|
||||||
|
...germanMessages.ra,
|
||||||
|
auth: {
|
||||||
|
...germanMessages.ra.auth,
|
||||||
|
auth_check_error: "Anmeldung fehlgeschlagen",
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
...germanMessages.ra.input,
|
||||||
|
password: {
|
||||||
|
...germanMessages.ra.input.password,
|
||||||
|
toggle_hidden: "Anzeigen",
|
||||||
|
toggle_visible: "Verstecken",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notification: {
|
||||||
|
...germanMessages.ra.notifiaction,
|
||||||
|
logged_out: "Abgemeldet",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ra: {
|
||||||
|
...germanMessages.ra,
|
||||||
|
input: {
|
||||||
|
...germanMessages.ra.input,
|
||||||
|
password: {
|
||||||
|
...germanMessages.ra.input.password,
|
||||||
|
toggle_hidden: "Anzeigen",
|
||||||
|
toggle_visible: "Verstecken",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-1
@@ -4,13 +4,31 @@ export default {
|
|||||||
...englishMessages,
|
...englishMessages,
|
||||||
synapseadmin: {
|
synapseadmin: {
|
||||||
auth: {
|
auth: {
|
||||||
homeserver: "Homeserver",
|
base_url: "Homeserver URL",
|
||||||
welcome: "Welcome to Synapse-admin",
|
welcome: "Welcome to Synapse-admin",
|
||||||
|
username_error: "Please enter fully qualified user ID: '@user:domain'",
|
||||||
|
protocol_error: "URL has to start with 'http://' or 'https://'",
|
||||||
|
url_error: "Not a valid Matrix server URL",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
save_and_show: "Create QR code",
|
||||||
|
save_only: "Save",
|
||||||
|
download_pdf: "Download PDF",
|
||||||
},
|
},
|
||||||
users: {
|
users: {
|
||||||
invalid_user_id:
|
invalid_user_id:
|
||||||
"Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver",
|
"Must be a fully qualified Matrix user-id, e.g. @user_id:homeserver",
|
||||||
},
|
},
|
||||||
|
rooms: {
|
||||||
|
details: "Room Details",
|
||||||
|
room_name: "Room Name",
|
||||||
|
make_public: "Make room public",
|
||||||
|
room_name_required: "Must be provided",
|
||||||
|
alias_required_if_public: "Must be provided for a public room",
|
||||||
|
alias: "Alias",
|
||||||
|
alias_too_long:
|
||||||
|
"Must not exceed 255 bytes including the domain of the homeserver.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resources: {
|
resources: {
|
||||||
users: {
|
users: {
|
||||||
@@ -50,6 +68,9 @@ export default {
|
|||||||
name: "Name",
|
name: "Name",
|
||||||
canonical_alias: "Alias",
|
canonical_alias: "Alias",
|
||||||
joined_members: "Members",
|
joined_members: "Members",
|
||||||
|
invite_members: "Invite Members",
|
||||||
|
|
||||||
|
invitees: "Invitations",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
@@ -60,5 +81,21 @@ export default {
|
|||||||
user_agent: "User agent",
|
user_agent: "User agent",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
servernotices: {
|
||||||
|
name: "Server Notices",
|
||||||
|
send: "Send server notices",
|
||||||
|
fields: {
|
||||||
|
body: "Message",
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
send: "Send note",
|
||||||
|
send_success: "Server notice successfully sent.",
|
||||||
|
send_failure: "An error has occurred.",
|
||||||
|
},
|
||||||
|
helper: {
|
||||||
|
send:
|
||||||
|
'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +1,8 @@
|
|||||||
import { fetchUtils } from "react-admin";
|
import { fetchUtils } from "react-admin";
|
||||||
|
|
||||||
const ensureHttpsForUrl = url => {
|
|
||||||
if (/^https:\/\//i.test(url)) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
const domain = url.replace(/http.?:\/\//g, "");
|
|
||||||
return "https://" + domain;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stripTrailingSlash = str => {
|
|
||||||
if (!str) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return str.endsWith("/") ? str.slice(0, -1) : str;
|
|
||||||
};
|
|
||||||
|
|
||||||
const authProvider = {
|
const authProvider = {
|
||||||
// called when the user attempts to log in
|
// called when the user attempts to log in
|
||||||
login: ({ homeserver, username, password }) => {
|
login: ({ base_url, username, password }) => {
|
||||||
console.log("login ");
|
console.log("login ");
|
||||||
const options = {
|
const options = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -28,17 +13,16 @@ const authProvider = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = window.decodeURIComponent(homeserver);
|
// use the base_url from login instead of the well_known entry from the
|
||||||
const trimmed_url = url.trim().replace(/\s/g, "");
|
// server, since the admin might want to access the admin API via some
|
||||||
const login_api_url =
|
// private address
|
||||||
ensureHttpsForUrl(trimmed_url) + "/_matrix/client/r0/login";
|
localStorage.setItem("base_url", base_url);
|
||||||
|
|
||||||
|
const decoded_base_url = window.decodeURIComponent(base_url);
|
||||||
|
const login_api_url = decoded_base_url + "/_matrix/client/r0/login";
|
||||||
|
|
||||||
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
|
return fetchUtils.fetchJson(login_api_url, options).then(({ json }) => {
|
||||||
const normalized_base_url = stripTrailingSlash(
|
localStorage.setItem("home_server", json.home_server);
|
||||||
json.well_known["m.homeserver"].base_url
|
|
||||||
);
|
|
||||||
localStorage.setItem("base_url", normalized_base_url);
|
|
||||||
localStorage.setItem("home_server_url", json.home_server);
|
|
||||||
localStorage.setItem("user_id", json.user_id);
|
localStorage.setItem("user_id", json.user_id);
|
||||||
localStorage.setItem("access_token", json.access_token);
|
localStorage.setItem("access_token", json.access_token);
|
||||||
localStorage.setItem("device_id", json.device_id);
|
localStorage.setItem("device_id", json.device_id);
|
||||||
|
|||||||
+41
-14
@@ -23,13 +23,15 @@ const resourceMap = {
|
|||||||
is_guest: !!u.is_guest,
|
is_guest: !!u.is_guest,
|
||||||
admin: !!u.admin,
|
admin: !!u.admin,
|
||||||
deactivated: !!u.deactivated,
|
deactivated: !!u.deactivated,
|
||||||
|
displayname: u.display_name || u.displayname,
|
||||||
}),
|
}),
|
||||||
data: "users",
|
data: "users",
|
||||||
total: (json, from, perPage) => {
|
total: json => json.total,
|
||||||
return json.next_token
|
create: data => ({
|
||||||
? parseInt(json.next_token, 10) + perPage
|
endpoint: `/_synapse/admin/v2/users/${data.id}`,
|
||||||
: from + json.users.length;
|
body: data,
|
||||||
},
|
method: "PUT",
|
||||||
|
}),
|
||||||
delete: id => ({
|
delete: id => ({
|
||||||
endpoint: `/_synapse/admin/v1/deactivate/${id}`,
|
endpoint: `/_synapse/admin/v1/deactivate/${id}`,
|
||||||
body: { erase: true },
|
body: { erase: true },
|
||||||
@@ -41,13 +43,22 @@ const resourceMap = {
|
|||||||
map: r => ({
|
map: r => ({
|
||||||
...r,
|
...r,
|
||||||
id: r.room_id,
|
id: r.room_id,
|
||||||
alias: r.canonical_alias,
|
|
||||||
members: r.joined_members,
|
|
||||||
}),
|
}),
|
||||||
data: "rooms",
|
data: "rooms",
|
||||||
total: json => {
|
total: json => json.total_rooms,
|
||||||
return json.total_rooms;
|
create: data => ({
|
||||||
},
|
endpoint: "/_matrix/client/r0/createRoom",
|
||||||
|
body: {
|
||||||
|
name: data.name,
|
||||||
|
room_alias_name: data.canonical_alias,
|
||||||
|
visibility: data.public ? "public" : "private",
|
||||||
|
invite:
|
||||||
|
Array.isArray(data.invitees) && data.invitees.length > 0
|
||||||
|
? data.invitees
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
},
|
},
|
||||||
connections: {
|
connections: {
|
||||||
path: "/_synapse/admin/v1/whois",
|
path: "/_synapse/admin/v1/whois",
|
||||||
@@ -57,6 +68,20 @@ const resourceMap = {
|
|||||||
}),
|
}),
|
||||||
data: "connections",
|
data: "connections",
|
||||||
},
|
},
|
||||||
|
servernotices: {
|
||||||
|
map: n => ({ id: n.event_id }),
|
||||||
|
create: data => ({
|
||||||
|
endpoint: "/_synapse/admin/v1/send_server_notice",
|
||||||
|
body: {
|
||||||
|
user_id: data.id,
|
||||||
|
content: {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: data.body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function filterNullValues(key, value) {
|
function filterNullValues(key, value) {
|
||||||
@@ -190,11 +215,13 @@ const dataProvider = {
|
|||||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||||
|
|
||||||
const res = resourceMap[resource];
|
const res = resourceMap[resource];
|
||||||
|
if (!("create" in res)) return Promise.reject();
|
||||||
|
|
||||||
const endpoint_url = homeserver + res.path;
|
const create = res["create"](params.data);
|
||||||
return jsonClient(`${endpoint_url}/${params.data.id}`, {
|
const endpoint_url = homeserver + create.endpoint;
|
||||||
method: "PUT",
|
return jsonClient(endpoint_url, {
|
||||||
body: JSON.stringify(params.data, filterNullValues),
|
method: create.method,
|
||||||
|
body: JSON.stringify(create.body, filterNullValues),
|
||||||
}).then(({ json }) => ({
|
}).then(({ json }) => ({
|
||||||
data: res.map(json),
|
data: res.map(json),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1246,6 +1246,25 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
||||||
|
|
||||||
|
"@progress/kendo-drawing@^1.6.0":
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@progress/kendo-drawing/-/kendo-drawing-1.6.0.tgz#66e9df431f52c7dd9fd5567be80dcbfa3a162281"
|
||||||
|
integrity sha512-9dGlFvW9fMgqAgcbLi+SfTeMUpNYdoVthwNxwAtsRQ+QwcgXJcdzFpLrLBXp17pXpDDFpiOyMqiwjffNGwtc3w==
|
||||||
|
dependencies:
|
||||||
|
pako "^1.0.5"
|
||||||
|
|
||||||
|
"@progress/kendo-file-saver@^1.0.1":
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@progress/kendo-file-saver/-/kendo-file-saver-1.0.7.tgz#5b602115d1b0b5e26f3e52451a3ed7c29ed76c51"
|
||||||
|
integrity sha512-8tsho/+DATzfTW4BBaHrkF3C3jqH2/bQ+XbjqA0KfmTiBRVK6ygK+tkvkYeDhFlQBbJ02MmJlEC6OmXvXRFkUg==
|
||||||
|
|
||||||
|
"@progress/kendo-react-pdf@^3.10.1":
|
||||||
|
version "3.10.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@progress/kendo-react-pdf/-/kendo-react-pdf-3.10.1.tgz#348517daaddb366bbe840a92ec2fffbfd07ac2d2"
|
||||||
|
integrity sha512-2EKfQCwLFEa+mgCLKQ70iWVu7q2Dh/wJl6pPJ6Ix42BA7SiA2k5UmDk819FLY9pnMgv7WDxwqBP+8CvdkLoP5w==
|
||||||
|
dependencies:
|
||||||
|
"@progress/kendo-file-saver" "^1.0.1"
|
||||||
|
|
||||||
"@redux-saga/core@^1.1.3":
|
"@redux-saga/core@^1.1.3":
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4"
|
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4"
|
||||||
@@ -7919,7 +7938,7 @@ p-try@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||||
|
|
||||||
pako@~1.0.5:
|
pako@^1.0.5, pako@~1.0.5:
|
||||||
version "1.0.11"
|
version "1.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||||
@@ -9087,6 +9106,20 @@ q@^1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||||
|
|
||||||
|
qr.js@0.0.0:
|
||||||
|
version "0.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
|
||||||
|
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=
|
||||||
|
|
||||||
|
qrcode.react@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-1.0.0.tgz#7e8889db3b769e555e8eb463d4c6de221c36d5de"
|
||||||
|
integrity sha512-jBXleohRTwvGBe1ngV+62QvEZ/9IZqQivdwzo9pJM4LQMoCM2VnvNBnKdjvGnKyDZ/l0nCDgsPod19RzlPvm/Q==
|
||||||
|
dependencies:
|
||||||
|
loose-envify "^1.4.0"
|
||||||
|
prop-types "^15.6.0"
|
||||||
|
qr.js "0.0.0"
|
||||||
|
|
||||||
qs@6.7.0:
|
qs@6.7.0:
|
||||||
version "6.7.0"
|
version "6.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||||
|
|||||||
Reference in New Issue
Block a user