From 9589907c89d29a6c0acd52c8cb789af9f93ce9af Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 19 Jan 2023 09:29:47 +0100 Subject: Implement signup approval in client --- .../admin-registration.service.ts | 63 +++++++++++ .../+admin/moderation/registration-list/index.ts | 4 + .../process-registration-modal.component.html | 67 +++++++++++ .../process-registration-modal.component.scss | 3 + .../process-registration-modal.component.ts | 107 ++++++++++++++++++ .../process-registration-validators.ts | 11 ++ .../registration-list.component.html | 120 ++++++++++++++++++++ .../registration-list.component.scss | 7 ++ .../registration-list.component.ts | 125 +++++++++++++++++++++ 9 files changed, 507 insertions(+) create mode 100644 client/src/app/+admin/moderation/registration-list/admin-registration.service.ts create mode 100644 client/src/app/+admin/moderation/registration-list/index.ts create mode 100644 client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html create mode 100644 client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss create mode 100644 client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts create mode 100644 client/src/app/+admin/moderation/registration-list/process-registration-validators.ts create mode 100644 client/src/app/+admin/moderation/registration-list/registration-list.component.html create mode 100644 client/src/app/+admin/moderation/registration-list/registration-list.component.scss create mode 100644 client/src/app/+admin/moderation/registration-list/registration-list.component.ts (limited to 'client/src/app/+admin/moderation/registration-list') diff --git a/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts new file mode 100644 index 000000000..012f942b3 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts @@ -0,0 +1,63 @@ +import { SortMeta } from 'primeng/api' +import { catchError } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService } from '@app/core' +import { ResultList, UserRegistration } from '@shared/models' +import { environment } from '../../../../environments/environment' + +@Injectable() +export class AdminRegistrationService { + private static BASE_REGISTRATION_URL = environment.apiUrl + '/api/v1/users/registrations' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService + ) { } + + listRegistrations (options: { + pagination: RestPagination + sort: SortMeta + search?: string + }) { + const { pagination, sort, search } = options + + const url = AdminRegistrationService.BASE_REGISTRATION_URL + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (search) { + params = params.append('search', search) + } + + return this.authHttp.get>(url, { params }) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } + + acceptRegistration (registration: UserRegistration, moderationResponse: string) { + const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept' + const body = { moderationResponse } + + return this.authHttp.post(url, body) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + rejectRegistration (registration: UserRegistration, moderationResponse: string) { + const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject' + const body = { moderationResponse } + + return this.authHttp.post(url, body) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + removeRegistration (registration: UserRegistration) { + const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + + return this.authHttp.delete(url) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } +} diff --git a/client/src/app/+admin/moderation/registration-list/index.ts b/client/src/app/+admin/moderation/registration-list/index.ts new file mode 100644 index 000000000..060b676a4 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/index.ts @@ -0,0 +1,4 @@ +export * from './admin-registration.service' +export * from './process-registration-modal.component' +export * from './process-registration-validators' +export * from './registration-list.component' diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html new file mode 100644 index 000000000..7a33bb94b --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html @@ -0,0 +1,67 @@ + + + +
+ + + +
+
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss new file mode 100644 index 000000000..3e03bed89 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss @@ -0,0 +1,3 @@ +@use '_variables' as *; +@use '_mixins' as *; + diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts new file mode 100644 index 000000000..fbe8deb41 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts @@ -0,0 +1,107 @@ +import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' +import { Notifier, ServerService } from '@app/core' +import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' +import { UserRegistration } from '@shared/models' +import { AdminRegistrationService } from './admin-registration.service' +import { REGISTRATION_MODERATION_RESPONSE_VALIDATOR } from './process-registration-validators' + +@Component({ + selector: 'my-process-registration-modal', + templateUrl: './process-registration-modal.component.html', + styleUrls: [ './process-registration-modal.component.scss' ] +}) +export class ProcessRegistrationModalComponent extends FormReactive implements OnInit { + @ViewChild('modal', { static: true }) modal: NgbModal + + @Output() registrationProcessed = new EventEmitter() + + registration: UserRegistration + + private openedModal: NgbModalRef + private processMode: 'accept' | 'reject' + + constructor ( + protected formReactiveService: FormReactiveService, + private server: ServerService, + private modalService: NgbModal, + private notifier: Notifier, + private registrationService: AdminRegistrationService + ) { + super() + } + + ngOnInit () { + this.buildForm({ + moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR + }) + } + + isAccept () { + return this.processMode === 'accept' + } + + isReject () { + return this.processMode === 'reject' + } + + openModal (registration: UserRegistration, mode: 'accept' | 'reject') { + this.processMode = mode + this.registration = registration + + this.openedModal = this.modalService.open(this.modal, { centered: true }) + } + + hide () { + this.form.reset() + + this.openedModal.close() + } + + getSubmitValue () { + if (this.isAccept()) { + return $localize`Accept registration` + } + + return $localize`Reject registration` + } + + processRegistration () { + if (this.isAccept()) return this.acceptRegistration() + + return this.rejectRegistration() + } + + isEmailEnabled () { + return this.server.getHTMLConfig().email.enabled + } + + private acceptRegistration () { + this.registrationService.acceptRegistration(this.registration, this.form.value.moderationResponse) + .subscribe({ + next: () => { + this.notifier.success($localize`${this.registration.username} account created`) + + this.registrationProcessed.emit() + this.hide() + }, + + error: err => this.notifier.error(err.message) + }) + } + + private rejectRegistration () { + this.registrationService.rejectRegistration(this.registration, this.form.value.moderationResponse) + .subscribe({ + next: () => { + this.notifier.success($localize`${this.registration.username} registration rejected`) + + this.registrationProcessed.emit() + this.hide() + }, + + error: err => this.notifier.error(err.message) + }) + } +} diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts new file mode 100644 index 000000000..e01a07d9d --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts @@ -0,0 +1,11 @@ +import { Validators } from '@angular/forms' +import { BuildFormValidator } from '@app/shared/form-validators' + +export const REGISTRATION_MODERATION_RESPONSE_VALIDATOR: BuildFormValidator = { + VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], + MESSAGES: { + required: $localize`Moderation response is required.`, + minlength: $localize`Moderation response must be at least 2 characters long.`, + maxlength: $localize`Moderation response cannot be more than 3000 characters long.` + } +} diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.html b/client/src/app/+admin/moderation/registration-list/registration-list.component.html new file mode 100644 index 000000000..4f9d06acc --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.html @@ -0,0 +1,120 @@ +

+ + Registration requests +

+ + + +
+
+ +
+
+
+ + + + + + Account + Email + Channel + Registration reason + State + Moderation response + Requested on + + + + + + + + + + + + + + +
+
+ {{ registration.username }} + {{ registration.accountDisplayName }} +
+
+ + + + + + + +
+
+ {{ registration.channelHandle }} + {{ registration.channelDisplayName }} +
+
+ + + + {{ registration.registrationReason }} + + + + + + + + + {{ registration.moderationResponse }} + + + {{ registration.createdAt | date: 'short' }} + +
+ + + + +
+
+
+ Registration reason: + +
+ +
+ Moderation response: + +
+
+
+ + +
+ + + + +
+ No registrations found matching current filters. + No registrations found. +
+ + +
+
+ + diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.scss b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss new file mode 100644 index 000000000..9cae08e85 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss @@ -0,0 +1,7 @@ +@use '_mixins' as *; +@use '_variables' as *; + +my-global-icon { + width: 24px; + height: 24px; +} diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts new file mode 100644 index 000000000..37514edf5 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts @@ -0,0 +1,125 @@ +import { SortMeta } from 'primeng/api' +import { Component, OnInit, ViewChild } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' +import { AdvancedInputFilter } from '@app/shared/shared-forms' +import { DropdownAction } from '@app/shared/shared-main' +import { UserRegistration, UserRegistrationState } from '@shared/models' +import { AdminRegistrationService } from './admin-registration.service' +import { ProcessRegistrationModalComponent } from './process-registration-modal.component' + +@Component({ + selector: 'my-registration-list', + templateUrl: './registration-list.component.html', + styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './registration-list.component.scss' ] +}) +export class RegistrationListComponent extends RestTable implements OnInit { + @ViewChild('processRegistrationModal', { static: true }) processRegistrationModal: ProcessRegistrationModalComponent + + registrations: (UserRegistration & { registrationReasonHTML?: string, moderationResponseHTML?: string })[] = [] + totalRecords = 0 + sort: SortMeta = { field: 'createdAt', order: -1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + registrationActions: DropdownAction[][] = [] + + inputFilters: AdvancedInputFilter[] = [] + + requiresEmailVerification: boolean + + constructor ( + protected route: ActivatedRoute, + protected router: Router, + private server: ServerService, + private notifier: Notifier, + private markdownRenderer: MarkdownService, + private adminRegistrationService: AdminRegistrationService + ) { + super() + + this.registrationActions = [ + [ + { + label: $localize`Accept this registration`, + handler: registration => this.openRegistrationRequestProcessModal(registration, 'accept'), + isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING + }, + { + label: $localize`Reject this registration`, + handler: registration => this.openRegistrationRequestProcessModal(registration, 'reject'), + isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING + }, + { + label: $localize`Remove this registration request`, + handler: registration => this.removeRegistration(registration), + isDisplayed: registration => registration.state.id !== UserRegistrationState.PENDING + } + ] + ] + } + + ngOnInit () { + this.initialize() + + this.server.getConfig() + .subscribe(config => { + this.requiresEmailVerification = config.signup.requiresEmailVerification + }) + } + + getIdentifier () { + return 'RegistrationListComponent' + } + + isRegistrationAccepted (registration: UserRegistration) { + return registration.state.id === UserRegistrationState.ACCEPTED + } + + isRegistrationRejected (registration: UserRegistration) { + return registration.state.id === UserRegistrationState.REJECTED + } + + onRegistrationProcessed () { + this.reloadData() + } + + protected reloadData () { + this.adminRegistrationService.listRegistrations({ + pagination: this.pagination, + sort: this.sort, + search: this.search + }).subscribe({ + next: async resultList => { + this.totalRecords = resultList.total + this.registrations = resultList.data + + for (const registration of this.registrations) { + registration.registrationReasonHTML = await this.toHtml(registration.registrationReason) + registration.moderationResponseHTML = await this.toHtml(registration.moderationResponse) + } + }, + + error: err => this.notifier.error(err.message) + }) + } + + private openRegistrationRequestProcessModal (registration: UserRegistration, mode: 'accept' | 'reject') { + this.processRegistrationModal.openModal(registration, mode) + } + + private removeRegistration (registration: UserRegistration) { + this.adminRegistrationService.removeRegistration(registration) + .subscribe({ + next: () => { + this.notifier.success($localize`Registration request deleted.`) + this.reloadData() + }, + + error: err => this.notifier.error(err.message) + }) + } + + private toHtml (text: string) { + return this.markdownRenderer.textMarkdownToHTML({ markdown: text }) + } +} -- cgit v1.2.3