aboutsummaryrefslogtreecommitdiffhomepage
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
parentb379759f55a35837b803a3b988674972db2903d1 (diff)
downloadPeerTube-9589907c89d29a6c0acd52c8cb789af9f93ce9af.tar.gz
PeerTube-9589907c89d29a6c0acd52c8cb789af9f93ce9af.tar.zst
PeerTube-9589907c89d29a6c0acd52c8cb789af9f93ce9af.zip
Implement signup approval in client
-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
-rw-r--r--client/src/app/+login/login.component.ts28
-rw-r--r--client/src/app/+my-library/my-ownership/my-ownership.component.scss4
-rw-r--r--client/src/app/+signup/+register/register.component.html35
-rw-r--r--client/src/app/+signup/+register/register.component.ts62
-rw-r--r--client/src/app/+signup/+register/shared/index.ts1
-rw-r--r--client/src/app/+signup/+register/shared/register-validators.ts18
-rw-r--r--client/src/app/+signup/+register/steps/register-step-about.component.html4
-rw-r--r--client/src/app/+signup/+register/steps/register-step-about.component.ts1
-rw-r--r--client/src/app/+signup/+register/steps/register-step-channel.component.ts6
-rw-r--r--client/src/app/+signup/+register/steps/register-step-terms.component.html14
-rw-r--r--client/src/app/+signup/+register/steps/register-step-terms.component.ts10
-rw-r--r--client/src/app/+signup/+register/steps/register-step-user.component.ts6
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts6
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html17
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts86
-rw-r--r--client/src/app/+signup/shared/shared-signup.module.ts11
-rw-r--r--client/src/app/+signup/shared/signup-success-after-email.component.html21
-rw-r--r--client/src/app/+signup/shared/signup-success-after-email.component.ts10
-rw-r--r--client/src/app/+signup/shared/signup-success-before-email.component.html35
-rw-r--r--client/src/app/+signup/shared/signup-success-before-email.component.ts12
-rw-r--r--client/src/app/+signup/shared/signup-success.component.html22
-rw-r--r--client/src/app/+signup/shared/signup-success.component.ts19
-rw-r--r--client/src/app/+signup/shared/signup.service.ts (renamed from client/src/app/shared/shared-users/user-signup.service.ts)41
-rw-r--r--client/src/app/menu/menu.component.html4
-rw-r--r--client/src/app/menu/menu.component.ts4
-rw-r--r--client/src/app/shared/form-validators/user-validators.ts7
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-details.component.html6
-rw-r--r--client/src/app/shared/shared-main/account/index.ts1
-rw-r--r--client/src/app/shared/shared-main/account/signup-label.component.html2
-rw-r--r--client/src/app/shared/shared-main/account/signup-label.component.ts9
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts6
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts12
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html8
-rw-r--r--client/src/app/shared/shared-moderation/account-blocklist.component.scss4
-rw-r--r--client/src/app/shared/shared-moderation/moderation.scss4
-rw-r--r--client/src/app/shared/shared-moderation/server-blocklist.component.scss4
-rw-r--r--client/src/app/shared/shared-users/index.ts1
-rw-r--r--client/src/app/shared/shared-users/shared-users.module.ts3
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss4
-rw-r--r--client/src/sass/class-helpers.scss6
-rw-r--r--client/src/sass/include/_badges.scss4
-rw-r--r--client/src/sass/include/_fonts.scss4
-rw-r--r--client/src/sass/include/_mixins.scss36
67 files changed, 1041 insertions, 215 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,
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index 5f6aa842e..c03af38f2 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -9,7 +9,7 @@ import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shar
9import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' 9import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
10import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 10import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
11import { getExternalAuthHref } from '@shared/core-utils' 11import { getExternalAuthHref } from '@shared/core-utils'
12import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' 12import { RegisteredExternalAuthConfig, ServerConfig, ServerErrorCode } from '@shared/models'
13 13
14@Component({ 14@Component({
15 selector: 'my-login', 15 selector: 'my-login',
@@ -197,6 +197,8 @@ The link will expire within 1 hour.`
197 } 197 }
198 198
199 private handleError (err: any) { 199 private handleError (err: any) {
200 console.log(err)
201
200 if (this.authService.isOTPMissingError(err)) { 202 if (this.authService.isOTPMissingError(err)) {
201 this.otpStep = true 203 this.otpStep = true
202 204
@@ -208,8 +210,26 @@ The link will expire within 1 hour.`
208 return 210 return
209 } 211 }
210 212
211 if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.` 213 if (err.message.includes('credentials are invalid')) {
212 else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.` 214 this.error = $localize`Incorrect username or password.`
213 else this.error = err.message 215 return
216 }
217
218 if (err.message.includes('blocked')) {
219 this.error = $localize`Your account is blocked.`
220 return
221 }
222
223 if (err.body?.code === ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL) {
224 this.error = $localize`This account is awaiting approval by moderators.`
225 return
226 }
227
228 if (err.body?.code === ServerErrorCode.ACCOUNT_APPROVAL_REJECTED) {
229 this.error = $localize`Registration approval has been rejected for this account.`
230 return
231 }
232
233 this.error = err.message
214 } 234 }
215} 235}
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.scss b/client/src/app/+my-library/my-ownership/my-ownership.component.scss
index a8450ff1b..98bed226d 100644
--- a/client/src/app/+my-library/my-ownership/my-ownership.component.scss
+++ b/client/src/app/+my-library/my-ownership/my-ownership.component.scss
@@ -2,10 +2,6 @@
2@use '_miniature' as *; 2@use '_miniature' as *;
3@use '_mixins' as *; 3@use '_mixins' as *;
4 4
5.chip {
6 @include chip;
7}
8
9.video-table-video { 5.video-table-video {
10 display: inline-flex; 6 display: inline-flex;
11 7
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html
index bafb96a49..86763e801 100644
--- a/client/src/app/+signup/+register/register.component.html
+++ b/client/src/app/+signup/+register/register.component.html
@@ -5,29 +5,34 @@
5 </div> 5 </div>
6 6
7 <ng-container *ngIf="!signupDisabled"> 7 <ng-container *ngIf="!signupDisabled">
8 <h1 i18n class="title-page-v2"> 8 <h1 class="title-page-v2">
9 <strong class="underline-orange">{{ instanceName }}</strong> 9 <strong class="underline-orange">{{ instanceName }}</strong>
10 > 10 >
11 Create an account 11 <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
12 </h1> 12 </h1>
13 13
14 <div class="register-content"> 14 <div class="register-content">
15 <my-custom-stepper linear> 15 <my-custom-stepper linear>
16 16
17 <cdk-step i18n-label label="About" [editable]="!signupSuccess"> 17 <cdk-step i18n-label label="About" [editable]="!signupSuccess">
18 <my-signup-step-title mascotImageName="about" i18n> 18 <my-signup-step-title mascotImageName="about">
19 <strong>Create an account</strong> 19 <strong>
20 <div>on {{ instanceName }}</div> 20 <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
21 </strong>
22
23 <div i18n>on {{ instanceName }}</div>
21 </my-signup-step-title> 24 </my-signup-step-title>
22 25
23 <my-register-step-about [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about> 26 <my-register-step-about [requiresApproval]="requiresApproval" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about>
24 27
25 <div class="step-buttons"> 28 <div class="step-buttons">
26 <a i18n class="skip-step underline-orange" routerLink="/login"> 29 <a i18n class="skip-step underline-orange" routerLink="/login">
27 <strong>I already have an account</strong>, I log in 30 <strong>I already have an account</strong>, I log in
28 </a> 31 </a>
29 32
30 <button i18n cdkStepperNext>Create an account</button> 33 <button cdkStepperNext>
34 <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
35 </button>
31 </div> 36 </div>
32 </cdk-step> 37 </cdk-step>
33 38
@@ -44,8 +49,8 @@
44 ></my-instance-about-accordion> 49 ></my-instance-about-accordion>
45 50
46 <my-register-step-terms 51 <my-register-step-terms
47 [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" 52 [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" [minimumAge]="minimumAge" [instanceName]="instanceName"
48 [minimumAge]="minimumAge" 53 [requiresApproval]="requiresApproval"
49 (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()" 54 (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
50 ></my-register-step-terms> 55 ></my-register-step-terms>
51 56
@@ -94,14 +99,15 @@
94 <div class="skip-step-description" i18n>You will be able to create a channel later</div> 99 <div class="skip-step-description" i18n>You will be able to create a channel later</div>
95 </div> 100 </div>
96 101
97 <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n> 102 <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()">
98 Create my account 103 <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
99 </button> 104 </button>
100 </div> 105 </div>
101 </cdk-step> 106 </cdk-step>
102 107
103 <cdk-step #lastStep i18n-label label="Done!" [editable]="false"> 108 <cdk-step #lastStep i18n-label label="Done!" [editable]="false">
104 <div *ngIf="!signupSuccess && !signupError" class="done-loader"> 109 <!-- Account creation can be a little bit long so display a loader -->
110 <div *ngIf="!requiresApproval && !signupSuccess && !signupError" class="done-loader">
105 <my-loader [loading]="true"></my-loader> 111 <my-loader [loading]="true"></my-loader>
106 112
107 <div i18n>PeerTube is creating your account...</div> 113 <div i18n>PeerTube is creating your account...</div>
@@ -109,7 +115,10 @@
109 115
110 <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div> 116 <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div>
111 117
112 <my-signup-success *ngIf="signupSuccess" [requiresEmailVerification]="requiresEmailVerification"></my-signup-success> 118 <my-signup-success-before-email
119 *ngIf="signupSuccess"
120 [requiresEmailVerification]="requiresEmailVerification" [requiresApproval]="requiresApproval" [instanceName]="instanceName"
121 ></my-signup-success-before-email>
113 122
114 <div *ngIf="signupError" class="steps-button"> 123 <div *ngIf="signupError" class="steps-button">
115 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> 124 <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts
index 958770ebf..9259d902c 100644
--- a/client/src/app/+signup/+register/register.component.ts
+++ b/client/src/app/+signup/+register/register.component.ts
@@ -5,10 +5,10 @@ import { ActivatedRoute } from '@angular/router'
5import { AuthService } from '@app/core' 5import { AuthService } from '@app/core'
6import { HooksService } from '@app/core/plugins/hooks.service' 6import { HooksService } from '@app/core/plugins/hooks.service'
7import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' 7import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
8import { UserSignupService } from '@app/shared/shared-users'
9import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' 8import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
10import { UserRegister } from '@shared/models' 9import { UserRegister } from '@shared/models'
11import { ServerConfig } from '@shared/models/server' 10import { ServerConfig } from '@shared/models/server'
11import { SignupService } from '../shared/signup.service'
12 12
13@Component({ 13@Component({
14 selector: 'my-register', 14 selector: 'my-register',
@@ -53,7 +53,7 @@ export class RegisterComponent implements OnInit {
53 constructor ( 53 constructor (
54 private route: ActivatedRoute, 54 private route: ActivatedRoute,
55 private authService: AuthService, 55 private authService: AuthService,
56 private userSignupService: UserSignupService, 56 private signupService: SignupService,
57 private hooks: HooksService 57 private hooks: HooksService
58 ) { } 58 ) { }
59 59
@@ -61,6 +61,10 @@ export class RegisterComponent implements OnInit {
61 return this.serverConfig.signup.requiresEmailVerification 61 return this.serverConfig.signup.requiresEmailVerification
62 } 62 }
63 63
64 get requiresApproval () {
65 return this.serverConfig.signup.requiresApproval
66 }
67
64 get minimumAge () { 68 get minimumAge () {
65 return this.serverConfig.signup.minimumAge 69 return this.serverConfig.signup.minimumAge
66 } 70 }
@@ -132,42 +136,49 @@ export class RegisterComponent implements OnInit {
132 skipChannelCreation () { 136 skipChannelCreation () {
133 this.formStepChannel.reset() 137 this.formStepChannel.reset()
134 this.lastStep.select() 138 this.lastStep.select()
139
135 this.signup() 140 this.signup()
136 } 141 }
137 142
138 async signup () { 143 async signup () {
139 this.signupError = undefined 144 this.signupError = undefined
140 145
141 const body: UserRegister = await this.hooks.wrapObject( 146 const termsForm = this.formStepTerms.value
147 const userForm = this.formStepUser.value
148 const channelForm = this.formStepChannel?.value
149
150 const channel = this.formStepChannel?.value?.name
151 ? { name: channelForm?.name, displayName: channelForm?.displayName }
152 : undefined
153
154 const body = await this.hooks.wrapObject(
142 { 155 {
143 ...this.formStepUser.value, 156 username: userForm.username,
157 password: userForm.password,
158 email: userForm.email,
159 displayName: userForm.displayName,
160
161 registrationReason: termsForm.registrationReason,
144 162
145 channel: this.formStepChannel?.value?.name 163 channel
146 ? this.formStepChannel.value
147 : undefined
148 }, 164 },
149 'signup', 165 'signup',
150 'filter:api.signup.registration.create.params' 166 'filter:api.signup.registration.create.params'
151 ) 167 )
152 168
153 this.userSignupService.signup(body).subscribe({ 169 const obs = this.requiresApproval
170 ? this.signupService.requestSignup(body)
171 : this.signupService.directSignup(body)
172
173 obs.subscribe({
154 next: () => { 174 next: () => {
155 if (this.requiresEmailVerification) { 175 if (this.requiresEmailVerification || this.requiresApproval) {
156 this.signupSuccess = true 176 this.signupSuccess = true
157 return 177 return
158 } 178 }
159 179
160 // Auto login 180 // Auto login
161 this.authService.login({ username: body.username, password: body.password }) 181 this.autoLogin(body)
162 .subscribe({
163 next: () => {
164 this.signupSuccess = true
165 },
166
167 error: err => {
168 this.signupError = err.message
169 }
170 })
171 }, 182 },
172 183
173 error: err => { 184 error: err => {
@@ -175,4 +186,17 @@ export class RegisterComponent implements OnInit {
175 } 186 }
176 }) 187 })
177 } 188 }
189
190 private autoLogin (body: UserRegister) {
191 this.authService.login({ username: body.username, password: body.password })
192 .subscribe({
193 next: () => {
194 this.signupSuccess = true
195 },
196
197 error: err => {
198 this.signupError = err.message
199 }
200 })
201 }
178} 202}
diff --git a/client/src/app/+signup/+register/shared/index.ts b/client/src/app/+signup/+register/shared/index.ts
new file mode 100644
index 000000000..affb54bf4
--- /dev/null
+++ b/client/src/app/+signup/+register/shared/index.ts
@@ -0,0 +1 @@
export * from './register-validators'
diff --git a/client/src/app/+signup/+register/shared/register-validators.ts b/client/src/app/+signup/+register/shared/register-validators.ts
new file mode 100644
index 000000000..f14803b68
--- /dev/null
+++ b/client/src/app/+signup/+register/shared/register-validators.ts
@@ -0,0 +1,18 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from '@app/shared/form-validators'
3
4export const REGISTER_TERMS_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ Validators.requiredTrue ],
6 MESSAGES: {
7 required: $localize`You must agree with the instance terms in order to register on it.`
8 }
9}
10
11export const REGISTER_REASON_VALIDATOR: BuildFormValidator = {
12 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
13 MESSAGES: {
14 required: $localize`Registration reason is required.`,
15 minlength: $localize`Registration reason must be at least 2 characters long.`,
16 maxlength: $localize`Registration reason cannot be more than 3000 characters long.`
17 }
18}
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.html b/client/src/app/+signup/+register/steps/register-step-about.component.html
index 769fe3127..580e8a92c 100644
--- a/client/src/app/+signup/+register/steps/register-step-about.component.html
+++ b/client/src/app/+signup/+register/steps/register-step-about.component.html
@@ -13,6 +13,10 @@
13 <li i18n>Have access to your <strong>watch history</strong></li> 13 <li i18n>Have access to your <strong>watch history</strong></li>
14 <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li> 14 <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li>
15 </ul> 15 </ul>
16
17 <p *ngIf="requiresApproval" i18n>
18 Moderators of {{ instanceName }} will have to approve your registration request once you have finished to fill the form.
19 </p>
16</div> 20</div>
17 21
18<div> 22<div>
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.ts b/client/src/app/+signup/+register/steps/register-step-about.component.ts
index 9a0941016..b176ffa59 100644
--- a/client/src/app/+signup/+register/steps/register-step-about.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-about.component.ts
@@ -7,6 +7,7 @@ import { ServerService } from '@app/core'
7 styleUrls: [ './register-step-about.component.scss' ] 7 styleUrls: [ './register-step-about.component.scss' ]
8}) 8})
9export class RegisterStepAboutComponent { 9export class RegisterStepAboutComponent {
10 @Input() requiresApproval: boolean
10 @Input() videoUploadDisabled: boolean 11 @Input() videoUploadDisabled: boolean
11 12
12 constructor (private serverService: ServerService) { 13 constructor (private serverService: ServerService) {
diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.ts b/client/src/app/+signup/+register/steps/register-step-channel.component.ts
index df92c5145..478ca0177 100644
--- a/client/src/app/+signup/+register/steps/register-step-channel.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-channel.component.ts
@@ -2,9 +2,9 @@ import { concat, of } from 'rxjs'
2import { pairwise } from 'rxjs/operators' 2import { pairwise } from 'rxjs/operators'
3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
4import { FormGroup } from '@angular/forms' 4import { FormGroup } from '@angular/forms'
5import { SignupService } from '@app/+signup/shared/signup.service'
5import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' 6import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators'
6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' 7import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { UserSignupService } from '@app/shared/shared-users'
8 8
9@Component({ 9@Component({
10 selector: 'my-register-step-channel', 10 selector: 'my-register-step-channel',
@@ -20,7 +20,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
20 20
21 constructor ( 21 constructor (
22 protected formReactiveService: FormReactiveService, 22 protected formReactiveService: FormReactiveService,
23 private userSignupService: UserSignupService 23 private signupService: SignupService
24 ) { 24 ) {
25 super() 25 super()
26 } 26 }
@@ -51,7 +51,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
51 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { 51 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
52 const name = this.form.value['name'] || '' 52 const name = this.form.value['name'] || ''
53 53
54 const newName = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, name) 54 const newName = this.signupService.getNewUsername(oldDisplayName, newDisplayName, name)
55 this.form.patchValue({ name: newName }) 55 this.form.patchValue({ name: newName })
56 } 56 }
57} 57}
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.html b/client/src/app/+signup/+register/steps/register-step-terms.component.html
index cbfb32518..1d753a3f2 100644
--- a/client/src/app/+signup/+register/steps/register-step-terms.component.html
+++ b/client/src/app/+signup/+register/steps/register-step-terms.component.html
@@ -1,4 +1,16 @@
1<form role="form" [formGroup]="form"> 1<form role="form" [formGroup]="form">
2
3 <div *ngIf="requiresApproval" class="form-group">
4 <label i18n for="registrationReason">Why do you want to join {{ instanceName }}?</label>
5
6 <textarea
7 id="registrationReason" formControlName="registrationReason" class="form-control" rows="4"
8 [ngClass]="{ 'input-error': formErrors['registrationReason'] }"
9 ></textarea>
10
11 <div *ngIf="formErrors.registrationReason" class="form-error">{{ formErrors.registrationReason }}</div>
12 </div>
13
2 <div class="form-group"> 14 <div class="form-group">
3 <my-peertube-checkbox inputName="terms" formControlName="terms"> 15 <my-peertube-checkbox inputName="terms" formControlName="terms">
4 <ng-template ptTemplate="label"> 16 <ng-template ptTemplate="label">
@@ -6,7 +18,7 @@
6 I am at least {{ minimumAge }} years old and agree 18 I am at least {{ minimumAge }} years old and agree
7 to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a> 19 to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a>
8 <ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> 20 <ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
9 of this instance 21 of {{ instanceName }}
10 </ng-container> 22 </ng-container>
11 </ng-template> 23 </ng-template>
12 </my-peertube-checkbox> 24 </my-peertube-checkbox>
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.ts b/client/src/app/+signup/+register/steps/register-step-terms.component.ts
index 2df963b30..1b1fb49ee 100644
--- a/client/src/app/+signup/+register/steps/register-step-terms.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-terms.component.ts
@@ -1,7 +1,7 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { FormGroup } from '@angular/forms' 2import { FormGroup } from '@angular/forms'
3import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators'
4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' 3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { REGISTER_REASON_VALIDATOR, REGISTER_TERMS_VALIDATOR } from '../shared'
5 5
6@Component({ 6@Component({
7 selector: 'my-register-step-terms', 7 selector: 'my-register-step-terms',
@@ -10,7 +10,9 @@ import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
10}) 10})
11export class RegisterStepTermsComponent extends FormReactive implements OnInit { 11export class RegisterStepTermsComponent extends FormReactive implements OnInit {
12 @Input() hasCodeOfConduct = false 12 @Input() hasCodeOfConduct = false
13 @Input() requiresApproval: boolean
13 @Input() minimumAge = 16 14 @Input() minimumAge = 16
15 @Input() instanceName: string
14 16
15 @Output() formBuilt = new EventEmitter<FormGroup>() 17 @Output() formBuilt = new EventEmitter<FormGroup>()
16 @Output() termsClick = new EventEmitter<void>() 18 @Output() termsClick = new EventEmitter<void>()
@@ -28,7 +30,11 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit {
28 30
29 ngOnInit () { 31 ngOnInit () {
30 this.buildForm({ 32 this.buildForm({
31 terms: USER_TERMS_VALIDATOR 33 terms: REGISTER_TERMS_VALIDATOR,
34
35 registrationReason: this.requiresApproval
36 ? REGISTER_REASON_VALIDATOR
37 : null
32 }) 38 })
33 39
34 setTimeout(() => this.formBuilt.emit(this.form)) 40 setTimeout(() => this.formBuilt.emit(this.form))
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.ts b/client/src/app/+signup/+register/steps/register-step-user.component.ts
index 822f8f5c5..0a5d2e437 100644
--- a/client/src/app/+signup/+register/steps/register-step-user.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-user.component.ts
@@ -2,6 +2,7 @@ import { concat, of } from 'rxjs'
2import { pairwise } from 'rxjs/operators' 2import { pairwise } from 'rxjs/operators'
3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
4import { FormGroup } from '@angular/forms' 4import { FormGroup } from '@angular/forms'
5import { SignupService } from '@app/+signup/shared/signup.service'
5import { 6import {
6 USER_DISPLAY_NAME_REQUIRED_VALIDATOR, 7 USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
7 USER_EMAIL_VALIDATOR, 8 USER_EMAIL_VALIDATOR,
@@ -9,7 +10,6 @@ import {
9 USER_USERNAME_VALIDATOR 10 USER_USERNAME_VALIDATOR
10} from '@app/shared/form-validators/user-validators' 11} from '@app/shared/form-validators/user-validators'
11import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' 12import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
12import { UserSignupService } from '@app/shared/shared-users'
13 13
14@Component({ 14@Component({
15 selector: 'my-register-step-user', 15 selector: 'my-register-step-user',
@@ -24,7 +24,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
24 24
25 constructor ( 25 constructor (
26 protected formReactiveService: FormReactiveService, 26 protected formReactiveService: FormReactiveService,
27 private userSignupService: UserSignupService 27 private signupService: SignupService
28 ) { 28 ) {
29 super() 29 super()
30 } 30 }
@@ -57,7 +57,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
57 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { 57 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
58 const username = this.form.value['username'] || '' 58 const username = this.form.value['username'] || ''
59 59
60 const newUsername = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, username) 60 const newUsername = this.signupService.getNewUsername(oldDisplayName, newDisplayName, username)
61 this.form.patchValue({ username: newUsername }) 61 this.form.patchValue({ username: newUsername })
62 } 62 }
63} 63}
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
index 06905f678..75b599e0e 100644
--- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
@@ -1,8 +1,8 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { SignupService } from '@app/+signup/shared/signup.service'
2import { Notifier, RedirectService, ServerService } from '@app/core' 3import { Notifier, RedirectService, ServerService } from '@app/core'
3import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' 4import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { UserSignupService } from '@app/shared/shared-users'
6 6
7@Component({ 7@Component({
8 selector: 'my-verify-account-ask-send-email', 8 selector: 'my-verify-account-ask-send-email',
@@ -15,7 +15,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
15 15
16 constructor ( 16 constructor (
17 protected formReactiveService: FormReactiveService, 17 protected formReactiveService: FormReactiveService,
18 private userSignupService: UserSignupService, 18 private signupService: SignupService,
19 private serverService: ServerService, 19 private serverService: ServerService,
20 private notifier: Notifier, 20 private notifier: Notifier,
21 private redirectService: RedirectService 21 private redirectService: RedirectService
@@ -34,7 +34,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
34 34
35 askSendVerifyEmail () { 35 askSendVerifyEmail () {
36 const email = this.form.value['verify-email-email'] 36 const email = this.form.value['verify-email-email']
37 this.userSignupService.askSendVerifyEmail(email) 37 this.signupService.askSendVerifyEmail(email)
38 .subscribe({ 38 .subscribe({
39 next: () => { 39 next: () => {
40 this.notifier.success($localize`An email with verification link will be sent to ${email}.`) 40 this.notifier.success($localize`An email with verification link will be sent to ${email}.`)
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html
index 122f3c28c..8c8b1098e 100644
--- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html
+++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html
@@ -1,14 +1,19 @@
1<div class="margin-content"> 1<div *ngIf="loaded" class="margin-content">
2 <h1 i18n class="title-page">Verify account email confirmation</h1> 2 <h1 i18n class="title-page">Verify email</h1>
3 3
4 <my-signup-success i18n *ngIf="!isPendingEmail && success" [requiresEmailVerification]="false"> 4 <my-signup-success-after-email
5 </my-signup-success> 5 *ngIf="displaySignupSuccess()"
6 [requiresApproval]="isRegistrationRequest() && requiresApproval"
7 >
8 </my-signup-success-after-email>
6 9
7 <div i18n class="alert alert-success" *ngIf="isPendingEmail && success">Email updated.</div> 10 <div i18n class="alert alert-success" *ngIf="!isRegistrationRequest() && isPendingEmail && success">Email updated.</div>
8 11
9 <div class="alert alert-danger" *ngIf="failed"> 12 <div class="alert alert-danger" *ngIf="failed">
10 <span i18n>An error occurred.</span> 13 <span i18n>An error occurred.</span>
11 14
12 <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email" [queryParams]="{ isPendingEmail: isPendingEmail }">Request new verification email</a> 15 <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email">
16 Request a new verification email
17 </a>
13 </div> 18 </div>
14</div> 19</div>
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts
index 88efce4a1..faf663391 100644
--- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts
@@ -1,7 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute } from '@angular/router' 2import { ActivatedRoute } from '@angular/router'
3import { AuthService, Notifier } from '@app/core' 3import { SignupService } from '@app/+signup/shared/signup.service'
4import { UserSignupService } from '@app/shared/shared-users' 4import { AuthService, Notifier, ServerService } from '@app/core'
5 5
6@Component({ 6@Component({
7 selector: 'my-verify-account-email', 7 selector: 'my-verify-account-email',
@@ -13,32 +13,82 @@ export class VerifyAccountEmailComponent implements OnInit {
13 failed = false 13 failed = false
14 isPendingEmail = false 14 isPendingEmail = false
15 15
16 requiresApproval: boolean
17 loaded = false
18
16 private userId: number 19 private userId: number
20 private registrationId: number
17 private verificationString: string 21 private verificationString: string
18 22
19 constructor ( 23 constructor (
20 private userSignupService: UserSignupService, 24 private signupService: SignupService,
25 private server: ServerService,
21 private authService: AuthService, 26 private authService: AuthService,
22 private notifier: Notifier, 27 private notifier: Notifier,
23 private route: ActivatedRoute 28 private route: ActivatedRoute
24 ) { 29 ) {
25 } 30 }
26 31
32 get instanceName () {
33 return this.server.getHTMLConfig().instance.name
34 }
35
27 ngOnInit () { 36 ngOnInit () {
28 const queryParams = this.route.snapshot.queryParams 37 const queryParams = this.route.snapshot.queryParams
38
39 this.server.getConfig().subscribe(config => {
40 this.requiresApproval = config.signup.requiresApproval
41
42 this.loaded = true
43 })
44
29 this.userId = queryParams['userId'] 45 this.userId = queryParams['userId']
46 this.registrationId = queryParams['registrationId']
47
30 this.verificationString = queryParams['verificationString'] 48 this.verificationString = queryParams['verificationString']
49
31 this.isPendingEmail = queryParams['isPendingEmail'] === 'true' 50 this.isPendingEmail = queryParams['isPendingEmail'] === 'true'
32 51
33 if (!this.userId || !this.verificationString) { 52 if (!this.verificationString) {
34 this.notifier.error($localize`Unable to find user id or verification string.`) 53 this.notifier.error($localize`Unable to find verification string in URL query.`)
35 } else { 54 return
36 this.verifyEmail() 55 }
56
57 if (!this.userId && !this.registrationId) {
58 this.notifier.error($localize`Unable to find user id or registration id in URL query.`)
59 return
37 } 60 }
61
62 this.verifyEmail()
63 }
64
65 isRegistrationRequest () {
66 return !!this.registrationId
67 }
68
69 displaySignupSuccess () {
70 if (!this.success) return false
71 if (!this.isRegistrationRequest() && this.isPendingEmail) return false
72
73 return true
38 } 74 }
39 75
40 verifyEmail () { 76 verifyEmail () {
41 this.userSignupService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail) 77 if (this.isRegistrationRequest()) {
78 return this.verifyRegistrationEmail()
79 }
80
81 return this.verifyUserEmail()
82 }
83
84 private verifyUserEmail () {
85 const options = {
86 userId: this.userId,
87 verificationString: this.verificationString,
88 isPendingEmail: this.isPendingEmail
89 }
90
91 this.signupService.verifyUserEmail(options)
42 .subscribe({ 92 .subscribe({
43 next: () => { 93 next: () => {
44 if (this.authService.isLoggedIn()) { 94 if (this.authService.isLoggedIn()) {
@@ -55,4 +105,24 @@ export class VerifyAccountEmailComponent implements OnInit {
55 } 105 }
56 }) 106 })
57 } 107 }
108
109 private verifyRegistrationEmail () {
110 const options = {
111 registrationId: this.registrationId,
112 verificationString: this.verificationString
113 }
114
115 this.signupService.verifyRegistrationEmail(options)
116 .subscribe({
117 next: () => {
118 this.success = true
119 },
120
121 error: err => {
122 this.failed = true
123
124 this.notifier.error(err.message)
125 }
126 })
127 }
58} 128}
diff --git a/client/src/app/+signup/shared/shared-signup.module.ts b/client/src/app/+signup/shared/shared-signup.module.ts
index 0aa08f3e2..0600f0af8 100644
--- a/client/src/app/+signup/shared/shared-signup.module.ts
+++ b/client/src/app/+signup/shared/shared-signup.module.ts
@@ -5,7 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedUsersModule } from '@app/shared/shared-users' 5import { SharedUsersModule } from '@app/shared/shared-users'
6import { SignupMascotComponent } from './signup-mascot.component' 6import { SignupMascotComponent } from './signup-mascot.component'
7import { SignupStepTitleComponent } from './signup-step-title.component' 7import { SignupStepTitleComponent } from './signup-step-title.component'
8import { SignupSuccessComponent } from './signup-success.component' 8import { SignupSuccessBeforeEmailComponent } from './signup-success-before-email.component'
9import { SignupSuccessAfterEmailComponent } from './signup-success-after-email.component'
10import { SignupService } from './signup.service'
9 11
10@NgModule({ 12@NgModule({
11 imports: [ 13 imports: [
@@ -16,7 +18,8 @@ import { SignupSuccessComponent } from './signup-success.component'
16 ], 18 ],
17 19
18 declarations: [ 20 declarations: [
19 SignupSuccessComponent, 21 SignupSuccessBeforeEmailComponent,
22 SignupSuccessAfterEmailComponent,
20 SignupStepTitleComponent, 23 SignupStepTitleComponent,
21 SignupMascotComponent 24 SignupMascotComponent
22 ], 25 ],
@@ -26,12 +29,14 @@ import { SignupSuccessComponent } from './signup-success.component'
26 SharedFormModule, 29 SharedFormModule,
27 SharedGlobalIconModule, 30 SharedGlobalIconModule,
28 31
29 SignupSuccessComponent, 32 SignupSuccessBeforeEmailComponent,
33 SignupSuccessAfterEmailComponent,
30 SignupStepTitleComponent, 34 SignupStepTitleComponent,
31 SignupMascotComponent 35 SignupMascotComponent
32 ], 36 ],
33 37
34 providers: [ 38 providers: [
39 SignupService
35 ] 40 ]
36}) 41})
37export class SharedSignupModule { } 42export class SharedSignupModule { }
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.html b/client/src/app/+signup/shared/signup-success-after-email.component.html
new file mode 100644
index 000000000..1c3536ada
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-success-after-email.component.html
@@ -0,0 +1,21 @@
1<my-signup-step-title mascotImageName="success">
2 <strong i18n>Email verified!</strong>
3</my-signup-step-title>
4
5<div class="alert pt-alert-primary">
6 <ng-container *ngIf="requiresApproval">
7 <p i18n>Your email has been verified and your account request has been sent!</p>
8
9 <p i18n>
10 A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected.
11 </p>
12 </ng-container>
13
14 <ng-container *ngIf="!requiresApproval">
15 <p i18n>Your email has been verified and your account has been created!</p>
16
17 <p i18n>
18 If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
19 </p>
20 </ng-container>
21</div>
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.ts b/client/src/app/+signup/shared/signup-success-after-email.component.ts
new file mode 100644
index 000000000..3d72fdae9
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-success-after-email.component.ts
@@ -0,0 +1,10 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-signup-success-after-email',
5 templateUrl: './signup-success-after-email.component.html',
6 styleUrls: [ './signup-success.component.scss' ]
7})
8export class SignupSuccessAfterEmailComponent {
9 @Input() requiresApproval: boolean
10}
diff --git a/client/src/app/+signup/shared/signup-success-before-email.component.html b/client/src/app/+signup/shared/signup-success-before-email.component.html
new file mode 100644
index 000000000..b9668ee82
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-success-before-email.component.html
@@ -0,0 +1,35 @@
1<my-signup-step-title mascotImageName="success">
2 <ng-container *ngIf="requiresApproval">
3 <strong i18n>Account request sent</strong>
4 </ng-container>
5
6 <ng-container *ngIf="!requiresApproval" i18n>
7 <strong>Welcome</strong>
8 <div>on {{ instanceName }}</div>
9 </ng-container>
10</my-signup-step-title>
11
12<div class="alert pt-alert-primary">
13 <p *ngIf="requiresApproval" i18n>Your account request has been sent!</p>
14 <p *ngIf="!requiresApproval" i18n>Your account has been created!</p>
15
16 <ng-container *ngIf="requiresEmailVerification">
17 <p i18n *ngIf="requiresApproval">
18 <strong>Check your emails</strong> to validate your account and complete your registration request.
19 </p>
20
21 <p i18n *ngIf="!requiresApproval">
22 <strong>Check your emails</strong> to validate your account and complete your registration.
23 </p>
24 </ng-container>
25
26 <ng-container *ngIf="!requiresEmailVerification">
27 <p i18n *ngIf="requiresApproval">
28 A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected.
29 </p>
30
31 <p *ngIf="!requiresApproval" i18n>
32 If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
33 </p>
34 </ng-container>
35</div>
diff --git a/client/src/app/+signup/shared/signup-success-before-email.component.ts b/client/src/app/+signup/shared/signup-success-before-email.component.ts
new file mode 100644
index 000000000..d72462340
--- /dev/null
+++ b/client/src/app/+signup/shared/signup-success-before-email.component.ts
@@ -0,0 +1,12 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-signup-success-before-email',
5 templateUrl: './signup-success-before-email.component.html',
6 styleUrls: [ './signup-success.component.scss' ]
7})
8export class SignupSuccessBeforeEmailComponent {
9 @Input() requiresApproval: boolean
10 @Input() requiresEmailVerification: boolean
11 @Input() instanceName: string
12}
diff --git a/client/src/app/+signup/shared/signup-success.component.html b/client/src/app/+signup/shared/signup-success.component.html
deleted file mode 100644
index c14889c72..000000000
--- a/client/src/app/+signup/shared/signup-success.component.html
+++ /dev/null
@@ -1,22 +0,0 @@
1<my-signup-step-title mascotImageName="success" i18n>
2 <strong>Welcome</strong>
3 <div>on {{ instanceName }}</div>
4</my-signup-step-title>
5
6<div class="alert pt-alert-primary">
7 <p i18n>Your account has been created!</p>
8
9 <p i18n *ngIf="requiresEmailVerification">
10 <strong>Check your emails</strong> to validate your account and complete your inscription.
11 </p>
12
13 <ng-container *ngIf="!requiresEmailVerification">
14 <p i18n>
15 If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>.
16 </p>
17
18 <p i18n>
19 To help moderators and other users to know <strong>who you are</strong>, don't forget to <a class="link-orange" routerLink="/my-account/settings">set up your account profile</a> by adding an <strong>avatar</strong> and a <strong>description</strong>.
20 </p>
21 </ng-container>
22</div>
diff --git a/client/src/app/+signup/shared/signup-success.component.ts b/client/src/app/+signup/shared/signup-success.component.ts
deleted file mode 100644
index a03f3819d..000000000
--- a/client/src/app/+signup/shared/signup-success.component.ts
+++ /dev/null
@@ -1,19 +0,0 @@
1import { Component, Input } from '@angular/core'
2import { ServerService } from '@app/core'
3
4@Component({
5 selector: 'my-signup-success',
6 templateUrl: './signup-success.component.html',
7 styleUrls: [ './signup-success.component.scss' ]
8})
9export class SignupSuccessComponent {
10 @Input() requiresEmailVerification: boolean
11
12 constructor (private serverService: ServerService) {
13
14 }
15
16 get instanceName () {
17 return this.serverService.getHTMLConfig().instance.name
18 }
19}
diff --git a/client/src/app/shared/shared-users/user-signup.service.ts b/client/src/app/+signup/shared/signup.service.ts
index 46fe34af1..f647298be 100644
--- a/client/src/app/shared/shared-users/user-signup.service.ts
+++ b/client/src/app/+signup/shared/signup.service.ts
@@ -2,17 +2,18 @@ import { catchError, tap } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http' 2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { RestExtractor, UserService } from '@app/core' 4import { RestExtractor, UserService } from '@app/core'
5import { UserRegister } from '@shared/models' 5import { UserRegister, UserRegistrationRequest } from '@shared/models'
6 6
7@Injectable() 7@Injectable()
8export class UserSignupService { 8export class SignupService {
9
9 constructor ( 10 constructor (
10 private authHttp: HttpClient, 11 private authHttp: HttpClient,
11 private restExtractor: RestExtractor, 12 private restExtractor: RestExtractor,
12 private userService: UserService 13 private userService: UserService
13 ) { } 14 ) { }
14 15
15 signup (userCreate: UserRegister) { 16 directSignup (userCreate: UserRegister) {
16 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) 17 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
17 .pipe( 18 .pipe(
18 tap(() => this.userService.setSignupInThisSession(true)), 19 tap(() => this.userService.setSignupInThisSession(true)),
@@ -20,8 +21,21 @@ export class UserSignupService {
20 ) 21 )
21 } 22 }
22 23
23 verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) { 24 requestSignup (userCreate: UserRegistrationRequest) {
24 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` 25 return this.authHttp.post(UserService.BASE_USERS_URL + 'registrations/request', userCreate)
26 .pipe(catchError(err => this.restExtractor.handleError(err)))
27 }
28
29 // ---------------------------------------------------------------------------
30
31 verifyUserEmail (options: {
32 userId: number
33 verificationString: string
34 isPendingEmail: boolean
35 }) {
36 const { userId, verificationString, isPendingEmail } = options
37
38 const url = `${UserService.BASE_USERS_URL}${userId}/verify-email`
25 const body = { 39 const body = {
26 verificationString, 40 verificationString,
27 isPendingEmail 41 isPendingEmail
@@ -31,13 +45,28 @@ export class UserSignupService {
31 .pipe(catchError(res => this.restExtractor.handleError(res))) 45 .pipe(catchError(res => this.restExtractor.handleError(res)))
32 } 46 }
33 47
48 verifyRegistrationEmail (options: {
49 registrationId: number
50 verificationString: string
51 }) {
52 const { registrationId, verificationString } = options
53
54 const url = `${UserService.BASE_USERS_URL}registrations/${registrationId}/verify-email`
55 const body = { verificationString }
56
57 return this.authHttp.post(url, body)
58 .pipe(catchError(res => this.restExtractor.handleError(res)))
59 }
60
34 askSendVerifyEmail (email: string) { 61 askSendVerifyEmail (email: string) {
35 const url = UserService.BASE_USERS_URL + '/ask-send-verify-email' 62 const url = UserService.BASE_USERS_URL + 'ask-send-verify-email'
36 63
37 return this.authHttp.post(url, { email }) 64 return this.authHttp.post(url, { email })
38 .pipe(catchError(err => this.restExtractor.handleError(err))) 65 .pipe(catchError(err => this.restExtractor.handleError(err)))
39 } 66 }
40 67
68 // ---------------------------------------------------------------------------
69
41 getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { 70 getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
42 // Don't update display name, the user seems to have changed it 71 // Don't update display name, the user seems to have changed it
43 if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername 72 if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index c5d08ab75..15b1a3c4a 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -103,7 +103,9 @@
103 <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a> 103 <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a>
104 <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a> 104 <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a>
105 105
106 <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> 106 <a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">
107 <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
108 </a>
107 </div> 109 </div>
108 110
109 <ng-container *ngFor="let menuSection of menuSections" > 111 <ng-container *ngFor="let menuSection of menuSections" >
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index 568cb98bb..fc6d74cff 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -92,6 +92,10 @@ export class MenuComponent implements OnInit {
92 return this.languageChooserModal.getCurrentLanguage() 92 return this.languageChooserModal.getCurrentLanguage()
93 } 93 }
94 94
95 get requiresApproval () {
96 return this.serverConfig.signup.requiresApproval
97 }
98
95 ngOnInit () { 99 ngOnInit () {
96 this.htmlServerConfig = this.serverService.getHTMLConfig() 100 this.htmlServerConfig = this.serverService.getHTMLConfig()
97 this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() 101 this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts
index b93de75ea..ed6e0582e 100644
--- a/client/src/app/shared/form-validators/user-validators.ts
+++ b/client/src/app/shared/form-validators/user-validators.ts
@@ -136,13 +136,6 @@ export const USER_DESCRIPTION_VALIDATOR: BuildFormValidator = {
136 } 136 }
137} 137}
138 138
139export const USER_TERMS_VALIDATOR: BuildFormValidator = {
140 VALIDATORS: [ Validators.requiredTrue ],
141 MESSAGES: {
142 required: $localize`You must agree with the instance terms in order to register on it.`
143 }
144}
145
146export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = { 139export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = {
147 VALIDATORS: [ 140 VALIDATORS: [
148 Validators.minLength(3), 141 Validators.minLength(3),
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
index 089be501d..2d3e26a25 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html
+++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html
@@ -8,7 +8,7 @@
8 8
9 <span class="moderation-expanded-text"> 9 <span class="moderation-expanded-text">
10 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 10 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
11 class="chip" 11 class="chip me-1"
12 > 12 >
13 <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar> 13 <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar>
14 <div> 14 <div>
@@ -29,7 +29,7 @@
29 <span class="moderation-expanded-label" i18n>Reportee</span> 29 <span class="moderation-expanded-label" i18n>Reportee</span>
30 <span class="moderation-expanded-text"> 30 <span class="moderation-expanded-text">
31 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }" 31 <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
32 class="chip" 32 class="chip me-1"
33 > 33 >
34 <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar> 34 <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar>
35 <div> 35 <div>
@@ -63,7 +63,7 @@
63 <div *ngIf="predefinedReasons" class="mt-2 d-flex"> 63 <div *ngIf="predefinedReasons" class="mt-2 d-flex">
64 <span> 64 <span>
65 <a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]" 65 <a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]"
66 [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" 66 [queryParams]="{ 'search': 'tag:' + reason.id }" class="pt-badge badge-secondary"
67 > 67 >
68 <div>{{ reason.label }}</div> 68 <div>{{ reason.label }}</div>
69 </a> 69 </a>
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts
index b80ddb9f5..dd41a5f05 100644
--- a/client/src/app/shared/shared-main/account/index.ts
+++ b/client/src/app/shared/shared-main/account/index.ts
@@ -1,3 +1,4 @@
1export * from './account.model' 1export * from './account.model'
2export * from './account.service' 2export * from './account.service'
3export * from './actor.model' 3export * from './actor.model'
4export * from './signup-label.component'
diff --git a/client/src/app/shared/shared-main/account/signup-label.component.html b/client/src/app/shared/shared-main/account/signup-label.component.html
new file mode 100644
index 000000000..35d6c5360
--- /dev/null
+++ b/client/src/app/shared/shared-main/account/signup-label.component.html
@@ -0,0 +1,2 @@
1<ng-container i18n *ngIf="requiresApproval">Request an account</ng-container>
2<ng-container i18n *ngIf="!requiresApproval">Create an account</ng-container>
diff --git a/client/src/app/shared/shared-main/account/signup-label.component.ts b/client/src/app/shared/shared-main/account/signup-label.component.ts
new file mode 100644
index 000000000..caacb9c6f
--- /dev/null
+++ b/client/src/app/shared/shared-main/account/signup-label.component.ts
@@ -0,0 +1,9 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-signup-label',
5 templateUrl: './signup-label.component.html'
6})
7export class SignupLabelComponent {
8 @Input() requiresApproval: boolean
9}
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index c1523bc50..eb1642d97 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -16,7 +16,7 @@ import {
16import { LoadingBarModule } from '@ngx-loading-bar/core' 16import { LoadingBarModule } from '@ngx-loading-bar/core'
17import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 17import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
18import { SharedGlobalIconModule } from '../shared-icons' 18import { SharedGlobalIconModule } from '../shared-icons'
19import { AccountService } from './account' 19import { AccountService, SignupLabelComponent } from './account'
20import { 20import {
21 AutofocusDirective, 21 AutofocusDirective,
22 BytesPipe, 22 BytesPipe,
@@ -113,6 +113,8 @@ import { VideoChannelService } from './video-channel'
113 UserQuotaComponent, 113 UserQuotaComponent,
114 UserNotificationsComponent, 114 UserNotificationsComponent,
115 115
116 SignupLabelComponent,
117
116 EmbedComponent, 118 EmbedComponent,
117 119
118 PluginPlaceholderComponent, 120 PluginPlaceholderComponent,
@@ -171,6 +173,8 @@ import { VideoChannelService } from './video-channel'
171 UserQuotaComponent, 173 UserQuotaComponent,
172 UserNotificationsComponent, 174 UserNotificationsComponent,
173 175
176 SignupLabelComponent,
177
174 EmbedComponent, 178 EmbedComponent,
175 179
176 PluginPlaceholderComponent, 180 PluginPlaceholderComponent,
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index bf8870a79..96e7b4dd0 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -83,6 +83,11 @@ export class UserNotification implements UserNotificationServer {
83 latestVersion: string 83 latestVersion: string
84 } 84 }
85 85
86 registration?: {
87 id: number
88 username: string
89 }
90
86 createdAt: string 91 createdAt: string
87 updatedAt: string 92 updatedAt: string
88 93
@@ -97,6 +102,8 @@ export class UserNotification implements UserNotificationServer {
97 102
98 accountUrl?: string 103 accountUrl?: string
99 104
105 registrationsUrl?: string
106
100 videoImportIdentifier?: string 107 videoImportIdentifier?: string
101 videoImportUrl?: string 108 videoImportUrl?: string
102 109
@@ -135,6 +142,7 @@ export class UserNotification implements UserNotificationServer {
135 142
136 this.plugin = hash.plugin 143 this.plugin = hash.plugin
137 this.peertube = hash.peertube 144 this.peertube = hash.peertube
145 this.registration = hash.registration
138 146
139 this.createdAt = hash.createdAt 147 this.createdAt = hash.createdAt
140 this.updatedAt = hash.updatedAt 148 this.updatedAt = hash.updatedAt
@@ -208,6 +216,10 @@ export class UserNotification implements UserNotificationServer {
208 this.accountUrl = this.buildAccountUrl(this.account) 216 this.accountUrl = this.buildAccountUrl(this.account)
209 break 217 break
210 218
219 case UserNotificationType.NEW_USER_REGISTRATION_REQUEST:
220 this.registrationsUrl = '/admin/moderation/registrations/list'
221 break
222
211 case UserNotificationType.NEW_FOLLOW: 223 case UserNotificationType.NEW_FOLLOW:
212 this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) 224 this.accountUrl = this.buildAccountUrl(this.actorFollow.follower)
213 break 225 break
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index e7cdb0183..a51e08292 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -215,6 +215,14 @@
215 </div> 215 </div>
216 </ng-container> 216 </ng-container>
217 217
218 <ng-container *ngSwitchCase="20"> <!-- UserNotificationType.NEW_USER_REGISTRATION_REQUEST -->
219 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
220
221 <div class="message" i18n>
222 User <a (click)="markAsRead(notification)" [routerLink]="notification.registrationsUrl">{{ notification.registration.username }}</a> wants to register on your instance
223 </div>
224 </ng-container>
225
218 <ng-container *ngSwitchDefault> 226 <ng-container *ngSwitchDefault>
219 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> 227 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
220 228
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.scss b/client/src/app/shared/shared-moderation/account-blocklist.component.scss
index 8b1239d34..00aaf3b9c 100644
--- a/client/src/app/shared/shared-moderation/account-blocklist.component.scss
+++ b/client/src/app/shared/shared-moderation/account-blocklist.component.scss
@@ -1,10 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4.chip {
5 @include chip;
6}
7
8.unblock-button { 4.unblock-button {
9 @include peertube-button; 5 @include peertube-button;
10 @include grey-button; 6 @include grey-button;
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss
index eaf5a8250..7c1e308cf 100644
--- a/client/src/app/shared/shared-moderation/moderation.scss
+++ b/client/src/app/shared/shared-moderation/moderation.scss
@@ -40,10 +40,6 @@
40 } 40 }
41} 41}
42 42
43.chip {
44 @include chip;
45}
46
47my-action-dropdown.show { 43my-action-dropdown.show {
48 ::ng-deep .dropdown-root { 44 ::ng-deep .dropdown-root {
49 display: block !important; 45 display: block !important;
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.scss b/client/src/app/shared/shared-moderation/server-blocklist.component.scss
index e29668a23..1a6b0435f 100644
--- a/client/src/app/shared/shared-moderation/server-blocklist.component.scss
+++ b/client/src/app/shared/shared-moderation/server-blocklist.component.scss
@@ -24,7 +24,3 @@ a {
24.block-button { 24.block-button {
25 @include create-button; 25 @include create-button;
26} 26}
27
28.chip {
29 @include chip;
30}
diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts
index 20e60486d..95d90e49e 100644
--- a/client/src/app/shared/shared-users/index.ts
+++ b/client/src/app/shared/shared-users/index.ts
@@ -1,5 +1,4 @@
1export * from './user-admin.service' 1export * from './user-admin.service'
2export * from './user-signup.service'
3export * from './two-factor.service' 2export * from './two-factor.service'
4 3
5export * from './shared-users.module' 4export * from './shared-users.module'
diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts
index 5a1675dc9..efffc6026 100644
--- a/client/src/app/shared/shared-users/shared-users.module.ts
+++ b/client/src/app/shared/shared-users/shared-users.module.ts
@@ -1,9 +1,7 @@
1
2import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
3import { SharedMainModule } from '../shared-main/shared-main.module' 2import { SharedMainModule } from '../shared-main/shared-main.module'
4import { TwoFactorService } from './two-factor.service' 3import { TwoFactorService } from './two-factor.service'
5import { UserAdminService } from './user-admin.service' 4import { UserAdminService } from './user-admin.service'
6import { UserSignupService } from './user-signup.service'
7 5
8@NgModule({ 6@NgModule({
9 imports: [ 7 imports: [
@@ -15,7 +13,6 @@ import { UserSignupService } from './user-signup.service'
15 exports: [], 13 exports: [],
16 14
17 providers: [ 15 providers: [
18 UserSignupService,
19 UserAdminService, 16 UserAdminService,
20 TwoFactorService 17 TwoFactorService
21 ] 18 ]
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index 6fdf24b2d..227c12130 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -53,8 +53,8 @@
53 <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> 53 <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
54 </div> 54 </div>
55 55
56 <div *ngIf="containedInPlaylists" class="video-contained-in-playlists"> 56 <div *ngIf="containedInPlaylists" class="fs-6">
57 <a *ngFor="let playlist of containedInPlaylists" class="chip rectangular bg-secondary text-light" [routerLink]="['/w/p/', playlist.playlistShortUUID]"> 57 <a *ngFor="let playlist of containedInPlaylists" class="pt-badge badge-secondary" [routerLink]="['/w/p/', playlist.playlistShortUUID]">
58 {{ playlist.playlistDisplayName }} 58 {{ playlist.playlistDisplayName }}
59 </a> 59 </a>
60 </div> 60 </div>
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
index ba2adfc5a..a397efdca 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
@@ -4,10 +4,6 @@
4 4
5$more-button-width: 40px; 5$more-button-width: 40px;
6 6
7.chip {
8 @include chip;
9}
10
11.video-miniature { 7.video-miniature {
12 font-size: 14px; 8 font-size: 14px;
13} 9}
diff --git a/client/src/sass/class-helpers.scss b/client/src/sass/class-helpers.scss
index bc965331a..feb3a6de2 100644
--- a/client/src/sass/class-helpers.scss
+++ b/client/src/sass/class-helpers.scss
@@ -284,3 +284,9 @@ label + .form-group-description {
284 border: 2px solid pvar(--mainColorLightest); 284 border: 2px solid pvar(--mainColorLightest);
285 } 285 }
286} 286}
287
288// ---------------------------------------------------------------------------
289
290.chip {
291 @include chip;
292}
diff --git a/client/src/sass/include/_badges.scss b/client/src/sass/include/_badges.scss
index 4bc70d4a9..7efd2fb81 100644
--- a/client/src/sass/include/_badges.scss
+++ b/client/src/sass/include/_badges.scss
@@ -9,6 +9,10 @@
9 font-weight: $font-semibold; 9 font-weight: $font-semibold;
10 line-height: 1.1; 10 line-height: 1.1;
11 11
12 &.badge-fs-normal {
13 font-size: 100%;
14 }
15
12 &.badge-primary { 16 &.badge-primary {
13 color: pvar(--mainBackgroundColor); 17 color: pvar(--mainBackgroundColor);
14 background-color: pvar(--mainColor); 18 background-color: pvar(--mainColor);
diff --git a/client/src/sass/include/_fonts.scss b/client/src/sass/include/_fonts.scss
index e5a40af34..514261d01 100644
--- a/client/src/sass/include/_fonts.scss
+++ b/client/src/sass/include/_fonts.scss
@@ -15,7 +15,3 @@
15 font-display: swap; 15 font-display: swap;
16 src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2'); 16 src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2');
17} 17}
18
19@mixin muted {
20 color: pvar(--greyForegroundColor) !important;
21}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index b5ccb6598..8816437d9 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -36,6 +36,10 @@
36 max-height: $font-size * $number-of-lines; 36 max-height: $font-size * $number-of-lines;
37} 37}
38 38
39@mixin muted {
40 color: pvar(--greyForegroundColor) !important;
41}
42
39@mixin fade-text ($fade-after, $background-color) { 43@mixin fade-text ($fade-after, $background-color) {
40 position: relative; 44 position: relative;
41 overflow: hidden; 45 overflow: hidden;
@@ -791,51 +795,39 @@
791} 795}
792 796
793@mixin chip { 797@mixin chip {
794 --chip-radius: 5rem; 798 --avatar-size: 1.2rem;
795 --chip-padding: .2rem .4rem;
796 $avatar-height: 1.2rem;
797 799
798 align-items: center;
799 border-radius: var(--chip-radius);
800 display: inline-flex; 800 display: inline-flex;
801 font-size: 90%;
802 color: pvar(--mainForegroundColor); 801 color: pvar(--mainForegroundColor);
803 height: $avatar-height; 802 height: var(--avatar-size);
804 line-height: 1rem;
805 margin: .1rem;
806 max-width: 320px; 803 max-width: 320px;
807 overflow: hidden; 804 overflow: hidden;
808 padding: var(--chip-padding);
809 text-decoration: none; 805 text-decoration: none;
810 text-overflow: ellipsis; 806 text-overflow: ellipsis;
811 vertical-align: middle; 807 vertical-align: middle;
812 white-space: nowrap; 808 white-space: nowrap;
813 809
814 &.rectangular {
815 --chip-radius: .2rem;
816 --chip-padding: .2rem .3rem;
817 }
818
819 my-actor-avatar { 810 my-actor-avatar {
820 @include margin-left(-.4rem);
821 @include margin-right(.2rem); 811 @include margin-right(.2rem);
812
813 border-radius: 5rem;
814 width: var(--avatar-size);
815 height: var(--avatar-size);
822 } 816 }
823 817
824 &.two-lines { 818 &.two-lines {
825 $avatar-height: 2rem; 819 --avatar-size: 2rem;
826 820
827 height: $avatar-height; 821 font-size: 14px;
822 line-height: 1rem;
828 823
829 my-actor-avatar { 824 my-actor-avatar {
830 display: inline-block; 825 display: inline-block;
831 } 826 }
832 827
833 div { 828 > div {
834 margin: 0 .1rem;
835
836 display: flex; 829 display: flex;
837 flex-direction: column; 830 flex-direction: column;
838 height: $avatar-height;
839 justify-content: center; 831 justify-content: center;
840 } 832 }
841 } 833 }