mirror of
https://github.com/etkecc/synapse-admin.git
synced 2025-02-16 11:29:48 +03:00
Restrict actions on specific users (#42)
* Restrict actions on specific users * update readme
This commit is contained in:
parent
a277ded227
commit
e328380c77
12 changed files with 124 additions and 46 deletions
80
README.md
80
README.md
|
@ -33,6 +33,7 @@ The following changes are already implemented:
|
||||||
* [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
|
* [Fix requests with invalid MXIDs on Bulk registration](https://github.com/etkecc/synapse-admin/pull/33)
|
||||||
* [Expose user avatar URL field in the UI](https://github.com/etkecc/synapse-admin/pull/27)
|
* [Expose user avatar URL field in the UI](https://github.com/etkecc/synapse-admin/pull/27)
|
||||||
* [Upgrade react-admin to v5](https://github.com/etkecc/synapse-admin/pull/40)
|
* [Upgrade react-admin to v5](https://github.com/etkecc/synapse-admin/pull/40)
|
||||||
|
* [Restrict actions on specific users](https://github.com/etkecc/synapse-admin/pull/42)
|
||||||
|
|
||||||
_the list will be updated as new changes are added_
|
_the list will be updated as new changes are added_
|
||||||
|
|
||||||
|
@ -126,37 +127,6 @@ You have three options:
|
||||||
|
|
||||||
- browse to http://localhost:8080
|
- browse to http://localhost:8080
|
||||||
|
|
||||||
### Restricting available homeserver
|
|
||||||
|
|
||||||
You can restrict the homeserver(s), so that the user can no longer define it himself.
|
|
||||||
|
|
||||||
Edit `config.json` to restrict either to a single homeserver:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"restrictBaseUrl": "https://your-matrixs-erver.example.com"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
or to a list of homeservers:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"restrictBaseUrl": ["https://your-first-matrix-server.example.com", "https://your-second-matrix-server.example.com"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `config.json` can be injected into a Docker container using a bind mount.
|
|
||||||
|
|
||||||
```yml
|
|
||||||
services:
|
|
||||||
synapse-admin:
|
|
||||||
...
|
|
||||||
volumes:
|
|
||||||
./config.json:/app/config.json:ro
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Serving Synapse-Admin on a different path
|
### Serving Synapse-Admin on a different path
|
||||||
|
|
||||||
The path prefix where synapse-admin is served can only be changed during the build step.
|
The path prefix where synapse-admin is served can only be changed during the build step.
|
||||||
|
@ -194,6 +164,54 @@ services:
|
||||||
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
- "traefik.http.middlewares.admin_path.stripprefix.prefixes=/admin"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
You can use `config.json` file to configure synapse-admin
|
||||||
|
|
||||||
|
The `config.json` can be injected into a Docker container using a bind mount.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
services:
|
||||||
|
synapse-admin:
|
||||||
|
...
|
||||||
|
volumes:
|
||||||
|
./config.json:/app/config.json:ro
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restricting available homeserver
|
||||||
|
|
||||||
|
You can restrict the homeserver(s), so that the user can no longer define it himself.
|
||||||
|
|
||||||
|
Edit `config.json` to restrict either to a single homeserver:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"restrictBaseUrl": "https://your-matrixs-erver.example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or to a list of homeservers:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"restrictBaseUrl": ["https://your-first-matrix-server.example.com", "https://your-second-matrix-server.example.com"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Protecting appservice managed users
|
||||||
|
|
||||||
|
To avoid accidental adjustments of appservice-managed users (e.g., puppets created by a bridge) and breaking the bridge,
|
||||||
|
you can specify the list of MXIDs (regexp) that should be prohibited from any changes, except display name and avatar.
|
||||||
|
|
||||||
|
Example for [mautrix-telegram](https://github.com/mautrix/telegram)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"asManagedUsers": ["^@telegram_[a-zA-Z0-9]+:example\\.com$"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
![Screenshots](./screenshots.jpg)
|
![Screenshots](./screenshots.jpg)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { createContext, useContext } from "react";
|
||||||
|
|
||||||
interface AppContextType {
|
interface AppContextType {
|
||||||
restrictBaseUrl: string | string[];
|
restrictBaseUrl: string | string[];
|
||||||
|
asManagedUsers: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppContext = createContext({});
|
export const AppContext = createContext({});
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
|
import { DeleteWithConfirmButton, DeleteWithConfirmButtonProps, useRecordContext } from "react-admin";
|
||||||
|
import { isASManaged } from "./mxid";
|
||||||
|
|
||||||
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
if (!record) return null;
|
if (!record) return null;
|
||||||
|
|
||||||
|
let isASManagedUser = false;
|
||||||
|
if (record.user_id) {
|
||||||
|
isASManagedUser = isASManaged(record.user_id);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DeleteWithConfirmButton
|
<DeleteWithConfirmButton
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -12,6 +18,7 @@ export const DeviceRemoveButton = (props: DeleteWithConfirmButtonProps) => {
|
||||||
confirmContent="resources.devices.action.erase.content"
|
confirmContent="resources.devices.action.erase.content"
|
||||||
mutationMode="pessimistic"
|
mutationMode="pessimistic"
|
||||||
redirect={false}
|
redirect={false}
|
||||||
|
disabled={isASManagedUser}
|
||||||
translateOptions={{
|
translateOptions={{
|
||||||
id: record.id,
|
id: record.id,
|
||||||
name: record.display_name ? record.display_name : record.id,
|
name: record.display_name ? record.display_name : record.id,
|
||||||
|
|
15
src/components/mxid.tsx
Normal file
15
src/components/mxid.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user is managed by an application service
|
||||||
|
* @param id The user ID to check
|
||||||
|
* @returns Whether the user is managed by an application service
|
||||||
|
*/
|
||||||
|
export const isASManaged = (id: string) => {
|
||||||
|
const managedUsersString = localStorage.getItem("as_managed_users");
|
||||||
|
try {
|
||||||
|
const asManagedUsers = JSON.parse(managedUsersString).map(regex => new RegExp(regex));
|
||||||
|
return asManagedUsers.some(regex => regex.test(id));
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
|
@ -152,6 +152,7 @@ const de: SynapseTranslationMessages = {
|
||||||
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
deactivate: "Sie müssen ein Passwort angeben, um ein Konto wieder zu aktivieren.",
|
||||||
erase: "DSGVO konformes Löschen der Benutzerdaten",
|
erase: "DSGVO konformes Löschen der Benutzerdaten",
|
||||||
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt",
|
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt",
|
||||||
|
erase_managed_user_error: "Die Löschung eines vom System verwalteten Benutzers ist nicht zulässig",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Lösche Benutzerdaten",
|
erase: "Lösche Benutzerdaten",
|
||||||
|
|
|
@ -143,6 +143,7 @@ const en: SynapseTranslationMessages = {
|
||||||
deactivate: "You must provide a password to re-activate an account.",
|
deactivate: "You must provide a password to re-activate an account.",
|
||||||
erase: "Mark the user as GDPR-erased",
|
erase: "Mark the user as GDPR-erased",
|
||||||
erase_admin_error: "Deleting own user is not allowed.",
|
erase_admin_error: "Deleting own user is not allowed.",
|
||||||
|
erase_managed_user_error: "Deleting a system-managed user is not allowed.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Erase user data",
|
erase: "Erase user data",
|
||||||
|
|
|
@ -141,6 +141,7 @@ const fr: SynapseTranslationMessages = {
|
||||||
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
|
||||||
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
erase: "Marquer l'utilisateur comme effacé conformément au RGPD",
|
||||||
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
|
||||||
|
erase_managed_user_error: "La suppression d'un utilisateur géré n'est pas autorisée.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Effacer les données de l'utilisateur",
|
erase: "Effacer les données de l'utilisateur",
|
||||||
|
|
|
@ -143,6 +143,7 @@ const it: SynapseTranslationMessages = {
|
||||||
action: {
|
action: {
|
||||||
erase: "Cancella i dati dell'utente",
|
erase: "Cancella i dati dell'utente",
|
||||||
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
erase_admin_error: "Non è consentito eliminare il proprio utente.",
|
||||||
|
erase_managed_user_error: "Non è consentito eliminare un utente gestito.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rooms: {
|
rooms: {
|
||||||
|
|
|
@ -160,6 +160,7 @@ const ru: SynapseTranslationMessages = {
|
||||||
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
|
||||||
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
|
||||||
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
erase_admin_error: "Удаление собственного пользователя запрещено.",
|
||||||
|
erase_managed_user_error: "Удаление управляемого системой пользователя запрещено.",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "Удалить данные пользователя",
|
erase: "Удалить данные пользователя",
|
||||||
|
|
|
@ -144,6 +144,7 @@ const zh: SynapseTranslationMessages = {
|
||||||
deactivate: "您必须提供一串密码来激活账户。",
|
deactivate: "您必须提供一串密码来激活账户。",
|
||||||
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
erase: "将用户标记为根据 GDPR 的要求抹除了",
|
||||||
erase_admin_error: "不允许删除自己的用户",
|
erase_admin_error: "不允许删除自己的用户",
|
||||||
|
erase_managed_user_error: "不允许删除受管理的用户",
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
erase: "抹除用户信息",
|
erase: "抹除用户信息",
|
||||||
|
|
|
@ -4,15 +4,17 @@ import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import { AppContext } from "./AppContext";
|
import { AppContext } from "./AppContext";
|
||||||
|
import storage from "./storage";
|
||||||
|
|
||||||
fetch("config.json")
|
fetch("config.json")
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(props =>
|
.then(props => {
|
||||||
createRoot(document.getElementById("root")).render(
|
storage.setItem("as_managed_users", JSON.stringify(props.asManagedUsers));
|
||||||
|
return createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AppContext.Provider value={props}>
|
<AppContext.Provider value={props}>
|
||||||
<App />
|
<App />
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
);
|
});
|
||||||
|
|
|
@ -61,6 +61,7 @@ import {
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import AvatarField from "../components/AvatarField";
|
import AvatarField from "../components/AvatarField";
|
||||||
|
import { isASManaged } from "../components/mxid";
|
||||||
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
|
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
|
||||||
import { DATE_FORMAT } from "../components/date";
|
import { DATE_FORMAT } from "../components/date";
|
||||||
import { DeviceRemoveButton } from "../components/devices";
|
import { DeviceRemoveButton } from "../components/devices";
|
||||||
|
@ -103,8 +104,9 @@ const userFilters = [
|
||||||
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
<BooleanInput label="resources.users.fields.show_locked" source="locked" alwaysOn />,
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean }> = props => {
|
const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSelected: boolean; asManagedUserIsSelected: boolean }> = props => {
|
||||||
const ownUserIsSelected = props.ownUserIsSelected;
|
const ownUserIsSelected = props.ownUserIsSelected;
|
||||||
|
const asManagedUserIsSelected = props.asManagedUserIsSelected;
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
@ -112,6 +114,9 @@ const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSele
|
||||||
if (ownUserIsSelected) {
|
if (ownUserIsSelected) {
|
||||||
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>);
|
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
} else if (asManagedUserIsSelected) {
|
||||||
|
notify(<Alert severity="error">{translate("resources.users.helper.erase_managed_user_error")}</Alert>);
|
||||||
|
ev.stopPropagation();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -121,6 +126,7 @@ const UserPreventSelfDelete: React.FC<{ children: React.ReactNode; ownUserIsSele
|
||||||
const UserBulkActionButtons = () => {
|
const UserBulkActionButtons = () => {
|
||||||
const record = useListContext();
|
const record = useListContext();
|
||||||
const [ownUserIsSelected, setOwnUserIsSelected] = useState(false);
|
const [ownUserIsSelected, setOwnUserIsSelected] = useState(false);
|
||||||
|
const [asManagedUserIsSelected, setAsManagedUserIsSelected] = useState(false);
|
||||||
const selectedIds = record.selectedIds;
|
const selectedIds = record.selectedIds;
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
@ -128,12 +134,13 @@ const UserBulkActionButtons = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
setOwnUserIsSelected(selectedIds.includes(ownUserId));
|
||||||
|
setAsManagedUserIsSelected(selectedIds.some(id => isASManaged(id)));
|
||||||
}, [selectedIds]);
|
}, [selectedIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ServerNoticeBulkButton />
|
<ServerNoticeBulkButton />
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<BulkDeleteButton
|
<BulkDeleteButton
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
confirmTitle="resources.users.helper.erase"
|
confirmTitle="resources.users.helper.erase"
|
||||||
|
@ -184,14 +191,16 @@ const UserEditActions = () => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
let ownUserIsSelected = false;
|
let ownUserIsSelected = false;
|
||||||
|
let asManagedUserIsSelected = false;
|
||||||
if (record && record.id) {
|
if (record && record.id) {
|
||||||
ownUserIsSelected = record.id === ownUserId;
|
ownUserIsSelected = record.id === ownUserId;
|
||||||
|
asManagedUserIsSelected = isASManaged(record.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TopToolbar>
|
<TopToolbar>
|
||||||
{!record?.deactivated && <ServerNoticeButton />}
|
{!record?.deactivated && <ServerNoticeButton />}
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
label="resources.users.action.erase"
|
label="resources.users.action.erase"
|
||||||
confirmTitle={translate("resources.users.helper.erase", {
|
confirmTitle={translate("resources.users.helper.erase", {
|
||||||
|
@ -236,12 +245,16 @@ export const UserCreate = (props: CreateProps) => (
|
||||||
const UserTitle = () => {
|
const UserTitle = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
let username = record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""
|
||||||
|
if (isASManaged(record?.id)) {
|
||||||
|
username += " 🤖";
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate("resources.users.name", {
|
{translate("resources.users.name", {
|
||||||
smart_count: 1,
|
smart_count: 1,
|
||||||
})}{" "}
|
})}{" "}
|
||||||
{record ? (record.displayname ? `"${record.displayname}"` : `"${record.name}"`) : ""}
|
{username}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -250,8 +263,10 @@ const UserEditToolbar = () => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
let ownUserIsSelected = false;
|
let ownUserIsSelected = false;
|
||||||
|
let asManagedUserIsSelected = false;
|
||||||
if (record && record.id) {
|
if (record && record.id) {
|
||||||
ownUserIsSelected = record.id === ownUserId;
|
ownUserIsSelected = record.id === ownUserId;
|
||||||
|
asManagedUserIsSelected = isASManaged(record.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -259,7 +274,7 @@ const UserEditToolbar = () => {
|
||||||
<div className={ToolbarClasses.defaultToolbar}>
|
<div className={ToolbarClasses.defaultToolbar}>
|
||||||
<Toolbar sx={{ justifyContent: "space-between" }}>
|
<Toolbar sx={{ justifyContent: "space-between" }}>
|
||||||
<SaveButton />
|
<SaveButton />
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<DeleteButton />
|
<DeleteButton />
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
@ -272,17 +287,31 @@ const UserBooleanInput = props => {
|
||||||
const record = useRecordContext();
|
const record = useRecordContext();
|
||||||
const ownUserId = localStorage.getItem("user_id");
|
const ownUserId = localStorage.getItem("user_id");
|
||||||
let ownUserIsSelected = false;
|
let ownUserIsSelected = false;
|
||||||
if (record && record.id === ownUserId) {
|
let asManagedUserIsSelected = false;
|
||||||
ownUserIsSelected = true;
|
if (record) {
|
||||||
|
ownUserIsSelected = record.id === ownUserId;
|
||||||
|
asManagedUserIsSelected = isASManaged(record.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
|
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
|
||||||
<BooleanInput {...props} disabled={ownUserIsSelected} />
|
<BooleanInput {...props} disabled={ownUserIsSelected || asManagedUserIsSelected} />
|
||||||
</UserPreventSelfDelete>
|
</UserPreventSelfDelete>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UserPasswordInput = props => {
|
||||||
|
const record = useRecordContext();
|
||||||
|
let asManagedUserIsSelected = false;
|
||||||
|
if (record) {
|
||||||
|
asManagedUserIsSelected = isASManaged(record.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PasswordInput {...props} helperText="resources.users.helper.erase_managed_user_error" disabled={asManagedUserIsSelected} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const UserEdit = (props: EditProps) => {
|
export const UserEdit = (props: EditProps) => {
|
||||||
const translate = useTranslate();
|
const translate = useTranslate();
|
||||||
|
|
||||||
|
@ -301,10 +330,10 @@ export const UserEdit = (props: EditProps) => {
|
||||||
</ImageInput>
|
</ImageInput>
|
||||||
<TextInput source="id" readOnly />
|
<TextInput source="id" readOnly />
|
||||||
<TextInput source="displayname" />
|
<TextInput source="displayname" />
|
||||||
<PasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
<UserPasswordInput source="password" autoComplete="new-password" helperText="resources.users.helper.password" />
|
||||||
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
|
||||||
<BooleanInput source="admin" />
|
<BooleanInput source="admin" />
|
||||||
<BooleanInput source="locked" />
|
<UserBooleanInput source="locked" />
|
||||||
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
|
||||||
<BooleanInput source="erased" disabled />
|
<BooleanInput source="erased" disabled />
|
||||||
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
|
||||||
|
@ -331,7 +360,7 @@ export const UserEdit = (props: EditProps) => {
|
||||||
|
|
||||||
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
|
<FormTab label={translate("resources.devices.name", { smart_count: 2 })} icon={<DevicesIcon />} path="devices">
|
||||||
<ReferenceManyField reference="devices" target="user_id" label={false}>
|
<ReferenceManyField reference="devices" target="user_id" label={false}>
|
||||||
<Datagrid style={{ width: "100%" }}>
|
<Datagrid style={{ width: "100%" }} bulkActionButtons="">
|
||||||
<TextField source="device_id" sortable={false} />
|
<TextField source="device_id" sortable={false} />
|
||||||
<TextField source="display_name" sortable={false} />
|
<TextField source="display_name" sortable={false} />
|
||||||
<TextField source="last_seen_ip" sortable={false} />
|
<TextField source="last_seen_ip" sortable={false} />
|
||||||
|
|
Loading…
Add table
Reference in a new issue