Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent self user delete #608

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ const de: SynapseTranslationMessages = {
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.",
erase: "DSGVO konformes Löschen der Benutzerdaten",
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt",
},
action: {
erase: "Lösche Benutzerdaten",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const en: SynapseTranslationMessages = {
password: "Changing password will log user out of all sessions.",
deactivate: "You must provide a password to re-activate an account.",
erase: "Mark the user as GDPR-erased",
erase_admin_error: "Deleting own user is not allowed.",
},
action: {
erase: "Erase user data",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ const fr: SynapseTranslationMessages = {
helper: {
deactivate: "Vous devrez fournir un mot de passe pour réactiver le compte.",
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: {
erase: "Effacer les données de l'utilisateur",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ interface SynapseTranslationMessages extends TranslationMessages {
password?: string;
deactivate: string;
erase: string;
erase_admin_error: string;
};
action: {
erase: string;
Expand Down
1 change: 1 addition & 0 deletions src/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const it: SynapseTranslationMessages = {
},
action: {
erase: "Cancella i dati dell'utente",
erase_admin_error: "Non è consentito eliminare il proprio utente.",
},
},
rooms: {
Expand Down
1 change: 1 addition & 0 deletions src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ const ru: SynapseTranslationMessages = {
password: "Смена пароля завершит все сессии пользователя.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
erase_admin_error: "Удаление собственного пользователя запрещено.",
},
action: {
erase: "Удалить данные пользователя",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const zh: SynapseTranslationMessages = {
helper: {
deactivate: "您必须提供一串密码来激活账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了",
erase_admin_error: "不允许删除自己的用户",
},
action: {
erase: "抹除用户信息",
Expand Down
111 changes: 94 additions & 17 deletions src/resources/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import PermMediaIcon from "@mui/icons-material/PermMedia";
import PersonPinIcon from "@mui/icons-material/PersonPin";
import SettingsInputComponentIcon from "@mui/icons-material/SettingsInputComponent";
import ViewListIcon from "@mui/icons-material/ViewList";
import { useEffect, useState } from "react";
import { Alert, ownerDocument } from "@mui/material";
import {
ArrayInput,
ArrayField,
Expand Down Expand Up @@ -42,11 +44,15 @@ import {
useRecordContext,
useTranslate,
Pagination,
SaveButton,
CreateButton,
ExportButton,
TopToolbar,
Toolbar,
NumberField,
useListContext,
useNotify,
ToolbarClasses,
} from "react-admin";
import { Link } from "react-router-dom";

Expand Down Expand Up @@ -92,16 +98,47 @@ const userFilters = [
<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 />
<BulkDeleteButton
label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic"
/>
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<BulkDeleteButton
label="resources.users.action.erase"
confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic"
/>
</UserPreventSelfDelete>
</>
);
};

export const UserList = (props: ListProps) => (
<List
Expand Down Expand Up @@ -137,17 +174,24 @@ const validateAddress = [required(), maxLength(255)];
const UserEditActions = () => {
const record = useRecordContext();
const translate = useTranslate();
const ownUserId = localStorage.getItem("user_id");
let ownUserIsSelected = false;
if (record && record.id) {
ownUserIsSelected = record.id === ownUserId;
}

return (
<TopToolbar>
{!record?.deactivated && <ServerNoticeButton />}
<DeleteButton
label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", {
smart_count: 1,
})}
mutationMode="pessimistic"
/>
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected}>
<DeleteButton
label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", {
smart_count: 1,
})}
mutationMode="pessimistic"
/>
</UserPreventSelfDelete>
</TopToolbar>
);
};
Expand Down Expand Up @@ -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) => {
const translate = useTranslate();

return (
<Edit {...props} title={<UserTitle />} actions={<UserEditActions />}>
<TabbedForm>
<TabbedForm toolbar={<UserEditToolbar />}>
<FormTab label={translate("resources.users.name", { smart_count: 1 })} icon={<PersonPinIcon />}>
<AvatarField source="avatar_src" sortable={false} sx={{ height: "120px", width: "120px", float: "right" }} />
<TextInput source="id" disabled />
Expand All @@ -202,7 +279,7 @@ export const UserEdit = (props: EditProps) => {
<SelectInput source="user_type" choices={choices_type} translateChoice={false} resettable />
<BooleanInput source="admin" />
<BooleanInput source="locked" />
<BooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
<UserBooleanInput source="deactivated" helperText="resources.users.helper.deactivate" />
<BooleanInput source="erased" disabled />
<DateField source="creation_ts_ms" showTime options={DATE_FORMAT} />
<TextField source="consent_version" />
Expand Down