Prevent self user delete

This commit is contained in:
Borislav Pantaleev 2024-08-31 01:00:29 +03:00
parent dbcb4f92dc
commit 056d9c6b4c
8 changed files with 101 additions and 17 deletions

View file

@ -142,6 +142,7 @@ const de: SynapseTranslationMessages = {
password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.", password: "Durch die Änderung des Passworts wird der Benutzer von allen Sitzungen abgemeldet.",
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",
}, },
action: { action: {
erase: "Lösche Benutzerdaten", erase: "Lösche Benutzerdaten",

View file

@ -141,6 +141,7 @@ const en: SynapseTranslationMessages = {
password: "Changing password will log user out of all sessions.", password: "Changing password will log user out of all sessions.",
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.",
}, },
action: { action: {
erase: "Erase user data", erase: "Erase user data",

View file

@ -139,6 +139,7 @@ const fr: SynapseTranslationMessages = {
helper: { helper: {
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.",
}, },
action: { action: {
erase: "Effacer les données de l'utilisateur", erase: "Effacer les données de l'utilisateur",

1
src/i18n/index.d.ts vendored
View file

@ -137,6 +137,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
password?: string; password?: string;
deactivate: string; deactivate: string;
erase: string; erase: string;
erase_admin_error: string;
}; };
action: { action: {
erase: string; erase: string;

View file

@ -141,6 +141,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.",
}, },
}, },
rooms: { rooms: {

View file

@ -150,6 +150,7 @@ const ru: SynapseTranslationMessages = {
password: "Смена пароля завершит все сессии пользователя.", password: "Смена пароля завершит все сессии пользователя.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.", deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR", erase: "Пометить пользователя как удалённого в соответствии с GDPR",
erase_admin_error: "Удаление собственного пользователя запрещено.",
}, },
action: { action: {
erase: "Удалить данные пользователя", erase: "Удалить данные пользователя",

View file

@ -134,6 +134,7 @@ const zh: SynapseTranslationMessages = {
helper: { helper: {
deactivate: "您必须提供一串密码来激活账户。", deactivate: "您必须提供一串密码来激活账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了", erase: "将用户标记为根据 GDPR 的要求抹除了",
erase_admin_error: "不允许删除自己的用户",
}, },
action: { action: {
erase: "抹除用户信息", erase: "抹除用户信息",

View file

@ -8,6 +8,8 @@ import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonPinIcon from "@mui/icons-material/PersonPin"; import PersonPinIcon from "@mui/icons-material/PersonPin";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent"; import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import ViewListIcon from "@mui/icons-material/ViewList"; import ViewListIcon from "@mui/icons-material/ViewList";
import { useEffect, useState } from "react";
import { Alert, ownerDocument } from "@mui/material";
import { import {
ArrayInput, ArrayInput,
ArrayField, ArrayField,
@ -42,11 +44,15 @@ import {
useRecordContext, useRecordContext,
useTranslate, useTranslate,
Pagination, Pagination,
SaveButton,
CreateButton, CreateButton,
ExportButton, ExportButton,
TopToolbar, TopToolbar,
Toolbar,
NumberField, NumberField,
useListContext, useListContext,
useNotify,
ToolbarClasses,
} from "react-admin"; } from "react-admin";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@ -92,16 +98,47 @@ const userFilters = [
<BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />, <BooleanInput label="resources.users.fields.show_deactivated" source="deactivated" alwaysOn />,
]; ];
const UserBulkActionButtons = () => ( const UserPreventSelfDelete: React.FC<{ children: React.ReactNode, ownUserIsSelected: boolean }> = (props) => {
<> const ownUserIsSelected = props.ownUserIsSelected;
const notify = useNotify();
const translate = useTranslate();
const handleDeleteClick = (ev: React.MouseEvent<HTMLDivElement>) => {
if (ownUserIsSelected) {
notify(<Alert severity="error">{translate("resources.users.helper.erase_admin_error")}</Alert>)
ev.stopPropagation();
}
};
return <div onClickCapture={handleDeleteClick}>
{props.children}
</div>
};
const UserBulkActionButtons = () => {
const record = useListContext();
const [ ownUserIsSelected, setOwnUserIsSelected ] = useState(false);
const selectedIds = record.selectedIds;
const ownUserId = localStorage.getItem("user_id");
const notify = useNotify();
const translate = useTranslate();
useEffect(() => {
setOwnUserIsSelected(selectedIds.includes(ownUserId));
}, [ selectedIds ]);
return <>
<ServerNoticeBulkButton /> <ServerNoticeBulkButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<BulkDeleteButton <BulkDeleteButton
label="resources.users.action.erase" label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase" confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic" mutationMode="pessimistic"
/> />
</UserPreventSelfDelete>
</> </>
); };
export const UserList = (props: ListProps) => ( export const UserList = (props: ListProps) => (
<List <List
@ -137,10 +174,16 @@ const validateAddress = [required(), maxLength(255)];
const UserEditActions = () => { const UserEditActions = () => {
const record = useRecordContext(); const record = useRecordContext();
const translate = useTranslate(); const translate = useTranslate();
const ownUserId = localStorage.getItem("user_id");
let ownUserIsSelected = false;
if (record && record.id) {
ownUserIsSelected = record.id === ownUserId;
}
return ( return (
<TopToolbar> <TopToolbar>
{!record?.deactivated && <ServerNoticeButton />} {!record?.deactivated && <ServerNoticeButton />}
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<DeleteButton <DeleteButton
label="resources.users.action.erase" label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", { confirmTitle={translate("resources.users.helper.erase", {
@ -148,6 +191,7 @@ const UserEditActions = () => {
})} })}
mutationMode="pessimistic" mutationMode="pessimistic"
/> />
</UserPreventSelfDelete>
</TopToolbar> </TopToolbar>
); );
}; };
@ -189,11 +233,44 @@ const UserTitle = () => {
); );
}; };
const UserEditToolbar = () => {
const record = useRecordContext();
const ownUserId = localStorage.getItem("user_id");
let ownUserIsSelected = false;
if (record && record.id) {
ownUserIsSelected = record.id === ownUserId;
}
return <>
<div className={ToolbarClasses.defaultToolbar}>
<Toolbar sx={{ justifyContent: "space-between" }}>
<SaveButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<DeleteButton />
</UserPreventSelfDelete>
</Toolbar>
</div>
</>
};
const UserBooleanInput = (props) => {
const record = useRecordContext();
const ownUserId = localStorage.getItem("user_id");
const isOwnUser = false;
let ownUserIsSelected = false;
if (record && (record.id === ownUserId)) {
ownUserIsSelected = true;
}
return <UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}><BooleanInput {...props} disabled={ownUserIsSelected} /></UserPreventSelfDelete>
}
export const UserEdit = (props: EditProps) => { export const UserEdit = (props: EditProps) => {
const translate = useTranslate(); const translate = useTranslate();
return ( return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}> <Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
<TabbedForm> <TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}> <FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} /> <AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
<TextInput source="id" disabled /> <TextInput source="id" disabled />
@ -202,7 +279,7 @@ export const UserEdit = (props: EditProps) => {
<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" /> <BooleanInput source="locked" />
<BooleanInput 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} />
<TextField source="consent_version" /> <TextField source="consent_version" />