Compare commits

...

37 Commits

Author SHA1 Message Date
Manuel Stahl
444f648191 Merge tag '0.8.3' into amp.chat
Change-Id: I011f6b7d22f636af768bac3c88527eeb300198ce
2021-08-25 16:00:00 +01:00
Manuel Stahl
af51cca461 Merge tag '0.8.2' into amp.chat
Change-Id: I791cad80267ddaa61966d3c2da9767e1a6a3c589
2021-07-05 16:00:00 +01:00
Manuel Stahl
364234f4f4 Merge tag '0.8.1' into amp.chat
Change-Id: Ice2d3474ef9284bcfea81a4e0043d798edcfaa03
2021-05-25 16:00:00 +01:00
Manuel Stahl
1fe0b2f330 Fix linting errors
Change-Id: Ie804aa67de43f66ecb5d7890826285b09656c2e5
2021-11-23 07:47:31 +00:00
Timo Paulssen
1aaa137afe Import users from CSV
Change-Id: Id05363ecc39aee4fdc4ac6afbcb039558b2a17ed
2021-08-25 09:39:49 +02:00
Manuel Stahl
7b5c0e2845 Merge tag '0.8.0' into amp.chat
Change-Id: I2e362a911083149c82a8c11b6c4594bb4c760a33
2021-05-04 15:07:42 +02:00
Manuel Stahl
da8cb12756 Merge tag '0.7.2' into amp.chat
Change-Id: Ideb662d56977082af5757fed21573ff25ca52e27
2021-05-04 14:32:28 +02:00
Manuel Stahl
56a359b704 Merge tag '0.7.1' into amp.chat
Change-Id: Ib19b2cd22bae62b22057a3782ac978f83097fdd5
2021-05-04 14:32:05 +02:00
Michael Albert
5906dcc129 Merge tag '0.7.0' into amp.chat
Change-Id: I44a26f1fa0946a2b2beeb014d6905cdd2d15aaf6
2021-02-17 23:30:02 +01:00
Elshad Shirinov
270d48607a Allow server admin to create rooms for other users and change user power levels
Change-Id: Ie96e9e0102454835536b6f42d247f9e714e28480
2020-11-12 18:46:33 +01:00
Michael Albert
931fafc21d Allow to set user_type 'limited'
Change-Id: Ic3942a2150b9dfe57c106eb595b49b774fe8a30c
2020-10-20 18:56:04 +02:00
Michael Albert
c604b47adc Allow to set a usertype
Change-Id: Ibfaa383b95dc5acc3b4dcd61f3f506f7c81f7dea
2020-10-20 13:57:23 +02:00
Manuel Stahl
fb8cff3e3e Merge tag '0.5.0' into amp.chat
Change-Id: I410e194bc7b153c69e00f40a4486a46924cd510a
2020-09-03 09:08:01 +02:00
Michael Albert
725e24d944 Add credentials to PDF
Fix Umlauts in PDF
Reorder elements of PDF

Change-Id: I49335584ef282e4b960275013ea7d16053b9f773
2020-08-24 07:57:40 +00:00
Manuel Stahl
dd00a76603 Merge tag '0.4.2' into amp.chat
Change-Id: Id12309f0a4d3ff9983325e69131d5eebe5bd0bde
2020-07-30 12:56:20 +02:00
Manuel Stahl
2915fd3e5b Merge tag '0.4.1' into amp.chat
Change-Id: I44c9f00e5aa7abe413f8a819e1143bebc4f08ce2
2020-07-28 15:09:48 +02:00
Michael Albert
a4662c2557 Translate room info
Change-Id: I7f3121da3c910592ecfcb4bca9dee34f2757f567
2020-06-10 07:18:58 +00:00
Michael Albert
f6ca169fbc Fix data provider
Change-Id: Id1c929f593833ed35327e70d1d0dc8182a4b7306
2020-05-25 21:20:49 +02:00
Michael Albert
07862591fd Possibility to encrypt new rooms
Change-Id: Ie415a0f8ecec646510ac8f2f0adca58064e30da5
2020-05-25 13:25:46 +02:00
Manuel Stahl
ab649fbf70 Merge branch 'master' into amp.chat
Change-Id: I6141964157bcb7218e2e6368a3ca8d20eb4855e9
2020-05-05 13:44:06 +02:00
Timo Paulssen
880223e5de Offer invitations in room creation
Turns the "Create Room" form into a tabbed form with
tabs that mimic the room display. In the "Members" tab
an AutocompleteArrayInput allows selecting multiple
users by their displayname.

The displayname is also what is displayed ìn the
invitations list.

Creating the room immediately sends out the invitations
as well.

Change-Id: I3915144114ffe4c629848363c9cb7917634d04d8
2020-04-28 19:36:15 +02:00
Manuel Stahl
76fdc80e3e Merge branch 'master' into amp.chat
Change-Id: I08a7a34e041993c29bb12fff52d07534374cda4e
2020-04-28 16:35:40 +02:00
Manuel Stahl
375649756f Add page to show room details
Change-Id: Iec4f402c4322d775cc14c567069a3295ad383b44
2020-04-28 16:30:47 +02:00
Manuel Stahl
662735a91f Adapt for changes in v2/users API
Change-Id: I927b81882fa20e5b3de3d9fc216e2136f7036bba
2020-04-28 16:30:47 +02:00
Manuel Stahl
0823976edd Cleanup room creation
Change-Id: Ieb5189513d21606f8d0bea5692112350a68f2e14
2020-04-23 16:31:26 +02:00
Michael Albert
d3cd2e9e33 Fix localStorage entry of homeserver url
Change-Id: I206e3b4428df1f51d4281ad4db26bd64bdffb85d
2020-04-21 17:42:43 +02:00
Timo Paulssen
24abcd4e4a Normalize alias a little, display initial sigil
turns all whitespace into underscores, shows leading
sigil if the alias is non-empty, so the user doesn't get
confused about whether they have to input a # or not.

Change-Id: Ic81e69cc3f0074d63a67b976c9bda32f8de025de
2020-04-20 19:31:33 +02:00
Timo Paulssen
c1c32e3268 Offer "alias" field in room create form
Tries its best to not allow aliases that are too
long (full alias including leading #, : in the
middle, and homeserver domain name must not exceed
255 bytes.

Change-Id: I1e784a94cb599eca7e30736d666b20e37aad5444
2020-04-20 19:31:33 +02:00
Timo Paulssen
ca15435625 Offer room creation form
A choice of public or private is offered, which maps to matrix'
visibility parameter. A name can also be provided.

Change-Id: I34d99acbc4624a9ed54ca6f6609573d5fc1049da
2020-04-20 19:31:33 +02:00
Michael Albert
e9c3901b68 Merge branch 'master' into amp.chat
Change-Id: Iac4e56401aab3f7f39b623b617990ec1952f8cd0
2020-04-20 16:57:23 +02:00
Michael Albert
7aec6f9369 Allow searching for users
Change-Id: Icf4a3b05b24c66971f55b22e7540a1dc904a3a92
2020-04-20 11:22:06 +00:00
Michael Albert
d2a3f07a59 Fix QR code creation
Change-Id: Ib6bbd1be6d4dca1f617043c3c2338924b2321ea3
2020-04-20 12:15:52 +02:00
Manuel Stahl
bf7867f106 Merge branch 'master' into amp.chat
Change-Id: I45b7a6db27456aaa2eca66b406cdaa49e492e61e
2020-02-11 18:56:53 +01:00
Michael Albert
f0e32abc4f Fix QR code creation
Change-Id: If05856a6fdafa43a93c6b57963820710db188d42
2020-02-11 17:35:19 +00:00
Michael Albert
61b1580735 Fix redirect after create/edit user
Change-Id: Icdb797bf6b1a47cbeff901b1952672584b2e8e8f
2020-02-11 17:34:32 +00:00
Manuel Stahl
0f7e4c1909 Create PDF with QR code on user create/edit
Change-Id: Ib89b68e956d96002ddbf6ac5ddcaea73b5b3e3fb
2020-02-10 13:10:08 +01:00
Michael Albert
c9bce409d2 Prefill user_id and password on user creation
Change-Id: I3f604f38c1842f155f3b39da20ba45992ba522be
2020-02-10 13:10:08 +01:00
17 changed files with 1033 additions and 42 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "synapse-admin", "name": "synapse-admin",
"version": "0.8.3", "version": "AMP/2021.08",
"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",
@ -21,8 +21,12 @@
"ra-test": "^3.15.0" "ra-test": "^3.15.0"
}, },
"dependencies": { "dependencies": {
"@progress/kendo-drawing": "^1.6.0",
"@progress/kendo-react-pdf": "^3.10.1",
"babel-preset-jest": "^24.9.0",
"papaparse": "^5.2.0", "papaparse": "^5.2.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"qrcode.react": "^1.0.0",
"ra-language-chinese": "^2.0.10", "ra-language-chinese": "^2.0.10",
"ra-language-german": "^3.13.4", "ra-language-german": "^3.13.4",
"react": "^17.0.0", "react": "^17.0.0",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

View File

@ -9,6 +9,32 @@
name="description" name="description"
content="Synapse-Admin" 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 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/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View File

@ -4,8 +4,9 @@ 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, RoomShow } from "./components/rooms"; import { RoomList, RoomCreate, RoomShow, RoomEdit } from "./components/rooms";
import { ReportList, ReportShow } from "./components/EventReports"; import { ReportList, ReportShow } from "./components/EventReports";
import ImportFeature from "./components/ImportFeature";
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 EqualizerIcon from "@material-ui/icons/Equalizer"; import EqualizerIcon from "@material-ui/icons/Equalizer";
@ -13,12 +14,12 @@ import { UserMediaStatsList } from "./components/statistics";
import RoomIcon from "@material-ui/icons/ViewList"; import RoomIcon from "@material-ui/icons/ViewList";
import ReportIcon from "@material-ui/icons/Warning"; import ReportIcon from "@material-ui/icons/Warning";
import FolderSharedIcon from "@material-ui/icons/FolderShared"; import FolderSharedIcon from "@material-ui/icons/FolderShared";
import { ImportFeature } from "./components/ImportFeature";
import { RoomDirectoryList } from "./components/RoomDirectory"; import { RoomDirectoryList } from "./components/RoomDirectory";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import germanMessages from "./i18n/de"; import germanMessages from "./i18n/de";
import englishMessages from "./i18n/en"; import englishMessages from "./i18n/en";
import chineseMessages from "./i18n/zh"; import chineseMessages from "./i18n/zh";
import ShowUserPdf from "./components/ShowUserPdf";
// TODO: Can we use lazy loading together with browser locale? // TODO: Can we use lazy loading together with browser locale?
const messages = { const messages = {
@ -39,7 +40,8 @@ const App = () => (
dataProvider={dataProvider} dataProvider={dataProvider}
i18nProvider={i18nProvider} i18nProvider={i18nProvider}
customRoutes={[ customRoutes={[
<Route key="userImport" path="/import_users" component={ImportFeature} />, <Route key="csvImport" path="/importcsv" component={ImportFeature} />,
<Route key="showpdf" path="/showpdf" component={ShowUserPdf} />,
]} ]}
> >
<Resource <Resource
@ -49,7 +51,14 @@ const App = () => (
edit={UserEdit} edit={UserEdit}
icon={UserIcon} 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 <Resource
name="user_media_statistics" name="user_media_statistics"
list={UserMediaStatsList} list={UserMediaStatsList}

View File

@ -20,6 +20,7 @@ import {
import { useTranslate } from "ra-core"; import { useTranslate } from "ra-core";
import Container from "@material-ui/core/Container/Container"; import Container from "@material-ui/core/Container/Container";
import { generateRandomUser } from "./users"; import { generateRandomUser } from "./users";
import ShowUserPdf from "./ShowUserPdf";
const LOGGING = true; const LOGGING = true;
@ -59,6 +60,8 @@ const FilePicker = props => {
const [progress, setProgress] = useState(null); const [progress, setProgress] = useState(null);
const [pdfRecords, setPdfRecords] = useState(null);
const [importResults, setImportResults] = useState(null); const [importResults, setImportResults] = useState(null);
const [skippedRecords, setSkippedRecords] = useState(null); const [skippedRecords, setSkippedRecords] = useState(null);
@ -66,17 +69,23 @@ const FilePicker = props => {
const [passwordMode, setPasswordMode] = useState(true); const [passwordMode, setPasswordMode] = useState(true);
const [useridMode, setUseridMode] = useState("ignore"); const [useridMode, setUseridMode] = useState("ignore");
const [showingPdf, setShowingPdf] = useState(false);
const translate = useTranslate(); const translate = useTranslate();
const notify = useNotify(); const notify = useNotify();
const dataProvider = useDataProvider(); const dataProvider = useDataProvider();
const onFileChange = async e => { const onFileChange = async e => {
if (progress !== null) return; if (progress !== null) {
return;
}
if (LOGGING) console.log("onFileChange was called");
setValues(null); setValues(null);
setError(null); setError(null);
setStats(null); setStats(null);
setPdfRecords(null);
setImportResults(null); setImportResults(null);
const file = e.target.files ? e.target.files[0] : null; const file = e.target.files ? e.target.files[0] : null;
/* Let's refuse some unreasonably big files instead of freezing /* Let's refuse some unreasonably big files instead of freezing
@ -126,6 +135,11 @@ const FilePicker = props => {
}); });
if (eF.length !== 0) { if (eF.length !== 0) {
if (LOGGING) {
console.log(meta.fields);
console.log(eF);
console.log(oF);
}
setError( setError(
translate("import_users.error.required_field", { field: eF[0] }) translate("import_users.error.required_field", { field: eF[0] })
); );
@ -226,6 +240,9 @@ const FilePicker = props => {
setProgress, setProgress,
setError setError
); );
setPdfRecords(results.recordsForPdf);
setImportResults(results); setImportResults(results);
// offer CSV download of skipped or errored records // offer CSV download of skipped or errored records
// (so that the user doesn't have to filter out successful // (so that the user doesn't have to filter out successful
@ -251,6 +268,8 @@ const FilePicker = props => {
let skippedRecords = []; let skippedRecords = [];
let erroredRecords = []; let erroredRecords = [];
let succeededRecords = []; let succeededRecords = [];
let recordsForPdf = [];
let changeStats = { let changeStats = {
toAdmin: 0, toAdmin: 0,
toGuest: 0, toGuest: 0,
@ -365,6 +384,14 @@ const FilePicker = props => {
await dataProvider.create("users", { data: recordData }); await dataProvider.create("users", { data: recordData });
} }
succeededRecords.push(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, erroredRecords,
succeededRecords, succeededRecords,
totalRecordCount: entriesCount, totalRecordCount: entriesCount,
recordsForPdf,
changeStats, changeStats,
wasDryRun: dryRun, wasDryRun: dryRun,
}; };
@ -618,6 +646,10 @@ const FilePicker = props => {
<br />, <br />,
] ]
: ""} : ""}
{translate(
"import_users.cards.results.for_print",
importResults.recordsForPdf.length
)}
<br /> <br />
{importResults.wasDryRun && [ {importResults.wasDryRun && [
translate("import_users.cards.results.simulated_only"), translate("import_users.cards.results.simulated_only"),
@ -655,20 +687,43 @@ const FilePicker = props => {
</CardActions> </CardActions>
); );
let allCards = []; let pdfDisplay =
if (uploadCard) allCards.push(uploadCard); pdfRecords && showingPdf && pdfRecords.length ? (
if (errorCards) allCards.push(errorCards); <ShowUserPdf records={pdfRecords} />
if (conflictCards) allCards.push(conflictCards); ) : null;
if (statsCards) allCards.push(...statsCards);
if (startImportCard) allCards.push(startImportCard);
if (resultsCard) allCards.push(resultsCard);
let cardContainer = <Card>{allCards}</Card>; let pdfActions = pdfRecords ? (
<CardActions>
<Button
size="large"
onClick={e => {
setShowingPdf(true);
}}
>
{translate("import_users.goToPdf")}
</Button>
</CardActions>
) : null;
return [ if (pdfRecords && showingPdf) {
<Title defaultTitle={translate("import_users.title")} />, return <Card>{pdfDisplay}</Card>;
cardContainer, } 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; export const ImportFeature = FilePicker;

View 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;

View 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;

View File

@ -1,31 +1,56 @@
import React, { Fragment } from "react"; import React, { Fragment } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Route, Link } from "react-router-dom";
import { import {
AutocompleteArrayInput,
AutocompleteInput,
BooleanInput,
BooleanField, BooleanField,
BulkDeleteButton, BulkDeleteButton,
DateField, Button,
Create,
Edit,
Datagrid, Datagrid,
DateField,
DeleteButton, DeleteButton,
Filter, Filter,
FormTab,
List, List,
NumberField, NumberField,
Pagination, Pagination,
ReferenceArrayInput,
ReferenceField, ReferenceField,
ReferenceInput,
ReferenceManyField, ReferenceManyField,
SearchInput, SearchInput,
SelectField, SelectField,
Show, Show,
SimpleForm,
Tab, Tab,
TabbedForm,
TabbedShowLayout, TabbedShowLayout,
TextField, TextField,
TextInput,
Toolbar,
TopToolbar, TopToolbar,
useDataProvider,
useRecordContext, useRecordContext,
useRefresh,
useTranslate, useTranslate,
} from "react-admin"; } from "react-admin";
import get from "lodash/get"; import get from "lodash/get";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles"; 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 FastForwardIcon from "@material-ui/icons/FastForward";
import HttpsIcon from "@material-ui/icons/Https"; import HttpsIcon from "@material-ui/icons/Https";
import NoEncryptionIcon from "@material-ui/icons/NoEncryption"; 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 UserIcon from "@material-ui/icons/Group";
import ViewListIcon from "@material-ui/icons/ViewList"; import ViewListIcon from "@material-ui/icons/ViewList";
import VisibilityIcon from "@material-ui/icons/Visibility"; import VisibilityIcon from "@material-ui/icons/Visibility";
import ContentSave from "@material-ui/icons/Save";
import EventIcon from "@material-ui/icons/Event"; import EventIcon from "@material-ui/icons/Event";
import { import {
RoomDirectoryBulkDeleteButton, RoomDirectoryBulkDeleteButton,
@ -78,20 +104,368 @@ const EncryptionField = ({ source, record = {}, emptyText }) => {
); );
}; };
const RoomTitle = ({ record }) => { const validateDisplayName = fieldval => {
const translate = useTranslate(); return fieldval == null
var name = ""; ? "synapseadmin.rooms.room_name_required"
if (record) { : fieldval.length === 0
name = record.name !== "" ? record.name : record.id; ? "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 ( return (
<span> <span>
{translate("resources.rooms.name", 1)} {name} {translate("resources.rooms.name", 1)} {record ? `"${record.name}"` : ""}
</span> </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 }) => { const RoomShowActions = ({ basePath, data, resource }) => {
var roomDirectoryStatus = ""; var roomDirectoryStatus = "";
if (data) { if (data) {

View File

@ -13,10 +13,12 @@ import {
ArrayInput, ArrayInput,
ArrayField, ArrayField,
Button, Button,
CreateButton,
Datagrid, Datagrid,
DateField, DateField,
Create, Create,
Edit, Edit,
ExportButton,
List, List,
Filter, Filter,
Toolbar, Toolbar,
@ -29,9 +31,10 @@ import {
PasswordInput, PasswordInput,
TextField, TextField,
TextInput, TextInput,
SearchInput,
ReferenceField, ReferenceField,
ReferenceManyField, ReferenceManyField,
SearchInput, SelectField,
SelectInput, SelectInput,
BulkDeleteButton, BulkDeleteButton,
DeleteButton, DeleteButton,
@ -39,21 +42,20 @@ import {
regex, regex,
useTranslate, useTranslate,
Pagination, Pagination,
CreateButton,
ExportButton,
TopToolbar, TopToolbar,
sanitizeListRestProps, sanitizeListRestProps,
NumberField, NumberField,
} from "react-admin"; } from "react-admin";
import { Link } from "react-router-dom"; import SaveQrButton from "./SaveQrButton";
import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices"; import { ServerNoticeButton, ServerNoticeBulkButton } from "./ServerNotices";
import { DeviceRemoveButton } from "./devices"; import { DeviceRemoveButton } from "./devices";
import { ProtectMediaButton, QuarantineMediaButton } from "./media"; import { ProtectMediaButton, QuarantineMediaButton } from "./media";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { Link } from "react-router-dom";
const redirect = () => { const redirect = () => {
return { return {
pathname: "/import_users", pathname: "/importcsv",
}; };
}; };
@ -173,12 +175,48 @@ export const UserList = props => {
<TextField source="displayname" /> <TextField source="displayname" />
<BooleanField source="is_guest" /> <BooleanField source="is_guest" />
<BooleanField source="admin" /> <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" /> <BooleanField source="deactivated" />
</Datagrid> </Datagrid>
</List> </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 // https://matrix.org/docs/spec/appendices#user-identifiers
const validateUser = regex( const validateUser = regex(
/^@[a-z0-9._=\-/]+:.*/, /^@[a-z0-9._=\-/]+:.*/,
@ -230,7 +268,17 @@ 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"
confirmTitle={translate("resources.users.helper.erase", { confirmTitle={translate("resources.users.helper.erase", {
@ -244,12 +292,20 @@ const UserEditToolbar = props => {
}; };
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" />
<BooleanInput source="admin" /> <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"> <ArrayInput source="threepids">
<SimpleFormIterator> <SimpleFormIterator>
<SelectInput <SelectInput
@ -277,6 +333,7 @@ const UserTitle = ({ record }) => {
</span> </span>
); );
}; };
export const UserEdit = props => { export const UserEdit = props => {
const classes = useStyles(); const classes = useStyles();
const translate = useTranslate(); const translate = useTranslate();
@ -295,6 +352,15 @@ export const UserEdit = props => {
<TextInput source="id" disabled /> <TextInput source="id" disabled />
<TextInput source="displayname" /> <TextInput source="displayname" />
<PasswordInput source="password" autoComplete="new-password" /> <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="admin" />
<BooleanInput <BooleanInput
source="deactivated" source="deactivated"

View File

@ -11,6 +11,11 @@ const de = {
protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen", protocol_error: "Die URL muss mit 'http://' oder 'https://' beginnen",
url_error: "Keine gültige Matrix Server URL", 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",
@ -18,6 +23,14 @@ const de = {
}, },
rooms: { rooms: {
details: "Raumdetails", 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: { tabs: {
basic: "Allgemein", basic: "Allgemein",
members: "Mitglieder", members: "Mitglieder",
@ -92,6 +105,8 @@ const de = {
with_error: with_error:
"%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern", "%{smart_count} Eintrag mit Fehlern ||| %{smart_count} Einträge mit Fehlern",
simulated_only: "Import-Vorgang war nur simuliert", simulated_only: "Import-Vorgang war nur simuliert",
for_print:
"%{smart_count} Eintrag zum Drucken verfügbar |||| %{smart_count} Einträge zum Drucken verfügbar",
}, },
}, },
}, },
@ -122,6 +137,18 @@ const de = {
creation_ts_ms: "Zeitpunkt der Erstellung", creation_ts_ms: "Zeitpunkt der Erstellung",
consent_version: "Zugestimmte Geschäftsbedingungen", consent_version: "Zugestimmte Geschäftsbedingungen",
auth_provider: "Provider", auth_provider: "Provider",
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: { helper: {
deactivate: deactivate:
@ -131,6 +158,11 @@ const de = {
action: { action: {
erase: "Lösche Benutzerdaten", erase: "Lösche Benutzerdaten",
}, },
roles: {
user: "Nutzer",
mod: "Moderator",
admin: "Administrator",
},
}, },
rooms: { rooms: {
name: "Raum |||| Räume", name: "Raum |||| Räume",
@ -139,6 +171,8 @@ const de = {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Mitglieder", joined_members: "Mitglieder",
invite_members: "Mitglieder einladen",
invitees: "Einladungen",
joined_local_members: "Lokale Mitglieder", joined_local_members: "Lokale Mitglieder",
joined_local_devices: "Lokale Endgeräte", joined_local_devices: "Lokale Endgeräte",
state_events: "Zustandsereignisse / Komplexität", state_events: "Zustandsereignisse / Komplexität",

View File

@ -11,12 +11,26 @@ const en = {
protocol_error: "URL has to start with 'http://' or 'https://'", protocol_error: "URL has to start with 'http://' or 'https://'",
url_error: "Not a valid Matrix server URL", 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",
tabs: { sso: "SSO" }, tabs: { sso: "SSO" },
}, },
rooms: { 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: { tabs: {
basic: "Basic", basic: "Basic",
members: "Members", members: "Members",
@ -91,6 +105,8 @@ const en = {
with_error: with_error:
"%{smart_count} entry with errors ||| %{smart_count} entries with errors", "%{smart_count} entry with errors ||| %{smart_count} entries with errors",
simulated_only: "Run was only simulated", simulated_only: "Run was only simulated",
for_print:
"%{smart_count} entry available for printing |||| %{smart_count} entries available for printing",
}, },
}, },
}, },
@ -121,6 +137,17 @@ const en = {
creation_ts_ms: "Creation timestamp", creation_ts_ms: "Creation timestamp",
consent_version: "Consent version", consent_version: "Consent version",
auth_provider: "Provider", auth_provider: "Provider",
// 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: { helper: {
deactivate: "You must provide a password to re-activate an account.", deactivate: "You must provide a password to re-activate an account.",
@ -129,6 +156,11 @@ const en = {
action: { action: {
erase: "Erase user data", erase: "Erase user data",
}, },
roles: {
user: "User",
mod: "Moderator",
admin: "Administrator",
},
}, },
rooms: { rooms: {
name: "Room |||| Rooms", name: "Room |||| Rooms",
@ -137,6 +169,8 @@ const en = {
name: "Name", name: "Name",
canonical_alias: "Alias", canonical_alias: "Alias",
joined_members: "Members", joined_members: "Members",
invite_members: "Invite Members",
invitees: "Invitations",
joined_local_members: "Local members", joined_local_members: "Local members",
joined_local_devices: "Local devices", joined_local_devices: "Local devices",
state_events: "State events / Complexity", state_events: "State events / Complexity",

View File

@ -25,6 +25,13 @@ const mxcUrlToHttp = mxcUrl => {
return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`; 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 = { const resourceMap = {
users: { users: {
path: "/_synapse/admin/v2/users", path: "/_synapse/admin/v2/users",
@ -35,6 +42,7 @@ 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,
// need timestamp in milliseconds // need timestamp in milliseconds
creation_ts_ms: u.creation_ts * 1000, creation_ts_ms: u.creation_ts * 1000,
}), }),
@ -63,8 +71,40 @@ const resourceMap = {
public: !!r.public, public: !!r.public,
}), }),
data: "rooms", data: "rooms",
total: json => { total: json => json.total_rooms,
return 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 => ({ delete: params => ({
endpoint: `/_synapse/admin/v1/rooms/${params.id}`, endpoint: `/_synapse/admin/v1/rooms/${params.id}`,
@ -324,7 +364,7 @@ const dataProvider = {
}, },
getOne: (resource, params) => { getOne: (resource, params) => {
console.log("getOne " + resource); console.log("getOne " + resource, params);
const homeserver = localStorage.getItem("base_url"); const homeserver = localStorage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) return Promise.reject(); if (!homeserver || !(resource in resourceMap)) return Promise.reject();
@ -385,10 +425,13 @@ const dataProvider = {
const res = resourceMap[resource]; const res = resourceMap[resource];
const transform = res.transformBeforeUpdate || (x => x);
const data = transform(params.data);
const endpoint_url = homeserver + res.path; const endpoint_url = homeserver + res.path;
return jsonClient(`${endpoint_url}/${params.data.id}`, { return jsonClient(`${endpoint_url}/${params.data.id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify(params.data, filterNullValues), body: JSON.stringify(data, filterNullValues),
}).then(({ json }) => ({ }).then(({ json }) => ({
data: res.map(json), data: res.map(json),
})); }));

View File

@ -569,7 +569,7 @@
dependencies: dependencies:
"@babel/helper-plugin-utils" "^7.10.4" "@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" 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" 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== integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
@ -1595,6 +1595,25 @@
schema-utils "^2.6.5" schema-utils "^2.6.5"
source-map "^0.7.3" 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": "@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"
@ -2827,6 +2846,13 @@ babel-plugin-istanbul@^6.0.0:
istanbul-lib-instrument "^4.0.0" istanbul-lib-instrument "^4.0.0"
test-exclude "^6.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: babel-plugin-jest-hoist@^26.6.2:
version "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" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d"
@ -2911,6 +2937,14 @@ babel-preset-current-node-syntax@^1.0.0:
"@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3"
"@babel/plugin-syntax-top-level-await" "^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: babel-preset-jest@^26.6.2:
version "26.6.2" version "26.6.2"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee"
@ -8312,7 +8346,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==
@ -9433,6 +9467,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"