Introduce basic user and profile management
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
da4ea84baf
commit
beb35a09c6
|
@ -32,7 +32,13 @@ export const FETCH_PERSON = gql`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const GET_PERSON = gql`
|
export const GET_PERSON = gql`
|
||||||
query($actorId: ID!) {
|
query(
|
||||||
|
$actorId: ID!
|
||||||
|
$organizedEventsPage: Int
|
||||||
|
$organizedEventsLimit: Int
|
||||||
|
$participationPage: Int
|
||||||
|
$participationLimit: Int
|
||||||
|
) {
|
||||||
person(id: $actorId) {
|
person(id: $actorId) {
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
|
@ -51,12 +57,65 @@ export const GET_PERSON = gql`
|
||||||
feedTokens {
|
feedTokens {
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
organizedEvents {
|
organizedEvents(page: $organizedEventsPage, limit: $organizedEventsLimit) {
|
||||||
|
total
|
||||||
|
elements {
|
||||||
|
id
|
||||||
uuid
|
uuid
|
||||||
title
|
title
|
||||||
beginsOn
|
beginsOn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
participations(page: $participationPage, limit: $participationLimit) {
|
||||||
|
total
|
||||||
|
elements {
|
||||||
|
id
|
||||||
|
event {
|
||||||
|
id
|
||||||
|
uuid
|
||||||
|
title
|
||||||
|
beginsOn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LIST_PROFILES = gql`
|
||||||
|
query ListProfiles(
|
||||||
|
$preferredUsername: String
|
||||||
|
$name: String
|
||||||
|
$domain: String
|
||||||
|
$local: Boolean
|
||||||
|
$suspended: Boolean
|
||||||
|
$page: Int
|
||||||
|
$limit: Int
|
||||||
|
) {
|
||||||
|
persons(
|
||||||
|
preferredUsername: $preferredUsername
|
||||||
|
name: $name
|
||||||
|
domain: $domain
|
||||||
|
local: $local
|
||||||
|
suspended: $suspended
|
||||||
|
page: $page
|
||||||
|
limit: $limit
|
||||||
|
) {
|
||||||
|
total
|
||||||
|
elements {
|
||||||
|
id
|
||||||
|
preferredUsername
|
||||||
|
domain
|
||||||
|
name
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -505,3 +564,19 @@ export const CREATE_GROUP = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const SUSPEND_PROFILE = gql`
|
||||||
|
mutation SuspendProfile($id: ID!) {
|
||||||
|
suspendProfile(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UNSUSPEND_PROFILE = gql`
|
||||||
|
mutation UnSuspendProfile($id: ID!) {
|
||||||
|
unsuspendProfile(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -485,6 +485,8 @@ export const EVENT_PERSON_PARTICIPATION = gql`
|
||||||
person(id: $actorId) {
|
person(id: $actorId) {
|
||||||
id
|
id
|
||||||
participations(eventId: $eventId) {
|
participations(eventId: $eventId) {
|
||||||
|
total
|
||||||
|
elements {
|
||||||
id
|
id
|
||||||
role
|
role
|
||||||
actor {
|
actor {
|
||||||
|
@ -496,6 +498,7 @@ export const EVENT_PERSON_PARTICIPATION = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql`
|
export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql`
|
||||||
|
@ -503,6 +506,8 @@ export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql`
|
||||||
eventPersonParticipationChanged(personId: $actorId) {
|
eventPersonParticipationChanged(personId: $actorId) {
|
||||||
id
|
id
|
||||||
participations(eventId: $eventId) {
|
participations(eventId: $eventId) {
|
||||||
|
total
|
||||||
|
elements {
|
||||||
id
|
id
|
||||||
role
|
role
|
||||||
actor {
|
actor {
|
||||||
|
@ -514,4 +519,5 @@ export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -178,6 +178,12 @@ export const LOGS = gql`
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
|
... on Person {
|
||||||
|
id
|
||||||
|
preferredUsername
|
||||||
|
domain
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
insertedAt
|
insertedAt
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,8 +64,8 @@ export const VALIDATE_EMAIL = gql`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DELETE_ACCOUNT = gql`
|
export const DELETE_ACCOUNT = gql`
|
||||||
mutation DeleteAccount($password: String!) {
|
mutation DeleteAccount($password: String, $userId: ID!) {
|
||||||
deleteAccount(password: $password) {
|
deleteAccount(password: $password, userId: $userId) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,3 +134,58 @@ export const SET_USER_SETTINGS = gql`
|
||||||
}
|
}
|
||||||
${USER_SETTINGS_FRAGMENT}
|
${USER_SETTINGS_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LIST_USERS = gql`
|
||||||
|
query ListUsers($email: String, $page: Int, $limit: Int) {
|
||||||
|
users(email: $email, page: $page, limit: $limit) {
|
||||||
|
total
|
||||||
|
elements {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
locale
|
||||||
|
confirmedAt
|
||||||
|
disabled
|
||||||
|
actors {
|
||||||
|
id
|
||||||
|
preferredUsername
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
name
|
||||||
|
summary
|
||||||
|
}
|
||||||
|
settings {
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_USER = gql`
|
||||||
|
query GetUser($id: ID!) {
|
||||||
|
user(id: $id) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
confirmedAt
|
||||||
|
confirmationSentAt
|
||||||
|
locale
|
||||||
|
disabled
|
||||||
|
defaultActor {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
actors {
|
||||||
|
id
|
||||||
|
preferredUsername
|
||||||
|
name
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
participations {
|
||||||
|
total
|
||||||
|
}
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -607,5 +607,25 @@
|
||||||
"Your timezone was detected as {timezone}.": "Your timezone was detected as {timezone}.",
|
"Your timezone was detected as {timezone}.": "Your timezone was detected as {timezone}.",
|
||||||
"Manage my settings": "Manage my settings",
|
"Manage my settings": "Manage my settings",
|
||||||
"Let's define a few settings": "Let's define a few settings",
|
"Let's define a few settings": "Let's define a few settings",
|
||||||
"All good, let's continue!": "All good, let's continue!"
|
"All good, let's continue!": "All good, let's continue!",
|
||||||
|
"Organize and take action, freely": "Organize and take action, freely",
|
||||||
|
"Let\\'s create a new common": "Let\\'s create a new common",
|
||||||
|
"Login status": "Login status",
|
||||||
|
"Suspended": "Suspended",
|
||||||
|
"Active": "Active",
|
||||||
|
"Local": "Local",
|
||||||
|
"User": "User",
|
||||||
|
"Confirmed": "Confirmed",
|
||||||
|
"Confirmed at": "Confirmed at",
|
||||||
|
"Language": "Language",
|
||||||
|
"Administrator": "Administrator",
|
||||||
|
"Moderator": "Moderator",
|
||||||
|
"{number} organized events": "No organized events|One organized event|{number} organized events",
|
||||||
|
"Begins on": "Begins on",
|
||||||
|
"{number} participations": "No participations|One participation|{number} participations",
|
||||||
|
"{profile} (by default)": "{profile} (by default)",
|
||||||
|
"Participations": "Participations",
|
||||||
|
"Nothing to see here": "Nothing to see here",
|
||||||
|
"Not confirmed": "Not confirmed",
|
||||||
|
"{actor} suspended profile {profile}": "{actor} suspended profile {profile}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -626,5 +626,26 @@
|
||||||
"Your timezone was detected as {timezone}.": "Votre fuseau horaire a été détecté en tant que {timezone}.",
|
"Your timezone was detected as {timezone}.": "Votre fuseau horaire a été détecté en tant que {timezone}.",
|
||||||
"Manage my settings": "Gérer mes paramètres",
|
"Manage my settings": "Gérer mes paramètres",
|
||||||
"Let's define a few settings": "Définissons quelques paramètres",
|
"Let's define a few settings": "Définissons quelques paramètres",
|
||||||
"All good, let's continue!": "C'est tout bon, continuons !"
|
"All good, let's continue!": "C'est tout bon, continuons !",
|
||||||
|
"Organize and take action, freely": "S'organiser et agir, librement",
|
||||||
|
"Let\\'s create a new common": "Créeons un nouveau common",
|
||||||
|
"Login status": "Statut de connexion",
|
||||||
|
"Suspended": "Suspendu·e",
|
||||||
|
"Active": "Actif·ve",
|
||||||
|
"Local": "Local·e",
|
||||||
|
"User": "Utilisateur·ice",
|
||||||
|
"{profile} (by default)": "{profile} (par défault)",
|
||||||
|
"Locale": "Locale",
|
||||||
|
"Confirmed": "Confirmé·e",
|
||||||
|
"Confirmed at": "Confirmé·e à",
|
||||||
|
"Language": "Langue",
|
||||||
|
"Administrator": "Administrateur·ice",
|
||||||
|
"Moderator": "Moderateur·ice",
|
||||||
|
"{number} organized events": "Aucun événement organisé|Un événement organisé|{number} événements organisés",
|
||||||
|
"{number} participations": "Aucune participation|Une participation|{number} participations",
|
||||||
|
"Begins on": "Commence le",
|
||||||
|
"Participations": "Participations",
|
||||||
|
"Nothing to see here": "Il n'y a rien à voir ici",
|
||||||
|
"Not confirmed": "Non confirmé·e",
|
||||||
|
"{actor} suspended profile {profile}": "{actor} a suspendu le profil {profile}"
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,10 @@ import ReportList from "@/views/Moderation/ReportList.vue";
|
||||||
import Report from "@/views/Moderation/Report.vue";
|
import Report from "@/views/Moderation/Report.vue";
|
||||||
import Logs from "@/views/Moderation/Logs.vue";
|
import Logs from "@/views/Moderation/Logs.vue";
|
||||||
import EditIdentity from "@/views/Account/children/EditIdentity.vue";
|
import EditIdentity from "@/views/Account/children/EditIdentity.vue";
|
||||||
|
import Users from "../views/Admin/Users.vue";
|
||||||
|
import Profiles from "../views/Admin/Profiles.vue";
|
||||||
|
import AdminProfile from "../views/Admin/AdminProfile.vue";
|
||||||
|
import AdminUserProfile from "../views/Admin/AdminUserProfile.vue";
|
||||||
|
|
||||||
export enum SettingsRouteName {
|
export enum SettingsRouteName {
|
||||||
SETTINGS = "SETTINGS",
|
SETTINGS = "SETTINGS",
|
||||||
|
@ -25,6 +29,10 @@ export enum SettingsRouteName {
|
||||||
RELAYS = "Relays",
|
RELAYS = "Relays",
|
||||||
RELAY_FOLLOWINGS = "Followings",
|
RELAY_FOLLOWINGS = "Followings",
|
||||||
RELAY_FOLLOWERS = "Followers",
|
RELAY_FOLLOWERS = "Followers",
|
||||||
|
USERS = "USERS",
|
||||||
|
PROFILES = "PROFILES",
|
||||||
|
ADMIN_PROFILE = "ADMIN_PROFILE",
|
||||||
|
ADMIN_USER_PROFILE = "ADMIN_USER_PROFILE",
|
||||||
MODERATION = "MODERATION",
|
MODERATION = "MODERATION",
|
||||||
REPORTS = "Reports",
|
REPORTS = "Reports",
|
||||||
REPORT = "Report",
|
REPORT = "Report",
|
||||||
|
@ -87,6 +95,34 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "admin/users",
|
||||||
|
name: SettingsRouteName.USERS,
|
||||||
|
component: Users,
|
||||||
|
props: true,
|
||||||
|
meta: { requiredAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "admin/users/:id",
|
||||||
|
name: SettingsRouteName.ADMIN_USER_PROFILE,
|
||||||
|
component: AdminUserProfile,
|
||||||
|
props: true,
|
||||||
|
meta: { requiredAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "admin/profiles",
|
||||||
|
name: SettingsRouteName.PROFILES,
|
||||||
|
component: Profiles,
|
||||||
|
props: true,
|
||||||
|
meta: { requiredAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "admin/profiles/:id",
|
||||||
|
name: SettingsRouteName.ADMIN_PROFILE,
|
||||||
|
component: AdminProfile,
|
||||||
|
props: true,
|
||||||
|
meta: { requiredAuth: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "admin/relays",
|
path: "admin/relays",
|
||||||
name: SettingsRouteName.RELAYS,
|
name: SettingsRouteName.RELAYS,
|
||||||
|
|
|
@ -66,3 +66,10 @@ export function usernameWithDomain(actor: IActor): string {
|
||||||
}
|
}
|
||||||
return actor.preferredUsername;
|
return actor.preferredUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function displayNameAndUsername(actor: IActor): string {
|
||||||
|
if (actor.name) {
|
||||||
|
return `${actor.name} (@${usernameWithDomain(actor)})`;
|
||||||
|
}
|
||||||
|
return usernameWithDomain(actor);
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export interface IPerson extends IActor {
|
||||||
goingToEvents: IEvent[];
|
goingToEvents: IEvent[];
|
||||||
participations: IParticipant[];
|
participations: IParticipant[];
|
||||||
memberships: Paginate<IMember>;
|
memberships: Paginate<IMember>;
|
||||||
|
user?: ICurrentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Person extends Actor implements IPerson {
|
export class Person extends Actor implements IPerson {
|
||||||
|
@ -26,6 +27,8 @@ export class Person extends Actor implements IPerson {
|
||||||
|
|
||||||
memberships!: Paginate<IMember>;
|
memberships!: Paginate<IMember>;
|
||||||
|
|
||||||
|
user!: ICurrentUser;
|
||||||
|
|
||||||
constructor(hash: IPerson | {} = {}) {
|
constructor(hash: IPerson | {} = {}) {
|
||||||
super(hash);
|
super(hash);
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,14 @@ export interface ICurrentUser {
|
||||||
settings: IUserSettings;
|
settings: IUserSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IUser extends ICurrentUser {
|
||||||
|
confirmedAt: Date;
|
||||||
|
confirmationSendAt: Date;
|
||||||
|
locale: String;
|
||||||
|
actors: IPerson[];
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export enum INotificationPendingParticipationEnum {
|
export enum INotificationPendingParticipationEnum {
|
||||||
NONE = "NONE",
|
NONE = "NONE",
|
||||||
DIRECT = "DIRECT",
|
DIRECT = "DIRECT",
|
||||||
|
|
|
@ -39,11 +39,13 @@ export enum ActionLogAction {
|
||||||
REPORT_UPDATE_RESOLVED = "REPORT_UPDATE_RESOLVED",
|
REPORT_UPDATE_RESOLVED = "REPORT_UPDATE_RESOLVED",
|
||||||
EVENT_DELETION = "EVENT_DELETION",
|
EVENT_DELETION = "EVENT_DELETION",
|
||||||
COMMENT_DELETION = "COMMENT_DELETION",
|
COMMENT_DELETION = "COMMENT_DELETION",
|
||||||
|
ACTOR_SUSPENSION = "ACTOR_SUSPENSION",
|
||||||
|
ACTOR_UNSUSPENSION = "ACTOR_UNSUSPENSION",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IActionLog {
|
export interface IActionLog {
|
||||||
id: string;
|
id: string;
|
||||||
object: IReport | IReportNote | IEvent;
|
object: IReport | IReportNote | IEvent | IComment | IActor;
|
||||||
actor: IActor;
|
actor: IActor;
|
||||||
action: ActionLogAction;
|
action: ActionLogAction;
|
||||||
insertedAt: Date;
|
insertedAt: Date;
|
||||||
|
|
318
js/src/views/Admin/AdminProfile.vue
Normal file
318
js/src/views/Admin/AdminProfile.vue
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="person" class="section">
|
||||||
|
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<router-link :to="{ name: RouteName.ADMIN }">{{ $t("Admin") }}</router-link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.PROFILES,
|
||||||
|
}"
|
||||||
|
>{{ $t("Profiles") }}</router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="is-active">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.PROFILES,
|
||||||
|
params: { id: person.id },
|
||||||
|
}"
|
||||||
|
>{{ person.name || person.preferredUsername }}</router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left" v-if="person.avatar">
|
||||||
|
<p class="image is-48x48">
|
||||||
|
<img :src="person.avatar.url" alt="" />
|
||||||
|
</p>
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content">
|
||||||
|
<strong v-if="person.name">{{ person.name }}</strong>
|
||||||
|
<small>@{{ usernameWithDomain(person) }}</small>
|
||||||
|
<p v-html="person.summary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<table v-if="metadata.length > 0" class="table is-fullwidth">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="{ key, value, link } in metadata" :key="key">
|
||||||
|
<td>{{ key }}</td>
|
||||||
|
<td v-if="link">
|
||||||
|
<router-link :to="link">
|
||||||
|
{{ value }}
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td v-else>{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="buttons">
|
||||||
|
<b-button
|
||||||
|
@click="suspendProfile"
|
||||||
|
v-if="person.domain && !person.suspended"
|
||||||
|
type="is-primary"
|
||||||
|
>{{ $t("Suspend") }}</b-button
|
||||||
|
>
|
||||||
|
<b-button
|
||||||
|
@click="unsuspendProfile"
|
||||||
|
v-if="person.domain && person.suspended"
|
||||||
|
type="is-primary"
|
||||||
|
>{{ $t("Unsuspend") }}</b-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<section>
|
||||||
|
<h2 class="subtitle">
|
||||||
|
{{
|
||||||
|
$tc("{number} organized events", person.organizedEvents.total, {
|
||||||
|
number: person.organizedEvents.total,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
|
<b-table
|
||||||
|
:data="person.organizedEvents.elements"
|
||||||
|
:loading="$apollo.queries.person.loading"
|
||||||
|
paginated
|
||||||
|
backend-pagination
|
||||||
|
:total="person.organizedEvents.total"
|
||||||
|
:per-page="EVENTS_PER_PAGE"
|
||||||
|
@page-change="onOrganizedEventsPageChange"
|
||||||
|
>
|
||||||
|
<template slot-scope="props">
|
||||||
|
<b-table-column field="beginsOn" :label="$t('Begins on')">
|
||||||
|
{{ props.row.beginsOn | formatDateTimeString }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="title" :label="$t('Title')">
|
||||||
|
<router-link :to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }">
|
||||||
|
{{ props.row.title }}
|
||||||
|
</router-link>
|
||||||
|
</b-table-column>
|
||||||
|
</template>
|
||||||
|
<template slot="empty">
|
||||||
|
<section class="section">
|
||||||
|
<div class="content has-text-grey has-text-centered">
|
||||||
|
<p>{{ $t("Nothing to see here") }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="subtitle">
|
||||||
|
{{
|
||||||
|
$tc("{number} participations", person.participations.total, {
|
||||||
|
number: person.participations.total,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
|
<b-table
|
||||||
|
:data="person.participations.elements.map((participation) => participation.event)"
|
||||||
|
:loading="$apollo.queries.person.loading"
|
||||||
|
paginated
|
||||||
|
backend-pagination
|
||||||
|
:total="person.participations.total"
|
||||||
|
:per-page="EVENTS_PER_PAGE"
|
||||||
|
@page-change="onParticipationsPageChange"
|
||||||
|
>
|
||||||
|
<template slot-scope="props">
|
||||||
|
<b-table-column field="beginsOn" :label="$t('Begins on')">
|
||||||
|
{{ props.row.beginsOn | formatDateTimeString }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="title" :label="$t('Title')">
|
||||||
|
<router-link :to="{ name: RouteName.EVENT, params: { uuid: props.row.uuid } }">
|
||||||
|
{{ props.row.title }}
|
||||||
|
</router-link>
|
||||||
|
</b-table-column>
|
||||||
|
</template>
|
||||||
|
<template slot="empty">
|
||||||
|
<section class="section">
|
||||||
|
<div class="content has-text-grey has-text-centered">
|
||||||
|
<p>{{ $t("Nothing to see here") }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
|
import { GET_PERSON, SUSPEND_PROFILE, UNSUSPEND_PROFILE } from "../../graphql/actor";
|
||||||
|
import { IPerson } from "../../types/actor";
|
||||||
|
import { usernameWithDomain } from "../../types/actor/actor.model";
|
||||||
|
import RouteName from "../../router/name";
|
||||||
|
import { IEvent } from "../../types/event.model";
|
||||||
|
|
||||||
|
const EVENTS_PER_PAGE = 10;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
person: {
|
||||||
|
query: GET_PERSON,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
actorId: this.id,
|
||||||
|
organizedEventsPage: 1,
|
||||||
|
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
skip() {
|
||||||
|
return !this.id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class AdminProfile extends Vue {
|
||||||
|
@Prop({ required: true }) id!: String;
|
||||||
|
|
||||||
|
person!: IPerson;
|
||||||
|
|
||||||
|
usernameWithDomain = usernameWithDomain;
|
||||||
|
|
||||||
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
EVENTS_PER_PAGE = EVENTS_PER_PAGE;
|
||||||
|
|
||||||
|
organizedEventsPage = 1;
|
||||||
|
participationsPage = 1;
|
||||||
|
|
||||||
|
get metadata(): Array<object> {
|
||||||
|
if (!this.person) return [];
|
||||||
|
const res: object[] = [
|
||||||
|
{
|
||||||
|
key: this.$t("Status") as string,
|
||||||
|
value: this.person.suspended ? this.$t("Suspended") : this.$t("Active"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: this.$t("Domain") as string,
|
||||||
|
value: this.person.domain ? this.person.domain : this.$t("Local"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (!this.person.domain && this.person.user) {
|
||||||
|
res.push({
|
||||||
|
key: this.$t("User") as string,
|
||||||
|
link: { name: RouteName.ADMIN_USER_PROFILE, params: { id: this.person.user.id } },
|
||||||
|
value: this.person.user.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async suspendProfile() {
|
||||||
|
this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||||
|
mutation: SUSPEND_PROFILE,
|
||||||
|
variables: {
|
||||||
|
id: this.id,
|
||||||
|
},
|
||||||
|
update: (store, { data }) => {
|
||||||
|
if (data == null) return;
|
||||||
|
const profileId = this.id;
|
||||||
|
|
||||||
|
const profileData = store.readQuery<{ person: IPerson }>({
|
||||||
|
query: GET_PERSON,
|
||||||
|
variables: {
|
||||||
|
actorId: profileId,
|
||||||
|
organizedEventsPage: 1,
|
||||||
|
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!profileData) return;
|
||||||
|
const { person } = profileData;
|
||||||
|
person.suspended = true;
|
||||||
|
person.avatar = null;
|
||||||
|
person.name = "";
|
||||||
|
person.summary = "";
|
||||||
|
store.writeQuery({
|
||||||
|
query: GET_PERSON,
|
||||||
|
variables: {
|
||||||
|
actorId: profileId,
|
||||||
|
},
|
||||||
|
data: { person },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async unsuspendProfile() {
|
||||||
|
const profileID = this.id;
|
||||||
|
this.$apollo.mutate<{ unsuspendProfile: { id: string } }>({
|
||||||
|
mutation: UNSUSPEND_PROFILE,
|
||||||
|
variables: {
|
||||||
|
id: this.id,
|
||||||
|
},
|
||||||
|
refetchQueries: [
|
||||||
|
{
|
||||||
|
query: GET_PERSON,
|
||||||
|
variables: {
|
||||||
|
actorId: profileID,
|
||||||
|
organizedEventsPage: 1,
|
||||||
|
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onOrganizedEventsPageChange(page: number) {
|
||||||
|
this.organizedEventsPage = page;
|
||||||
|
await this.$apollo.queries.person.fetchMore({
|
||||||
|
variables: {
|
||||||
|
actorId: this.id,
|
||||||
|
organizedEventsPage: this.organizedEventsPage,
|
||||||
|
organizedEventsLimit: EVENTS_PER_PAGE,
|
||||||
|
},
|
||||||
|
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||||
|
if (!fetchMoreResult) return previousResult;
|
||||||
|
const newOrganizedEvents = fetchMoreResult.person.organizedEvents.elements;
|
||||||
|
return {
|
||||||
|
person: {
|
||||||
|
...previousResult.person,
|
||||||
|
organizedEvents: {
|
||||||
|
__typename: previousResult.person.organizedEvents.__typename,
|
||||||
|
total: previousResult.person.organizedEvents.total,
|
||||||
|
elements: [...previousResult.person.organizedEvents.elements, ...newOrganizedEvents],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onParticipationsPageChange(page: number) {
|
||||||
|
this.participationsPage = page;
|
||||||
|
await this.$apollo.queries.person.fetchMore({
|
||||||
|
variables: {
|
||||||
|
actorId: this.id,
|
||||||
|
participationPage: this.participationsPage,
|
||||||
|
participationLimit: EVENTS_PER_PAGE,
|
||||||
|
},
|
||||||
|
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||||
|
if (!fetchMoreResult) return previousResult;
|
||||||
|
const newParticipations = fetchMoreResult.person.participations.elements;
|
||||||
|
return {
|
||||||
|
person: {
|
||||||
|
...previousResult.person,
|
||||||
|
participations: {
|
||||||
|
__typename: previousResult.person.participations.__typename,
|
||||||
|
total: previousResult.person.participations.total,
|
||||||
|
elements: [...previousResult.person.participations.elements, ...newParticipations],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
table,
|
||||||
|
section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
</style>
|
165
js/src/views/Admin/AdminUserProfile.vue
Normal file
165
js/src/views/Admin/AdminUserProfile.vue
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="user" class="section">
|
||||||
|
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<router-link :to="{ name: RouteName.ADMIN }">{{ $t("Admin") }}</router-link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.USERS,
|
||||||
|
}"
|
||||||
|
>{{ $t("Users") }}</router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="is-active">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.ADMIN_USER_PROFILE,
|
||||||
|
params: { id: user.id },
|
||||||
|
}"
|
||||||
|
>{{ user.email }}</router-link
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<table v-if="metadata.length > 0" class="table is-fullwidth">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="{ key, value, link, elements } in metadata" :key="key">
|
||||||
|
<td>{{ key }}</td>
|
||||||
|
<td v-if="elements && elements.length > 0">
|
||||||
|
<ul v-for="{ value, link: elementLink, active } in elements" :key="value">
|
||||||
|
<li>
|
||||||
|
<router-link :to="elementLink">
|
||||||
|
<span v-if="active">{{ $t("{profile} (by default)", { profile: value }) }}</span>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td v-else-if="elements">
|
||||||
|
{{ $t("None") }}
|
||||||
|
</td>
|
||||||
|
<td v-else-if="link">
|
||||||
|
<router-link :to="link">
|
||||||
|
{{ value }}
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td v-else>{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="buttons">
|
||||||
|
<b-button @click="deleteAccount" v-if="!user.disabled" type="is-primary">{{
|
||||||
|
$t("Suspend")
|
||||||
|
}}</b-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
|
import { GET_USER, DELETE_ACCOUNT } from "../../graphql/user";
|
||||||
|
import { usernameWithDomain } from "../../types/actor/actor.model";
|
||||||
|
import RouteName from "../../router/name";
|
||||||
|
import { IUser, ICurrentUserRole } from "../../types/current-user.model";
|
||||||
|
import { IPerson } from "../../types/actor";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
user: {
|
||||||
|
query: GET_USER,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
skip() {
|
||||||
|
return !this.id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class AdminUserProfile extends Vue {
|
||||||
|
@Prop({ required: true }) id!: String;
|
||||||
|
|
||||||
|
user!: IUser;
|
||||||
|
|
||||||
|
usernameWithDomain = usernameWithDomain;
|
||||||
|
|
||||||
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
get metadata(): Array<object> {
|
||||||
|
if (!this.user) return [];
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: this.$i18n.t("Email"),
|
||||||
|
value: this.user.email,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: this.$i18n.t("Language"),
|
||||||
|
value: this.user.locale,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: this.$i18n.t("Role"),
|
||||||
|
value: this.roleName(this.user.role),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: this.$i18n.t("Login status"),
|
||||||
|
value: this.user.disabled ? this.$i18n.t("Disabled") : this.$t("Activated"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: this.$i18n.t("Profiles"),
|
||||||
|
elements: this.user.actors.map((actor: IPerson) => {
|
||||||
|
return {
|
||||||
|
link: { name: RouteName.ADMIN_PROFILE, params: { id: actor.id } },
|
||||||
|
value: actor.name
|
||||||
|
? `${actor.name} (${actor.preferredUsername})`
|
||||||
|
: actor.preferredUsername,
|
||||||
|
active: this.user.defaultActor ? actor.id === this.user.defaultActor.id : false,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: this.$i18n.t("Confirmed"),
|
||||||
|
value:
|
||||||
|
this.$options.filters && this.user.confirmedAt
|
||||||
|
? this.$options.filters.formatDateTimeString(this.user.confirmedAt)
|
||||||
|
: this.$i18n.t("Not confirmed"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: this.$i18n.t("Participations"),
|
||||||
|
value: this.user.participations.total,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
roleName(role: ICurrentUserRole): string {
|
||||||
|
switch (role) {
|
||||||
|
case ICurrentUserRole.ADMINISTRATOR:
|
||||||
|
return this.$t("Administrator") as string;
|
||||||
|
case ICurrentUserRole.MODERATOR:
|
||||||
|
return this.$t("Moderator") as string;
|
||||||
|
case ICurrentUserRole.USER:
|
||||||
|
default:
|
||||||
|
return this.$t("User") as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAccount() {
|
||||||
|
await this.$apollo.mutate<{ suspendProfile: { id: string } }>({
|
||||||
|
mutation: DELETE_ACCOUNT,
|
||||||
|
variables: {
|
||||||
|
userId: this.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return this.$router.push({ name: RouteName.USERS });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
table {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -16,15 +16,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="tile is-parent is-vertical">
|
<div class="tile is-parent is-vertical">
|
||||||
<article class="tile is-child box">
|
<article class="tile is-child box">
|
||||||
|
<router-link :to="{ name: RouteName.USERS }">
|
||||||
<p class="dashboard-number">{{ dashboard.numberOfUsers }}</p>
|
<p class="dashboard-number">{{ dashboard.numberOfUsers }}</p>
|
||||||
<p>{{ $t("Users") }}</p>
|
<p>{{ $t("Users") }}</p>
|
||||||
|
</router-link>
|
||||||
</article>
|
</article>
|
||||||
<router-link :to="{ name: RouteName.REPORTS }">
|
|
||||||
<article class="tile is-child box">
|
<article class="tile is-child box">
|
||||||
|
<router-link :to="{ name: RouteName.REPORTS }">
|
||||||
<p class="dashboard-number">{{ dashboard.numberOfReports }}</p>
|
<p class="dashboard-number">{{ dashboard.numberOfReports }}</p>
|
||||||
<p>{{ $t("Opened reports") }}</p>
|
<p>{{ $t("Opened reports") }}</p>
|
||||||
</article>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
|
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
|
||||||
|
@ -80,4 +82,10 @@ export default class Dashboard extends Vue {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.125;
|
line-height: 1.125;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
article.tile {
|
||||||
|
a {
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
138
js/src/views/Admin/Profiles.vue
Normal file
138
js/src/views/Admin/Profiles.vue
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="persons">
|
||||||
|
<b-switch v-model="local">{{ $t("Local") }}</b-switch>
|
||||||
|
<b-switch v-model="suspended">{{ $t("Suspended") }}</b-switch>
|
||||||
|
<b-table
|
||||||
|
:data="persons.elements"
|
||||||
|
:loading="$apollo.queries.persons.loading"
|
||||||
|
paginated
|
||||||
|
backend-pagination
|
||||||
|
backend-filtering
|
||||||
|
:total="persons.total"
|
||||||
|
:per-page="PROFILES_PER_PAGE"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
@filters-change="onFiltersChange"
|
||||||
|
>
|
||||||
|
<template slot-scope="props">
|
||||||
|
<b-table-column field="preferredUsername" :label="$t('Username')" searchable>
|
||||||
|
<template slot="searchable" slot-scope="props">
|
||||||
|
<b-input
|
||||||
|
v-model="props.filters.preferredUsername"
|
||||||
|
placeholder="Search..."
|
||||||
|
icon="magnify"
|
||||||
|
size="is-small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<router-link :to="{ name: RouteName.ADMIN_PROFILE, params: { id: props.row.id } }">
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left" v-if="props.row.avatar">
|
||||||
|
<p class="image is-48x48">
|
||||||
|
<img :src="props.row.avatar.url" />
|
||||||
|
</p>
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content">
|
||||||
|
<strong v-if="props.row.name">{{ props.row.name }}</strong
|
||||||
|
><br v-if="props.row.name" />
|
||||||
|
<small>@{{ props.row.preferredUsername }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</router-link>
|
||||||
|
</b-table-column>
|
||||||
|
|
||||||
|
<b-table-column field="domain" :label="$t('Domain')" searchable>
|
||||||
|
<template slot="searchable" slot-scope="props">
|
||||||
|
<b-input
|
||||||
|
v-model="props.filters.domain"
|
||||||
|
placeholder="Search..."
|
||||||
|
icon="magnify"
|
||||||
|
size="is-small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
{{ props.row.domain }}
|
||||||
|
</b-table-column>
|
||||||
|
</template>
|
||||||
|
<template slot="empty">
|
||||||
|
<section class="section">
|
||||||
|
<div class="content has-text-grey has-text-centered">
|
||||||
|
<p>{{ $t("No profile matches the filters") }}</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||||
|
import { LIST_PROFILES } from "../../graphql/actor";
|
||||||
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
|
const PROFILES_PER_PAGE = 10;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
persons: {
|
||||||
|
query: LIST_PROFILES,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
preferredUsername: this.preferredUsername,
|
||||||
|
name: this.name,
|
||||||
|
domain: this.domain,
|
||||||
|
local: this.local,
|
||||||
|
suspended: this.suspended,
|
||||||
|
page: 1,
|
||||||
|
limit: PROFILES_PER_PAGE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class Profiles extends Vue {
|
||||||
|
page = 1;
|
||||||
|
preferredUsername = "";
|
||||||
|
name = "";
|
||||||
|
domain = "";
|
||||||
|
local = true;
|
||||||
|
suspended = false;
|
||||||
|
|
||||||
|
PROFILES_PER_PAGE = PROFILES_PER_PAGE;
|
||||||
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
async onPageChange(page: number) {
|
||||||
|
this.page = page;
|
||||||
|
await this.$apollo.queries.persons.fetchMore({
|
||||||
|
variables: {
|
||||||
|
preferredUsername: this.preferredUsername,
|
||||||
|
name: this.name,
|
||||||
|
domain: this.domain,
|
||||||
|
local: this.local,
|
||||||
|
suspended: this.suspended,
|
||||||
|
page: this.page,
|
||||||
|
limit: PROFILES_PER_PAGE,
|
||||||
|
},
|
||||||
|
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||||
|
if (!fetchMoreResult) return previousResult;
|
||||||
|
const newProfiles = fetchMoreResult.persons.elements;
|
||||||
|
return {
|
||||||
|
persons: {
|
||||||
|
__typename: previousResult.persons.__typename,
|
||||||
|
total: previousResult.persons.total,
|
||||||
|
elements: [...previousResult.persons.elements, ...newProfiles],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFiltersChange({ preferredUsername, domain }: { preferredUsername: string; domain: string }) {
|
||||||
|
this.preferredUsername = preferredUsername;
|
||||||
|
this.domain = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("domain")
|
||||||
|
domainNotLocal() {
|
||||||
|
this.local = this.domain === "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
132
js/src/views/Admin/Users.vue
Normal file
132
js/src/views/Admin/Users.vue
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="users">
|
||||||
|
<b-table
|
||||||
|
:data="users.elements"
|
||||||
|
:loading="$apollo.queries.users.loading"
|
||||||
|
paginated
|
||||||
|
backend-pagination
|
||||||
|
backend-filtering
|
||||||
|
detailed
|
||||||
|
:show-detail-icon="true"
|
||||||
|
:total="users.total"
|
||||||
|
:per-page="USERS_PER_PAGE"
|
||||||
|
:has-detailed-visible="(row => row.actors.length > 0)"
|
||||||
|
@page-change="onPageChange"
|
||||||
|
@filters-change="onFiltersChange"
|
||||||
|
>
|
||||||
|
<template slot-scope="props">
|
||||||
|
<b-table-column field="id" width="40" numeric>
|
||||||
|
{{ props.row.id }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="email" :label="$t('Email')" searchable>
|
||||||
|
<template slot="searchable" slot-scope="props">
|
||||||
|
<b-input
|
||||||
|
v-model="props.filters.email"
|
||||||
|
placeholder="Search..."
|
||||||
|
icon="magnify"
|
||||||
|
size="is-small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: RouteName.ADMIN_USER_PROFILE, params: { id: props.row.id } }"
|
||||||
|
:class="{ disabled: props.row.disabled }"
|
||||||
|
>
|
||||||
|
{{ props.row.email }}
|
||||||
|
</router-link>
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="confirmedAt" :label="$t('Confirmed at')" :centered="true">
|
||||||
|
{{ props.row.confirmedAt | formatDateTimeString }}
|
||||||
|
</b-table-column>
|
||||||
|
<b-table-column field="locale" :label="$t('Language')" :centered="true">
|
||||||
|
{{ props.row.locale }}
|
||||||
|
</b-table-column>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template slot="detail" slot-scope="props">
|
||||||
|
<router-link
|
||||||
|
v-for="actor in props.row.actors"
|
||||||
|
:key="actor.id"
|
||||||
|
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: actor.id } }"
|
||||||
|
>
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left">
|
||||||
|
<p class="image is-64x64">
|
||||||
|
<img :src="actor.avatar.url" />
|
||||||
|
</p>
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="content">
|
||||||
|
<strong v-if="actor.name">{{ actor.name }}</strong>
|
||||||
|
<small>@{{ actor.preferredUsername }}</small>
|
||||||
|
<p>{{ actor.summary }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</b-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-property-decorator";
|
||||||
|
import { LIST_USERS } from "../../graphql/user";
|
||||||
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
|
const USERS_PER_PAGE = 10;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
apollo: {
|
||||||
|
users: {
|
||||||
|
query: LIST_USERS,
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
email: this.email,
|
||||||
|
page: 1,
|
||||||
|
limit: USERS_PER_PAGE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class Users extends Vue {
|
||||||
|
page = 1;
|
||||||
|
email = "";
|
||||||
|
|
||||||
|
USERS_PER_PAGE = USERS_PER_PAGE;
|
||||||
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
async onPageChange(page: number) {
|
||||||
|
this.page = page;
|
||||||
|
await this.$apollo.queries.users.fetchMore({
|
||||||
|
variables: {
|
||||||
|
email: this.email,
|
||||||
|
page: this.page,
|
||||||
|
limit: USERS_PER_PAGE,
|
||||||
|
},
|
||||||
|
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||||
|
if (!fetchMoreResult) return previousResult;
|
||||||
|
const newFollowings = fetchMoreResult.users.elements;
|
||||||
|
return {
|
||||||
|
users: {
|
||||||
|
__typename: previousResult.users.__typename,
|
||||||
|
total: previousResult.users.total,
|
||||||
|
elements: [...previousResult.users.elements, ...newFollowings],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onFiltersChange({ email }: { email: string }) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../../variables.scss";
|
||||||
|
a.disabled {
|
||||||
|
color: $danger;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -9,7 +9,11 @@
|
||||||
tag="span"
|
tag="span"
|
||||||
path="{actor} closed {report}"
|
path="{actor} closed {report}"
|
||||||
>
|
>
|
||||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
<router-link
|
||||||
|
slot="actor"
|
||||||
|
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||||
|
>@{{ log.actor.preferredUsername }}</router-link
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||||
slot="report"
|
slot="report"
|
||||||
|
@ -21,7 +25,11 @@
|
||||||
tag="span"
|
tag="span"
|
||||||
path="{actor} reopened {report}"
|
path="{actor} reopened {report}"
|
||||||
>
|
>
|
||||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
<router-link
|
||||||
|
slot="actor"
|
||||||
|
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||||
|
>@{{ log.actor.preferredUsername }}</router-link
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||||
slot="report"
|
slot="report"
|
||||||
|
@ -33,7 +41,11 @@
|
||||||
tag="span"
|
tag="span"
|
||||||
path="{actor} marked {report} as resolved"
|
path="{actor} marked {report} as resolved"
|
||||||
>
|
>
|
||||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
<router-link
|
||||||
|
slot="actor"
|
||||||
|
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||||
|
>@{{ log.actor.preferredUsername }}</router-link
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
:to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||||
slot="report"
|
slot="report"
|
||||||
|
@ -45,7 +57,11 @@
|
||||||
tag="span"
|
tag="span"
|
||||||
path="{actor} added a note on {report}"
|
path="{actor} added a note on {report}"
|
||||||
>
|
>
|
||||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
<router-link
|
||||||
|
slot="actor"
|
||||||
|
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||||
|
>@{{ log.actor.preferredUsername }}</router-link
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="log.object.report"
|
v-if="log.object.report"
|
||||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.report.id } }"
|
:to="{ name: RouteName.REPORT, params: { reportId: log.object.report.id } }"
|
||||||
|
@ -59,8 +75,28 @@
|
||||||
tag="span"
|
tag="span"
|
||||||
path='{actor} deleted an event named "{title}"'
|
path='{actor} deleted an event named "{title}"'
|
||||||
>
|
>
|
||||||
<span slot="actor">@{{ log.actor.preferredUsername }}</span>
|
<router-link
|
||||||
<span slot="title">{{ log.object.title }}</span>
|
slot="actor"
|
||||||
|
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||||
|
>@{{ log.actor.preferredUsername }}</router-link
|
||||||
|
>
|
||||||
|
<b slot="title">{{ log.object.title }}</b>
|
||||||
|
</i18n>
|
||||||
|
<i18n
|
||||||
|
v-else-if="log.action === ActionLogAction.ACTOR_SUSPENSION"
|
||||||
|
tag="span"
|
||||||
|
path="{actor} suspended profile {profile}"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
slot="actor"
|
||||||
|
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.actor.id } }"
|
||||||
|
>@{{ log.actor.preferredUsername }}</router-link
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
slot="profile"
|
||||||
|
:to="{ name: RouteName.ADMIN_PROFILE, params: { id: log.object.id } }"
|
||||||
|
>{{ displayNameAndUsername(log.object) }}
|
||||||
|
</router-link>
|
||||||
</i18n>
|
</i18n>
|
||||||
<br />
|
<br />
|
||||||
<small>{{ log.insertedAt | formatDateTimeString }}</small>
|
<small>{{ log.insertedAt | formatDateTimeString }}</small>
|
||||||
|
@ -78,6 +114,7 @@ import { IActionLog, ActionLogAction } from "@/types/report.model";
|
||||||
import { LOGS } from "@/graphql/report";
|
import { LOGS } from "@/graphql/report";
|
||||||
import ReportCard from "@/components/Report/ReportCard.vue";
|
import ReportCard from "@/components/Report/ReportCard.vue";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
import { displayNameAndUsername } from "../../types/actor";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -95,6 +132,8 @@ export default class ReportList extends Vue {
|
||||||
ActionLogAction = ActionLogAction;
|
ActionLogAction = ActionLogAction;
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
displayNameAndUsername = displayNameAndUsername;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -32,8 +32,8 @@
|
||||||
<td>
|
<td>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.PROFILE,
|
name: RouteName.ADMIN_PROFILE,
|
||||||
params: { name: report.reported.preferredUsername },
|
params: { id: report.reported.id },
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
@ -53,8 +53,8 @@
|
||||||
<td v-else>
|
<td v-else>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.PROFILE,
|
name: RouteName.ADMIN_PROFILE,
|
||||||
params: { name: report.reporter.preferredUsername },
|
params: { id: report.reporter.id },
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-for="comment in report.comments" v-if="report.comments.length > 0">
|
<ul v-for="comment in report.comments" v-if="report.comments.length > 0" :key="comment.id">
|
||||||
<li>
|
<li>
|
||||||
<div class="box" v-if="comment">
|
<div class="box" v-if="comment">
|
||||||
<article class="media">
|
<article class="media">
|
||||||
|
@ -173,11 +173,9 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 class="title" v-if="report.notes.length > 0">{{ $t("Notes") }}</h2>
|
<h2 class="title" v-if="report.notes.length > 0">{{ $t("Notes") }}</h2>
|
||||||
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`">
|
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`" :key="note.id">
|
||||||
<p>{{ note.content }}</p>
|
<p>{{ note.content }}</p>
|
||||||
<router-link
|
<router-link :to="{ name: RouteName.ADMIN_PROFILE, params: { id: note.moderator.id } }">
|
||||||
:to="{ name: RouteName.PROFILE, params: { name: note.moderator.preferredUsername } }"
|
|
||||||
>
|
|
||||||
<img alt class="image" :src="note.moderator.avatar.url" v-if="note.moderator.avatar" />
|
<img alt class="image" :src="note.moderator.avatar.url" v-if="note.moderator.avatar" />
|
||||||
@{{ note.moderator.preferredUsername }}
|
@{{ note.moderator.preferredUsername }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
@ -122,6 +122,14 @@ export default class Settings extends Vue {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: this.$t("Users") as string,
|
||||||
|
to: { name: RouteName.USERS } as Route,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: this.$t("Profiles") as string,
|
||||||
|
to: { name: RouteName.PROFILES } as Route,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -423,7 +423,8 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||||
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||||
}
|
}
|
||||||
|
|
||||||
with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor),
|
# We completely delete the actor if activity is remote
|
||||||
|
with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor, reserve_username: local),
|
||||||
{:ok, activity} <- create_activity(data, local),
|
{:ok, activity} <- create_activity(data, local),
|
||||||
:ok <- maybe_federate(activity) do
|
:ok <- maybe_federate(activity) do
|
||||||
{:ok, activity, actor}
|
{:ok, activity, actor}
|
||||||
|
|
|
@ -59,7 +59,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
|
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
|
||||||
Logger.info("Handle incoming to create notes")
|
Logger.info("Handle incoming to create notes")
|
||||||
|
|
||||||
with object_data <-
|
with object_data when is_map(object_data) <-
|
||||||
object |> Converter.Comment.as_to_model_data(),
|
object |> Converter.Comment.as_to_model_data(),
|
||||||
{:existing_comment, {:error, :comment_not_found}} <-
|
{:existing_comment, {:error, :comment_not_found}} <-
|
||||||
{:existing_comment, Conversations.get_comment_from_url_with_preload(object_data.url)},
|
{:existing_comment, Conversations.get_comment_from_url_with_preload(object_data.url)},
|
||||||
|
@ -87,7 +87,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object}) do
|
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object}) do
|
||||||
Logger.info("Handle incoming to create event")
|
Logger.info("Handle incoming to create event")
|
||||||
|
|
||||||
with object_data <-
|
with object_data when is_map(object_data) <-
|
||||||
object |> Converter.Event.as_to_model_data(),
|
object |> Converter.Event.as_to_model_data(),
|
||||||
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
|
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
|
||||||
{:ok, %Activity{} = activity, %Event{} = event} <-
|
{:ok, %Activity{} = activity, %Event{} = event} <-
|
||||||
|
|
|
@ -36,7 +36,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||||
Logger.debug(inspect(object))
|
Logger.debug(inspect(object))
|
||||||
|
|
||||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||||
{:ok, %Actor{id: actor_id, domain: domain}} <-
|
{:ok, %Actor{id: actor_id, domain: domain, suspended: false}} <-
|
||||||
ActivityPub.get_or_fetch_actor_by_url(author_url),
|
ActivityPub.get_or_fetch_actor_by_url(author_url),
|
||||||
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))},
|
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))},
|
||||||
{:mentions, mentions} <-
|
{:mentions, mentions} <-
|
||||||
|
@ -90,6 +90,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||||
|
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
{:ok, %Actor{suspended: true}} ->
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||||
Logger.debug(inspect(object))
|
Logger.debug(inspect(object))
|
||||||
|
|
||||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||||
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain}}} <-
|
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain, suspended: false}}} <-
|
||||||
{:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)},
|
{:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)},
|
||||||
{:address, address_id} <-
|
{:address, address_id} <-
|
||||||
{:address, get_address(object["location"])},
|
{:address, get_address(object["location"])},
|
||||||
|
@ -87,6 +87,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||||
updated_at: object["updated"],
|
updated_at: object["updated"],
|
||||||
publish_at: object["published"]
|
publish_at: object["published"]
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{:ok, %Actor{suspended: true}} ->
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||||
alias Mobilizon.Service.Statistics
|
alias Mobilizon.Service.Statistics
|
||||||
alias Mobilizon.Storage.Page
|
alias Mobilizon.Storage.Page
|
||||||
alias Mobilizon.Users.User
|
alias Mobilizon.Users.User
|
||||||
|
require Logger
|
||||||
|
|
||||||
def list_action_logs(
|
def list_action_logs(
|
||||||
_parent,
|
_parent,
|
||||||
|
@ -96,6 +97,20 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp transform_action_log(Actor, :suspend, %ActionLog{changes: changes}) do
|
||||||
|
%{
|
||||||
|
action: :actor_suspension,
|
||||||
|
object: convert_changes_to_struct(Actor, changes)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp transform_action_log(Actor, :unsuspend, %ActionLog{changes: changes}) do
|
||||||
|
%{
|
||||||
|
action: :actor_unsuspension,
|
||||||
|
object: convert_changes_to_struct(Actor, changes)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
|
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
|
||||||
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
|
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
|
||||||
with data <- for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}),
|
with data <- for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}),
|
||||||
|
|
|
@ -3,10 +3,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||||
Handles the person-related GraphQL calls
|
Handles the person-related GraphQL calls
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import Mobilizon.Users.Guards
|
||||||
|
|
||||||
alias Mobilizon.Actors
|
alias Mobilizon.Actors
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
|
alias Mobilizon.Admin
|
||||||
alias Mobilizon.Events
|
alias Mobilizon.Events
|
||||||
alias Mobilizon.Events.Participant
|
alias Mobilizon.Events.Participant
|
||||||
|
alias Mobilizon.Storage.Page
|
||||||
alias Mobilizon.Users
|
alias Mobilizon.Users
|
||||||
alias Mobilizon.Users.User
|
alias Mobilizon.Users.User
|
||||||
|
|
||||||
|
@ -17,8 +21,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||||
@doc """
|
@doc """
|
||||||
Get a person
|
Get a person
|
||||||
"""
|
"""
|
||||||
def get_person(_parent, %{id: id}, _resolution) do
|
def get_person(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}}) do
|
||||||
with %Actor{} = actor <- Actors.get_actor_with_preload(id),
|
with %Actor{suspended: suspended} = actor <- Actors.get_actor_with_preload(id, true),
|
||||||
|
true <- suspended == false or is_moderator(role),
|
||||||
actor <- proxify_pictures(actor) do
|
actor <- proxify_pictures(actor) do
|
||||||
{:ok, actor}
|
{:ok, actor}
|
||||||
else
|
else
|
||||||
|
@ -41,6 +46,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_persons(
|
||||||
|
_parent,
|
||||||
|
%{
|
||||||
|
preferred_username: preferred_username,
|
||||||
|
name: name,
|
||||||
|
domain: domain,
|
||||||
|
local: local,
|
||||||
|
suspended: suspended,
|
||||||
|
page: page,
|
||||||
|
limit: limit
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
context: %{current_user: %User{role: role}}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
when is_moderator(role) do
|
||||||
|
{:ok,
|
||||||
|
Actors.list_actors(:Person, preferred_username, name, domain, local, suspended, page, limit)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_persons(_parent, _args, _resolution) do
|
||||||
|
{:error, "You need to be logged-in and a moderator to list persons"}
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the current actor for the currently logged-in user
|
Returns the current actor for the currently logged-in user
|
||||||
"""
|
"""
|
||||||
|
@ -201,25 +230,41 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||||
with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
|
with {:is_owned, %Actor{} = _actor} <- User.owns_actor(user, actor_id),
|
||||||
{:no_participant, {:ok, %Participant{} = participant}} <-
|
{:no_participant, {:ok, %Participant{} = participant}} <-
|
||||||
{:no_participant, Events.get_participant(event_id, actor_id)} do
|
{:no_participant, Events.get_participant(event_id, actor_id)} do
|
||||||
{:ok, [participant]}
|
{:ok, %Page{elements: [participant], total: 1}}
|
||||||
else
|
else
|
||||||
{:is_owned, nil} ->
|
{:is_owned, nil} ->
|
||||||
{:error, "Actor id is not owned by authenticated user"}
|
{:error, "Actor id is not owned by authenticated user"}
|
||||||
|
|
||||||
{:no_participant, _} ->
|
{:no_participant, _} ->
|
||||||
{:ok, []}
|
{:ok, %Page{elements: [], total: 0}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of events this person is going to
|
Returns the list of events this person is going to
|
||||||
"""
|
"""
|
||||||
def person_participations(%Actor{id: actor_id}, _args, %{context: %{current_user: user}}) do
|
def person_participations(%Actor{id: actor_id} = actor, %{page: page, limit: limit}, %{
|
||||||
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
context: %{current_user: %User{role: role} = user}
|
||||||
participations <- Events.list_event_participations_for_actor(actor) do
|
}) do
|
||||||
{:ok, participations}
|
{:is_owned, actor_found} = User.owns_actor(user, actor_id)
|
||||||
|
|
||||||
|
res =
|
||||||
|
cond do
|
||||||
|
not is_nil(actor_found) ->
|
||||||
|
true
|
||||||
|
|
||||||
|
is_moderator(role) ->
|
||||||
|
true
|
||||||
|
|
||||||
|
true ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
with {:is_owned, true} <- {:is_owned, res},
|
||||||
|
%Page{} = page <- Events.list_event_participations_for_actor(actor, page, limit) do
|
||||||
|
{:ok, page}
|
||||||
else
|
else
|
||||||
{:is_owned, nil} ->
|
{:is_owned, false} ->
|
||||||
{:error, "Actor id is not owned by authenticated user"}
|
{:error, "Actor id is not owned by authenticated user"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -243,6 +288,95 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||||
|> proxify_banner
|
|> proxify_banner
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def user_for_person(%Actor{type: :Person, user_id: user_id}, _args, %{
|
||||||
|
context: %{current_user: %User{role: role}}
|
||||||
|
})
|
||||||
|
when is_moderator(role) do
|
||||||
|
with false <- is_nil(user_id),
|
||||||
|
%User{} = user <- Users.get_user(user_id) do
|
||||||
|
{:ok, user}
|
||||||
|
else
|
||||||
|
true ->
|
||||||
|
{:ok, nil}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, "User not found"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_for_person(_, _args, _resolution), do: {:error, nil}
|
||||||
|
|
||||||
|
def organized_events_for_person(
|
||||||
|
%Actor{user_id: actor_user_id} = actor,
|
||||||
|
%{page: page, limit: limit},
|
||||||
|
%{
|
||||||
|
context: %{current_user: %User{id: user_id, role: role}}
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
with true <- actor_user_id == user_id or is_moderator(role),
|
||||||
|
%Page{} = page <- Events.list_organized_events_for_actor(actor, page, limit) do
|
||||||
|
{:ok, page}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def suspend_profile(_parent, %{id: id}, %{
|
||||||
|
context: %{current_user: %User{role: role} = user}
|
||||||
|
})
|
||||||
|
when is_moderator(role) do
|
||||||
|
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||||
|
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||||
|
%Actor{suspended: false} = actor <- Actors.get_remote_actor_with_preload(id),
|
||||||
|
{:ok, _} <- Actors.delete_actor(actor),
|
||||||
|
{:ok, _} <- Admin.log_action(moderator_actor, "suspend", actor) do
|
||||||
|
{:ok, actor}
|
||||||
|
else
|
||||||
|
{:moderator_actor, nil} ->
|
||||||
|
{:error, "No actor found for the moderator user"}
|
||||||
|
|
||||||
|
%Actor{suspended: true} ->
|
||||||
|
{:error, "Actor already suspended"}
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
{:error, "No remote profile found with this ID"}
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
{:error, "Error while performing background task"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def suspend_profile(_parent, _args, _resolution) do
|
||||||
|
{:error, "Only moderators and administrators can suspend a profile"}
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsuspend_profile(_parent, %{id: id}, %{
|
||||||
|
context: %{current_user: %User{role: role} = user}
|
||||||
|
})
|
||||||
|
when is_moderator(role) do
|
||||||
|
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||||
|
{:moderator_actor, Users.get_actor_for_user(user)},
|
||||||
|
%Actor{preferred_username: preferred_username, domain: domain} = actor <-
|
||||||
|
Actors.get_remote_actor_with_preload(id, true),
|
||||||
|
{:ok, _} <- Actors.update_actor(actor, %{suspended: false}),
|
||||||
|
{:ok, %Actor{} = actor} <-
|
||||||
|
ActivityPub.make_actor_from_nickname("#{preferred_username}@#{domain}"),
|
||||||
|
{:ok, _} <- Admin.log_action(moderator_actor, "unsuspend", actor) do
|
||||||
|
{:ok, actor}
|
||||||
|
else
|
||||||
|
{:moderator_actor, nil} ->
|
||||||
|
{:error, "No actor found for the moderator user"}
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
{:error, "No remote profile found with this ID"}
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
{:error, "Error while performing background task"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsuspend_profile(_parent, _args, _resolution) do
|
||||||
|
{:error, "Only moderators and administrators can unsuspend a profile"}
|
||||||
|
end
|
||||||
|
|
||||||
# We check that the actor is not the last administrator/creator of a group
|
# We check that the actor is not the last administrator/creator of a group
|
||||||
@spec last_admin_of_a_group?(integer()) :: boolean()
|
@spec last_admin_of_a_group?(integer()) :: boolean()
|
||||||
defp last_admin_of_a_group?(actor_id) do
|
defp last_admin_of_a_group?(actor_id) do
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||||
alias Mobilizon.{Actors, Config, Events, Users}
|
alias Mobilizon.{Actors, Config, Events, Users}
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Crypto
|
alias Mobilizon.Crypto
|
||||||
|
alias Mobilizon.Federation.ActivityPub
|
||||||
alias Mobilizon.Storage.{Page, Repo}
|
alias Mobilizon.Storage.{Page, Repo}
|
||||||
alias Mobilizon.Users.{Setting, User}
|
alias Mobilizon.Users.{Setting, User}
|
||||||
|
|
||||||
|
@ -20,8 +21,11 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||||
@doc """
|
@doc """
|
||||||
Find an user by its ID
|
Find an user by its ID
|
||||||
"""
|
"""
|
||||||
def find_user(_parent, %{id: id}, _resolution) do
|
def find_user(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
|
||||||
Users.get_user_with_actors(id)
|
when is_moderator(role) do
|
||||||
|
with {:ok, %User{} = user} <- Users.get_user_with_actors(id) do
|
||||||
|
{:ok, user}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -38,19 +42,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||||
@doc """
|
@doc """
|
||||||
List instance users
|
List instance users
|
||||||
"""
|
"""
|
||||||
def list_and_count_users(
|
def list_users(
|
||||||
_parent,
|
_parent,
|
||||||
%{page: page, limit: limit, sort: sort, direction: direction},
|
%{email: email, page: page, limit: limit, sort: sort, direction: direction},
|
||||||
%{context: %{current_user: %User{role: role}}}
|
%{context: %{current_user: %User{role: role}}}
|
||||||
)
|
)
|
||||||
when is_moderator(role) do
|
when is_moderator(role) do
|
||||||
total = Task.async(&Users.count_users/0)
|
{:ok, Users.list_users(email, page, limit, sort, direction)}
|
||||||
elements = Task.async(fn -> Users.list_users(page, limit, sort, direction) end)
|
|
||||||
|
|
||||||
{:ok, %{total: Task.await(total), elements: Task.await(elements)}}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_and_count_users(_parent, _args, _resolution) do
|
def list_users(_parent, _args, _resolution) do
|
||||||
{:error, "You need to have admin access to list users"}
|
{:error, "You need to have admin access to list users"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -242,9 +243,9 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||||
def user_participations(
|
def user_participations(
|
||||||
%User{id: user_id},
|
%User{id: user_id},
|
||||||
args,
|
args,
|
||||||
%{context: %{current_user: %User{id: logged_user_id}}}
|
%{context: %{current_user: %User{id: logged_user_id, role: role}}}
|
||||||
) do
|
) do
|
||||||
with true <- user_id == logged_user_id,
|
with true <- user_id == logged_user_id or is_moderator(role),
|
||||||
%Page{} = page <-
|
%Page{} = page <-
|
||||||
Events.list_participations_for_user(
|
Events.list_participations_for_user(
|
||||||
user_id,
|
user_id,
|
||||||
|
@ -379,27 +380,58 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||||
def delete_account(_parent, %{password: password}, %{
|
def delete_account(_parent, %{password: password}, %{
|
||||||
context: %{current_user: %User{password_hash: password_hash} = user}
|
context: %{current_user: %User{password_hash: password_hash} = user}
|
||||||
}) do
|
}) do
|
||||||
with {:current_password, true} <-
|
case {:current_password, Argon2.verify_pass(password, password_hash)} do
|
||||||
{:current_password, Argon2.verify_pass(password, password_hash)},
|
{:current_password, true} ->
|
||||||
actors <- Users.get_actors_for_user(user),
|
do_delete_account(user)
|
||||||
# Detach actors from user
|
|
||||||
:ok <- Enum.each(actors, fn actor -> Actors.update_actor(actor, %{user_id: nil}) end),
|
|
||||||
# Launch a background job to delete actors
|
|
||||||
:ok <- Enum.each(actors, &Actors.delete_actor/1),
|
|
||||||
# Delete user
|
|
||||||
{:ok, user} <- Users.delete_user(user) do
|
|
||||||
{:ok, user}
|
|
||||||
else
|
|
||||||
{:current_password, false} ->
|
{:current_password, false} ->
|
||||||
{:error, "The password provided is invalid"}
|
{:error, "The password provided is invalid"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_account(_parent, %{user_id: user_id}, %{
|
||||||
|
context: %{current_user: %User{role: role}}
|
||||||
|
})
|
||||||
|
when is_moderator(role) do
|
||||||
|
with %User{} = user <- Users.get_user(user_id) do
|
||||||
|
do_delete_account(%User{} = user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def delete_account(_parent, _args, _resolution) do
|
def delete_account(_parent, _args, _resolution) do
|
||||||
{:error, "You need to be logged-in to delete your account"}
|
{:error, "You need to be logged-in to delete your account"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp do_delete_account(%User{} = user) do
|
||||||
|
with actors <- Users.get_actors_for_user(user),
|
||||||
|
activated <- not is_nil(user.confirmed_at),
|
||||||
|
# Detach actors from user
|
||||||
|
:ok <-
|
||||||
|
if(activated,
|
||||||
|
do: :ok,
|
||||||
|
else: Enum.each(actors, fn actor -> Actors.update_actor(actor, %{user_id: nil}) end)
|
||||||
|
),
|
||||||
|
# Launch a background job to delete actors
|
||||||
|
:ok <-
|
||||||
|
Enum.each(actors, fn actor ->
|
||||||
|
ActivityPub.delete(actor, true)
|
||||||
|
end),
|
||||||
|
# Delete user
|
||||||
|
{:ok, user} <- Users.delete_user(user, reserve_email: activated) do
|
||||||
|
{:ok, user}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@spec user_settings(User.t(), map(), map()) :: {:ok, list(Setting.t())} | {:error, String.t()}
|
@spec user_settings(User.t(), map(), map()) :: {:ok, list(Setting.t())} | {:error, String.t()}
|
||||||
|
def user_settings(%User{} = user, _args, %{
|
||||||
|
context: %{current_user: %User{role: role}}
|
||||||
|
})
|
||||||
|
when is_moderator(role) do
|
||||||
|
with {:setting, settings} <- {:setting, Users.get_setting(user)} do
|
||||||
|
{:ok, settings}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def user_settings(%User{id: user_id} = user, _args, %{
|
def user_settings(%User{id: user_id} = user, _args, %{
|
||||||
context: %{current_user: %User{id: logged_user_id}}
|
context: %{current_user: %User{id: logged_user_id}}
|
||||||
}) do
|
}) do
|
||||||
|
|
|
@ -17,9 +17,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||||
Represents a person identity
|
Represents a person identity
|
||||||
"""
|
"""
|
||||||
object :person do
|
object :person do
|
||||||
interfaces([:actor])
|
interfaces([:actor, :action_log_object])
|
||||||
field(:id, :id, description: "Internal ID for this person")
|
field(:id, :id, description: "Internal ID for this person")
|
||||||
field(:user, :user, description: "The user this actor is associated to")
|
|
||||||
|
field(:user, :user,
|
||||||
|
description: "The user this actor is associated to",
|
||||||
|
resolve: &Person.user_for_person/3
|
||||||
|
)
|
||||||
|
|
||||||
field(:member_of, list_of(:member), description: "The list of groups this person is member of")
|
field(:member_of, list_of(:member), description: "The list of groups this person is member of")
|
||||||
|
|
||||||
|
@ -52,16 +56,21 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||||
)
|
)
|
||||||
|
|
||||||
# This one should have a privacy setting
|
# This one should have a privacy setting
|
||||||
field(:organized_events, list_of(:event),
|
field(:organized_events, :paginated_event_list,
|
||||||
resolve: dataloader(Events),
|
|
||||||
description: "A list of the events this actor has organized"
|
description: "A list of the events this actor has organized"
|
||||||
)
|
) do
|
||||||
|
arg(:page, :integer, default_value: 1)
|
||||||
|
arg(:limit, :integer, default_value: 10)
|
||||||
|
resolve(&Person.organized_events_for_person/3)
|
||||||
|
end
|
||||||
|
|
||||||
@desc "The list of events this person goes to"
|
@desc "The list of events this person goes to"
|
||||||
field(:participations, list_of(:participant),
|
field(:participations, :paginated_participant_list,
|
||||||
description: "The list of events this person goes to"
|
description: "The list of events this person goes to"
|
||||||
) do
|
) do
|
||||||
arg(:event_id, :id)
|
arg(:event_id, :id)
|
||||||
|
arg(:page, :integer, default_value: 1)
|
||||||
|
arg(:limit, :integer, default_value: 10)
|
||||||
resolve(&Person.person_participations/3)
|
resolve(&Person.person_participations/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -95,6 +104,17 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||||
field :identities, list_of(:person) do
|
field :identities, list_of(:person) do
|
||||||
resolve(&Person.identities/3)
|
resolve(&Person.identities/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
field :persons, :persons do
|
||||||
|
arg(:preferred_username, :string, default_value: "")
|
||||||
|
arg(:name, :string, default_value: "")
|
||||||
|
arg(:domain, :string, default_value: "")
|
||||||
|
arg(:local, :boolean, default_value: true)
|
||||||
|
arg(:suspended, :boolean, default_value: false)
|
||||||
|
arg(:page, :integer, default_value: 1)
|
||||||
|
arg(:limit, :integer, default_value: 10)
|
||||||
|
resolve(&Person.list_persons/3)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
object :person_mutations do
|
object :person_mutations do
|
||||||
|
@ -168,6 +188,16 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
|
||||||
|
|
||||||
resolve(handle_errors(&Person.register_person/3))
|
resolve(handle_errors(&Person.register_person/3))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
field :suspend_profile, :deleted_object do
|
||||||
|
arg(:id, :id, description: "The profile ID to suspend")
|
||||||
|
resolve(&Person.suspend_profile/3)
|
||||||
|
end
|
||||||
|
|
||||||
|
field :unsuspend_profile, :person do
|
||||||
|
arg(:id, :id, description: "The profile ID to unsuspend")
|
||||||
|
resolve(&Person.unsuspend_profile/3)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
object :person_subscriptions do
|
object :person_subscriptions do
|
||||||
|
|
|
@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||||
|
|
||||||
use Absinthe.Schema.Notation
|
use Absinthe.Schema.Notation
|
||||||
|
|
||||||
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Conversations.Comment
|
alias Mobilizon.Conversations.Comment
|
||||||
alias Mobilizon.Events.Event
|
alias Mobilizon.Events.Event
|
||||||
alias Mobilizon.Reports.{Note, Report}
|
alias Mobilizon.Reports.{Note, Report}
|
||||||
|
@ -29,6 +30,8 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||||
value(:event_deletion)
|
value(:event_deletion)
|
||||||
value(:comment_deletion)
|
value(:comment_deletion)
|
||||||
value(:event_update)
|
value(:event_update)
|
||||||
|
value(:actor_suspension)
|
||||||
|
value(:actor_unsuspension)
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc "The objects that can be in an action log"
|
@desc "The objects that can be in an action log"
|
||||||
|
@ -48,6 +51,9 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||||
%Comment{}, _ ->
|
%Comment{}, _ ->
|
||||||
:comment
|
:comment
|
||||||
|
|
||||||
|
%Actor{type: "Person"}, _ ->
|
||||||
|
:person
|
||||||
|
|
||||||
_, _ ->
|
_, _ ->
|
||||||
nil
|
nil
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||||
field(:id, non_null(:id), description: "The user's ID")
|
field(:id, non_null(:id), description: "The user's ID")
|
||||||
field(:email, non_null(:string), description: "The user's email")
|
field(:email, non_null(:string), description: "The user's email")
|
||||||
|
|
||||||
field(:profiles, non_null(list_of(:person)),
|
field(:actors, non_null(list_of(:person)),
|
||||||
description: "The user's list of profiles (identities)"
|
description: "The user's list of profiles (identities)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -51,6 +51,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||||
|
|
||||||
field(:locale, :string, description: "The user's locale")
|
field(:locale, :string, description: "The user's locale")
|
||||||
|
|
||||||
|
field(:disabled, :boolean, description: "Whether the user is disabled")
|
||||||
|
|
||||||
field(:participations, :paginated_participant_list,
|
field(:participations, :paginated_participant_list,
|
||||||
description: "The list of participations this user has"
|
description: "The list of participations this user has"
|
||||||
) do
|
) do
|
||||||
|
@ -144,13 +146,14 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||||
|
|
||||||
@desc "List instance users"
|
@desc "List instance users"
|
||||||
field :users, :users do
|
field :users, :users do
|
||||||
|
arg(:email, :string, default_value: "")
|
||||||
arg(:page, :integer, default_value: 1)
|
arg(:page, :integer, default_value: 1)
|
||||||
arg(:limit, :integer, default_value: 10)
|
arg(:limit, :integer, default_value: 10)
|
||||||
|
|
||||||
arg(:sort, :sortable_user_field, default_value: :id)
|
arg(:sort, :sortable_user_field, default_value: :id)
|
||||||
arg(:direction, :sort_direction, default_value: :desc)
|
arg(:direction, :sort_direction, default_value: :desc)
|
||||||
|
|
||||||
resolve(&User.list_and_count_users/3)
|
resolve(&User.list_users/3)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -233,7 +236,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||||
|
|
||||||
@desc "Delete an account"
|
@desc "Delete an account"
|
||||||
field :delete_account, :deleted_object do
|
field :delete_account, :deleted_object do
|
||||||
arg(:password, non_null(:string))
|
arg(:password, :string)
|
||||||
|
arg(:user_id, :id)
|
||||||
resolve(&User.delete_account/3)
|
resolve(&User.delete_account/3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ defmodule Mix.Tasks.Mobilizon.Actors.Refresh do
|
||||||
#{total} actors to process
|
#{total} actors to process
|
||||||
""")
|
""")
|
||||||
|
|
||||||
query = from(a in Actor, where: not is_nil(a.domain))
|
query = from(a in Actor, where: not is_nil(a.domain) and not a.suspended)
|
||||||
|
|
||||||
{:ok, _res} =
|
{:ok, _res} =
|
||||||
Repo.transaction(
|
Repo.transaction(
|
||||||
|
|
|
@ -14,21 +14,24 @@ defmodule Mix.Tasks.Mobilizon.Users.Delete do
|
||||||
OptionParser.parse(
|
OptionParser.parse(
|
||||||
rest,
|
rest,
|
||||||
strict: [
|
strict: [
|
||||||
assume_yes: :boolean
|
assume_yes: :boolean,
|
||||||
|
force: :boolean
|
||||||
],
|
],
|
||||||
aliases: [
|
aliases: [
|
||||||
y: :assume_yes
|
y: :assume_yes,
|
||||||
|
f: :force
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
assume_yes? = Keyword.get(options, :assume_yes, false)
|
assume_yes? = Keyword.get(options, :assume_yes, false)
|
||||||
|
force? = Keyword.get(options, :force, false)
|
||||||
|
|
||||||
Mix.Task.run("app.start")
|
Mix.Task.run("app.start")
|
||||||
|
|
||||||
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
|
with {:ok, %User{} = user} <- Users.get_user_by_email(email),
|
||||||
true <- assume_yes? or Mix.shell().yes?("Continue with deleting user #{user.email}?"),
|
true <- assume_yes? or Mix.shell().yes?("Continue with deleting user #{user.email}?"),
|
||||||
{:ok, %User{} = user} <-
|
{:ok, %User{} = user} <-
|
||||||
Users.delete_user(user) do
|
Users.delete_user(user, reserve_email: !force?) do
|
||||||
Mix.shell().info("""
|
Mix.shell().info("""
|
||||||
The user #{user.email} has been deleted
|
The user #{user.email} has been deleted
|
||||||
""")
|
""")
|
||||||
|
|
|
@ -73,9 +73,9 @@ defmodule Mobilizon.Actors do
|
||||||
Gets an actor with preloaded relations.
|
Gets an actor with preloaded relations.
|
||||||
"""
|
"""
|
||||||
@spec get_actor_with_preload(integer | String.t()) :: Actor.t() | nil
|
@spec get_actor_with_preload(integer | String.t()) :: Actor.t() | nil
|
||||||
def get_actor_with_preload(id) do
|
def get_actor_with_preload(id, include_suspended \\ false) do
|
||||||
id
|
id
|
||||||
|> actor_with_preload_query()
|
|> actor_with_preload_query(include_suspended)
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -90,6 +90,14 @@ defmodule Mobilizon.Actors do
|
||||||
|> Repo.one()
|
|> Repo.one()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec get_remote_actor_with_preload(integer | String.t(), boolean()) :: Actor.t() | nil
|
||||||
|
def get_remote_actor_with_preload(id, include_suspended \\ false) do
|
||||||
|
id
|
||||||
|
|> actor_with_preload_query(include_suspended)
|
||||||
|
|> filter_external()
|
||||||
|
|> Repo.one()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets an actor by its URL (ActivityPub ID). The `:preload` option allows to
|
Gets an actor by its URL (ActivityPub ID). The `:preload` option allows to
|
||||||
preload the followers relation.
|
preload the followers relation.
|
||||||
|
@ -255,27 +263,41 @@ defmodule Mobilizon.Actors do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_actor(%Actor{} = actor) do
|
@delete_actor_default_options [reserve_username: true]
|
||||||
Workers.Background.enqueue("delete_actor", %{"actor_id" => actor.id})
|
|
||||||
|
def delete_actor(%Actor{} = actor, options \\ @delete_actor_default_options) do
|
||||||
|
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
|
||||||
|
|
||||||
|
Workers.Background.enqueue("delete_actor", %{
|
||||||
|
"actor_id" => actor.id,
|
||||||
|
"reserve_username" => Keyword.get(delete_actor_options, :reserve_username, true)
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes an actor.
|
Deletes an actor.
|
||||||
"""
|
"""
|
||||||
@spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
@spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def perform(:delete_actor, %Actor{} = actor) do
|
def perform(:delete_actor, %Actor{} = actor, options \\ @delete_actor_default_options) do
|
||||||
actor = Repo.preload(actor, @actor_preloads)
|
actor = Repo.preload(actor, @actor_preloads)
|
||||||
|
|
||||||
transaction =
|
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
|
||||||
|
|
||||||
|
multi =
|
||||||
Multi.new()
|
Multi.new()
|
||||||
|> Multi.run(:delete_organized_events, fn _, _ -> delete_actor_organized_events(actor) end)
|
|> Multi.run(:delete_organized_events, fn _, _ -> delete_actor_organized_events(actor) end)
|
||||||
|> Multi.run(:empty_comments, fn _, _ -> delete_actor_empty_comments(actor) end)
|
|> Multi.run(:empty_comments, fn _, _ -> delete_actor_empty_comments(actor) end)
|
||||||
|> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end)
|
|> Multi.run(:remove_banner, fn _, _ -> remove_banner(actor) end)
|
||||||
|> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end)
|
|> Multi.run(:remove_avatar, fn _, _ -> remove_avatar(actor) end)
|
||||||
|> Multi.update(:actor, Actor.delete_changeset(actor))
|
|
||||||
|> Repo.transaction()
|
|
||||||
|
|
||||||
case transaction do
|
multi =
|
||||||
|
if Keyword.get(delete_actor_options, :reserve_username, true) do
|
||||||
|
Multi.update(multi, :actor, Actor.delete_changeset(actor))
|
||||||
|
else
|
||||||
|
Multi.delete(multi, :actor, actor)
|
||||||
|
end
|
||||||
|
|
||||||
|
case Repo.transaction(multi) do
|
||||||
{:ok, %{actor: %Actor{} = actor}} ->
|
{:ok, %{actor: %Actor{} = actor}} ->
|
||||||
{:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
|
{:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
|
||||||
{:ok, actor}
|
{:ok, actor}
|
||||||
|
@ -295,8 +317,57 @@ defmodule Mobilizon.Actors do
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of actors.
|
Returns the list of actors.
|
||||||
"""
|
"""
|
||||||
@spec list_actors :: [Actor.t()]
|
@spec list_actors(String.t(), String.t(), boolean, boolean, integer, integer) :: Page.t()
|
||||||
def list_actors, do: Repo.all(Actor)
|
def list_actors(
|
||||||
|
type \\ :Person,
|
||||||
|
preferred_username \\ "",
|
||||||
|
name \\ "",
|
||||||
|
domain \\ "",
|
||||||
|
local \\ true,
|
||||||
|
suspended \\ false,
|
||||||
|
page \\ nil,
|
||||||
|
limit \\ nil
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_actors(
|
||||||
|
:Person,
|
||||||
|
preferred_username,
|
||||||
|
name,
|
||||||
|
domain,
|
||||||
|
local,
|
||||||
|
suspended,
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
) do
|
||||||
|
person_query()
|
||||||
|
|> filter_suspended(suspended)
|
||||||
|
|> filter_preferred_username(preferred_username)
|
||||||
|
|> filter_name(name)
|
||||||
|
|> filter_domain(domain)
|
||||||
|
|> filter_remote(local)
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter_preferred_username(query, ""), do: query
|
||||||
|
|
||||||
|
defp filter_preferred_username(query, preferred_username),
|
||||||
|
do: where(query, [a], ilike(a.preferred_username, ^"%#{preferred_username}%"))
|
||||||
|
|
||||||
|
defp filter_name(query, ""), do: query
|
||||||
|
|
||||||
|
defp filter_name(query, name),
|
||||||
|
do: where(query, [a], ilike(a.name, ^"%#{name}%"))
|
||||||
|
|
||||||
|
defp filter_domain(query, ""), do: query
|
||||||
|
|
||||||
|
defp filter_domain(query, domain),
|
||||||
|
do: where(query, [a], ilike(a.domain, ^"%#{domain}%"))
|
||||||
|
|
||||||
|
defp filter_remote(query, true), do: filter_local(query)
|
||||||
|
defp filter_remote(query, false), do: filter_external(query)
|
||||||
|
|
||||||
|
defp filter_suspended(query, true), do: where(query, [a], a.suspended)
|
||||||
|
defp filter_suspended(query, false), do: where(query, [a], not a.suspended)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of local actors by their username.
|
Returns the list of local actors by their username.
|
||||||
|
@ -945,13 +1016,19 @@ defmodule Mobilizon.Actors do
|
||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec actor_with_preload_query(integer | String.t()) :: Ecto.Query.t()
|
@spec actor_with_preload_query(integer | String.t(), boolean()) :: Ecto.Query.t()
|
||||||
defp actor_with_preload_query(actor_id) do
|
defp actor_with_preload_query(actor_id, include_suspended \\ false)
|
||||||
from(
|
|
||||||
a in Actor,
|
defp actor_with_preload_query(actor_id, false) do
|
||||||
where: a.id == ^actor_id and not a.suspended,
|
actor_id
|
||||||
preload: [:organized_events, :followers, :followings]
|
|> actor_with_preload_query(true)
|
||||||
)
|
|> where([a], not a.suspended)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp actor_with_preload_query(actor_id, true) do
|
||||||
|
Actor
|
||||||
|
|> where([a], a.id == ^actor_id)
|
||||||
|
|> preload([a], [:organized_events, :followers, :followings])
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec actor_by_username_query(String.t()) :: Ecto.Query.t()
|
@spec actor_by_username_query(String.t()) :: Ecto.Query.t()
|
||||||
|
@ -1000,6 +1077,11 @@ defmodule Mobilizon.Actors do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec person_query :: Ecto.Query.t()
|
||||||
|
defp person_query do
|
||||||
|
from(a in Actor, where: a.type == ^:Person)
|
||||||
|
end
|
||||||
|
|
||||||
@spec group_query :: Ecto.Query.t()
|
@spec group_query :: Ecto.Query.t()
|
||||||
defp group_query do
|
defp group_query do
|
||||||
from(a in Actor, where: a.type == ^:Group)
|
from(a in Actor, where: a.type == ^:Group)
|
||||||
|
|
|
@ -16,7 +16,9 @@ defmodule Mobilizon.Admin do
|
||||||
defenum(ActionLogAction, [
|
defenum(ActionLogAction, [
|
||||||
"update",
|
"update",
|
||||||
"create",
|
"create",
|
||||||
"delete"
|
"delete",
|
||||||
|
"suspend",
|
||||||
|
"unsuspend"
|
||||||
])
|
])
|
||||||
|
|
||||||
alias Ecto.Multi
|
alias Ecto.Multi
|
||||||
|
|
|
@ -401,6 +401,14 @@ defmodule Mobilizon.Events do
|
||||||
{:ok, events, events_count}
|
{:ok, events, events_count}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec list_organized_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||||
|
def list_organized_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||||
|
actor_id
|
||||||
|
|> event_for_actor_query()
|
||||||
|
|> preload_for_event()
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
|
end
|
||||||
|
|
||||||
@spec list_organized_events_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
@spec list_organized_events_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||||
def list_organized_events_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
|
def list_organized_events_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
|
||||||
group_id
|
group_id
|
||||||
|
@ -842,13 +850,11 @@ defmodule Mobilizon.Events do
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of participations for an actor.
|
Returns the list of participations for an actor.
|
||||||
"""
|
"""
|
||||||
@spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
@spec list_event_participations_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||||
[Participant.t()]
|
|
||||||
def list_event_participations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
def list_event_participations_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||||
actor_id
|
actor_id
|
||||||
|> event_participations_for_actor_query()
|
|> event_participations_for_actor_query()
|
||||||
|> Page.paginate(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|> Repo.all()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -1414,7 +1420,8 @@ defmodule Mobilizon.Events do
|
||||||
join: e in Event,
|
join: e in Event,
|
||||||
on: p.event_id == e.id,
|
on: p.event_id == e.id,
|
||||||
where: p.actor_id == ^actor_id and p.role != ^:not_approved,
|
where: p.actor_id == ^actor_id and p.role != ^:not_approved,
|
||||||
preload: [:event]
|
preload: [:event],
|
||||||
|
order_by: [desc: e.begins_on]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ defmodule Mobilizon.Media.File do
|
||||||
@optional_attrs [:content_type, :size]
|
@optional_attrs [:content_type, :size]
|
||||||
@attrs @required_attrs ++ @optional_attrs
|
@attrs @required_attrs ++ @optional_attrs
|
||||||
|
|
||||||
|
@derive Jason.Encoder
|
||||||
embedded_schema do
|
embedded_schema do
|
||||||
field(:name, :string)
|
field(:name, :string)
|
||||||
field(:url, :string)
|
field(:url, :string)
|
||||||
|
|
|
@ -25,6 +25,7 @@ defmodule Mobilizon.Users.User do
|
||||||
reset_password_token: String.t(),
|
reset_password_token: String.t(),
|
||||||
locale: String.t(),
|
locale: String.t(),
|
||||||
default_actor: Actor.t(),
|
default_actor: Actor.t(),
|
||||||
|
disabled: boolean(),
|
||||||
actors: [Actor.t()],
|
actors: [Actor.t()],
|
||||||
feed_tokens: [FeedToken.t()]
|
feed_tokens: [FeedToken.t()]
|
||||||
}
|
}
|
||||||
|
@ -40,7 +41,8 @@ defmodule Mobilizon.Users.User do
|
||||||
:reset_password_sent_at,
|
:reset_password_sent_at,
|
||||||
:reset_password_token,
|
:reset_password_token,
|
||||||
:locale,
|
:locale,
|
||||||
:unconfirmed_email
|
:unconfirmed_email,
|
||||||
|
:disabled
|
||||||
]
|
]
|
||||||
@attrs @required_attrs ++ @optional_attrs
|
@attrs @required_attrs ++ @optional_attrs
|
||||||
|
|
||||||
|
@ -64,6 +66,7 @@ defmodule Mobilizon.Users.User do
|
||||||
field(:reset_password_token, :string)
|
field(:reset_password_token, :string)
|
||||||
field(:unconfirmed_email, :string)
|
field(:unconfirmed_email, :string)
|
||||||
field(:locale, :string, default: "en")
|
field(:locale, :string, default: "en")
|
||||||
|
field(:disabled, :boolean, default: false)
|
||||||
|
|
||||||
belongs_to(:default_actor, Actor)
|
belongs_to(:default_actor, Actor)
|
||||||
has_many(:actors, Actor)
|
has_many(:actors, Actor)
|
||||||
|
@ -91,6 +94,13 @@ defmodule Mobilizon.Users.User do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def delete_changeset(%__MODULE__{} = user) do
|
||||||
|
user
|
||||||
|
|> change()
|
||||||
|
|> put_change(:disabled, true)
|
||||||
|
|> put_change(:default_actor_id, nil)
|
||||||
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec registration_changeset(t, map) :: Ecto.Changeset.t()
|
@spec registration_changeset(t, map) :: Ecto.Changeset.t()
|
||||||
def registration_changeset(%__MODULE__{} = user, attrs) do
|
def registration_changeset(%__MODULE__{} = user, attrs) do
|
||||||
|
|
|
@ -8,8 +8,10 @@ defmodule Mobilizon.Users do
|
||||||
|
|
||||||
import Mobilizon.Storage.Ecto
|
import Mobilizon.Storage.Ecto
|
||||||
|
|
||||||
|
alias Ecto.Multi
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Events
|
alias Mobilizon.Events
|
||||||
|
alias Mobilizon.Events.FeedToken
|
||||||
alias Mobilizon.Storage.{Page, Repo}
|
alias Mobilizon.Storage.{Page, Repo}
|
||||||
alias Mobilizon.Users.{Setting, User}
|
alias Mobilizon.Users.{Setting, User}
|
||||||
|
|
||||||
|
@ -46,7 +48,8 @@ defmodule Mobilizon.Users do
|
||||||
@spec get_user!(integer | String.t()) :: User.t()
|
@spec get_user!(integer | String.t()) :: User.t()
|
||||||
def get_user!(id), do: Repo.get!(User, id)
|
def get_user!(id), do: Repo.get!(User, id)
|
||||||
|
|
||||||
@spec get_user(integer | String.t()) :: User.t() | nil
|
@spec get_user(integer | String.t() | nil) :: User.t() | nil
|
||||||
|
def get_user(nil), do: nil
|
||||||
def get_user(id), do: Repo.get(User, id)
|
def get_user(id), do: Repo.get(User, id)
|
||||||
|
|
||||||
def get_user_with_settings!(id) do
|
def get_user_with_settings!(id) do
|
||||||
|
@ -105,11 +108,35 @@ defmodule Mobilizon.Users do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@delete_user_default_options [reserve_email: true]
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes an user.
|
Deletes an user.
|
||||||
"""
|
"""
|
||||||
@spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
@spec delete_user(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def delete_user(%User{} = user), do: Repo.delete(user)
|
def delete_user(%User{id: user_id} = user, options \\ @delete_user_default_options) do
|
||||||
|
delete_user_options = Keyword.merge(@delete_user_default_options, options)
|
||||||
|
|
||||||
|
multi =
|
||||||
|
Multi.new()
|
||||||
|
|> Multi.delete_all(:settings, from(s in Setting, where: s.user_id == ^user_id))
|
||||||
|
|> Multi.delete_all(:feed_tokens, from(f in FeedToken, where: f.user_id == ^user_id))
|
||||||
|
|
||||||
|
multi =
|
||||||
|
if Keyword.get(delete_user_options, :reserve_email, true) do
|
||||||
|
Multi.update(multi, :user, User.delete_changeset(user))
|
||||||
|
else
|
||||||
|
Multi.delete(multi, :user, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
case Repo.transaction(multi) do
|
||||||
|
{:ok, %{user: %User{} = user}} ->
|
||||||
|
{:ok, user}
|
||||||
|
|
||||||
|
{:error, remove, error, _} when remove in [:settings, :feed_tokens] ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Get an user with its actors
|
Get an user with its actors
|
||||||
|
@ -196,12 +223,22 @@ defmodule Mobilizon.Users do
|
||||||
@doc """
|
@doc """
|
||||||
Returns the list of users.
|
Returns the list of users.
|
||||||
"""
|
"""
|
||||||
@spec list_users(integer | nil, integer | nil, atom | nil, atom | nil) :: [User.t()]
|
@spec list_users(String.t(), integer | nil, integer | nil, atom | nil, atom | nil) :: Page.t()
|
||||||
def list_users(page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil) do
|
def list_users(email \\ "", page \\ nil, limit \\ nil, sort \\ nil, direction \\ nil)
|
||||||
|
|
||||||
|
def list_users("", page, limit, sort, direction) do
|
||||||
User
|
User
|
||||||
|> Page.paginate(page, limit)
|
|
||||||
|> sort(sort, direction)
|
|> sort(sort, direction)
|
||||||
|> Repo.all()
|
|> preload([u], [:actors, :feed_tokens, :settings, :default_actor])
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_users(email, page, limit, sort, direction) do
|
||||||
|
User
|
||||||
|
|> where([u], ilike(u.email, ^"%#{email}%"))
|
||||||
|
|> sort(sort, direction)
|
||||||
|
|> preload([u], [:actors, :feed_tokens, :settings, :default_actor])
|
||||||
|
|> Page.build_page(page, limit)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule Mobilizon.Service.Export.Feed do
|
||||||
alias Mobilizon.{Actors, Events, Users}
|
alias Mobilizon.{Actors, Events, Users}
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Events.{Event, FeedToken}
|
alias Mobilizon.Events.{Event, FeedToken}
|
||||||
|
alias Mobilizon.Storage.Page
|
||||||
alias Mobilizon.Users.User
|
alias Mobilizon.Users.User
|
||||||
|
|
||||||
alias Mobilizon.Web.{Endpoint, MediaProxy}
|
alias Mobilizon.Web.{Endpoint, MediaProxy}
|
||||||
|
@ -148,12 +149,12 @@ defmodule Mobilizon.Service.Export.Feed do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp fetch_identity_participations(%Actor{} = actor) do
|
defp fetch_identity_participations(%Actor{} = actor) do
|
||||||
with events <- Events.list_event_participations_for_actor(actor) do
|
with %Page{} = page <- Events.list_event_participations_for_actor(actor) do
|
||||||
events
|
page
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp participations_to_events(participations) do
|
defp participations_to_events(%Page{elements: participations}) do
|
||||||
participations
|
participations
|
||||||
|> Enum.map(& &1.event_id)
|
|> Enum.map(& &1.event_id)
|
||||||
|> Enum.map(&Events.get_event_with_preload!/1)
|
|> Enum.map(&Events.get_event_with_preload!/1)
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule Mobilizon.Service.Export.ICalendar do
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Addresses.Address
|
alias Mobilizon.Addresses.Address
|
||||||
alias Mobilizon.Events.{Event, FeedToken}
|
alias Mobilizon.Events.{Event, FeedToken}
|
||||||
|
alias Mobilizon.Storage.Page
|
||||||
alias Mobilizon.Users.User
|
alias Mobilizon.Users.User
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -123,7 +124,7 @@ defmodule Mobilizon.Service.Export.ICalendar do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp participations_to_events(participations) do
|
defp participations_to_events(%Page{elements: participations}) do
|
||||||
participations
|
participations
|
||||||
|> Enum.map(& &1.event_id)
|
|> Enum.map(& &1.event_id)
|
||||||
|> Enum.map(&Events.get_event_with_preload!/1)
|
|> Enum.map(&Events.get_event_with_preload!/1)
|
||||||
|
|
|
@ -9,9 +9,11 @@ defmodule Mobilizon.Service.Workers.Background do
|
||||||
use Mobilizon.Service.Workers.Helper, queue: "background"
|
use Mobilizon.Service.Workers.Helper, queue: "background"
|
||||||
|
|
||||||
@impl Oban.Worker
|
@impl Oban.Worker
|
||||||
def perform(%{"op" => "delete_actor", "actor_id" => actor_id}, _job) do
|
def perform(%{"op" => "delete_actor", "actor_id" => actor_id} = args, _job) do
|
||||||
with %Actor{} = actor <- Actors.get_actor(actor_id) do
|
with reserve_username when is_boolean(reserve_username) <-
|
||||||
Actors.perform(:delete_actor, actor)
|
Map.get(args, "reserve_username", true),
|
||||||
|
%Actor{} = actor <- Actors.get_actor(actor_id) do
|
||||||
|
Actors.perform(:delete_actor, actor, reserve_username: reserve_username)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Mobilizon.Storage.Repo.Migrations.AddDisabledParameterToUsers do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:users) do
|
||||||
|
add(:disabled, :boolean, default: false, null: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,9 +12,8 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
||||||
import ExUnit.CaptureLog
|
import ExUnit.CaptureLog
|
||||||
import Mock
|
import Mock
|
||||||
|
|
||||||
alias Mobilizon.{Actors, Conversations, Events, Resources, Tombstone}
|
alias Mobilizon.{Actors, Conversations, Events}
|
||||||
alias Mobilizon.Actors.{Actor, Member}
|
alias Mobilizon.Actors.{Actor, Member}
|
||||||
alias Mobilizon.Collections.Resource
|
|
||||||
alias Mobilizon.Conversations.Comment
|
alias Mobilizon.Conversations.Comment
|
||||||
alias Mobilizon.Events.{Event, Participant}
|
alias Mobilizon.Events.{Event, Participant}
|
||||||
alias Mobilizon.Resources.Resource
|
alias Mobilizon.Resources.Resource
|
||||||
|
@ -557,7 +556,6 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
||||||
assert is_nil(resource.parent_id)
|
assert is_nil(resource.parent_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@parent_url "http://mobilizon1.com/resource/e4ce71bd-6bcc-4c61-9774-7999dde0cc68"
|
|
||||||
test "it accepts incoming resources that are in a folder" do
|
test "it accepts incoming resources that are in a folder" do
|
||||||
creator =
|
creator =
|
||||||
insert(:actor,
|
insert(:actor,
|
||||||
|
@ -978,12 +976,13 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
|
||||||
{:ok, _activity, _actor} = Transmogrifier.handle_incoming(data)
|
{:ok, _activity, _actor} = Transmogrifier.handle_incoming(data)
|
||||||
assert %{success: 1, failure: 0} == Oban.drain_queue(:background)
|
assert %{success: 1, failure: 0} == Oban.drain_queue(:background)
|
||||||
|
|
||||||
assert {:ok, %Actor{suspended: true}} = Actors.get_actor_by_url(url)
|
assert {:error, :actor_not_found} = Actors.get_actor_by_url(url)
|
||||||
assert {:error, :event_not_found} = Events.get_event(event1.id)
|
assert {:error, :event_not_found} = Events.get_event(event1.id)
|
||||||
assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
|
# Tombstone are cascade deleted, seems correct for now
|
||||||
|
# assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
|
||||||
assert %Comment{deleted_at: deleted_at} = Conversations.get_comment(comment1.id)
|
assert %Comment{deleted_at: deleted_at} = Conversations.get_comment(comment1.id)
|
||||||
refute is_nil(deleted_at)
|
refute is_nil(deleted_at)
|
||||||
assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
|
# assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it fails for incoming actor deletes with spoofed origin" do
|
test "it fails for incoming actor deletes with spoofed origin" do
|
||||||
|
|
|
@ -273,6 +273,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
person(id: $actor_id) {
|
person(id: $actor_id) {
|
||||||
id,
|
id,
|
||||||
participations(eventId: $event_id) {
|
participations(eventId: $event_id) {
|
||||||
|
elements {
|
||||||
id,
|
id,
|
||||||
role,
|
role,
|
||||||
actor {
|
actor {
|
||||||
|
@ -284,6 +285,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
|
@ -295,7 +297,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
)
|
)
|
||||||
|
|
||||||
assert res["errors"] == nil
|
assert res["errors"] == nil
|
||||||
assert res["data"]["person"]["participations"] == []
|
assert res["data"]["person"]["participations"]["elements"] == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_event/3 creates an event with options", %{conn: conn, actor: actor, user: user} do
|
test "create_event/3 creates an event with options", %{conn: conn, actor: actor, user: user} do
|
||||||
|
@ -979,6 +981,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
person(id: "#{actor.id}") {
|
person(id: "#{actor.id}") {
|
||||||
id,
|
id,
|
||||||
participations(eventId: #{event.id}) {
|
participations(eventId: #{event.id}) {
|
||||||
|
elements {
|
||||||
id,
|
id,
|
||||||
role,
|
role,
|
||||||
actor {
|
actor {
|
||||||
|
@ -990,6 +993,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
|
@ -998,7 +1002,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
||||||
|
|
||||||
assert json_response(res, 200)["errors"] == nil
|
assert json_response(res, 200)["errors"] == nil
|
||||||
assert json_response(res, 200)["data"]["person"]["participations"] == []
|
assert json_response(res, 200)["data"]["person"]["participations"]["elements"] == []
|
||||||
|
|
||||||
mutation = """
|
mutation = """
|
||||||
mutation {
|
mutation {
|
||||||
|
@ -1042,6 +1046,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
person(id: "#{actor.id}") {
|
person(id: "#{actor.id}") {
|
||||||
id,
|
id,
|
||||||
participations(eventId: #{event.id}) {
|
participations(eventId: #{event.id}) {
|
||||||
|
elements {
|
||||||
role,
|
role,
|
||||||
actor {
|
actor {
|
||||||
id
|
id
|
||||||
|
@ -1052,6 +1057,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
|
@ -1061,7 +1067,7 @@ defmodule Mobilizon.Web.Resolvers.EventTest do
|
||||||
|
|
||||||
assert json_response(res, 200)["errors"] == nil
|
assert json_response(res, 200)["errors"] == nil
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["person"]["participations"] == [
|
assert json_response(res, 200)["data"]["person"]["participations"]["elements"] == [
|
||||||
%{
|
%{
|
||||||
"actor" => %{"id" => to_string(actor.id)},
|
"actor" => %{"id" => to_string(actor.id)},
|
||||||
"event" => %{"id" => to_string(event.id)},
|
"event" => %{"id" => to_string(event.id)},
|
||||||
|
|
|
@ -201,6 +201,23 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
"Event with this ID \"1042\" doesn't exist"
|
"Event with this ID \"1042\" doesn't exist"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@person_participations """
|
||||||
|
query PersonParticipations($actorId: ID!, $eventId: ID) {
|
||||||
|
person(id: $actorId) {
|
||||||
|
participations(eventId: $eventId) {
|
||||||
|
total,
|
||||||
|
elements {
|
||||||
|
event {
|
||||||
|
uuid,
|
||||||
|
title
|
||||||
|
},
|
||||||
|
role
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
test "actor_leave_event/3 should delete a participant from an event", %{
|
test "actor_leave_event/3 should delete a participant from an event", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
user: user,
|
user: user,
|
||||||
|
@ -241,26 +258,15 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
assert json_response(res, 200)["data"]["leaveEvent"]["actor"]["id"] ==
|
assert json_response(res, 200)["data"]["leaveEvent"]["actor"]["id"] ==
|
||||||
to_string(participant2.actor.id)
|
to_string(participant2.actor.id)
|
||||||
|
|
||||||
query = """
|
|
||||||
{
|
|
||||||
person(id: "#{actor.id}") {
|
|
||||||
participations(eventId: "#{event.id}") {
|
|
||||||
event {
|
|
||||||
uuid,
|
|
||||||
title
|
|
||||||
},
|
|
||||||
role
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
res =
|
res =
|
||||||
conn
|
conn
|
||||||
|> auth_conn(user)
|
|> auth_conn(user)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @person_participations,
|
||||||
|
variables: %{actorId: actor.id, eventId: event.id}
|
||||||
|
)
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["person"]["participations"] == [
|
assert res["data"]["person"]["participations"]["elements"] == [
|
||||||
%{
|
%{
|
||||||
"event" => %{
|
"event" => %{
|
||||||
"uuid" => event.uuid,
|
"uuid" => event.uuid,
|
||||||
|
@ -270,26 +276,15 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
query = """
|
|
||||||
{
|
|
||||||
person(id: "#{actor2.id}") {
|
|
||||||
participations(eventId: "#{event.id}") {
|
|
||||||
event {
|
|
||||||
uuid,
|
|
||||||
title
|
|
||||||
},
|
|
||||||
role
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
res =
|
res =
|
||||||
conn
|
conn
|
||||||
|> auth_conn(user2)
|
|> auth_conn(user2)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @person_participations,
|
||||||
|
variables: %{actorId: actor2.id, eventId: event.id}
|
||||||
|
)
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["person"]["participations"] == []
|
assert res["data"]["person"]["participations"]["elements"] == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "actor_leave_event/3 should check if the participant is the only creator", %{
|
test "actor_leave_event/3 should check if the participant is the only creator", %{
|
||||||
|
|
|
@ -14,7 +14,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
@non_existent_username "nonexistent"
|
@non_existent_username "nonexistent"
|
||||||
|
|
||||||
describe "Person Resolver" do
|
describe "Person Resolver" do
|
||||||
test "get_person/3 returns a person by its username", context do
|
test "get_person/3 returns a person by its username", %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
actor = insert(:actor, user: user)
|
actor = insert(:actor, user: user)
|
||||||
|
|
||||||
|
@ -27,7 +27,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
context.conn
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["person"]["preferredUsername"] ==
|
assert json_response(res, 200)["data"]["person"]["preferredUsername"] ==
|
||||||
|
@ -42,7 +43,8 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
context.conn
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["person"] == nil
|
assert json_response(res, 200)["data"]["person"] == nil
|
||||||
|
@ -506,7 +508,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
|
|
||||||
assert_enqueued(
|
assert_enqueued(
|
||||||
worker: Workers.Background,
|
worker: Workers.Background,
|
||||||
args: %{"actor_id" => person_id, "op" => "delete_actor"}
|
args: %{"actor_id" => person_id, "op" => "delete_actor", "reserve_username" => true}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert %{success: 1, failure: 0} == Oban.drain_queue(:background)
|
assert %{success: 1, failure: 0} == Oban.drain_queue(:background)
|
||||||
|
@ -538,6 +540,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
{
|
{
|
||||||
loggedPerson {
|
loggedPerson {
|
||||||
participations {
|
participations {
|
||||||
|
elements {
|
||||||
event {
|
event {
|
||||||
uuid,
|
uuid,
|
||||||
title
|
title
|
||||||
|
@ -545,6 +548,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
|
@ -552,7 +556,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
|> auth_conn(user)
|
|> auth_conn(user)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["loggedPerson"]["participations"] == []
|
assert json_response(res, 200)["data"]["loggedPerson"]["participations"]["elements"] == []
|
||||||
|
|
||||||
event = insert(:event, %{organizer_actor: actor})
|
event = insert(:event, %{organizer_actor: actor})
|
||||||
insert(:participant, %{actor: actor, event: event})
|
insert(:participant, %{actor: actor, event: event})
|
||||||
|
@ -562,11 +566,27 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
|> auth_conn(user)
|
|> auth_conn(user)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "logged_person"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["loggedPerson"]["participations"] == [
|
assert json_response(res, 200)["data"]["loggedPerson"]["participations"]["elements"] == [
|
||||||
%{"event" => %{"title" => event.title, "uuid" => event.uuid}}
|
%{"event" => %{"title" => event.title, "uuid" => event.uuid}}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@person_participations """
|
||||||
|
query PersonParticipations($actorId: ID!) {
|
||||||
|
person(id: $actorId) {
|
||||||
|
participations {
|
||||||
|
total,
|
||||||
|
elements {
|
||||||
|
event {
|
||||||
|
uuid,
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
test "find_person/3 can return the events an identity is going to if it's the same actor",
|
test "find_person/3 can return the events an identity is going to if it's the same actor",
|
||||||
context do
|
context do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
@ -574,47 +594,27 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
insert(:actor, user: user)
|
insert(:actor, user: user)
|
||||||
actor_from_other_user = insert(:actor)
|
actor_from_other_user = insert(:actor)
|
||||||
|
|
||||||
query = """
|
res =
|
||||||
{
|
context.conn
|
||||||
person(id: "#{actor.id}") {
|
|> auth_conn(user)
|
||||||
participations {
|
|> AbsintheHelpers.graphql_query(
|
||||||
event {
|
query: @person_participations,
|
||||||
uuid,
|
variables: %{actorId: actor.id}
|
||||||
title
|
)
|
||||||
}
|
|
||||||
}
|
assert res["data"]["person"]["participations"]["elements"] == []
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
res =
|
res =
|
||||||
context.conn
|
context.conn
|
||||||
|> auth_conn(user)
|
|> auth_conn(user)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @person_participations,
|
||||||
|
variables: %{actorId: actor_from_other_user.id}
|
||||||
|
)
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["person"]["participations"] == []
|
assert res["data"]["person"]["participations"]["elements"] == nil
|
||||||
|
|
||||||
query = """
|
assert hd(res["errors"])["message"] ==
|
||||||
{
|
|
||||||
person(id: "#{actor_from_other_user.id}") {
|
|
||||||
participations {
|
|
||||||
event {
|
|
||||||
uuid,
|
|
||||||
title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
res =
|
|
||||||
context.conn
|
|
||||||
|> auth_conn(user)
|
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["person"]["participations"] == nil
|
|
||||||
|
|
||||||
assert hd(json_response(res, 200)["errors"])["message"] ==
|
|
||||||
"Actor id is not owned by authenticated user"
|
"Actor id is not owned by authenticated user"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -629,6 +629,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
{
|
{
|
||||||
person(id: "#{actor.id}") {
|
person(id: "#{actor.id}") {
|
||||||
participations(eventId: "#{event.id}") {
|
participations(eventId: "#{event.id}") {
|
||||||
|
elements {
|
||||||
event {
|
event {
|
||||||
uuid,
|
uuid,
|
||||||
title
|
title
|
||||||
|
@ -636,6 +637,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
|
@ -643,7 +645,7 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
|> auth_conn(user)
|
|> auth_conn(user)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "person"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["person"]["participations"] == [
|
assert json_response(res, 200)["data"]["person"]["participations"]["elements"] == [
|
||||||
%{
|
%{
|
||||||
"event" => %{
|
"event" => %{
|
||||||
"uuid" => event.uuid,
|
"uuid" => event.uuid,
|
||||||
|
@ -653,4 +655,114 @@ defmodule Mobilizon.GraphQL.Resolvers.PersonTest do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "suspend_profile/3" do
|
||||||
|
@suspend_profile_mutation """
|
||||||
|
mutation SuspendProfile($id: ID!) {
|
||||||
|
suspendProfile(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@person_query """
|
||||||
|
query Person($id: ID!) {
|
||||||
|
person(id: $id) {
|
||||||
|
id,
|
||||||
|
suspended
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@moderation_logs_query """
|
||||||
|
{
|
||||||
|
actionLogs {
|
||||||
|
action,
|
||||||
|
actor {
|
||||||
|
id,
|
||||||
|
preferredUsername
|
||||||
|
},
|
||||||
|
object {
|
||||||
|
...on Person {
|
||||||
|
id,
|
||||||
|
preferredUsername
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
test "suspends a remote profile", %{conn: conn} do
|
||||||
|
modo = insert(:user, role: :moderator)
|
||||||
|
%Actor{id: modo_actor_id} = insert(:actor, user: modo)
|
||||||
|
%Actor{id: remote_profile_id} = insert(:actor, domain: "mobilizon.org", user: nil)
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(modo)
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @suspend_profile_mutation,
|
||||||
|
variables: %{id: remote_profile_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert is_nil(res["errors"])
|
||||||
|
assert res["data"]["suspendProfile"]["id"] == to_string(remote_profile_id)
|
||||||
|
|
||||||
|
assert %{success: 1, failure: 0} == Oban.drain_queue(:background)
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(modo)
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @person_query,
|
||||||
|
variables: %{id: remote_profile_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res["data"]["person"]["suspended"] == true
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(modo)
|
||||||
|
|> AbsintheHelpers.graphql_query(query: @moderation_logs_query)
|
||||||
|
|
||||||
|
actionlog = hd(res["data"]["actionLogs"])
|
||||||
|
refute is_nil(actionlog)
|
||||||
|
assert actionlog["action"] == "ACTOR_SUSPENSION"
|
||||||
|
assert actionlog["actor"]["id"] == to_string(modo_actor_id)
|
||||||
|
assert actionlog["object"]["id"] == to_string(remote_profile_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't suspend if profile is local", %{conn: conn} do
|
||||||
|
modo = insert(:user, role: :moderator)
|
||||||
|
%Actor{} = insert(:actor, user: modo)
|
||||||
|
%Actor{id: profile_id} = insert(:actor)
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(modo)
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @suspend_profile_mutation,
|
||||||
|
variables: %{id: profile_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hd(res["errors"])["message"] == "No remote profile found with this ID"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "doesn't suspend if user is not at least moderator", %{conn: conn} do
|
||||||
|
fake_modo = insert(:user)
|
||||||
|
%Actor{} = insert(:actor, user: fake_modo)
|
||||||
|
%Actor{id: remote_profile_id} = insert(:actor, domain: "mobilizon.org", user: nil)
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(fake_modo)
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @suspend_profile_mutation,
|
||||||
|
variables: %{id: remote_profile_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert hd(res["errors"])["message"] ==
|
||||||
|
"Only moderators and administrators can suspend a profile"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,8 +57,9 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||||
@valid_single_actor_params %{preferred_username: "test2", keys: "yolo"}
|
@valid_single_actor_params %{preferred_username: "test2", keys: "yolo"}
|
||||||
|
|
||||||
describe "Resolver: Get an user" do
|
describe "Resolver: Get an user" do
|
||||||
test "find_user/3 returns an user by its id", context do
|
test "find_user/3 returns an user by its id", %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
modo = insert(:user, role: :moderator)
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
{
|
{
|
||||||
|
@ -69,7 +70,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
context.conn
|
conn
|
||||||
|
|> auth_conn(modo)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "user"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "user"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["user"]["email"] == user.email
|
assert json_response(res, 200)["data"]["user"]["email"] == user.email
|
||||||
|
@ -83,7 +85,8 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
res =
|
res =
|
||||||
context.conn
|
conn
|
||||||
|
|> auth_conn(modo)
|
||||||
|> get("/api", AbsintheHelpers.query_skeleton(query, "user"))
|
|> get("/api", AbsintheHelpers.query_skeleton(query, "user"))
|
||||||
|
|
||||||
assert json_response(res, 200)["data"]["user"] == nil
|
assert json_response(res, 200)["data"]["user"] == nil
|
||||||
|
@ -1414,9 +1417,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||||
|
|
||||||
assert MapSet.new([actor1.id, actor2.id]) == MapSet.new([actor1_id, actor2_id])
|
assert MapSet.new([actor1.id, actor2.id]) == MapSet.new([actor1_id, actor2_id])
|
||||||
|
|
||||||
assert_raise Ecto.NoResultsError, fn ->
|
assert Users.get_user(user.id).disabled == true
|
||||||
Users.get_user!(user.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
assert %{success: 2, failure: 0} == Oban.drain_queue(:background)
|
assert %{success: 2, failure: 0} == Oban.drain_queue(:background)
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,8 @@ defmodule Mobilizon.ActorsTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "list_actors/0 returns all actors", %{actor: %Actor{id: actor_id}} do
|
test "list_actors/0 returns all actors", %{actor: %Actor{id: actor_id}} do
|
||||||
assert actor_id == hd(Actors.list_actors()).id
|
assert %Page{total: 1, elements: [%Actor{id: id}]} = Actors.list_actors()
|
||||||
|
assert id == actor_id
|
||||||
end
|
end
|
||||||
|
|
||||||
test "get_actor!/1 returns the actor with given id", %{actor: %Actor{id: actor_id} = actor} do
|
test "get_actor!/1 returns the actor with given id", %{actor: %Actor{id: actor_id} = actor} do
|
||||||
|
@ -316,7 +317,7 @@ defmodule Mobilizon.ActorsTest do
|
||||||
|
|
||||||
assert_enqueued(
|
assert_enqueued(
|
||||||
worker: Workers.Background,
|
worker: Workers.Background,
|
||||||
args: %{"actor_id" => actor.id, "op" => "delete_actor"}
|
args: %{"actor_id" => actor.id, "op" => "delete_actor", "reserve_username" => true}
|
||||||
)
|
)
|
||||||
|
|
||||||
assert %{success: 1, failure: 0} == Oban.drain_queue(:background)
|
assert %{success: 1, failure: 0} == Oban.drain_queue(:background)
|
||||||
|
|
|
@ -4,8 +4,7 @@ defmodule Mobilizon.EventsTest do
|
||||||
import Mobilizon.Factory
|
import Mobilizon.Factory
|
||||||
|
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.{Conversations, Events}
|
alias Mobilizon.Events
|
||||||
alias Mobilizon.Conversations.Comment
|
|
||||||
alias Mobilizon.Events.{Event, Participant, Session, Tag, TagRelation, Track}
|
alias Mobilizon.Events.{Event, Participant, Session, Tag, TagRelation, Track}
|
||||||
alias Mobilizon.Service.Workers
|
alias Mobilizon.Service.Workers
|
||||||
alias Mobilizon.Storage.Page
|
alias Mobilizon.Storage.Page
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule Mobilizon.UsersTest do
|
defmodule Mobilizon.UsersTest do
|
||||||
use Mobilizon.DataCase
|
use Mobilizon.DataCase
|
||||||
|
|
||||||
|
alias Mobilizon.Storage.Page
|
||||||
alias Mobilizon.Users
|
alias Mobilizon.Users
|
||||||
alias Mobilizon.Users.{Setting, User}
|
alias Mobilizon.Users.{Setting, User}
|
||||||
import Mobilizon.Factory
|
import Mobilizon.Factory
|
||||||
|
@ -13,7 +14,7 @@ defmodule Mobilizon.UsersTest do
|
||||||
|
|
||||||
test "list_users/0 returns all users" do
|
test "list_users/0 returns all users" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
users = Users.list_users(nil, nil, :id, :desc)
|
%Page{elements: users, total: 1} = Users.list_users("", nil, nil, :id, :desc)
|
||||||
assert [user.id] == users |> Enum.map(& &1.id)
|
assert [user.id] == users |> Enum.map(& &1.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -52,9 +53,15 @@ defmodule Mobilizon.UsersTest do
|
||||||
assert user = Users.get_user!(user.id)
|
assert user = Users.get_user!(user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "delete_user/1 deletes the user" do
|
test "delete_user/1 empties the user" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
assert {:ok, %User{}} = Users.delete_user(user)
|
assert {:ok, %User{}} = Users.delete_user(user)
|
||||||
|
assert Users.get_user(user.id).disabled == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "delete_user/1 deletes the user" do
|
||||||
|
user = insert(:user)
|
||||||
|
assert {:ok, %User{}} = Users.delete_user(user, reserve_email: false)
|
||||||
assert_raise Ecto.NoResultsError, fn -> Users.get_user!(user.id) end
|
assert_raise Ecto.NoResultsError, fn -> Users.get_user!(user.id) end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,12 @@ defmodule Mix.Tasks.Mobilizon.UsersTest do
|
||||||
test "delete existing user" do
|
test "delete existing user" do
|
||||||
insert(:user, email: @email)
|
insert(:user, email: @email)
|
||||||
Delete.run([@email, "-y"])
|
Delete.run([@email, "-y"])
|
||||||
|
assert {:ok, %User{disabled: true}} = Users.get_user_by_email(@email)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "full delete existing user" do
|
||||||
|
insert(:user, email: @email)
|
||||||
|
Delete.run([@email, "-y", "-f"])
|
||||||
assert {:error, :user_not_found} == Users.get_user_by_email(@email)
|
assert {:error, :user_not_found} == Users.get_user_by_email(@email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue