aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-01-19 09:29:47 +0100
committerChocobozzz <chocobozzz@cpy.re>2023-01-19 13:53:40 +0100
commit9589907c89d29a6c0acd52c8cb789af9f93ce9af (patch)
treef1d238e6144231bbfbed5614e05a21eca8aa6fc2 /client/src/app/+admin
parentb379759f55a35837b803a3b988674972db2903d1 (diff)
downloadPeerTube-9589907c89d29a6c0acd52c8cb789af9f93ce9af.tar.gz
PeerTube-9589907c89d29a6c0acd52c8cb789af9f93ce9af.tar.zst
PeerTube-9589907c89d29a6c0acd52c8cb789af9f93ce9af.zip
Implement signup approval in client
Diffstat (limited to 'client/src/app/+admin')
-rw-r--r--client/src/app/+admin/admin.component.ts12
-rw-r--r--client/src/app/+admin/admin.module.ts16
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html19
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts1
-rw-r--r--client/src/app/+admin/moderation/index.ts1
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts15
-rw-r--r--client/src/app/+admin/moderation/registration-list/admin-registration.service.ts63
-rw-r--r--client/src/app/+admin/moderation/registration-list/index.ts4
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html67
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss3
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts107
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-validators.ts11
-rw-r--r--client/src/app/+admin/moderation/registration-list/registration-list.component.html120
-rw-r--r--client/src/app/+admin/moderation/registration-list/registration-list.component.scss7
-rw-r--r--client/src/app/+admin/moderation/registration-list/registration-list.component.ts125
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.html19
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.scss10
-rw-r--r--client/src/app/+admin/shared/shared-admin.module.ts7
-rw-r--r--client/src/app/+admin/shared/user-email-info.component.html13
-rw-r--r--client/src/app/+admin/shared/user-email-info.component.scss10
-rw-r--r--client/src/app/+admin/shared/user-email-info.component.ts20
-rw-r--r--client/src/app/+admin/system/jobs/job.service.ts2
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.ts2
23 files changed, 615 insertions, 39 deletions
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index 746549555..630bfe253 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -96,6 +96,14 @@ export class AdminComponent implements OnInit {
96 children: [] 96 children: []
97 } 97 }
98 98
99 if (this.hasRegistrationsRight()) {
100 moderationItems.children.push({
101 label: $localize`Registrations`,
102 routerLink: '/admin/moderation/registrations/list',
103 iconName: 'user'
104 })
105 }
106
99 if (this.hasAbusesRight()) { 107 if (this.hasAbusesRight()) {
100 moderationItems.children.push({ 108 moderationItems.children.push({
101 label: $localize`Reports`, 109 label: $localize`Reports`,
@@ -229,4 +237,8 @@ export class AdminComponent implements OnInit {
229 private hasVideosRight () { 237 private hasVideosRight () {
230 return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS) 238 return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
231 } 239 }
240
241 private hasRegistrationsRight () {
242 return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS)
243 }
232} 244}
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index f01967ea6..891ff4ed1 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -30,7 +30,13 @@ import { FollowersListComponent, FollowModalComponent, VideoRedundanciesListComp
30import { FollowingListComponent } from './follows/following-list/following-list.component' 30import { FollowingListComponent } from './follows/following-list/following-list.component'
31import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' 31import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
32import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' 32import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
33import { AbuseListComponent, VideoBlockListComponent } from './moderation' 33import {
34 AbuseListComponent,
35 AdminRegistrationService,
36 ProcessRegistrationModalComponent,
37 RegistrationListComponent,
38 VideoBlockListComponent
39} from './moderation'
34import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' 40import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
35import { 41import {
36 UserCreateComponent, 42 UserCreateComponent,
@@ -116,7 +122,10 @@ import { JobsComponent } from './system/jobs/jobs.component'
116 EditLiveConfigurationComponent, 122 EditLiveConfigurationComponent,
117 EditAdvancedConfigurationComponent, 123 EditAdvancedConfigurationComponent,
118 EditInstanceInformationComponent, 124 EditInstanceInformationComponent,
119 EditHomepageComponent 125 EditHomepageComponent,
126
127 RegistrationListComponent,
128 ProcessRegistrationModalComponent
120 ], 129 ],
121 130
122 exports: [ 131 exports: [
@@ -130,7 +139,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
130 ConfigService, 139 ConfigService,
131 PluginApiService, 140 PluginApiService,
132 EditConfigurationService, 141 EditConfigurationService,
133 VideoAdminService 142 VideoAdminService,
143 AdminRegistrationService
134 ] 144 ]
135}) 145})
136export class AdminModule { } 146export class AdminModule { }
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
index 174f5d29c..0f3803f97 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -171,12 +171,21 @@
171 </ng-container> 171 </ng-container>
172 172
173 <ng-container ngProjectAs="extra"> 173 <ng-container ngProjectAs="extra">
174 <my-peertube-checkbox [ngClass]="getDisabledSignupClass()" 174 <div class="form-group">
175 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" 175 <my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
176 i18n-labelText labelText="Signup requires email verification" 176 inputName="signupRequiresApproval" formControlName="requiresApproval"
177 ></my-peertube-checkbox> 177 i18n-labelText labelText="Signup requires approval by moderators"
178 ></my-peertube-checkbox>
179 </div>
178 180
179 <div [ngClass]="getDisabledSignupClass()" class="mt-3"> 181 <div class="form-group">
182 <my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
183 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
184 i18n-labelText labelText="Signup requires email verification"
185 ></my-peertube-checkbox>
186 </div>
187
188 <div [ngClass]="getDisabledSignupClass()">
180 <label i18n for="signupLimit">Signup limit</label> 189 <label i18n for="signupLimit">Signup limit</label>
181 190
182 <div class="number-with-unit"> 191 <div class="number-with-unit">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 168f4702c..2afe80a03 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -132,6 +132,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
132 signup: { 132 signup: {
133 enabled: null, 133 enabled: null,
134 limit: SIGNUP_LIMIT_VALIDATOR, 134 limit: SIGNUP_LIMIT_VALIDATOR,
135 requiresApproval: null,
135 requiresEmailVerification: null, 136 requiresEmailVerification: null,
136 minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR 137 minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
137 }, 138 },
diff --git a/client/src/app/+admin/moderation/index.ts b/client/src/app/+admin/moderation/index.ts
index 9dab270cc..135b4b408 100644
--- a/client/src/app/+admin/moderation/index.ts
+++ b/client/src/app/+admin/moderation/index.ts
@@ -1,4 +1,5 @@
1export * from './abuse-list' 1export * from './abuse-list'
2export * from './instance-blocklist' 2export * from './instance-blocklist'
3export * from './video-block-list' 3export * from './video-block-list'
4export * from './registration-list'
4export * from './moderation.routes' 5export * from './moderation.routes'
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index 1ad301039..378d2bed7 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -4,6 +4,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
4import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' 4import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
5import { UserRightGuard } from '@app/core' 5import { UserRightGuard } from '@app/core'
6import { UserRight } from '@shared/models' 6import { UserRight } from '@shared/models'
7import { RegistrationListComponent } from './registration-list'
7 8
8export const ModerationRoutes: Routes = [ 9export const ModerationRoutes: Routes = [
9 { 10 {
@@ -68,7 +69,19 @@ export const ModerationRoutes: Routes = [
68 } 69 }
69 }, 70 },
70 71
71 // We move this component in admin overview pages 72 {
73 path: 'registrations/list',
74 component: RegistrationListComponent,
75 canActivate: [ UserRightGuard ],
76 data: {
77 userRight: UserRight.MANAGE_REGISTRATIONS,
78 meta: {
79 title: $localize`User registrations`
80 }
81 }
82 },
83
84 // We moved this component in admin overview pages
72 { 85 {
73 path: 'video-comments', 86 path: 'video-comments',
74 redirectTo: 'video-comments/list', 87 redirectTo: 'video-comments/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 @@
1import { SortMeta } from 'primeng/api'
2import { catchError } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core'
5import { RestExtractor, RestPagination, RestService } from '@app/core'
6import { ResultList, UserRegistration } from '@shared/models'
7import { environment } from '../../../../environments/environment'
8
9@Injectable()
10export class AdminRegistrationService {
11 private static BASE_REGISTRATION_URL = environment.apiUrl + '/api/v1/users/registrations'
12
13 constructor (
14 private authHttp: HttpClient,
15 private restExtractor: RestExtractor,
16 private restService: RestService
17 ) { }
18
19 listRegistrations (options: {
20 pagination: RestPagination
21 sort: SortMeta
22 search?: string
23 }) {
24 const { pagination, sort, search } = options
25
26 const url = AdminRegistrationService.BASE_REGISTRATION_URL
27
28 let params = new HttpParams()
29 params = this.restService.addRestGetParams(params, pagination, sort)
30
31 if (search) {
32 params = params.append('search', search)
33 }
34
35 return this.authHttp.get<ResultList<UserRegistration>>(url, { params })
36 .pipe(
37 catchError(res => this.restExtractor.handleError(res))
38 )
39 }
40
41 acceptRegistration (registration: UserRegistration, moderationResponse: string) {
42 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept'
43 const body = { moderationResponse }
44
45 return this.authHttp.post(url, body)
46 .pipe(catchError(res => this.restExtractor.handleError(res)))
47 }
48
49 rejectRegistration (registration: UserRegistration, moderationResponse: string) {
50 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject'
51 const body = { moderationResponse }
52
53 return this.authHttp.post(url, body)
54 .pipe(catchError(res => this.restExtractor.handleError(res)))
55 }
56
57 removeRegistration (registration: UserRegistration) {
58 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id
59
60 return this.authHttp.delete(url)
61 .pipe(catchError(res => this.restExtractor.handleError(res)))
62 }
63}
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 @@
1export * from './admin-registration.service'
2export * from './process-registration-modal.component'
3export * from './process-registration-validators'
4export * 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 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">
4 <ng-container *ngIf="isAccept()">Accept {{ registration.username }} registration</ng-container>
5 <ng-container *ngIf="isReject()">Reject {{ registration.username }} registration</ng-container>
6 </h4>
7
8 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
9 </div>
10
11 <form novalidate [formGroup]="form" (ngSubmit)="processRegistration()">
12 <div class="modal-body mb-3">
13
14 <div i18n *ngIf="!registration.emailVerified" class="alert alert-warning">
15 Registration email has not been verified.
16 </div>
17
18 <div class="description">
19 <ng-container *ngIf="isAccept()">
20 <p i18n>
21 <strong>Accepting</strong>&nbsp;<em>{{ registration.username }}</em> registration will create the account and channel.
22 </p>
23
24 <p *ngIf="isEmailEnabled()" i18n>
25 An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below.
26 </p>
27
28 <div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n>
29 Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its account has been created.
30 </div>
31 </ng-container>
32
33 <ng-container *ngIf="isReject()">
34 <p i18n>
35 An email will be sent to <em>{{ registration.email }}</em> explaining its registration request has been <strong>rejected</strong> with the moderation response you'll write below.
36 </p>
37
38 <div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n>
39 Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its registration request has been rejected.
40 </div>
41 </ng-container>
42 </div>
43
44 <div class="form-group">
45 <label for="moderationResponse" i18n>Send a message to the user</label>
46
47 <textarea
48 formControlName="moderationResponse" ngbAutofocus name="moderationResponse" id="moderationResponse"
49 [ngClass]="{ 'input-error': formErrors['moderationResponse'] }" class="form-control"
50 ></textarea>
51
52 <div *ngIf="formErrors.moderationResponse" class="form-error">
53 {{ formErrors.moderationResponse }}
54 </div>
55 </div>
56 </div>
57
58 <div class="modal-footer inputs">
59 <input
60 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
61 (click)="hide()" (key.enter)="hide()"
62 >
63
64 <input type="submit" [value]="getSubmitValue()" class="peertube-button orange-button" [disabled]="!form.valid">
65 </div>
66 </form>
67</ng-template>
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 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
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 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core'
3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { UserRegistration } from '@shared/models'
7import { AdminRegistrationService } from './admin-registration.service'
8import { REGISTRATION_MODERATION_RESPONSE_VALIDATOR } from './process-registration-validators'
9
10@Component({
11 selector: 'my-process-registration-modal',
12 templateUrl: './process-registration-modal.component.html',
13 styleUrls: [ './process-registration-modal.component.scss' ]
14})
15export class ProcessRegistrationModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal
17
18 @Output() registrationProcessed = new EventEmitter()
19
20 registration: UserRegistration
21
22 private openedModal: NgbModalRef
23 private processMode: 'accept' | 'reject'
24
25 constructor (
26 protected formReactiveService: FormReactiveService,
27 private server: ServerService,
28 private modalService: NgbModal,
29 private notifier: Notifier,
30 private registrationService: AdminRegistrationService
31 ) {
32 super()
33 }
34
35 ngOnInit () {
36 this.buildForm({
37 moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR
38 })
39 }
40
41 isAccept () {
42 return this.processMode === 'accept'
43 }
44
45 isReject () {
46 return this.processMode === 'reject'
47 }
48
49 openModal (registration: UserRegistration, mode: 'accept' | 'reject') {
50 this.processMode = mode
51 this.registration = registration
52
53 this.openedModal = this.modalService.open(this.modal, { centered: true })
54 }
55
56 hide () {
57 this.form.reset()
58
59 this.openedModal.close()
60 }
61
62 getSubmitValue () {
63 if (this.isAccept()) {
64 return $localize`Accept registration`
65 }
66
67 return $localize`Reject registration`
68 }
69
70 processRegistration () {
71 if (this.isAccept()) return this.acceptRegistration()
72
73 return this.rejectRegistration()
74 }
75
76 isEmailEnabled () {
77 return this.server.getHTMLConfig().email.enabled
78 }
79
80 private acceptRegistration () {
81 this.registrationService.acceptRegistration(this.registration, this.form.value.moderationResponse)
82 .subscribe({
83 next: () => {
84 this.notifier.success($localize`${this.registration.username} account created`)
85
86 this.registrationProcessed.emit()
87 this.hide()
88 },
89
90 error: err => this.notifier.error(err.message)
91 })
92 }
93
94 private rejectRegistration () {
95 this.registrationService.rejectRegistration(this.registration, this.form.value.moderationResponse)
96 .subscribe({
97 next: () => {
98 this.notifier.success($localize`${this.registration.username} registration rejected`)
99
100 this.registrationProcessed.emit()
101 this.hide()
102 },
103
104 error: err => this.notifier.error(err.message)
105 })
106 }
107}
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 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from '@app/shared/form-validators'
3
4export const REGISTRATION_MODERATION_RESPONSE_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
6 MESSAGES: {
7 required: $localize`Moderation response is required.`,
8 minlength: $localize`Moderation response must be at least 2 characters long.`,
9 maxlength: $localize`Moderation response cannot be more than 3000 characters long.`
10 }
11}
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 @@
1<h1>
2 <my-global-icon iconName="user" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>Registration requests</ng-container>
4</h1>
5
6<p-table
7 [value]="registrations" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} registrations"
12 [expandedRowKeys]="expandedRows"
13>
14 <ng-template pTemplate="caption">
15 <div class="caption">
16 <div class="ms-auto">
17 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
18 </div>
19 </div>
20 </ng-template>
21
22 <ng-template pTemplate="header">
23 <tr> <!-- header -->
24 <th style="width: 40px;"></th>
25 <th style="width: 150px;"></th>
26 <th i18n>Account</th>
27 <th i18n>Email</th>
28 <th i18n>Channel</th>
29 <th i18n>Registration reason</th>
30 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
31 <th i18n>Moderation response</th>
32 <th style="width: 150px;" i18n pSortableColumn="createdAt">Requested on <p-sortIcon field="createdAt"></p-sortIcon></th>
33 </tr>
34 </ng-template>
35
36 <ng-template pTemplate="body" let-expanded="expanded" let-registration>
37 <tr>
38 <td class="expand-cell" [pRowToggler]="registration">
39 <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
40 </td>
41
42 <td class="action-cell">
43 <my-action-dropdown
44 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
45 i18n-label label="Actions" [actions]="registrationActions" [entry]="registration"
46 ></my-action-dropdown>
47 </td>
48
49 <td>
50 <div class="chip two-lines">
51 <div>
52 <span>{{ registration.username }}</span>
53 <span class="muted">{{ registration.accountDisplayName }}</span>
54 </div>
55 </div>
56 </td>
57
58 <td>
59 <my-user-email-info [entry]="registration" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info>
60 </td>
61
62 <td>
63 <div class="chip two-lines">
64 <div>
65 <span>{{ registration.channelHandle }}</span>
66 <span class="muted">{{ registration.channelDisplayName }}</span>
67 </div>
68 </div>
69 </td>
70
71 <td container="body" placement="left auto" [ngbTooltip]="registration.registrationReason">
72 {{ registration.registrationReason }}
73 </td>
74
75 <td class="c-hand abuse-states" [pRowToggler]="registration">
76 <my-global-icon *ngIf="isRegistrationAccepted(registration)" [title]="registration.state.label" iconName="tick"></my-global-icon>
77 <my-global-icon *ngIf="isRegistrationRejected(registration)" [title]="registration.state.label" iconName="cross"></my-global-icon>
78 </td>
79
80 <td container="body" placement="left auto" [ngbTooltip]="registration.moderationResponse">
81 {{ registration.moderationResponse }}
82 </td>
83
84 <td class="c-hand" [pRowToggler]="registration">{{ registration.createdAt | date: 'short' }}</td>
85 </tr>
86 </ng-template>
87
88 <ng-template pTemplate="rowexpansion" let-registration>
89 <tr>
90 <td colspan="9">
91 <div class="moderation-expanded">
92 <div class="left">
93 <div class="d-flex">
94 <span class="moderation-expanded-label" i18n>Registration reason:</span>
95 <span class="moderation-expanded-text" [innerHTML]="registration.registrationReasonHTML"></span>
96 </div>
97
98 <div *ngIf="registration.moderationResponse">
99 <span class="moderation-expanded-label" i18n>Moderation response:</span>
100 <span class="moderation-expanded-text" [innerHTML]="registration.moderationResponseHTML"></span>
101 </div>
102 </div>
103 </div>
104 </td>
105 </tr>
106 </ng-template>
107
108 <ng-template pTemplate="emptymessage">
109 <tr>
110 <td colspan="9">
111 <div class="no-results">
112 <ng-container *ngIf="search" i18n>No registrations found matching current filters.</ng-container>
113 <ng-container *ngIf="!search" i18n>No registrations found.</ng-container>
114 </div>
115 </td>
116 </tr>
117 </ng-template>
118</p-table>
119
120<my-process-registration-modal #processRegistrationModal (registrationProcessed)="onRegistrationProcessed()"></my-process-registration-modal>
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 @@
1@use '_mixins' as *;
2@use '_variables' as *;
3
4my-global-icon {
5 width: 24px;
6 height: 24px;
7}
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 @@
1import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router'
4import { MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
5import { AdvancedInputFilter } from '@app/shared/shared-forms'
6import { DropdownAction } from '@app/shared/shared-main'
7import { UserRegistration, UserRegistrationState } from '@shared/models'
8import { AdminRegistrationService } from './admin-registration.service'
9import { ProcessRegistrationModalComponent } from './process-registration-modal.component'
10
11@Component({
12 selector: 'my-registration-list',
13 templateUrl: './registration-list.component.html',
14 styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './registration-list.component.scss' ]
15})
16export class RegistrationListComponent extends RestTable implements OnInit {
17 @ViewChild('processRegistrationModal', { static: true }) processRegistrationModal: ProcessRegistrationModalComponent
18
19 registrations: (UserRegistration & { registrationReasonHTML?: string, moderationResponseHTML?: string })[] = []
20 totalRecords = 0
21 sort: SortMeta = { field: 'createdAt', order: -1 }
22 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
23
24 registrationActions: DropdownAction<UserRegistration>[][] = []
25
26 inputFilters: AdvancedInputFilter[] = []
27
28 requiresEmailVerification: boolean
29
30 constructor (
31 protected route: ActivatedRoute,
32 protected router: Router,
33 private server: ServerService,
34 private notifier: Notifier,
35 private markdownRenderer: MarkdownService,
36 private adminRegistrationService: AdminRegistrationService
37 ) {
38 super()
39
40 this.registrationActions = [
41 [
42 {
43 label: $localize`Accept this registration`,
44 handler: registration => this.openRegistrationRequestProcessModal(registration, 'accept'),
45 isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING
46 },
47 {
48 label: $localize`Reject this registration`,
49 handler: registration => this.openRegistrationRequestProcessModal(registration, 'reject'),
50 isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING
51 },
52 {
53 label: $localize`Remove this registration request`,
54 handler: registration => this.removeRegistration(registration),
55 isDisplayed: registration => registration.state.id !== UserRegistrationState.PENDING
56 }
57 ]
58 ]
59 }
60
61 ngOnInit () {
62 this.initialize()
63
64 this.server.getConfig()
65 .subscribe(config => {
66 this.requiresEmailVerification = config.signup.requiresEmailVerification
67 })
68 }
69
70 getIdentifier () {
71 return 'RegistrationListComponent'
72 }
73
74 isRegistrationAccepted (registration: UserRegistration) {
75 return registration.state.id === UserRegistrationState.ACCEPTED
76 }
77
78 isRegistrationRejected (registration: UserRegistration) {
79 return registration.state.id === UserRegistrationState.REJECTED
80 }
81
82 onRegistrationProcessed () {
83 this.reloadData()
84 }
85
86 protected reloadData () {
87 this.adminRegistrationService.listRegistrations({
88 pagination: this.pagination,
89 sort: this.sort,
90 search: this.search
91 }).subscribe({
92 next: async resultList => {
93 this.totalRecords = resultList.total
94 this.registrations = resultList.data
95
96 for (const registration of this.registrations) {
97 registration.registrationReasonHTML = await this.toHtml(registration.registrationReason)
98 registration.moderationResponseHTML = await this.toHtml(registration.moderationResponse)
99 }
100 },
101
102 error: err => this.notifier.error(err.message)
103 })
104 }
105
106 private openRegistrationRequestProcessModal (registration: UserRegistration, mode: 'accept' | 'reject') {
107 this.processRegistrationModal.openModal(registration, mode)
108 }
109
110 private removeRegistration (registration: UserRegistration) {
111 this.adminRegistrationService.removeRegistration(registration)
112 .subscribe({
113 next: () => {
114 this.notifier.success($localize`Registration request deleted.`)
115 this.reloadData()
116 },
117
118 error: err => this.notifier.error(err.message)
119 })
120 }
121
122 private toHtml (text: string) {
123 return this.markdownRenderer.textMarkdownToHTML({ markdown: text })
124 }
125}
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html
index a96ce561c..5e5ac368c 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html
@@ -95,7 +95,7 @@
95 <div class="chip two-lines"> 95 <div class="chip two-lines">
96 <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar> 96 <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar>
97 <div> 97 <div>
98 <span class="user-table-primary-text">{{ user.account.displayName }}</span> 98 <span>{{ user.account.displayName }}</span>
99 <span class="muted">{{ user.username }}</span> 99 <span class="muted">{{ user.username }}</span>
100 </div> 100 </div>
101 </div> 101 </div>
@@ -110,23 +110,10 @@
110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span> 110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span>
111 </td> 111 </td>
112 112
113 <td *ngIf="isSelected('email')" [title]="user.email"> 113 <td *ngIf="isSelected('email')">
114 <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus"> 114 <my-user-email-info [entry]="user" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info>
115 <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a>
116 </ng-container>
117 </td> 115 </td>
118 116
119 <ng-template #emailWithVerificationStatus>
120 <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
121 <em>? {{ user.email }}</em>
122 </td>
123 <ng-template #emailVerifiedNotFalse>
124 <td i18n-title title="User's email is verified / User can login without email verification">
125 &#x2713; {{ user.email }}
126 </td>
127 </ng-template>
128 </ng-template>
129
130 <td *ngIf="isSelected('quota')"> 117 <td *ngIf="isSelected('quota')">
131 <div class="progress" i18n-title title="Total video quota"> 118 <div class="progress" i18n-title title="Total video quota">
132 <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }" 119 <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }"
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
index 23e0d29ee..2a3b955d2 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
@@ -10,12 +10,6 @@ tr.banned > td {
10 background-color: lighten($color: $red, $amount: 40) !important; 10 background-color: lighten($color: $red, $amount: 40) !important;
11} 11}
12 12
13.table-email {
14 @include disable-default-a-behaviour;
15
16 color: pvar(--mainForegroundColor);
17}
18
19.banned-info { 13.banned-info {
20 font-style: italic; 14 font-style: italic;
21} 15}
@@ -37,10 +31,6 @@ my-global-icon {
37 width: 18px; 31 width: 18px;
38} 32}
39 33
40.chip {
41 @include chip;
42}
43
44.progress { 34.progress {
45 @include progressbar($small: true); 35 @include progressbar($small: true);
46 36
diff --git a/client/src/app/+admin/shared/shared-admin.module.ts b/client/src/app/+admin/shared/shared-admin.module.ts
index bef7d54ef..a5c300d12 100644
--- a/client/src/app/+admin/shared/shared-admin.module.ts
+++ b/client/src/app/+admin/shared/shared-admin.module.ts
@@ -1,5 +1,6 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { SharedMainModule } from '../../shared/shared-main/shared-main.module' 2import { SharedMainModule } from '../../shared/shared-main/shared-main.module'
3import { UserEmailInfoComponent } from './user-email-info.component'
3import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' 4import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
4 5
5@NgModule({ 6@NgModule({
@@ -8,11 +9,13 @@ import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
8 ], 9 ],
9 10
10 declarations: [ 11 declarations: [
11 UserRealQuotaInfoComponent 12 UserRealQuotaInfoComponent,
13 UserEmailInfoComponent
12 ], 14 ],
13 15
14 exports: [ 16 exports: [
15 UserRealQuotaInfoComponent 17 UserRealQuotaInfoComponent,
18 UserEmailInfoComponent
16 ], 19 ],
17 20
18 providers: [] 21 providers: []
diff --git a/client/src/app/+admin/shared/user-email-info.component.html b/client/src/app/+admin/shared/user-email-info.component.html
new file mode 100644
index 000000000..244240619
--- /dev/null
+++ b/client/src/app/+admin/shared/user-email-info.component.html
@@ -0,0 +1,13 @@
1<ng-container>
2 <a [href]="'mailto:' + entry.email" [title]="getTitle()">
3 <ng-container *ngIf="!requiresEmailVerification">
4 {{ entry.email }}
5 </ng-container>
6
7 <ng-container *ngIf="requiresEmailVerification">
8 <em *ngIf="!entry.emailVerified">? {{ entry.email }}</em>
9
10 <ng-container *ngIf="entry.emailVerified === true">&#x2713; {{ entry.email }}</ng-container>
11 </ng-container>
12 </a>
13</ng-container>
diff --git a/client/src/app/+admin/shared/user-email-info.component.scss b/client/src/app/+admin/shared/user-email-info.component.scss
new file mode 100644
index 000000000..d34947edd
--- /dev/null
+++ b/client/src/app/+admin/shared/user-email-info.component.scss
@@ -0,0 +1,10 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4a {
5 color: pvar(--mainForegroundColor);
6
7 &:hover {
8 text-decoration: underline;
9 }
10}
diff --git a/client/src/app/+admin/shared/user-email-info.component.ts b/client/src/app/+admin/shared/user-email-info.component.ts
new file mode 100644
index 000000000..e33948b60
--- /dev/null
+++ b/client/src/app/+admin/shared/user-email-info.component.ts
@@ -0,0 +1,20 @@
1import { Component, Input } from '@angular/core'
2import { User, UserRegistration } from '@shared/models/users'
3
4@Component({
5 selector: 'my-user-email-info',
6 templateUrl: './user-email-info.component.html',
7 styleUrls: [ './user-email-info.component.scss' ]
8})
9export class UserEmailInfoComponent {
10 @Input() entry: User | UserRegistration
11 @Input() requiresEmailVerification: boolean
12
13 getTitle () {
14 if (this.entry.emailVerified) {
15 return $localize`User email has been verified`
16 }
17
18 return $localize`User email hasn't been verified`
19 }
20}
diff --git a/client/src/app/+admin/system/jobs/job.service.ts b/client/src/app/+admin/system/jobs/job.service.ts
index ef8ddd3b4..031e2bad8 100644
--- a/client/src/app/+admin/system/jobs/job.service.ts
+++ b/client/src/app/+admin/system/jobs/job.service.ts
@@ -19,7 +19,7 @@ export class JobService {
19 private restExtractor: RestExtractor 19 private restExtractor: RestExtractor
20 ) {} 20 ) {}
21 21
22 getJobs (options: { 22 listJobs (options: {
23 jobState?: JobStateClient 23 jobState?: JobStateClient
24 jobType: JobTypeClient 24 jobType: JobTypeClient
25 pagination: RestPagination 25 pagination: RestPagination
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts
index b8f3c3a68..12dc88a70 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.ts
+++ b/client/src/app/+admin/system/jobs/jobs.component.ts
@@ -125,7 +125,7 @@ export class JobsComponent extends RestTable implements OnInit {
125 if (this.jobState === 'all') jobState = null 125 if (this.jobState === 'all') jobState = null
126 126
127 this.jobsService 127 this.jobsService
128 .getJobs({ 128 .listJobs({
129 jobState, 129 jobState,
130 jobType: this.jobType, 130 jobType: this.jobType,
131 pagination: this.pagination, 131 pagination: this.pagination,