feat(reports): allow to suspend a profile or a user account directly from the report view

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2023-08-31 17:08:55 +02:00
parent b105c508c0
commit 69588dbf4c
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
5 changed files with 172 additions and 5 deletions

View file

@ -42,6 +42,11 @@ const REPORT_FRAGMENT = gql`
id
reported {
...ActorFragment
... on Person {
user {
id
}
}
}
reporter {
...ActorFragment

View file

@ -1592,5 +1592,14 @@
"Event deleted and report resolved": "Event deleted and report resolved",
"Event deleted": "Event deleted",
"Comment deleted and report resolved": "Comment deleted and report resolved",
"Comment under event {eventTitle}": "Comment under event {eventTitle}"
"Comment under event {eventTitle}": "Comment under event {eventTitle}",
"Suspend profile": "Suspend profile",
"Do you really want to suspend this profile? All of the profiles content will be deleted.": "Do you really want to suspend this profile? All of the profiles content will be deleted.",
"There will be no way to restore the profile's data!": "There will be no way to restore the profile's data!",
"Suspend the profile": "Suspend the profile",
"The following user's profiles will be deleted, with all their data:": "The following user's profiles will be deleted, with all their data:",
"Do you really want to suspend the account « {emailAccount} » ?": "Do you really want to suspend the account « {emailAccount} » ?",
"There will be no way to restore the user's data!": "There will be no way to restore the user's data!",
"User suspended and report resolved": "User suspended and report resolved",
"Profile suspended and report resolved": "Profile suspended and report resolved"
}

View file

@ -1590,5 +1590,14 @@
"Event deleted and report resolved": "Événement supprimé et signalement résolu",
"Event deleted": "Événement supprimé",
"Comment deleted and report resolved": "Commentaire supprimé et signalement résolu",
"Comment under event {eventTitle}": "Commentaire sous l'événement {eventTitle}"
"Comment under event {eventTitle}": "Commentaire sous l'événement {eventTitle}",
"Suspend the profile?": "Suspendre le profil ?",
"Do you really want to suspend this profile? All of the profiles content will be deleted.": "Voulez-vous vraiment suspendre ce profil ? Tout le contenu du profil sera supprimé.",
"There will be no way to restore the profile's data!": "Il n'y aura aucun moyen de restorer les données du profil !",
"Suspend the profile": "Suspendre le profil",
"The following user's profiles will be deleted, with all their data:": "Les profils suivants de l'utilisateur·ice seront supprimés, avec toutes leurs données :",
"Do you really want to suspend the account « {emailAccount} » ?": "Voulez-vous vraiment suspendre le compte « {emailAccount} » ?",
"There will be no way to restore the user's data!": "Il n'y aura aucun moyen de restorer les données de l'utilisateur·ice !",
"User suspended and report resolved": "Utilisateur suspendu et signalement résolu",
"Profile suspended and report resolved": "Profil suspendu et signalement résolu"
}

View file

@ -88,7 +88,7 @@
<td>
{{ t("Reported identity") }}
</td>
<td>
<td class="flex items-center justify-between pr-6">
<router-link
:to="{
name: RouteName.ADMIN_PROFILE,
@ -103,6 +103,20 @@
/>
{{ displayNameAndUsername(report.reported) }}
</router-link>
<o-button
v-if="report.reported.domain"
variant="danger"
@click="suspendProfile(report.reported.id as string)"
icon-left="delete"
>{{ t("Suspend the profile") }}</o-button
>
<o-button
v-else-if="(report.reported as IPerson).user"
variant="danger"
@click="suspendUser((report.reported as IPerson).user as IUser)"
icon-left="delete"
>{{ t("Suspend the account") }}</o-button
>
</td>
</tr>
<tr>
@ -333,7 +347,7 @@ import { ActorType, AntiSpamFeedback, ReportStatusEnum } from "@/types/enums";
import RouteName from "@/router/name";
import { GraphQLError } from "graphql";
import { ApolloCache, FetchResult } from "@apollo/client/core";
import { useMutation, useQuery } from "@vue/apollo-composable";
import { useLazyQuery, useMutation, useQuery } from "@vue/apollo-composable";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { useHead } from "@vueuse/head";
import { useI18n } from "vue-i18n";
@ -348,6 +362,10 @@ import { useFeatures } from "@/composition/apollo/config";
import { IEvent } from "@/types/event.model";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
import EventComment from "@/components/Comment/EventComment.vue";
import { SUSPEND_PROFILE } from "@/graphql/actor";
import { GET_USER, SUSPEND_USER } from "@/graphql/user";
import { IUser } from "@/types/current-user.model";
import { waitApolloQuery } from "@/vue-apollo";
const router = useRouter();
@ -663,6 +681,105 @@ const reportToAntispam = (spam: boolean) => {
},
});
};
const { mutate: doSuspendProfile, onDone: onSuspendProfileDone } = useMutation<
{
suspendProfile: { id: string };
},
{ id: string }
>(SUSPEND_PROFILE);
const { mutate: doSuspendUser, onDone: onSuspendUserDone } = useMutation<
{ suspendProfile: { id: string } },
{ userId: string }
>(SUSPEND_USER);
const userLazyQuery = useLazyQuery<{ user: IUser }, { id: string }>(GET_USER);
const suspendProfile = async (actorId: string): Promise<void> => {
dialog?.confirm({
title: t("Suspend the profile?"),
message:
t(
"Do you really want to suspend this profile? All of the profiles content will be deleted."
) +
`<p><b>` +
t("There will be no way to restore the profile's data!") +
`</b></p>`,
confirmText: t("Suspend the profile"),
cancelText: t("Cancel"),
variant: "danger",
onConfirm: async () => {
doSuspendProfile({
id: actorId,
});
return router.push({ name: RouteName.USERS });
},
});
};
const userSuspendedProfilesMessages = (user: IUser) => {
return (
t("The following user's profiles will be deleted, with all their data:") +
`<ul class="list-disc pl-3">` +
user.actors
.map((person) => `<li>${displayNameAndUsername(person)}</li>`)
.join("") +
`</ul><b>`
);
};
const cachedReportedUser = ref<IUser | undefined>();
const suspendUser = async (user: IUser): Promise<void> => {
try {
if (!cachedReportedUser.value) {
userLazyQuery.load(GET_USER, { id: user.id });
const userLazyQueryResult = await waitApolloQuery<
{ user: IUser },
{ id: string }
>(userLazyQuery);
console.debug("data", userLazyQueryResult);
cachedReportedUser.value = userLazyQueryResult.data.user;
}
dialog?.confirm({
title: t("Suspend the account?"),
message:
t("Do you really want to suspend the account « {emailAccount} » ?", {
emailAccount: cachedReportedUser.value.email,
}) +
" " +
userSuspendedProfilesMessages(cachedReportedUser.value) +
"<b>" +
t("There will be no way to restore the user's data!") +
`</b>`,
confirmText: t("Suspend the account"),
cancelText: t("Cancel"),
variant: "danger",
onConfirm: async () => {
doSuspendUser({
userId: user.id,
});
return router.push({ name: RouteName.USERS });
},
});
} catch (e) {
console.error(e);
}
};
onSuspendUserDone(async () => {
await router.push({ name: RouteName.REPORTS });
notifier?.success(t("User suspended and report resolved"));
});
onSuspendProfileDone(async () => {
await router.push({ name: RouteName.REPORTS });
notifier?.success(t("Profile suspended and report resolved"));
});
</script>
<style lang="scss" scoped>
tbody td img.image,

View file

@ -1,7 +1,13 @@
import { ApolloClient, NormalizedCacheObject } from "@apollo/client/core";
import {
ApolloClient,
ApolloQueryResult,
NormalizedCacheObject,
OperationVariables,
} from "@apollo/client/core";
import buildCurrentUserResolver from "@/apollo/user";
import { cache } from "./apollo/memory";
import { fullLink } from "./apollo/link";
import { UseQueryReturn } from "@vue/apollo-composable";
export const apolloClient = new ApolloClient<NormalizedCacheObject>({
cache,
@ -9,3 +15,24 @@ export const apolloClient = new ApolloClient<NormalizedCacheObject>({
connectToDevTools: true,
resolvers: buildCurrentUserResolver(cache),
});
export function waitApolloQuery<
TResult = any,
TVariables extends OperationVariables = OperationVariables,
>({
onResult,
onError,
}: UseQueryReturn<TResult, TVariables>): Promise<ApolloQueryResult<TResult>> {
return new Promise((res, rej) => {
const { off: offResult } = onResult((result) => {
if (result.loading === false) {
offResult();
res(result);
}
});
const { off: offError } = onError((error) => {
offError();
rej(error);
});
});
}