Compare commits
36 Commits
master
...
AMP/2021.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
af51cca461 | ||
![]() |
364234f4f4 | ||
![]() |
1fe0b2f330 | ||
![]() |
1aaa137afe | ||
![]() |
7b5c0e2845 | ||
![]() |
da8cb12756 | ||
![]() |
56a359b704 | ||
![]() |
5906dcc129 | ||
![]() |
270d48607a | ||
![]() |
931fafc21d | ||
![]() |
c604b47adc | ||
![]() |
fb8cff3e3e | ||
![]() |
725e24d944 | ||
![]() |
dd00a76603 | ||
![]() |
2915fd3e5b | ||
![]() |
a4662c2557 | ||
![]() |
f6ca169fbc | ||
![]() |
07862591fd | ||
![]() |
ab649fbf70 | ||
![]() |
880223e5de | ||
![]() |
76fdc80e3e | ||
![]() |
375649756f | ||
![]() |
662735a91f | ||
![]() |
0823976edd | ||
![]() |
d3cd2e9e33 | ||
![]() |
24abcd4e4a | ||
![]() |
c1c32e3268 | ||
![]() |
ca15435625 | ||
![]() |
e9c3901b68 | ||
![]() |
7aec6f9369 | ||
![]() |
d2a3f07a59 | ||
![]() |
bf7867f106 | ||
![]() |
f0e32abc4f | ||
![]() |
61b1580735 | ||
![]() |
0f7e4c1909 | ||
![]() |
c9bce409d2 |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "synapse-admin",
|
||||
"version": "0.8.2",
|
||||
"version": "AMP/2021.07",
|
||||
"description": "Admin GUI for the Matrix.org server Synapse",
|
||||
"author": "Awesome Technologies Innovationslabor GmbH",
|
||||
"license": "Apache-2.0",
|
||||
@ -21,8 +21,12 @@
|
||||
"ra-test": "^3.15.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@progress/kendo-drawing": "^1.6.0",
|
||||
"@progress/kendo-react-pdf": "^3.10.1",
|
||||
"babel-preset-jest": "^24.9.0",
|
||||
"papaparse": "^5.2.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"qrcode.react": "^1.0.0",
|
||||
"ra-language-chinese": "^2.0.10",
|
||||
"ra-language-german": "^3.13.4",
|
||||
"react": "^17.0.0",
|
||||
|
BIN
public/fonts/DejaVu/DejaVuSans-Bold.ttf
Normal file
BIN
public/fonts/DejaVu/DejaVuSans-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/DejaVu/DejaVuSans-Mono.ttf
Normal file
BIN
public/fonts/DejaVu/DejaVuSans-Mono.ttf
Normal file
Binary file not shown.
BIN
public/fonts/DejaVu/DejaVuSans-Oblique.ttf
Normal file
BIN
public/fonts/DejaVu/DejaVuSans-Oblique.ttf
Normal file
Binary file not shown.
BIN
public/fonts/DejaVu/DejaVuSans.ttf
Normal file
BIN
public/fonts/DejaVu/DejaVuSans.ttf
Normal file
Binary file not shown.
BIN
public/images/logo.png
Normal file
BIN
public/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 358 KiB |
@ -9,6 +9,32 @@
|
||||
name="description"
|
||||
content="Synapse-Admin"
|
||||
/>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans";
|
||||
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans";
|
||||
font-weight: bold;
|
||||
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Bold.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans";
|
||||
font-style: italic;
|
||||
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Oblique.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans";
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Oblique.ttf") format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "DejaVu Sans Mono";
|
||||
src: url("%PUBLIC_URL%/fonts/DejaVu/DejaVuSans-Mono.ttf") format("truetype");
|
||||
}
|
||||
</style>
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
@ -46,4 +72,4 @@
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
17
src/App.js
17
src/App.js
@ -4,8 +4,9 @@ import polyglotI18nProvider from "ra-i18n-polyglot";
|
||||
import authProvider from "./synapse/authProvider";
|
||||
import dataProvider from "./synapse/dataProvider";
|
||||
import { UserList, UserCreate, UserEdit } from "./components/users";
|
||||
import { RoomList, RoomShow } from "./components/rooms";
|
||||
import { RoomList, RoomCreate, RoomShow, RoomEdit } from "./components/rooms";
|
||||
import { ReportList, ReportShow } from "./components/EventReports";
|
||||
import ImportFeature from "./components/ImportFeature";
|
||||
import LoginPage from "./components/LoginPage";
|
||||
import UserIcon from "@material-ui/icons/Group";
|
||||
import EqualizerIcon from "@material-ui/icons/Equalizer";
|
||||
@ -13,12 +14,12 @@ import { UserMediaStatsList } from "./components/statistics";
|
||||
import RoomIcon from "@material-ui/icons/ViewList";
|
||||
import ReportIcon from "@material-ui/icons/Warning";
|
||||
import FolderSharedIcon from "@material-ui/icons/FolderShared";
|
||||
import { ImportFeature } from "./components/ImportFeature";
|
||||
import { RoomDirectoryList } from "./components/RoomDirectory";
|
||||
import { Route } from "react-router-dom";
|
||||
import germanMessages from "./i18n/de";
|
||||
import englishMessages from "./i18n/en";
|
||||
import chineseMessages from "./i18n/zh";
|
||||
import ShowUserPdf from "./components/ShowUserPdf";
|
||||
|
||||
// TODO: Can we use lazy loading together with browser locale?
|
||||
const messages = {
|
||||
@ -39,7 +40,8 @@ const App = () => (
|
||||
dataProvider={dataProvider}
|
||||
i18nProvider={i18nProvider}
|
||||
customRoutes={[
|
||||
<Route key="userImport" path="/import_users" component={ImportFeature} />,
|
||||
<Route key="csvImport" path="/importcsv" component={ImportFeature} />,
|
||||
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
|
||||
]}
|
||||
>
|
||||
<Resource
|
||||
@ -49,7 +51,14 @@ const App = () => (
|
||||
edit={UserEdit}
|
||||
icon={UserIcon}
|
||||
/>
|
||||
<Resource name="rooms" list={RoomList} show={RoomShow} icon={RoomIcon} />
|
||||
<Resource
|
||||
name="rooms"
|
||||
list={RoomList}
|
||||
create={RoomCreate}
|
||||
show={RoomShow}
|
||||
edit={RoomEdit}
|
||||
icon={RoomIcon}
|
||||
/>
|
||||
<Resource
|
||||
name="user_media_statistics"
|
||||
list={UserMediaStatsList}
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
import { useTranslate } from "ra-core";
|
||||
import Container from "@material-ui/core/Container/Container";
|
||||
import { generateRandomUser } from "./users";
|
||||
import ShowUserPdf from "./ShowUserPdf";
|
||||
|
||||
const LOGGING = true;
|
||||
|
||||
@ -59,6 +60,8 @@ const FilePicker = props => {
|
||||
|
||||
const [progress, setProgress] = useState(null);
|
||||
|
||||
const [pdfRecords, setPdfRecords] = useState(null);
|
||||
|
||||
const [importResults, setImportResults] = useState(null);
|
||||
const [skippedRecords, setSkippedRecords] = useState(null);
|
||||
|
||||
@ -66,17 +69,23 @@ const FilePicker = props => {
|
||||
const [passwordMode, setPasswordMode] = useState(true);
|
||||
const [useridMode, setUseridMode] = useState("ignore");
|
||||
|
||||
const [showingPdf, setShowingPdf] = useState(false);
|
||||
|
||||
const translate = useTranslate();
|
||||
const notify = useNotify();
|
||||
|
||||
const dataProvider = useDataProvider();
|
||||
|
||||
const onFileChange = async e => {
|
||||
if (progress !== null) return;
|
||||
|
||||
if (progress !== null) {
|
||||
return;
|
||||
}
|
||||
if (LOGGING) console.log("onFileChange was called");
|
||||
setValues(null);
|
||||
setError(null);
|
||||
setStats(null);
|
||||
setPdfRecords(null);
|
||||
|
||||
setImportResults(null);
|
||||
const file = e.target.files ? e.target.files[0] : null;
|
||||
/* Let's refuse some unreasonably big files instead of freezing
|
||||
@ -126,6 +135,11 @@ const FilePicker = props => {
|
||||
});
|
||||
|
||||
if (eF.length !== 0) {
|
||||
if (LOGGING) {
|
||||
console.log(meta.fields);
|
||||
console.log(eF);
|
||||
console.log(oF);
|
||||
}
|
||||
setError(
|
||||
translate("import_users.error.required_field", { field: eF[0] })
|
||||
);
|
||||
@ -226,6 +240,9 @@ const FilePicker = props => {
|
||||
setProgress,
|
||||
setError
|
||||
);
|
||||
|
||||
setPdfRecords(results.recordsForPdf);
|
||||
|
||||
setImportResults(results);
|
||||
// offer CSV download of skipped or errored records
|
||||
// (so that the user doesn't have to filter out successful
|
||||
@ -251,6 +268,8 @@ const FilePicker = props => {
|
||||
let skippedRecords = [];
|
||||
let erroredRecords = [];
|
||||
let succeededRecords = [];
|
||||
let recordsForPdf = [];
|
||||
|
||||
let changeStats = {
|
||||
toAdmin: 0,
|
||||
toGuest: 0,
|
||||
@ -365,6 +384,14 @@ const FilePicker = props => {
|
||||
await dataProvider.create("users", { data: recordData });
|
||||
}
|
||||
succeededRecords.push(recordData);
|
||||
|
||||
if (recordData.password !== undefined) {
|
||||
recordsForPdf.push({
|
||||
id: recordData.id,
|
||||
password: recordData.password,
|
||||
displayname: recordData.displayname,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -389,6 +416,7 @@ const FilePicker = props => {
|
||||
erroredRecords,
|
||||
succeededRecords,
|
||||
totalRecordCount: entriesCount,
|
||||
recordsForPdf,
|
||||
changeStats,
|
||||
wasDryRun: dryRun,
|
||||
};
|
||||
@ -618,6 +646,10 @@ const FilePicker = props => {
|
||||
<br />,
|
||||
]
|
||||
: ""}
|
||||
{translate(
|
||||
"import_users.cards.results.for_print",
|
||||
importResults.recordsForPdf.length
|
||||
)}
|
||||
<br />
|
||||
{importResults.wasDryRun && [
|
||||
translate("import_users.cards.results.simulated_only"),
|
||||
@ -655,20 +687,43 @@ const FilePicker = props => {
|
||||
</CardActions>
|
||||
);
|
||||
|
||||
let allCards = [];
|
||||
if (uploadCard) allCards.push(uploadCard);
|
||||
if (errorCards) allCards.push(errorCards);
|
||||
if (conflictCards) allCards.push(conflictCards);
|
||||
if (statsCards) allCards.push(...statsCards);
|
||||
if (startImportCard) allCards.push(startImportCard);
|
||||
if (resultsCard) allCards.push(resultsCard);
|
||||
let pdfDisplay =
|
||||
pdfRecords && showingPdf && pdfRecords.length ? (
|
||||
<ShowUserPdf records={pdfRecords} />
|
||||
) : null;
|
||||
|
||||
let cardContainer = <Card>{allCards}</Card>;
|
||||
let pdfActions = pdfRecords ? (
|
||||
<CardActions>
|
||||
<Button
|
||||
size="large"
|
||||
onClick={e => {
|
||||
setShowingPdf(true);
|
||||
}}
|
||||
>
|
||||
{translate("import_users.goToPdf")}
|
||||
</Button>
|
||||
</CardActions>
|
||||
) : null;
|
||||
|
||||
return [
|
||||
<Title defaultTitle={translate("import_users.title")} />,
|
||||
cardContainer,
|
||||
];
|
||||
if (pdfRecords && showingPdf) {
|
||||
return <Card>{pdfDisplay}</Card>;
|
||||
} else {
|
||||
let allCards = [];
|
||||
if (uploadCard) allCards.push(uploadCard);
|
||||
if (errorCards) allCards.push(errorCards);
|
||||
if (conflictCards) allCards.push(conflictCards);
|
||||
if (statsCards) allCards.push(...statsCards);
|
||||
if (startImportCard) allCards.push(startImportCard);
|
||||
if (resultsCard) allCards.push(resultsCard);
|
||||
if (pdfActions) allCards.push(pdfActions);
|
||||
|
||||
let cardContainer = <Card>{allCards}</Card>;
|
||||
|
||||
return [
|
||||
<Title defaultTitle={translate("import_users.title")} />,
|
||||
cardContainer,
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
export const ImportFeature = FilePicker;
|
||||
|
35
src/components/SaveQrButton.js
Normal file
35
src/components/SaveQrButton.js
Normal file
@ -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;
|
263
src/components/ShowUserPdf.js
Normal file
263
src/components/ShowUserPdf.js
Normal file
@ -0,0 +1,263 @@
|
||||
import React, { useRef } 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";
|
||||
import { string, any } from "prop-types";
|
||||
|
||||
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";
|
||||
const origUrlString = "user=" + username + "&password=" + password;
|
||||
|
||||
var urlString = xor(origUrlString, magicString); // xor with magic string
|
||||
if (origUrlString !== xor(urlString, magicString)) {
|
||||
console.error(
|
||||
"xoring this url string with magicString twice gave different results:",
|
||||
origUrlString,
|
||||
urlString,
|
||||
xor(urlString, magicString)
|
||||
);
|
||||
}
|
||||
urlString = btoa(urlString); // to base64
|
||||
|
||||
return serverUrl + "/#" + urlString;
|
||||
}
|
||||
|
||||
UserPdfPage.propTypes = {
|
||||
classes: any,
|
||||
displayname: string,
|
||||
qrCode: any,
|
||||
serverUrl: string,
|
||||
username: string,
|
||||
password: string,
|
||||
};
|
||||
|
||||
function UserPdfPage({
|
||||
classes,
|
||||
displayname,
|
||||
qrCode,
|
||||
serverUrl,
|
||||
username,
|
||||
password,
|
||||
}) {
|
||||
return (
|
||||
<div className={classes.page}>
|
||||
<div className={classes.header}>
|
||||
<div className={classes.name}>{displayname}</div>
|
||||
<img className={classes.logo} alt="Logo" src="images/logo.png" />
|
||||
</div>
|
||||
<div className={classes.body}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td width="200px">
|
||||
<div className={classes.code_note}>
|
||||
Ihr persönlicher Anmeldecode:
|
||||
</div>
|
||||
</td>
|
||||
<td className={classes.table_cell}>
|
||||
<div className={classes.credentials_note}>
|
||||
Ihre persönlichen Zugangsdaten:
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div className={classes.qr}>{qrCode}</div>
|
||||
</td>
|
||||
<td className={classes.table_cell}>
|
||||
<div className={classes.credentials_text}>
|
||||
<br />
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Heimserver:</td>
|
||||
<td>
|
||||
<span className={classes.credentials}>
|
||||
{serverUrl}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Benutzername:</td>
|
||||
<td>
|
||||
<span className={classes.credentials}>
|
||||
{username}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Passwort:</td>
|
||||
<td>
|
||||
<span className={classes.credentials}>
|
||||
{password}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={classes.note}>
|
||||
Hier können Sie Ihre selbst gewählte Schlüsselsicherungs-Passphrase
|
||||
notieren:
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
page: {
|
||||
height: 800,
|
||||
width: 566,
|
||||
padding: "none",
|
||||
backgroundColor: "white",
|
||||
boxShadow: "5px 5px 5px black",
|
||||
margin: "auto",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
fontFamily: "DejaVu Sans, Sans-Serif",
|
||||
fontSize: 15,
|
||||
},
|
||||
header: {
|
||||
height: 144,
|
||||
width: 534,
|
||||
marginLeft: 32,
|
||||
marginTop: 15,
|
||||
},
|
||||
name: {
|
||||
width: 240,
|
||||
fontSize: 35,
|
||||
float: "left",
|
||||
marginTop: 100,
|
||||
},
|
||||
logo: {
|
||||
width: 90,
|
||||
marginTop: 50,
|
||||
marginRight: 70,
|
||||
float: "right",
|
||||
},
|
||||
body: {
|
||||
clear: "both",
|
||||
},
|
||||
table_cell: {
|
||||
verticalAlign: "top",
|
||||
},
|
||||
code_note: {
|
||||
marginLeft: 32,
|
||||
marginTop: 86,
|
||||
},
|
||||
qr: {
|
||||
marginTop: 15,
|
||||
marginLeft: 32,
|
||||
},
|
||||
credentials_note: {
|
||||
marginTop: 86,
|
||||
marginLeft: 10,
|
||||
},
|
||||
credentials_text: {
|
||||
marginLeft: 10,
|
||||
fontSize: 12,
|
||||
},
|
||||
credentials: {
|
||||
fontFamily: "DejaVu Sans Mono, monospace",
|
||||
},
|
||||
note: {
|
||||
fontSize: 18,
|
||||
marginTop: 100,
|
||||
marginLeft: 32,
|
||||
marginRight: 32,
|
||||
},
|
||||
}));
|
||||
|
||||
const ShowUserPdf = props => {
|
||||
const classes = useStyles();
|
||||
const userPdf = useRef(null);
|
||||
|
||||
const exportPDF = () => {
|
||||
userPdf.current.save();
|
||||
};
|
||||
|
||||
let userRecords;
|
||||
|
||||
if (props.records) {
|
||||
userRecords = props.records;
|
||||
}
|
||||
|
||||
if (
|
||||
props.location &&
|
||||
props.location.state &&
|
||||
props.location.state.id &&
|
||||
props.location.state.password
|
||||
) {
|
||||
userRecords = [
|
||||
{
|
||||
id: props.location.state.id,
|
||||
password: props.location.state.password,
|
||||
displayname: props.location.state.displayname,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title title="PDF" />
|
||||
<Button label="synapseadmin.action.download_pdf" onClick={exportPDF} />
|
||||
|
||||
<PDFExport
|
||||
paperSize={"A4"}
|
||||
fileName="Users.pdf"
|
||||
title=""
|
||||
subject=""
|
||||
keywords=""
|
||||
ref={userPdf}
|
||||
//ref={r => (resume = r)}
|
||||
>
|
||||
{userRecords.map(record => {
|
||||
if (record.id && record.password) {
|
||||
const username = record.id.substring(1, record.id.indexOf(":"));
|
||||
const serverUrl =
|
||||
"https://" + record.id.substring(record.id.indexOf(":") + 1);
|
||||
const qrString = calculateQrString(
|
||||
serverUrl,
|
||||
username,
|
||||
record.password
|
||||
);
|
||||
const qrCode = <QRCode value={qrString} size={128} />;
|
||||
return (
|
||||
<UserPdfPage
|
||||
classes={classes}
|
||||
displayname={record.displayname}
|
||||
qrCode={qrCode}
|
||||
serverUrl={serverUrl}
|
||||
username={username}
|
||||
password={record.password}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
/* Skip empty PDF pages */
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
</PDFExport>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowUserPdf;
|
@ -1,31 +1,56 @@
|
||||
import React, { Fragment } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Route, Link } from "react-router-dom";
|
||||
import {
|
||||
AutocompleteArrayInput,
|
||||
AutocompleteInput,
|
||||
BooleanInput,
|
||||
BooleanField,
|
||||
BulkDeleteButton,
|
||||
DateField,
|
||||
Button,
|
||||
Create,
|
||||
Edit,
|
||||
Datagrid,
|
||||
DateField,
|
||||
DeleteButton,
|
||||
Filter,
|
||||
FormTab,
|
||||
List,
|
||||
NumberField,
|
||||
Pagination,
|
||||
ReferenceArrayInput,
|
||||
ReferenceField,
|
||||
ReferenceInput,
|
||||
ReferenceManyField,
|
||||
SearchInput,
|
||||
SelectField,
|
||||
Show,
|
||||
SimpleForm,
|
||||
Tab,
|
||||
TabbedForm,
|
||||
TabbedShowLayout,
|
||||
TextField,
|
||||
TextInput,
|
||||
Toolbar,
|
||||
TopToolbar,
|
||||
useDataProvider,
|
||||
useRecordContext,
|
||||
useRefresh,
|
||||
useTranslate,
|
||||
} from "react-admin";
|
||||
import get from "lodash/get";
|
||||
import PropTypes from "prop-types";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { Tooltip, Typography, Chip } from "@material-ui/core";
|
||||
import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
Chip,
|
||||
Drawer,
|
||||
styled,
|
||||
withStyles,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from "@material-ui/core";
|
||||
import FastForwardIcon from "@material-ui/icons/FastForward";
|
||||
import HttpsIcon from "@material-ui/icons/Https";
|
||||
import NoEncryptionIcon from "@material-ui/icons/NoEncryption";
|
||||
@ -33,6 +58,7 @@ import PageviewIcon from "@material-ui/icons/Pageview";
|
||||
import UserIcon from "@material-ui/icons/Group";
|
||||
import ViewListIcon from "@material-ui/icons/ViewList";
|
||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||
import ContentSave from "@material-ui/icons/Save";
|
||||
import EventIcon from "@material-ui/icons/Event";
|
||||
import {
|
||||
RoomDirectoryBulkDeleteButton,
|
||||
@ -78,20 +104,368 @@ const EncryptionField = ({ source, record = {}, emptyText }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const RoomTitle = ({ record }) => {
|
||||
const translate = useTranslate();
|
||||
var name = "";
|
||||
if (record) {
|
||||
name = record.name !== "" ? record.name : record.id;
|
||||
const validateDisplayName = fieldval => {
|
||||
return fieldval == null
|
||||
? "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="#"
|
||||
/>
|
||||
<ReferenceInput
|
||||
reference="users"
|
||||
source="owner"
|
||||
filterToQuery={searchText => ({ user_id: searchText })}
|
||||
>
|
||||
<AutocompleteInput
|
||||
optionText="displayname"
|
||||
suggestionText="displayname"
|
||||
/>
|
||||
</ReferenceInput>
|
||||
<BooleanInput source="public" label="synapseadmin.rooms.make_public" />
|
||||
<BooleanInput
|
||||
source="encrypt"
|
||||
initialValue={true}
|
||||
label="synapseadmin.rooms.encrypt"
|
||||
/>
|
||||
</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)} {name}
|
||||
{translate("resources.rooms.name", 1)} {record ? `"${record.name}"` : ""}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// Explicitely passing "to" prop
|
||||
// Toolbar adds all kinds of unsupported props to its children :(
|
||||
const StyledLink = styles => {
|
||||
const Styled = styled(Link)(styles);
|
||||
return ({ to, children }) => <Styled to={to}>{children}</Styled>;
|
||||
};
|
||||
|
||||
const RoomMemberEditToolbar = ({ backLink, translate, onSave, ...props }) => {
|
||||
const SaveLink = StyledLink({
|
||||
textDecoration: "none",
|
||||
});
|
||||
const CancelLink = StyledLink({
|
||||
textDecoration: "none",
|
||||
marginLeft: "1em",
|
||||
});
|
||||
const SaveIcon = styled(ContentSave)({
|
||||
width: "1rem",
|
||||
marginRight: "0.25em",
|
||||
});
|
||||
|
||||
return (
|
||||
<Toolbar {...props}>
|
||||
<SaveLink to={backLink}>
|
||||
<Button onClick={onSave} variant="contained">
|
||||
<React.Fragment>
|
||||
<SaveIcon />
|
||||
{translate("ra.action.save")}
|
||||
</React.Fragment>
|
||||
</Button>
|
||||
</SaveLink>
|
||||
<CancelLink to={backLink}>
|
||||
<Button>
|
||||
<React.Fragment>{translate("ra.action.cancel")}</React.Fragment>
|
||||
</Button>
|
||||
</CancelLink>
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
|
||||
const RoomMemberIdField = ({ memberId, data = {} }) => {
|
||||
const value = get(data[memberId], "id");
|
||||
|
||||
return (
|
||||
<Typography component="span" variant="body2">
|
||||
{value}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
const RoomMemberRoleInput = ({ memberId, data = {}, translate, onChange }) => {
|
||||
const roleValue = get(data[memberId], "role");
|
||||
const [role, setRole] = React.useState(roleValue);
|
||||
|
||||
React.useEffect(() => {
|
||||
onChange(roleValue);
|
||||
}, [onChange, roleValue]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={role}
|
||||
onChange={event => {
|
||||
setRole(event.target.value);
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
>
|
||||
<MenuItem value={"user"}>
|
||||
{translate("resources.users.roles.user")}
|
||||
</MenuItem>
|
||||
<MenuItem value={"mod"}>
|
||||
{translate("resources.users.roles.mod")}
|
||||
</MenuItem>
|
||||
<MenuItem value={"admin"}>
|
||||
{translate("resources.users.roles.admin")}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const RoomMemberEdit = ({ backLink, memberId, ...props }) => {
|
||||
const translate = useTranslate();
|
||||
const refresh = useRefresh();
|
||||
const dataProvider = useDataProvider();
|
||||
|
||||
const [role, setRole] = React.useState();
|
||||
|
||||
const { id } = props;
|
||||
|
||||
return (
|
||||
<Edit title=" " {...props}>
|
||||
<SimpleForm
|
||||
toolbar={
|
||||
<RoomMemberEditToolbar
|
||||
backLink={backLink}
|
||||
translate={translate}
|
||||
onSave={() => {
|
||||
dataProvider
|
||||
.update("rooms", {
|
||||
data: {
|
||||
id,
|
||||
member_roles: [{ member_id: memberId, role }],
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
refresh();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ReferenceManyField
|
||||
reference="room_members"
|
||||
target="room_id"
|
||||
label="resources.users.fields.id"
|
||||
>
|
||||
<RoomMemberIdField memberId={memberId} />
|
||||
</ReferenceManyField>
|
||||
<ReferenceManyField
|
||||
reference="room_members"
|
||||
target="room_id"
|
||||
label="resources.users.fields.role"
|
||||
>
|
||||
<RoomMemberRoleInput
|
||||
memberId={memberId}
|
||||
translate={translate}
|
||||
onChange={setRole}
|
||||
/>
|
||||
</ReferenceManyField>
|
||||
</SimpleForm>
|
||||
</Edit>
|
||||
);
|
||||
};
|
||||
|
||||
const drawerStyles = {
|
||||
paper: {
|
||||
width: 300,
|
||||
},
|
||||
};
|
||||
const StyledDrawer = withStyles(drawerStyles)(({ classes, ...props }) => (
|
||||
<Drawer {...props} classes={classes} />
|
||||
));
|
||||
|
||||
export const RoomEdit = props => {
|
||||
const translate = useTranslate();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Edit {...props} title={<RoomTitle />}>
|
||||
<TabbedForm>
|
||||
<FormTab label="synapseadmin.rooms.tabs.members" icon={<UserIcon />}>
|
||||
<ReferenceArrayInput
|
||||
reference="users"
|
||||
source="invitees"
|
||||
filterToQuery={searchText => ({ user_id: searchText })}
|
||||
>
|
||||
<AutocompleteArrayInput
|
||||
optionText="displayname"
|
||||
suggestionText="displayname"
|
||||
/>
|
||||
</ReferenceArrayInput>
|
||||
|
||||
<ReferenceManyField
|
||||
reference="room_members"
|
||||
target="room_id"
|
||||
addLabel={false}
|
||||
>
|
||||
<Datagrid
|
||||
style={{ width: "100%" }}
|
||||
rowClick={(id, basePath, record) =>
|
||||
`/rooms/${encodeURIComponent(
|
||||
record.parentId
|
||||
)}/${encodeURIComponent(id)}`
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
source="id"
|
||||
sortable={false}
|
||||
label="resources.users.fields.id"
|
||||
/>
|
||||
<ReferenceField
|
||||
label="resources.users.fields.displayname"
|
||||
source="id"
|
||||
reference="users"
|
||||
sortable={false}
|
||||
link=""
|
||||
>
|
||||
<TextField source="displayname" sortable={false} />
|
||||
</ReferenceField>
|
||||
<SelectField
|
||||
source="role"
|
||||
label="resources.users.fields.role"
|
||||
choices={[
|
||||
{
|
||||
id: "user",
|
||||
name: translate("resources.users.roles.user"),
|
||||
},
|
||||
{ id: "mod", name: translate("resources.users.roles.mod") },
|
||||
{
|
||||
id: "admin",
|
||||
name: translate("resources.users.roles.admin"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Datagrid>
|
||||
</ReferenceManyField>
|
||||
</FormTab>
|
||||
</TabbedForm>
|
||||
</Edit>
|
||||
<Route path="/rooms/:roomId/:memberId">
|
||||
{({ match }) => {
|
||||
const isMatch = !!match && !!match.params;
|
||||
|
||||
return (
|
||||
<StyledDrawer open={isMatch} anchor="right">
|
||||
{isMatch ? (
|
||||
<RoomMemberEdit
|
||||
{...props}
|
||||
memberId={
|
||||
isMatch ? decodeURIComponent(match.params.memberId) : null
|
||||
}
|
||||
backLink={`/rooms/${match.params.roomId}`}
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</StyledDrawer>
|
||||
);
|
||||
}}
|
||||
</Route>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const RoomShowActions = ({ basePath, data, resource }) => {
|
||||
var roomDirectoryStatus = "";
|
||||
if (data) {
|
||||
|
@ -12,10 +12,12 @@ import {
|
||||
ArrayInput,
|
||||
ArrayField,
|
||||
Button,
|
||||
CreateButton,
|
||||
Datagrid,
|
||||
DateField,
|
||||
Create,
|
||||
Edit,
|
||||
ExportButton,
|
||||
List,
|
||||
Filter,
|
||||
Toolbar,
|
||||
@ -28,9 +30,10 @@ import {
|
||||
PasswordInput,
|
||||
TextField,
|
||||
TextInput,
|
||||
SearchInput,
|
||||
ReferenceField,
|
||||
ReferenceManyField,
|
||||
SearchInput,
|
||||
SelectField,
|
||||
SelectInput,
|
||||
BulkDeleteButton,
|
||||
DeleteButton,
|
||||
@ -38,20 +41,19 @@ import {
|
||||
regex,
|
||||
useTranslate,
|
||||
Pagination,
|
||||
CreateButton,
|
||||
ExportButton,
|
||||
TopToolbar,
|
||||
sanitizeListRestProps,
|
||||
NumberField,
|
||||
} from "react-admin";
|
||||
import { Link } from "react-router-dom";
|
||||
import SaveQrButton from "./SaveQrButton";
|
||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
|
||||
import { DeviceRemoveButton } from "./devices";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const redirect = () => {
|
||||
return {
|
||||
pathname: "/import_users",
|
||||
pathname: "/importcsv",
|
||||
};
|
||||
};
|
||||
|
||||
@ -171,12 +173,48 @@ export const UserList = props => {
|
||||
<TextField source="displayname" />
|
||||
<BooleanField source="is_guest" />
|
||||
<BooleanField source="admin" />
|
||||
<SelectField
|
||||
source="user_type"
|
||||
choices={[
|
||||
{ id: null, name: "resources.users.type.default" },
|
||||
{ id: "free", name: "resources.users.type.free" },
|
||||
{ id: "limited", name: "resources.users.type.limited" },
|
||||
]}
|
||||
/>
|
||||
<BooleanField source="deactivated" />
|
||||
</Datagrid>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
// redirect to the related Author show page
|
||||
const redirectToPdf = (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={redirectToPdf}
|
||||
submitOnEnter={true}
|
||||
/>
|
||||
<SaveButton
|
||||
label="synapseadmin.action.save_only"
|
||||
redirect="list"
|
||||
submitOnEnter={false}
|
||||
variant="text"
|
||||
/>
|
||||
</Toolbar>
|
||||
);
|
||||
|
||||
// https://matrix.org/docs/spec/appendices#user-identifiers
|
||||
const validateUser = regex(
|
||||
/^@[a-z0-9._=\-/]+:.*/,
|
||||
@ -228,7 +266,17 @@ const UserEditToolbar = props => {
|
||||
const translate = useTranslate();
|
||||
return (
|
||||
<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
|
||||
label="resources.users.action.erase"
|
||||
confirmTitle={translate("resources.users.helper.erase", {
|
||||
@ -242,12 +290,20 @@ const UserEditToolbar = props => {
|
||||
};
|
||||
|
||||
export const UserCreate = props => (
|
||||
<Create {...props}>
|
||||
<SimpleForm>
|
||||
<Create record={generateRandomUser()} {...props}>
|
||||
<SimpleForm toolbar={<UserCreateToolbar />}>
|
||||
<TextInput source="id" autoComplete="off" validate={validateUser} />
|
||||
<TextInput source="displayname" />
|
||||
<PasswordInput source="password" autoComplete="new-password" />
|
||||
<BooleanInput source="admin" />
|
||||
<SelectInput
|
||||
source="user_type"
|
||||
choices={[
|
||||
{ id: null, name: "resources.users.type.default" },
|
||||
{ id: "free", name: "resources.users.type.free" },
|
||||
{ id: "limited", name: "resources.users.type.limited" },
|
||||
]}
|
||||
/>
|
||||
<ArrayInput source="threepids">
|
||||
<SimpleFormIterator>
|
||||
<SelectInput
|
||||
@ -275,6 +331,7 @@ const UserTitle = ({ record }) => {
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserEdit = props => {
|
||||
const classes = useStyles();
|
||||
const translate = useTranslate();
|
||||
@ -293,6 +350,15 @@ export const UserEdit = props => {
|
||||
<TextInput source="id" disabled />
|
||||
<TextInput source="displayname" />
|
||||
<PasswordInput source="password" autoComplete="new-password" />
|
||||
<SelectInput
|
||||
source="user_type"
|
||||
choices={[
|
||||
{ id: null, name: "resources.users.type.default" },
|
||||
{ id: "free", name: "resources.users.type.free" },
|
||||
{ id: "limited", name: "resources.users.type.limited" },
|
||||
]}
|
||||
emptyText="resources.users.type.default"
|
||||
/>
|
||||
<BooleanInput source="admin" />
|
||||
<BooleanInput
|
||||
source="deactivated"
|
||||
|
@ -11,12 +11,25 @@ const de = {
|
||||
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: {
|
||||
invalid_user_id:
|
||||
"Muss eine vollständige Matrix Benutzer-ID sein, z.B. @benutzer_id:homeserver",
|
||||
},
|
||||
rooms: {
|
||||
details: "Raumdetails",
|
||||
room_name: "Raumname",
|
||||
make_public: "Öffentlicher Raum",
|
||||
encrypt: "Verschlüsselter 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",
|
||||
tabs: {
|
||||
basic: "Allgemein",
|
||||
members: "Mitglieder",
|
||||
@ -91,6 +104,8 @@ const de = {
|
||||
with_error:
|
||||
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
|
||||
simulated_only: "Import-Vorgang war nur simuliert",
|
||||
for_print:
|
||||
"%{smart_count} Eintrag zum Drucken verfügbar |||| %{smart_count} Einträge zum Drucken verfügbar",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -120,6 +135,18 @@ const de = {
|
||||
address: "Adresse",
|
||||
creation_ts_ms: "Zeitpunkt der Erstellung",
|
||||
consent_version: "Zugestimmte Geschäftsbedingungen",
|
||||
user_type: "Kontotyp",
|
||||
// Devices:
|
||||
device_id: "Geräte-ID",
|
||||
display_name: "Gerätename",
|
||||
last_seen_ts: "Zeitstempel",
|
||||
last_seen_ip: "IP-Adresse",
|
||||
role: "Rolle",
|
||||
},
|
||||
type: {
|
||||
default: "Standard",
|
||||
free: "Basic",
|
||||
limited: "Eingeschränkt",
|
||||
},
|
||||
helper: {
|
||||
deactivate:
|
||||
@ -129,6 +156,11 @@ const de = {
|
||||
action: {
|
||||
erase: "Lösche Benutzerdaten",
|
||||
},
|
||||
roles: {
|
||||
user: "Nutzer",
|
||||
mod: "Moderator",
|
||||
admin: "Administrator",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
name: "Raum |||| Räume",
|
||||
@ -137,6 +169,8 @@ const de = {
|
||||
name: "Name",
|
||||
canonical_alias: "Alias",
|
||||
joined_members: "Mitglieder",
|
||||
invite_members: "Mitglieder einladen",
|
||||
invitees: "Einladungen",
|
||||
joined_local_members: "Lokale Mitglieder",
|
||||
joined_local_devices: "Lokale Endgeräte",
|
||||
state_events: "Zustandsereignisse / Komplexität",
|
||||
@ -258,8 +292,7 @@ const de = {
|
||||
send_failure: "Beim Versenden ist ein Fehler aufgetreten.",
|
||||
},
|
||||
helper: {
|
||||
send:
|
||||
"Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
|
||||
send: "Diese API löscht die lokalen Medien von der Festplatte des eigenen Servers. Dies umfasst alle lokalen Miniaturbilder und Kopien von Medien. Diese API wirkt sich nicht auf Medien aus, die sich in externen Medien-Repositories befinden.",
|
||||
},
|
||||
},
|
||||
pushers: {
|
||||
@ -288,8 +321,7 @@ const de = {
|
||||
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.',
|
||||
send: 'Sendet eine Serverbenachrichtigung an die ausgewählten Nutzer. Hierfür muss das Feature "Server Notices" auf dem Server aktiviert sein.',
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
|
@ -11,11 +11,25 @@ const en = {
|
||||
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: {
|
||||
invalid_user_id:
|
||||
"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",
|
||||
encrypt: "Encrypt room",
|
||||
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.",
|
||||
tabs: {
|
||||
basic: "Basic",
|
||||
members: "Members",
|
||||
@ -90,6 +104,8 @@ const en = {
|
||||
with_error:
|
||||
"%{smart_count} entry with errors ||| %{smart_count} entries with errors",
|
||||
simulated_only: "Run was only simulated",
|
||||
for_print:
|
||||
"%{smart_count} entry available for printing |||| %{smart_count} entries available for printing",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -119,6 +135,17 @@ const en = {
|
||||
address: "Address",
|
||||
creation_ts_ms: "Creation timestamp",
|
||||
consent_version: "Consent version",
|
||||
// Devices:
|
||||
device_id: "Device-ID",
|
||||
display_name: "Device name",
|
||||
last_seen_ts: "Timestamp",
|
||||
last_seen_ip: "IP address",
|
||||
role: "Role",
|
||||
},
|
||||
type: {
|
||||
default: "Standard",
|
||||
free: "Basic",
|
||||
limited: "Limited",
|
||||
},
|
||||
helper: {
|
||||
deactivate: "You must provide a password to re-activate an account.",
|
||||
@ -127,6 +154,11 @@ const en = {
|
||||
action: {
|
||||
erase: "Erase user data",
|
||||
},
|
||||
roles: {
|
||||
user: "User",
|
||||
mod: "Moderator",
|
||||
admin: "Administrator",
|
||||
},
|
||||
},
|
||||
rooms: {
|
||||
name: "Room |||| Rooms",
|
||||
@ -135,6 +167,8 @@ const en = {
|
||||
name: "Name",
|
||||
canonical_alias: "Alias",
|
||||
joined_members: "Members",
|
||||
invite_members: "Invite Members",
|
||||
invitees: "Invitations",
|
||||
joined_local_members: "Local members",
|
||||
joined_local_devices: "Local devices",
|
||||
state_events: "State events / Complexity",
|
||||
@ -254,8 +288,7 @@ const en = {
|
||||
send_failure: "An error has occurred.",
|
||||
},
|
||||
helper: {
|
||||
send:
|
||||
"This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
|
||||
send: "This API deletes the local media from the disk of your own server. This includes any local thumbnails and copies of media downloaded. This API will not affect media that has been uploaded to external media repositories.",
|
||||
},
|
||||
},
|
||||
pushers: {
|
||||
@ -284,8 +317,7 @@ const en = {
|
||||
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.',
|
||||
send: 'Sends a server notice to the selected users. The feature "Server Notices" has to be activated at the server.',
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
|
@ -245,8 +245,7 @@ const zh = {
|
||||
send_failure: "出现了一个错误。",
|
||||
},
|
||||
helper: {
|
||||
send:
|
||||
"这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
||||
send: "这个API会删除您硬盘上的本地媒体。包含了任何的本地缓存和下载的媒体备份。这个API不会影响上传到外部媒体存储库上的媒体文件。",
|
||||
},
|
||||
},
|
||||
pushers: {
|
||||
@ -275,8 +274,7 @@ const zh = {
|
||||
send_failure: "出现了一个错误。",
|
||||
},
|
||||
helper: {
|
||||
send:
|
||||
'向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
|
||||
send: '向选中的用户发送服务器提示。服务器配置中的 "服务器提示(Server Notices)" 选项需要被设置为启用。',
|
||||
},
|
||||
},
|
||||
user_media_statistics: {
|
||||
|
@ -25,6 +25,13 @@ const mxcUrlToHttp = mxcUrl => {
|
||||
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`;
|
||||
};
|
||||
|
||||
const POWER_LEVELS = {
|
||||
admin: 100,
|
||||
mod: 50,
|
||||
user: 0,
|
||||
};
|
||||
const roleToPowerLevel = role => POWER_LEVELS[role] || 0;
|
||||
|
||||
const resourceMap = {
|
||||
users: {
|
||||
path: "/_synapse/admin/v2/users",
|
||||
@ -35,6 +42,7 @@ const resourceMap = {
|
||||
is_guest: !!u.is_guest,
|
||||
admin: !!u.admin,
|
||||
deactivated: !!u.deactivated,
|
||||
displayname: u.display_name || u.displayname,
|
||||
// need timestamp in milliseconds
|
||||
creation_ts_ms: u.creation_ts * 1000,
|
||||
}),
|
||||
@ -63,8 +71,40 @@ const resourceMap = {
|
||||
public: !!r.public,
|
||||
}),
|
||||
data: "rooms",
|
||||
total: json => {
|
||||
return json.total_rooms;
|
||||
total: json => json.total_rooms,
|
||||
create: data => ({
|
||||
endpoint: "/_synapse/admin/v1/rooms",
|
||||
body: {
|
||||
owner: data.owner,
|
||||
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,
|
||||
initial_state: data.encrypt
|
||||
? [
|
||||
{
|
||||
type: "m.room.encryption",
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
},
|
||||
method: "POST",
|
||||
}),
|
||||
transformBeforeUpdate: data => {
|
||||
return {
|
||||
...data,
|
||||
member_roles: (data.member_roles || []).map(member => ({
|
||||
member_id: member.member_id,
|
||||
power_level: roleToPowerLevel(member.role),
|
||||
})),
|
||||
};
|
||||
},
|
||||
delete: params => ({
|
||||
endpoint: `/_synapse/admin/v1/rooms/${params.id}`,
|
||||
@ -298,7 +338,7 @@ const dataProvider = {
|
||||
},
|
||||
|
||||
getOne: (resource, params) => {
|
||||
console.log("getOne " + resource);
|
||||
console.log("getOne " + resource, params);
|
||||
const homeserver = localStorage.getItem("base_url");
|
||||
if (!homeserver || !(resource in resourceMap)) return Promise.reject();
|
||||
|
||||
@ -359,10 +399,13 @@ const dataProvider = {
|
||||
|
||||
const res = resourceMap[resource];
|
||||
|
||||
const transform = res.transformBeforeUpdate || (x => x);
|
||||
const data = transform(params.data);
|
||||
|
||||
const endpoint_url = homeserver + res.path;
|
||||
return jsonClient(`${endpoint_url}/${params.data.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(params.data, filterNullValues),
|
||||
body: JSON.stringify(data, filterNullValues),
|
||||
}).then(({ json }) => ({
|
||||
data: res.map(json),
|
||||
}));
|
||||
|
52
yarn.lock
52
yarn.lock
@ -569,7 +569,7 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.10.4"
|
||||
|
||||
"@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
|
||||
"@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
|
||||
integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
|
||||
@ -1584,6 +1584,25 @@
|
||||
schema-utils "^2.6.5"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@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":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@redux-saga/core/-/core-1.1.3.tgz#3085097b57a4ea8db5528d58673f20ce0950f6a4"
|
||||
@ -2797,6 +2816,13 @@ babel-plugin-istanbul@^6.0.0:
|
||||
istanbul-lib-instrument "^4.0.0"
|
||||
test-exclude "^6.0.0"
|
||||
|
||||
babel-plugin-jest-hoist@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756"
|
||||
integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==
|
||||
dependencies:
|
||||
"@types/babel__traverse" "^7.0.6"
|
||||
|
||||
babel-plugin-jest-hoist@^26.6.2:
|
||||
version "26.6.2"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d"
|
||||
@ -2881,6 +2907,14 @@ babel-preset-current-node-syntax@^1.0.0:
|
||||
"@babel/plugin-syntax-optional-chaining" "^7.8.3"
|
||||
"@babel/plugin-syntax-top-level-await" "^7.8.3"
|
||||
|
||||
babel-preset-jest@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc"
|
||||
integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==
|
||||
dependencies:
|
||||
"@babel/plugin-syntax-object-rest-spread" "^7.0.0"
|
||||
babel-plugin-jest-hoist "^24.9.0"
|
||||
|
||||
babel-preset-jest@^26.6.2:
|
||||
version "26.6.2"
|
||||
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee"
|
||||
@ -8245,7 +8279,7 @@ p-try@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||
|
||||
pako@~1.0.5:
|
||||
pako@^1.0.5, pako@~1.0.5:
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
|
||||
@ -9356,6 +9390,20 @@ q@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
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:
|
||||
version "6.7.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||
|
Loading…
Reference in New Issue
Block a user