Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Provide options to delete media and redact events on user erase
  • Loading branch information
aine-etke committed Sep 28, 2024
commit 3d70da75a32d70ce4676a5ae72f09e468a9877ce
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Synapse-Admin provides a support link in the main menu - `Contact support`. By d

### Supported Synapse

It needs at least [Synapse](https://github.com/element-hq/synapse) v1.93.0 for all functions to work as expected!
It needs at least [Synapse](https://github.com/element-hq/synapse) v1.116.0 for all functions to work as expected!

You get your server version with the request `/_synapse/admin/v1/server_version`.
See also [Synapse version API](https://element-hq.github.io/synapse/latest/admin_api/version_api.html).
Expand Down
111 changes: 111 additions & 0 deletions src/components/DeleteUserButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import { Fragment, useState } from "react";
import { SimpleForm, BooleanInput, useTranslate, RaRecord, useNotify, useRedirect, useDelete, NotificationType, useDeleteMany, Identifier, useUnselectAll } from "react-admin";
import ActionDelete from "@mui/icons-material/Delete";
import ActionCheck from "@mui/icons-material/CheckCircle";
import AlertError from "@mui/icons-material/ErrorOutline";

interface DeleteUserButtonProps {
selectedIds: Identifier[];
confirmTitle: string;
confirmContent: string;
}

const resourceName = "users";

const DeleteUserButton: React.FC<DeleteUserButtonProps> = (props) => {
const translate = useTranslate();
const [open, setOpen] = useState(false);
const [deleteMedia, setDeleteMedia] = useState(false);
const [redactEvents, setRedactEvents] = useState(false);

const notify = useNotify();
const redirect = useRedirect();

const [deleteMany, { isLoading }] = useDeleteMany();
const unselectAll = useUnselectAll(resourceName);
const recordIds = props.selectedIds;

const handleDialogOpen = () => setOpen(true);
const handleDialogClose = () => setOpen(false);

const handleDelete = (values: {deleteMedia: boolean, redactEvents: boolean}) => {
deleteMany(
resourceName,
{ ids: recordIds, meta: values },
{
onSuccess: () => {
handleDialogClose();
unselectAll();
redirect("/users");
},
onError: (error) =>
notify("ra.notification.data_provider_error", { type: 'error' as NotificationType }),
}
);
};

const handleConfirm = () => {
setOpen(false);
handleDelete({ deleteMedia: deleteMedia, redactEvents: redactEvents });
};

return (
<Fragment>
<Button
onClick={handleDialogOpen}
disabled={isLoading}
className={"ra-delete-button"}
key="button"
size="small"
sx={{
"&.MuiButton-sizeSmall": {
lineHeight: 1.5,
},
}}
color={"error"}
startIcon={<ActionDelete />}
>
{translate("ra.action.delete")}
</Button>
<Dialog open={open} onClose={handleDialogClose}>
<DialogTitle>{translate(props.confirmTitle)}</DialogTitle>
<DialogContent>
<DialogContentText>{translate(props.confirmContent)}</DialogContentText>
<SimpleForm toolbar={false}>
<BooleanInput
source="deleteMedia"
value={deleteMedia}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setDeleteMedia(event.target.checked)}
label="resources.users.action.delete_media"
defaultValue={false}
/>
<BooleanInput
source="redactEvents"
value={redactEvents}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setRedactEvents(event.target.checked)}
label="resources.users.action.redact_events"
defaultValue={false}
/>
</SimpleForm>
</DialogContent>
<DialogActions>
<Button disabled={false} onClick={handleDialogClose} startIcon={<AlertError />}>
{translate("ra.action.cancel")}
</Button>
<Button
disabled={false}
onClick={handleConfirm}
className={"ra-confirm RaConfirm-confirmPrimary"}
autoFocus
startIcon={<ActionCheck />}
>
{translate("ra.action.confirm")}
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
};

export default DeleteUserButton;
5 changes: 4 additions & 1 deletion src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,15 @@ 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_text: "Das bedeutet, dass die von dem/den Benutzer(n) gesendeten Nachrichten für alle, die zum Zeitpunkt des Sendens im Raum waren, sichtbar bleiben, aber für Benutzer, die dem Raum später beitreten, nicht sichtbar sind.",
erase_admin_error: "Das Löschen des eigenen Benutzers ist nicht erlaubt.",
modify_managed_user_error: "Das Ändern eines vom System verwalteten Benutzers ist nicht zulässig.",
},
action: {
erase: "Lösche Benutzerdaten",
erase_avatar: "Avatar löschen"
erase_avatar: "Avatar löschen",
delete_media: "Alle von dem/den Benutzer(n) hochgeladenen Medien löschen",
redact_events: "Schwärzen aller vom Benutzer gesendeten Ereignisse (-s)",
},
},
rooms: {
Expand Down
5 changes: 4 additions & 1 deletion src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,15 @@ 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_text: "This means messages sent by the user(-s) will still be visible by anyone who was in the room when these messages were sent, but hidden from users joining the room afterward.",
erase_admin_error: "Deleting own user is not allowed.",
modify_managed_user_error: "Modifying a system-managed user is not allowed.",
},
action: {
erase: "Erase user data",
erase_avatar: "Erase avatar"
erase_avatar: "Erase avatar",
delete_media: "Delete all media uploaded by the user(-s)",
redact_events: "Redact all events sent by the user(-s)",
},
},
rooms: {
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,15 @@ const fa: SynapseTranslationMessages = {
password: "با تغییر رمز عبور کاربر از تمام دستگاه ها خارج می شود.",
deactivate: "برای فعالسازی مجدد حساب باید رمز عبور وارد کنید.",
erase: "کاربر را به عنوان GDPR پاک شده علامت گذاری کنید",
erase_text: "وهذا يعني أن الرسائل المرسلة من قبل المستخدم (المستخدمين) ستظل مرئية من قبل أي شخص كان في الغرفة عند إرسال هذه الرسائل، ولكنها مخفية عن المستخدمين الذين ينضمون إلى الغرفة بعد ذلك.",
erase_admin_error: "حذف المستخدم الخاص غير مسموح به.",
modify_managed_user_error: "لا يُسمح بتغيير المستخدم الذي يديره النظام.",
},
action: {
erase: "پاک کردن اطلاعات کاربر",
erase_avatar: "محو الصورة الرمزية",
delete_media: "حذف جميع الوسائط التي تم تحميلها بواسطة المستخدم (المستخدمين)",
redact_events: "تنقيح جميع الأحداث المرسلة من قبل المستخدم (-s)",
},
},
rooms: {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,15 @@ 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_text: "Cela signifie que les messages envoyés par le(s) utilisateur(s) seront toujours visibles par toute personne qui se trouvait dans la salle au moment où ces messages ont été envoyés, mais qu'ils seront cachés aux utilisateurs qui rejoindront la salle par la suite.",
erase_admin_error: "La suppression de son propre utilisateur n'est pas autorisée.",
modify_managed_user_error: "La modification d'un utilisateur géré par le système n'est pas autorisée.",
},
action: {
erase: "Effacer les données de l'utilisateur",
erase_avatar: "Effacer l'avatar",
delete_media: "Supprimer tous les médias téléchargés par le(s) utilisateur(s)",
redact_events: "Expurger tous les événements envoyés par l'utilisateur(-s)",
},
},
rooms: {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,15 @@ interface SynapseTranslationMessages extends TranslationMessages {
password?: string;
deactivate: string;
erase: string;
erase_text: string;
erase_admin_error: string;
modify_managed_user_error: string;
};
action: {
erase: string;
erase_avatar: string;
delete_media: string;
redact_events: string;
};
};
rooms: {
Expand Down
8 changes: 6 additions & 2 deletions src/i18n/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,15 @@ const it: SynapseTranslationMessages = {
password: "Cambiando la password l'utente verrà disconnesso da tutte le sessioni attive.",
deactivate: "Devi fornire una password per riattivare l'account.",
erase: "Constrassegna l'utente come cancellato dal GDPR",
erase_text: "Ciò significa che i messaggi inviati dall'utente (o dagli utenti) saranno ancora visibili da chiunque si trovasse nella stanza al momento dell'invio, ma saranno nascosti agli utenti che si uniranno alla stanza in seguito.",
erase_admin_error: "Non è consentito eliminare il proprio utente.",
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
},
action: {
erase: "Cancella i dati dell'utente",
erase_admin_error: "Non è consentito eliminare il proprio utente.",
modify_managed_user_error: "La modifica di un utente gestito dal sistema non è consentita.",
erase_avatar: "Cancella l'avatar dell'utente",
delete_media: "Elimina tutti i media caricati dall'utente(-s)",
redact_events: "Ridurre tutti gli eventi inviati dall'utente(-s)",
},
},
rooms: {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,15 @@ const ru: SynapseTranslationMessages = {
password: "Смена пароля завершит все сессии пользователя.",
deactivate: "Вы должны предоставить пароль для реактивации учётной записи.",
erase: "Пометить пользователя как удалённого в соответствии с GDPR",
erase_text: "Это означает, что сообщения, отправленные пользователем (-ами), будут по-прежнему видны всем, кто находился в комнате в момент их отправки, но будут скрыты от пользователей, присоединившихся к комнате после этого.",
erase_admin_error: "Удаление собственного пользователя запрещено.",
modify_managed_user_error: "Изменение пользователя, управляемого системой, не допускается.",
},
action: {
erase: "Удалить данные пользователя",
erase_avatar: "Удалить аватар",
delete_media: "Удаление всех медиафайлов, загруженных пользователем (-ами)",
redact_events: "Удаление всех событий, отправленных пользователем (-ами)",
},
},
rooms: {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,15 @@ const zh: SynapseTranslationMessages = {
helper: {
deactivate: "您必须提供一串密码来激活账户。",
erase: "将用户标记为根据 GDPR 的要求抹除了",
erase_text: "这意味着用户发送的信息对于发送信息时在房间内的任何人来说都是可见的,但对于之后加入房间的用户来说则是隐藏的。",
erase_admin_error: "不允许删除自己的用户",
modify_managed_user_error: "不允许修改系统管理的用户。",
},
action: {
erase: "抹除用户信息",
erase_avatar: "抹掉头像",
delete_media: "删除用户上传的所有媒体",
redact_events: "重新编辑用户(-s)发送的所有事件",
},
},
rooms: {
Expand Down
20 changes: 10 additions & 10 deletions src/resources/users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
ResourceProps,
SearchInput,
SelectInput,
BulkDeleteButton,
DeleteButton,
maxLength,
regex,
Expand All @@ -61,6 +60,7 @@ import {
import { Link } from "react-router-dom";

import AvatarField from "../components/AvatarField";
import DeleteUserButton from "../components/DeleteUserButton";
import { isASManaged } from "../components/mxid";
import { ServerNoticeButton, ServerNoticeBulkButton } from "../components/ServerNotices";
import { DATE_FORMAT } from "../components/date";
Expand Down Expand Up @@ -141,10 +141,10 @@ const UserBulkActionButtons = () => {
<>
<ServerNoticeBulkButton />
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
<BulkDeleteButton
label="resources.users.action.erase"
<DeleteUserButton
selectedIds={selectedIds}
confirmTitle="resources.users.helper.erase"
mutationMode="pessimistic"
confirmContent="resources.users.helper.erase_text"
/>
</UserPreventSelfDelete>
</>
Expand Down Expand Up @@ -200,15 +200,15 @@ const UserEditActions = () => {
return (
<TopToolbar>
{!record?.deactivated && <ServerNoticeButton />}
{record && record.id && (
<UserPreventSelfDelete ownUserIsSelected={ownUserIsSelected} asManagedUserIsSelected={asManagedUserIsSelected}>
<DeleteButton
label="resources.users.action.erase"
confirmTitle={translate("resources.users.helper.erase", {
smart_count: 1,
})}
mutationMode="pessimistic"
<DeleteUserButton
selectedIds={[record?.id]}
confirmTitle="resources.users.helper.erase"
confirmContent="resources.users.helper.erase_text"
/>
</UserPreventSelfDelete>
)}
</TopToolbar>
);
};
Expand Down
53 changes: 52 additions & 1 deletion src/synapse/dataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ const filterUndefined = (obj: Record<string, any>) => {
return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined));
};

interface Action {
endpoint: string;
method?: string;
body?: Record<string, any>;
}

interface Room {
room_id: string;
name?: string;
Expand Down Expand Up @@ -282,6 +288,24 @@ const resourceMap = {
body: { erase: true },
method: "POST",
}),
afterDelete: (params: DeleteParams) => {
let actions : Action[] = [];
if (params.meta?.deleteMedia) {
actions.push({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/media`,
method: "DELETE",
});
}

if (params.meta?.redactEvents) {
actions.push({
endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(returnMXID(params.id))}/redact`,
method: "POST",
});
}

return actions;
},
},
rooms: {
path: "/_synapse/admin/v1/rooms",
Expand Down Expand Up @@ -702,6 +726,17 @@ const baseDataProvider: SynapseDataProvider = {
method: "method" in del ? del.method : "DELETE",
body: "body" in del ? JSON.stringify(del.body) : null,
});

if ("afterDelete" in res) {
const del = res.afterDelete(params);
del.map(async action => {
const endpoint_url = homeserver + action.endpoint;
return jsonClient(endpoint_url, {
method: "method" in action ? action.method : "DELETE",
body: "body" in action ? JSON.stringify(action.body) : null,
});
});
}
return { data: json };
} else {
const endpoint_url = homeserver + res.path;
Expand All @@ -714,7 +749,7 @@ const baseDataProvider: SynapseDataProvider = {
},

deleteMany: async (resource, params) => {
console.log("deleteMany " + resource);
console.log("deleteMany " + resource, "params", params);
const homeserver = storage.getItem("base_url");
if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set");

Expand All @@ -731,6 +766,22 @@ const baseDataProvider: SynapseDataProvider = {
});
})
);

if ("afterDelete" in res) {
await Promise.all(
params.ids.map(id => {
const del = res.afterDelete({ ...params, id: id });
del.map(async action => {
const endpoint_url = homeserver + action.endpoint;
return jsonClient(endpoint_url, {
method: "method" in action ? action.method : "DELETE",
body: "body" in action ? JSON.stringify(action.body) : null,
});
});
})
);
}

return {
data: responses.map(({ json }) => json),
};
Expand Down