From beb35a09c652f65ba711e5fef05d5a4296e86b39 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 11 Jun 2020 19:13:21 +0200 Subject: [PATCH] Introduce basic user and profile management Signed-off-by: Thomas Citharel --- js/src/graphql/actor.ts | 85 ++++- js/src/graphql/event.ts | 30 +- js/src/graphql/report.ts | 6 + js/src/graphql/user.ts | 59 +++- js/src/i18n/en_US.json | 22 +- js/src/i18n/fr_FR.json | 23 +- js/src/router/settings.ts | 36 ++ js/src/types/actor/actor.model.ts | 7 + js/src/types/actor/person.model.ts | 3 + js/src/types/current-user.model.ts | 8 + js/src/types/report.model.ts | 4 +- js/src/views/Admin/AdminProfile.vue | 318 ++++++++++++++++++ js/src/views/Admin/AdminUserProfile.vue | 165 +++++++++ js/src/views/Admin/Dashboard.vue | 20 +- js/src/views/Admin/Profiles.vue | 138 ++++++++ js/src/views/Admin/Users.vue | 132 ++++++++ js/src/views/Moderation/Logs.vue | 51 ++- js/src/views/Moderation/Report.vue | 16 +- js/src/views/Settings.vue | 8 + lib/federation/activity_pub/activity_pub.ex | 3 +- lib/federation/activity_pub/transmogrifier.ex | 4 +- .../activity_stream/converter/comment.ex | 5 +- .../activity_stream/converter/event.ex | 5 +- lib/graphql/resolvers/admin.ex | 15 + lib/graphql/resolvers/person.ex | 152 ++++++++- lib/graphql/resolvers/user.ex | 76 +++-- lib/graphql/schema/actors/person.ex | 42 ++- lib/graphql/schema/admin.ex | 6 + lib/graphql/schema/user.ex | 10 +- lib/mix/tasks/mobilizon/actors/refresh.ex | 2 +- lib/mix/tasks/mobilizon/users/delete.ex | 9 +- lib/mobilizon/actors/actors.ex | 118 ++++++- lib/mobilizon/admin/admin.ex | 4 +- lib/mobilizon/events/events.ex | 17 +- lib/mobilizon/media/file.ex | 1 + lib/mobilizon/users/user.ex | 12 +- lib/mobilizon/users/users.ex | 49 ++- lib/service/export/feed.ex | 7 +- lib/service/export/icalendar.ex | 3 +- lib/service/workers/background.ex | 8 +- ...155254_add_disabled_parameter_to_users.exs | 9 + .../activity_pub/transmogrifier_test.exs | 11 +- test/graphql/resolvers/event_test.exs | 58 ++-- test/graphql/resolvers/participant_test.exs | 59 ++-- test/graphql/resolvers/person_test.exs | 208 +++++++++--- test/graphql/resolvers/user_test.exs | 13 +- test/mobilizon/actors/actors_test.exs | 5 +- test/mobilizon/events/events_test.exs | 3 +- test/mobilizon/users/users_test.exs | 11 +- ...abinthe_helpers.ex => absinthe_helpers.ex} | 0 test/tasks/users_test.exs | 6 + 51 files changed, 1808 insertions(+), 254 deletions(-) create mode 100644 js/src/views/Admin/AdminProfile.vue create mode 100644 js/src/views/Admin/AdminUserProfile.vue create mode 100644 js/src/views/Admin/Profiles.vue create mode 100644 js/src/views/Admin/Users.vue create mode 100644 priv/repo/migrations/20200612155254_add_disabled_parameter_to_users.exs rename test/support/{abinthe_helpers.ex => absinthe_helpers.ex} (100%) diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index a67728b83..d9cc41a1d 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -32,7 +32,13 @@ export const FETCH_PERSON = gql` `; export const GET_PERSON = gql` - query($actorId: ID!) { + query( + $actorId: ID! + $organizedEventsPage: Int + $organizedEventsLimit: Int + $participationPage: Int + $participationLimit: Int + ) { person(id: $actorId) { id url @@ -51,10 +57,63 @@ export const GET_PERSON = gql` feedTokens { token } - organizedEvents { - uuid - title - beginsOn + organizedEvents(page: $organizedEventsPage, limit: $organizedEventsLimit) { + total + elements { + id + uuid + title + 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 + } + } +`; diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index dd26d4689..7d619c956 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -485,13 +485,16 @@ export const EVENT_PERSON_PARTICIPATION = gql` person(id: $actorId) { id participations(eventId: $eventId) { - id - role - actor { - id - } - event { + total + elements { id + role + actor { + id + } + event { + id + } } } } @@ -503,13 +506,16 @@ export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql` eventPersonParticipationChanged(personId: $actorId) { id participations(eventId: $eventId) { - id - role - actor { - id - } - event { + total + elements { id + role + actor { + id + } + event { + id + } } } } diff --git a/js/src/graphql/report.ts b/js/src/graphql/report.ts index 807b17c2a..8a9169051 100644 --- a/js/src/graphql/report.ts +++ b/js/src/graphql/report.ts @@ -178,6 +178,12 @@ export const LOGS = gql` id title } + ... on Person { + id + preferredUsername + domain + name + } } insertedAt } diff --git a/js/src/graphql/user.ts b/js/src/graphql/user.ts index af86686da..190ecb350 100644 --- a/js/src/graphql/user.ts +++ b/js/src/graphql/user.ts @@ -64,8 +64,8 @@ export const VALIDATE_EMAIL = gql` `; export const DELETE_ACCOUNT = gql` - mutation DeleteAccount($password: String!) { - deleteAccount(password: $password) { + mutation DeleteAccount($password: String, $userId: ID!) { + deleteAccount(password: $password, userId: $userId) { id } } @@ -134,3 +134,58 @@ export const SET_USER_SETTINGS = gql` } ${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 + } + } +`; diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 4d8be4669..b9ce8d3c4 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -607,5 +607,25 @@ "Your timezone was detected as {timezone}.": "Your timezone was detected as {timezone}.", "Manage my settings": "Manage my 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}" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index ad71613a7..db852021e 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -626,5 +626,26 @@ "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", "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}" } diff --git a/js/src/router/settings.ts b/js/src/router/settings.ts index 9cd5fd4f4..2885bfffc 100644 --- a/js/src/router/settings.ts +++ b/js/src/router/settings.ts @@ -12,6 +12,10 @@ import ReportList from "@/views/Moderation/ReportList.vue"; import Report from "@/views/Moderation/Report.vue"; import Logs from "@/views/Moderation/Logs.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 { SETTINGS = "SETTINGS", @@ -25,6 +29,10 @@ export enum SettingsRouteName { RELAYS = "Relays", RELAY_FOLLOWINGS = "Followings", RELAY_FOLLOWERS = "Followers", + USERS = "USERS", + PROFILES = "PROFILES", + ADMIN_PROFILE = "ADMIN_PROFILE", + ADMIN_USER_PROFILE = "ADMIN_USER_PROFILE", MODERATION = "MODERATION", REPORTS = "Reports", REPORT = "Report", @@ -87,6 +95,34 @@ export const settingsRoutes: RouteConfig[] = [ props: 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", name: SettingsRouteName.RELAYS, diff --git a/js/src/types/actor/actor.model.ts b/js/src/types/actor/actor.model.ts index c450ec47a..e1ceeb183 100644 --- a/js/src/types/actor/actor.model.ts +++ b/js/src/types/actor/actor.model.ts @@ -66,3 +66,10 @@ export function usernameWithDomain(actor: IActor): string { } return actor.preferredUsername; } + +export function displayNameAndUsername(actor: IActor): string { + if (actor.name) { + return `${actor.name} (@${usernameWithDomain(actor)})`; + } + return usernameWithDomain(actor); +} diff --git a/js/src/types/actor/person.model.ts b/js/src/types/actor/person.model.ts index a3e9094c5..b34b36196 100644 --- a/js/src/types/actor/person.model.ts +++ b/js/src/types/actor/person.model.ts @@ -15,6 +15,7 @@ export interface IPerson extends IActor { goingToEvents: IEvent[]; participations: IParticipant[]; memberships: Paginate; + user?: ICurrentUser; } export class Person extends Actor implements IPerson { @@ -26,6 +27,8 @@ export class Person extends Actor implements IPerson { memberships!: Paginate; + user!: ICurrentUser; + constructor(hash: IPerson | {} = {}) { super(hash); diff --git a/js/src/types/current-user.model.ts b/js/src/types/current-user.model.ts index ba4d03361..327421746 100644 --- a/js/src/types/current-user.model.ts +++ b/js/src/types/current-user.model.ts @@ -19,6 +19,14 @@ export interface ICurrentUser { settings: IUserSettings; } +export interface IUser extends ICurrentUser { + confirmedAt: Date; + confirmationSendAt: Date; + locale: String; + actors: IPerson[]; + disabled: boolean; +} + export enum INotificationPendingParticipationEnum { NONE = "NONE", DIRECT = "DIRECT", diff --git a/js/src/types/report.model.ts b/js/src/types/report.model.ts index ef80d4ead..bf515819f 100644 --- a/js/src/types/report.model.ts +++ b/js/src/types/report.model.ts @@ -39,11 +39,13 @@ export enum ActionLogAction { REPORT_UPDATE_RESOLVED = "REPORT_UPDATE_RESOLVED", EVENT_DELETION = "EVENT_DELETION", COMMENT_DELETION = "COMMENT_DELETION", + ACTOR_SUSPENSION = "ACTOR_SUSPENSION", + ACTOR_UNSUSPENSION = "ACTOR_UNSUSPENSION", } export interface IActionLog { id: string; - object: IReport | IReportNote | IEvent; + object: IReport | IReportNote | IEvent | IComment | IActor; actor: IActor; action: ActionLogAction; insertedAt: Date; diff --git a/js/src/views/Admin/AdminProfile.vue b/js/src/views/Admin/AdminProfile.vue new file mode 100644 index 000000000..97ff1b3fd --- /dev/null +++ b/js/src/views/Admin/AdminProfile.vue @@ -0,0 +1,318 @@ + + + + diff --git a/js/src/views/Admin/AdminUserProfile.vue b/js/src/views/Admin/AdminUserProfile.vue new file mode 100644 index 000000000..4afe2eac5 --- /dev/null +++ b/js/src/views/Admin/AdminUserProfile.vue @@ -0,0 +1,165 @@ + + + + diff --git a/js/src/views/Admin/Dashboard.vue b/js/src/views/Admin/Dashboard.vue index 3f93b95cd..34b82b83e 100644 --- a/js/src/views/Admin/Dashboard.vue +++ b/js/src/views/Admin/Dashboard.vue @@ -16,15 +16,17 @@
-

{{ dashboard.numberOfUsers }}

-

{{ $t("Users") }}

+ +

{{ dashboard.numberOfUsers }}

+

{{ $t("Users") }}

+
- -
+
+

{{ dashboard.numberOfReports }}

{{ $t("Opened reports") }}

-
- + +
@@ -80,4 +82,10 @@ export default class Dashboard extends Vue { font-weight: 700; line-height: 1.125; } + +article.tile { + a { + color: #4a4a4a; + } +} diff --git a/js/src/views/Admin/Profiles.vue b/js/src/views/Admin/Profiles.vue new file mode 100644 index 000000000..e4a2ee53c --- /dev/null +++ b/js/src/views/Admin/Profiles.vue @@ -0,0 +1,138 @@ + + diff --git a/js/src/views/Admin/Users.vue b/js/src/views/Admin/Users.vue new file mode 100644 index 000000000..f34e3a681 --- /dev/null +++ b/js/src/views/Admin/Users.vue @@ -0,0 +1,132 @@ + + + + diff --git a/js/src/views/Moderation/Logs.vue b/js/src/views/Moderation/Logs.vue index 77123b298..7ba5199b9 100644 --- a/js/src/views/Moderation/Logs.vue +++ b/js/src/views/Moderation/Logs.vue @@ -9,7 +9,11 @@ tag="span" path="{actor} closed {report}" > - @{{ log.actor.preferredUsername }} + @{{ log.actor.preferredUsername }} - @{{ log.actor.preferredUsername }} + @{{ log.actor.preferredUsername }} - @{{ log.actor.preferredUsername }} + @{{ log.actor.preferredUsername }} - @{{ log.actor.preferredUsername }} + @{{ log.actor.preferredUsername }} - @{{ log.actor.preferredUsername }} - {{ log.object.title }} + @{{ log.actor.preferredUsername }} + {{ log.object.title }} + + + @{{ log.actor.preferredUsername }} + {{ displayNameAndUsername(log.object) }} +
{{ log.insertedAt | formatDateTimeString }} @@ -78,6 +114,7 @@ import { IActionLog, ActionLogAction } from "@/types/report.model"; import { LOGS } from "@/graphql/report"; import ReportCard from "@/components/Report/ReportCard.vue"; import RouteName from "../../router/name"; +import { displayNameAndUsername } from "../../types/actor"; @Component({ components: { @@ -95,6 +132,8 @@ export default class ReportList extends Vue { ActionLogAction = ActionLogAction; RouteName = RouteName; + + displayNameAndUsername = displayNameAndUsername; }