From 9589907c89d29a6c0acd52c8cb789af9f93ce9af Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 19 Jan 2023 09:29:47 +0100 Subject: Implement signup approval in client --- client/src/app/+admin/admin.component.ts | 12 ++ client/src/app/+admin/admin.module.ts | 16 ++- .../edit-basic-configuration.component.html | 19 +++- .../edit-custom-config.component.ts | 1 + client/src/app/+admin/moderation/index.ts | 1 + .../src/app/+admin/moderation/moderation.routes.ts | 15 ++- .../admin-registration.service.ts | 63 +++++++++++ .../+admin/moderation/registration-list/index.ts | 4 + .../process-registration-modal.component.html | 67 +++++++++++ .../process-registration-modal.component.scss | 3 + .../process-registration-modal.component.ts | 107 ++++++++++++++++++ .../process-registration-validators.ts | 11 ++ .../registration-list.component.html | 120 ++++++++++++++++++++ .../registration-list.component.scss | 7 ++ .../registration-list.component.ts | 125 +++++++++++++++++++++ .../users/user-list/user-list.component.html | 19 +--- .../users/user-list/user-list.component.scss | 10 -- .../src/app/+admin/shared/shared-admin.module.ts | 7 +- .../+admin/shared/user-email-info.component.html | 13 +++ .../+admin/shared/user-email-info.component.scss | 10 ++ .../app/+admin/shared/user-email-info.component.ts | 20 ++++ client/src/app/+admin/system/jobs/job.service.ts | 2 +- .../src/app/+admin/system/jobs/jobs.component.ts | 2 +- client/src/app/+login/login.component.ts | 28 ++++- .../my-ownership/my-ownership.component.scss | 4 - .../app/+signup/+register/register.component.html | 35 +++--- .../app/+signup/+register/register.component.ts | 62 ++++++---- client/src/app/+signup/+register/shared/index.ts | 1 + .../+register/shared/register-validators.ts | 18 +++ .../steps/register-step-about.component.html | 4 + .../steps/register-step-about.component.ts | 1 + .../steps/register-step-channel.component.ts | 6 +- .../steps/register-step-terms.component.html | 14 ++- .../steps/register-step-terms.component.ts | 10 +- .../steps/register-step-user.component.ts | 6 +- .../verify-account-ask-send-email.component.ts | 6 +- .../verify-account-email.component.html | 17 ++- .../verify-account-email.component.ts | 86 ++++++++++++-- .../src/app/+signup/shared/shared-signup.module.ts | 11 +- .../signup-success-after-email.component.html | 21 ++++ .../shared/signup-success-after-email.component.ts | 10 ++ .../signup-success-before-email.component.html | 35 ++++++ .../signup-success-before-email.component.ts | 12 ++ .../+signup/shared/signup-success.component.html | 22 ---- .../app/+signup/shared/signup-success.component.ts | 19 ---- client/src/app/+signup/shared/signup.service.ts | 85 ++++++++++++++ client/src/app/menu/menu.component.html | 4 +- client/src/app/menu/menu.component.ts | 4 + .../app/shared/form-validators/user-validators.ts | 7 -- .../shared-abuse-list/abuse-details.component.html | 6 +- client/src/app/shared/shared-main/account/index.ts | 1 + .../account/signup-label.component.html | 2 + .../shared-main/account/signup-label.component.ts | 9 ++ .../app/shared/shared-main/shared-main.module.ts | 6 +- .../shared-main/users/user-notification.model.ts | 12 ++ .../users/user-notifications.component.html | 8 ++ .../account-blocklist.component.scss | 4 - .../app/shared/shared-moderation/moderation.scss | 4 - .../server-blocklist.component.scss | 4 - client/src/app/shared/shared-users/index.ts | 1 - .../app/shared/shared-users/shared-users.module.ts | 3 - .../app/shared/shared-users/user-signup.service.ts | 56 --------- .../video-miniature.component.html | 4 +- .../video-miniature.component.scss | 4 - client/src/sass/class-helpers.scss | 6 + client/src/sass/include/_badges.scss | 4 + client/src/sass/include/_fonts.scss | 4 - client/src/sass/include/_mixins.scss | 36 +++--- 68 files changed, 1091 insertions(+), 265 deletions(-) create mode 100644 client/src/app/+admin/moderation/registration-list/admin-registration.service.ts create mode 100644 client/src/app/+admin/moderation/registration-list/index.ts create mode 100644 client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html create mode 100644 client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss create mode 100644 client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts create mode 100644 client/src/app/+admin/moderation/registration-list/process-registration-validators.ts create mode 100644 client/src/app/+admin/moderation/registration-list/registration-list.component.html create mode 100644 client/src/app/+admin/moderation/registration-list/registration-list.component.scss create mode 100644 client/src/app/+admin/moderation/registration-list/registration-list.component.ts create mode 100644 client/src/app/+admin/shared/user-email-info.component.html create mode 100644 client/src/app/+admin/shared/user-email-info.component.scss create mode 100644 client/src/app/+admin/shared/user-email-info.component.ts create mode 100644 client/src/app/+signup/+register/shared/index.ts create mode 100644 client/src/app/+signup/+register/shared/register-validators.ts create mode 100644 client/src/app/+signup/shared/signup-success-after-email.component.html create mode 100644 client/src/app/+signup/shared/signup-success-after-email.component.ts create mode 100644 client/src/app/+signup/shared/signup-success-before-email.component.html create mode 100644 client/src/app/+signup/shared/signup-success-before-email.component.ts delete mode 100644 client/src/app/+signup/shared/signup-success.component.html delete mode 100644 client/src/app/+signup/shared/signup-success.component.ts create mode 100644 client/src/app/+signup/shared/signup.service.ts create mode 100644 client/src/app/shared/shared-main/account/signup-label.component.html create mode 100644 client/src/app/shared/shared-main/account/signup-label.component.ts delete mode 100644 client/src/app/shared/shared-users/user-signup.service.ts 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 { children: [] } + if (this.hasRegistrationsRight()) { + moderationItems.children.push({ + label: $localize`Registrations`, + routerLink: '/admin/moderation/registrations/list', + iconName: 'user' + }) + } + if (this.hasAbusesRight()) { moderationItems.children.push({ label: $localize`Reports`, @@ -229,4 +237,8 @@ export class AdminComponent implements OnInit { private hasVideosRight () { return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS) } + + private hasRegistrationsRight () { + return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS) + } } 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 import { FollowingListComponent } from './follows/following-list/following-list.component' import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' -import { AbuseListComponent, VideoBlockListComponent } from './moderation' +import { + AbuseListComponent, + AdminRegistrationService, + ProcessRegistrationModalComponent, + RegistrationListComponent, + VideoBlockListComponent +} from './moderation' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' import { UserCreateComponent, @@ -116,7 +122,10 @@ import { JobsComponent } from './system/jobs/jobs.component' EditLiveConfigurationComponent, EditAdvancedConfigurationComponent, EditInstanceInformationComponent, - EditHomepageComponent + EditHomepageComponent, + + RegistrationListComponent, + ProcessRegistrationModalComponent ], exports: [ @@ -130,7 +139,8 @@ import { JobsComponent } from './system/jobs/jobs.component' ConfigService, PluginApiService, EditConfigurationService, - VideoAdminService + VideoAdminService, + AdminRegistrationService ] }) export 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 @@ - +
+ +
-
+
+ +
+ +
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 { signup: { enabled: null, limit: SIGNUP_LIMIT_VALIDATOR, + requiresApproval: null, requiresEmailVerification: null, minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR }, 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 @@ export * from './abuse-list' export * from './instance-blocklist' export * from './video-block-list' +export * from './registration-list' export * 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 import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' import { UserRightGuard } from '@app/core' import { UserRight } from '@shared/models' +import { RegistrationListComponent } from './registration-list' export const ModerationRoutes: Routes = [ { @@ -68,7 +69,19 @@ export const ModerationRoutes: Routes = [ } }, - // We move this component in admin overview pages + { + path: 'registrations/list', + component: RegistrationListComponent, + canActivate: [ UserRightGuard ], + data: { + userRight: UserRight.MANAGE_REGISTRATIONS, + meta: { + title: $localize`User registrations` + } + } + }, + + // We moved this component in admin overview pages { path: 'video-comments', 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 @@ +import { SortMeta } from 'primeng/api' +import { catchError } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService } from '@app/core' +import { ResultList, UserRegistration } from '@shared/models' +import { environment } from '../../../../environments/environment' + +@Injectable() +export class AdminRegistrationService { + private static BASE_REGISTRATION_URL = environment.apiUrl + '/api/v1/users/registrations' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService + ) { } + + listRegistrations (options: { + pagination: RestPagination + sort: SortMeta + search?: string + }) { + const { pagination, sort, search } = options + + const url = AdminRegistrationService.BASE_REGISTRATION_URL + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (search) { + params = params.append('search', search) + } + + return this.authHttp.get>(url, { params }) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } + + acceptRegistration (registration: UserRegistration, moderationResponse: string) { + const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept' + const body = { moderationResponse } + + return this.authHttp.post(url, body) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + rejectRegistration (registration: UserRegistration, moderationResponse: string) { + const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject' + const body = { moderationResponse } + + return this.authHttp.post(url, body) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + removeRegistration (registration: UserRegistration) { + const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + + return this.authHttp.delete(url) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } +} diff --git a/client/src/app/+admin/moderation/registration-list/index.ts b/client/src/app/+admin/moderation/registration-list/index.ts new file mode 100644 index 000000000..060b676a4 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/index.ts @@ -0,0 +1,4 @@ +export * from './admin-registration.service' +export * from './process-registration-modal.component' +export * from './process-registration-validators' +export * from './registration-list.component' diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html new file mode 100644 index 000000000..7a33bb94b --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html @@ -0,0 +1,67 @@ + + + +
+ + + +
+
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss new file mode 100644 index 000000000..3e03bed89 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss @@ -0,0 +1,3 @@ +@use '_variables' as *; +@use '_mixins' as *; + diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts new file mode 100644 index 000000000..fbe8deb41 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts @@ -0,0 +1,107 @@ +import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' +import { Notifier, ServerService } from '@app/core' +import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' +import { UserRegistration } from '@shared/models' +import { AdminRegistrationService } from './admin-registration.service' +import { REGISTRATION_MODERATION_RESPONSE_VALIDATOR } from './process-registration-validators' + +@Component({ + selector: 'my-process-registration-modal', + templateUrl: './process-registration-modal.component.html', + styleUrls: [ './process-registration-modal.component.scss' ] +}) +export class ProcessRegistrationModalComponent extends FormReactive implements OnInit { + @ViewChild('modal', { static: true }) modal: NgbModal + + @Output() registrationProcessed = new EventEmitter() + + registration: UserRegistration + + private openedModal: NgbModalRef + private processMode: 'accept' | 'reject' + + constructor ( + protected formReactiveService: FormReactiveService, + private server: ServerService, + private modalService: NgbModal, + private notifier: Notifier, + private registrationService: AdminRegistrationService + ) { + super() + } + + ngOnInit () { + this.buildForm({ + moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR + }) + } + + isAccept () { + return this.processMode === 'accept' + } + + isReject () { + return this.processMode === 'reject' + } + + openModal (registration: UserRegistration, mode: 'accept' | 'reject') { + this.processMode = mode + this.registration = registration + + this.openedModal = this.modalService.open(this.modal, { centered: true }) + } + + hide () { + this.form.reset() + + this.openedModal.close() + } + + getSubmitValue () { + if (this.isAccept()) { + return $localize`Accept registration` + } + + return $localize`Reject registration` + } + + processRegistration () { + if (this.isAccept()) return this.acceptRegistration() + + return this.rejectRegistration() + } + + isEmailEnabled () { + return this.server.getHTMLConfig().email.enabled + } + + private acceptRegistration () { + this.registrationService.acceptRegistration(this.registration, this.form.value.moderationResponse) + .subscribe({ + next: () => { + this.notifier.success($localize`${this.registration.username} account created`) + + this.registrationProcessed.emit() + this.hide() + }, + + error: err => this.notifier.error(err.message) + }) + } + + private rejectRegistration () { + this.registrationService.rejectRegistration(this.registration, this.form.value.moderationResponse) + .subscribe({ + next: () => { + this.notifier.success($localize`${this.registration.username} registration rejected`) + + this.registrationProcessed.emit() + this.hide() + }, + + error: err => this.notifier.error(err.message) + }) + } +} diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts new file mode 100644 index 000000000..e01a07d9d --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts @@ -0,0 +1,11 @@ +import { Validators } from '@angular/forms' +import { BuildFormValidator } from '@app/shared/form-validators' + +export const REGISTRATION_MODERATION_RESPONSE_VALIDATOR: BuildFormValidator = { + VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], + MESSAGES: { + required: $localize`Moderation response is required.`, + minlength: $localize`Moderation response must be at least 2 characters long.`, + maxlength: $localize`Moderation response cannot be more than 3000 characters long.` + } +} diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.html b/client/src/app/+admin/moderation/registration-list/registration-list.component.html new file mode 100644 index 000000000..4f9d06acc --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.html @@ -0,0 +1,120 @@ +

+ + Registration requests +

+ + + +
+
+ +
+
+
+ + + + + + Account + Email + Channel + Registration reason + State + Moderation response + Requested on + + + + + + + + + + + + + + +
+
+ {{ registration.username }} + {{ registration.accountDisplayName }} +
+
+ + + + + + + +
+
+ {{ registration.channelHandle }} + {{ registration.channelDisplayName }} +
+
+ + + + {{ registration.registrationReason }} + + + + + + + + + {{ registration.moderationResponse }} + + + {{ registration.createdAt | date: 'short' }} + +
+ + + + +
+
+
+ Registration reason: + +
+ +
+ Moderation response: + +
+
+
+ + +
+ + + + +
+ No registrations found matching current filters. + No registrations found. +
+ + +
+
+ + diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.scss b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss new file mode 100644 index 000000000..9cae08e85 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss @@ -0,0 +1,7 @@ +@use '_mixins' as *; +@use '_variables' as *; + +my-global-icon { + width: 24px; + height: 24px; +} diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts new file mode 100644 index 000000000..37514edf5 --- /dev/null +++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts @@ -0,0 +1,125 @@ +import { SortMeta } from 'primeng/api' +import { Component, OnInit, ViewChild } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' +import { AdvancedInputFilter } from '@app/shared/shared-forms' +import { DropdownAction } from '@app/shared/shared-main' +import { UserRegistration, UserRegistrationState } from '@shared/models' +import { AdminRegistrationService } from './admin-registration.service' +import { ProcessRegistrationModalComponent } from './process-registration-modal.component' + +@Component({ + selector: 'my-registration-list', + templateUrl: './registration-list.component.html', + styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './registration-list.component.scss' ] +}) +export class RegistrationListComponent extends RestTable implements OnInit { + @ViewChild('processRegistrationModal', { static: true }) processRegistrationModal: ProcessRegistrationModalComponent + + registrations: (UserRegistration & { registrationReasonHTML?: string, moderationResponseHTML?: string })[] = [] + totalRecords = 0 + sort: SortMeta = { field: 'createdAt', order: -1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + registrationActions: DropdownAction[][] = [] + + inputFilters: AdvancedInputFilter[] = [] + + requiresEmailVerification: boolean + + constructor ( + protected route: ActivatedRoute, + protected router: Router, + private server: ServerService, + private notifier: Notifier, + private markdownRenderer: MarkdownService, + private adminRegistrationService: AdminRegistrationService + ) { + super() + + this.registrationActions = [ + [ + { + label: $localize`Accept this registration`, + handler: registration => this.openRegistrationRequestProcessModal(registration, 'accept'), + isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING + }, + { + label: $localize`Reject this registration`, + handler: registration => this.openRegistrationRequestProcessModal(registration, 'reject'), + isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING + }, + { + label: $localize`Remove this registration request`, + handler: registration => this.removeRegistration(registration), + isDisplayed: registration => registration.state.id !== UserRegistrationState.PENDING + } + ] + ] + } + + ngOnInit () { + this.initialize() + + this.server.getConfig() + .subscribe(config => { + this.requiresEmailVerification = config.signup.requiresEmailVerification + }) + } + + getIdentifier () { + return 'RegistrationListComponent' + } + + isRegistrationAccepted (registration: UserRegistration) { + return registration.state.id === UserRegistrationState.ACCEPTED + } + + isRegistrationRejected (registration: UserRegistration) { + return registration.state.id === UserRegistrationState.REJECTED + } + + onRegistrationProcessed () { + this.reloadData() + } + + protected reloadData () { + this.adminRegistrationService.listRegistrations({ + pagination: this.pagination, + sort: this.sort, + search: this.search + }).subscribe({ + next: async resultList => { + this.totalRecords = resultList.total + this.registrations = resultList.data + + for (const registration of this.registrations) { + registration.registrationReasonHTML = await this.toHtml(registration.registrationReason) + registration.moderationResponseHTML = await this.toHtml(registration.moderationResponse) + } + }, + + error: err => this.notifier.error(err.message) + }) + } + + private openRegistrationRequestProcessModal (registration: UserRegistration, mode: 'accept' | 'reject') { + this.processRegistrationModal.openModal(registration, mode) + } + + private removeRegistration (registration: UserRegistration) { + this.adminRegistrationService.removeRegistration(registration) + .subscribe({ + next: () => { + this.notifier.success($localize`Registration request deleted.`) + this.reloadData() + }, + + error: err => this.notifier.error(err.message) + }) + } + + private toHtml (text: string) { + return this.markdownRenderer.textMarkdownToHTML({ markdown: text }) + } +} 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 @@
- {{ user.account.displayName }} + {{ user.account.displayName }} {{ user.username }}
@@ -110,23 +110,10 @@ {{ user.role.label }} - - - {{ user.email }} - + + - - - ? {{ user.email }} - - - - ✓ {{ user.email }} - - - -
td { background-color: lighten($color: $red, $amount: 40) !important; } -.table-email { - @include disable-default-a-behaviour; - - color: pvar(--mainForegroundColor); -} - .banned-info { font-style: italic; } @@ -37,10 +31,6 @@ my-global-icon { width: 18px; } -.chip { - @include chip; -} - .progress { @include progressbar($small: true); 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 @@ import { NgModule } from '@angular/core' import { SharedMainModule } from '../../shared/shared-main/shared-main.module' +import { UserEmailInfoComponent } from './user-email-info.component' import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' @NgModule({ @@ -8,11 +9,13 @@ import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' ], declarations: [ - UserRealQuotaInfoComponent + UserRealQuotaInfoComponent, + UserEmailInfoComponent ], exports: [ - UserRealQuotaInfoComponent + UserRealQuotaInfoComponent, + UserEmailInfoComponent ], 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 @@ + + + + {{ entry.email }} + + + + ? {{ entry.email }} + + ✓ {{ entry.email }} + + + 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 @@ +@use '_variables' as *; +@use '_mixins' as *; + +a { + color: pvar(--mainForegroundColor); + + &:hover { + text-decoration: underline; + } +} 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 @@ +import { Component, Input } from '@angular/core' +import { User, UserRegistration } from '@shared/models/users' + +@Component({ + selector: 'my-user-email-info', + templateUrl: './user-email-info.component.html', + styleUrls: [ './user-email-info.component.scss' ] +}) +export class UserEmailInfoComponent { + @Input() entry: User | UserRegistration + @Input() requiresEmailVerification: boolean + + getTitle () { + if (this.entry.emailVerified) { + return $localize`User email has been verified` + } + + return $localize`User email hasn't been verified` + } +} 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 { private restExtractor: RestExtractor ) {} - getJobs (options: { + listJobs (options: { jobState?: JobStateClient jobType: JobTypeClient 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 { if (this.jobState === 'all') jobState = null this.jobsService - .getJobs({ + .listJobs({ jobState, jobType: this.jobType, 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 import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' import { getExternalAuthHref } from '@shared/core-utils' -import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' +import { RegisteredExternalAuthConfig, ServerConfig, ServerErrorCode } from '@shared/models' @Component({ selector: 'my-login', @@ -197,6 +197,8 @@ The link will expire within 1 hour.` } private handleError (err: any) { + console.log(err) + if (this.authService.isOTPMissingError(err)) { this.otpStep = true @@ -208,8 +210,26 @@ The link will expire within 1 hour.` return } - if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.` - else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.` - else this.error = err.message + if (err.message.includes('credentials are invalid')) { + this.error = $localize`Incorrect username or password.` + return + } + + if (err.message.includes('blocked')) { + this.error = $localize`Your account is blocked.` + return + } + + if (err.body?.code === ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL) { + this.error = $localize`This account is awaiting approval by moderators.` + return + } + + if (err.body?.code === ServerErrorCode.ACCOUNT_APPROVAL_REJECTED) { + this.error = $localize`Registration approval has been rejected for this account.` + return + } + + this.error = err.message } } 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 @@ @use '_miniature' as *; @use '_mixins' as *; -.chip { - @include chip; -} - .video-table-video { display: inline-flex; 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 @@
-

+

{{ instanceName }} > - Create an account +

- - Create an account -
on {{ instanceName }}
+ + + + + +
on {{ instanceName }}
- +
I already have an account, I log in - +
@@ -44,8 +49,8 @@ > @@ -94,14 +99,15 @@
You will be able to create a channel later
-
-
+ +
PeerTube is creating your account...
@@ -109,7 +115,10 @@
{{ signupError }}
- +
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' import { AuthService } from '@app/core' import { HooksService } from '@app/core/plugins/hooks.service' import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' -import { UserSignupService } from '@app/shared/shared-users' import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' import { UserRegister } from '@shared/models' import { ServerConfig } from '@shared/models/server' +import { SignupService } from '../shared/signup.service' @Component({ selector: 'my-register', @@ -53,7 +53,7 @@ export class RegisterComponent implements OnInit { constructor ( private route: ActivatedRoute, private authService: AuthService, - private userSignupService: UserSignupService, + private signupService: SignupService, private hooks: HooksService ) { } @@ -61,6 +61,10 @@ export class RegisterComponent implements OnInit { return this.serverConfig.signup.requiresEmailVerification } + get requiresApproval () { + return this.serverConfig.signup.requiresApproval + } + get minimumAge () { return this.serverConfig.signup.minimumAge } @@ -132,42 +136,49 @@ export class RegisterComponent implements OnInit { skipChannelCreation () { this.formStepChannel.reset() this.lastStep.select() + this.signup() } async signup () { this.signupError = undefined - const body: UserRegister = await this.hooks.wrapObject( + const termsForm = this.formStepTerms.value + const userForm = this.formStepUser.value + const channelForm = this.formStepChannel?.value + + const channel = this.formStepChannel?.value?.name + ? { name: channelForm?.name, displayName: channelForm?.displayName } + : undefined + + const body = await this.hooks.wrapObject( { - ...this.formStepUser.value, + username: userForm.username, + password: userForm.password, + email: userForm.email, + displayName: userForm.displayName, + + registrationReason: termsForm.registrationReason, - channel: this.formStepChannel?.value?.name - ? this.formStepChannel.value - : undefined + channel }, 'signup', 'filter:api.signup.registration.create.params' ) - this.userSignupService.signup(body).subscribe({ + const obs = this.requiresApproval + ? this.signupService.requestSignup(body) + : this.signupService.directSignup(body) + + obs.subscribe({ next: () => { - if (this.requiresEmailVerification) { + if (this.requiresEmailVerification || this.requiresApproval) { this.signupSuccess = true return } // Auto login - this.authService.login({ username: body.username, password: body.password }) - .subscribe({ - next: () => { - this.signupSuccess = true - }, - - error: err => { - this.signupError = err.message - } - }) + this.autoLogin(body) }, error: err => { @@ -175,4 +186,17 @@ export class RegisterComponent implements OnInit { } }) } + + private autoLogin (body: UserRegister) { + this.authService.login({ username: body.username, password: body.password }) + .subscribe({ + next: () => { + this.signupSuccess = true + }, + + error: err => { + this.signupError = err.message + } + }) + } } 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 @@ +import { Validators } from '@angular/forms' +import { BuildFormValidator } from '@app/shared/form-validators' + +export const REGISTER_TERMS_VALIDATOR: BuildFormValidator = { + VALIDATORS: [ Validators.requiredTrue ], + MESSAGES: { + required: $localize`You must agree with the instance terms in order to register on it.` + } +} + +export const REGISTER_REASON_VALIDATOR: BuildFormValidator = { + VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], + MESSAGES: { + required: $localize`Registration reason is required.`, + minlength: $localize`Registration reason must be at least 2 characters long.`, + maxlength: $localize`Registration reason cannot be more than 3000 characters long.` + } +} 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 @@
  • Have access to your watch history
  • Create your channel to publish videos
  • + +

    + Moderators of {{ instanceName }} will have to approve your registration request once you have finished to fill the form. +

    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' styleUrls: [ './register-step-about.component.scss' ] }) export class RegisterStepAboutComponent { + @Input() requiresApproval: boolean @Input() videoUploadDisabled: boolean 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' import { pairwise } from 'rxjs/operators' import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { FormGroup } from '@angular/forms' +import { SignupService } from '@app/+signup/shared/signup.service' import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' -import { UserSignupService } from '@app/shared/shared-users' @Component({ selector: 'my-register-step-channel', @@ -20,7 +20,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit constructor ( protected formReactiveService: FormReactiveService, - private userSignupService: UserSignupService + private signupService: SignupService ) { super() } @@ -51,7 +51,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { const name = this.form.value['name'] || '' - const newName = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, name) + const newName = this.signupService.getNewUsername(oldDisplayName, newDisplayName, name) this.form.patchValue({ name: newName }) } } 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 @@
    + +
    + + + + +
    {{ formErrors.registrationReason }}
    +
    +
    @@ -6,7 +18,7 @@ I am at least {{ minimumAge }} years old and agree to the Terms and to the Code of Conduct - of this instance + of {{ instanceName }} 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 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { FormGroup } from '@angular/forms' -import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' +import { REGISTER_REASON_VALIDATOR, REGISTER_TERMS_VALIDATOR } from '../shared' @Component({ selector: 'my-register-step-terms', @@ -10,7 +10,9 @@ import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' }) export class RegisterStepTermsComponent extends FormReactive implements OnInit { @Input() hasCodeOfConduct = false + @Input() requiresApproval: boolean @Input() minimumAge = 16 + @Input() instanceName: string @Output() formBuilt = new EventEmitter() @Output() termsClick = new EventEmitter() @@ -28,7 +30,11 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit { ngOnInit () { this.buildForm({ - terms: USER_TERMS_VALIDATOR + terms: REGISTER_TERMS_VALIDATOR, + + registrationReason: this.requiresApproval + ? REGISTER_REASON_VALIDATOR + : null }) 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' import { pairwise } from 'rxjs/operators' import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { FormGroup } from '@angular/forms' +import { SignupService } from '@app/+signup/shared/signup.service' import { USER_DISPLAY_NAME_REQUIRED_VALIDATOR, USER_EMAIL_VALIDATOR, @@ -9,7 +10,6 @@ import { USER_USERNAME_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' -import { UserSignupService } from '@app/shared/shared-users' @Component({ selector: 'my-register-step-user', @@ -24,7 +24,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { constructor ( protected formReactiveService: FormReactiveService, - private userSignupService: UserSignupService + private signupService: SignupService ) { super() } @@ -57,7 +57,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { const username = this.form.value['username'] || '' - const newUsername = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, username) + const newUsername = this.signupService.getNewUsername(oldDisplayName, newDisplayName, username) this.form.patchValue({ username: newUsername }) } } 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 @@ import { Component, OnInit } from '@angular/core' +import { SignupService } from '@app/+signup/shared/signup.service' import { Notifier, RedirectService, ServerService } from '@app/core' import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' -import { UserSignupService } from '@app/shared/shared-users' @Component({ selector: 'my-verify-account-ask-send-email', @@ -15,7 +15,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements constructor ( protected formReactiveService: FormReactiveService, - private userSignupService: UserSignupService, + private signupService: SignupService, private serverService: ServerService, private notifier: Notifier, private redirectService: RedirectService @@ -34,7 +34,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements askSendVerifyEmail () { const email = this.form.value['verify-email-email'] - this.userSignupService.askSendVerifyEmail(email) + this.signupService.askSendVerifyEmail(email) .subscribe({ next: () => { 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 @@ -
    -

    Verify account email confirmation

    +
    +

    Verify email

    - - + + -
    Email updated.
    +
    Email updated.
    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 @@ import { Component, OnInit } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { AuthService, Notifier } from '@app/core' -import { UserSignupService } from '@app/shared/shared-users' +import { SignupService } from '@app/+signup/shared/signup.service' +import { AuthService, Notifier, ServerService } from '@app/core' @Component({ selector: 'my-verify-account-email', @@ -13,32 +13,82 @@ export class VerifyAccountEmailComponent implements OnInit { failed = false isPendingEmail = false + requiresApproval: boolean + loaded = false + private userId: number + private registrationId: number private verificationString: string constructor ( - private userSignupService: UserSignupService, + private signupService: SignupService, + private server: ServerService, private authService: AuthService, private notifier: Notifier, private route: ActivatedRoute ) { } + get instanceName () { + return this.server.getHTMLConfig().instance.name + } + ngOnInit () { const queryParams = this.route.snapshot.queryParams + + this.server.getConfig().subscribe(config => { + this.requiresApproval = config.signup.requiresApproval + + this.loaded = true + }) + this.userId = queryParams['userId'] + this.registrationId = queryParams['registrationId'] + this.verificationString = queryParams['verificationString'] + this.isPendingEmail = queryParams['isPendingEmail'] === 'true' - if (!this.userId || !this.verificationString) { - this.notifier.error($localize`Unable to find user id or verification string.`) - } else { - this.verifyEmail() + if (!this.verificationString) { + this.notifier.error($localize`Unable to find verification string in URL query.`) + return + } + + if (!this.userId && !this.registrationId) { + this.notifier.error($localize`Unable to find user id or registration id in URL query.`) + return } + + this.verifyEmail() + } + + isRegistrationRequest () { + return !!this.registrationId + } + + displaySignupSuccess () { + if (!this.success) return false + if (!this.isRegistrationRequest() && this.isPendingEmail) return false + + return true } verifyEmail () { - this.userSignupService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail) + if (this.isRegistrationRequest()) { + return this.verifyRegistrationEmail() + } + + return this.verifyUserEmail() + } + + private verifyUserEmail () { + const options = { + userId: this.userId, + verificationString: this.verificationString, + isPendingEmail: this.isPendingEmail + } + + this.signupService.verifyUserEmail(options) .subscribe({ next: () => { if (this.authService.isLoggedIn()) { @@ -55,4 +105,24 @@ export class VerifyAccountEmailComponent implements OnInit { } }) } + + private verifyRegistrationEmail () { + const options = { + registrationId: this.registrationId, + verificationString: this.verificationString + } + + this.signupService.verifyRegistrationEmail(options) + .subscribe({ + next: () => { + this.success = true + }, + + error: err => { + this.failed = true + + this.notifier.error(err.message) + } + }) + } } 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' import { SharedUsersModule } from '@app/shared/shared-users' import { SignupMascotComponent } from './signup-mascot.component' import { SignupStepTitleComponent } from './signup-step-title.component' -import { SignupSuccessComponent } from './signup-success.component' +import { SignupSuccessBeforeEmailComponent } from './signup-success-before-email.component' +import { SignupSuccessAfterEmailComponent } from './signup-success-after-email.component' +import { SignupService } from './signup.service' @NgModule({ imports: [ @@ -16,7 +18,8 @@ import { SignupSuccessComponent } from './signup-success.component' ], declarations: [ - SignupSuccessComponent, + SignupSuccessBeforeEmailComponent, + SignupSuccessAfterEmailComponent, SignupStepTitleComponent, SignupMascotComponent ], @@ -26,12 +29,14 @@ import { SignupSuccessComponent } from './signup-success.component' SharedFormModule, SharedGlobalIconModule, - SignupSuccessComponent, + SignupSuccessBeforeEmailComponent, + SignupSuccessAfterEmailComponent, SignupStepTitleComponent, SignupMascotComponent ], providers: [ + SignupService ] }) export 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 @@ + + Email verified! + + +
    + +

    Your email has been verified and your account request has been sent!

    + +

    + A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected. +

    +
    + + +

    Your email has been verified and your account has been created!

    + +

    + If you need help to use PeerTube, you can have a look at the documentation. +

    +
    +
    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 @@ +import { Component, Input } from '@angular/core' + +@Component({ + selector: 'my-signup-success-after-email', + templateUrl: './signup-success-after-email.component.html', + styleUrls: [ './signup-success.component.scss' ] +}) +export class SignupSuccessAfterEmailComponent { + @Input() requiresApproval: boolean +} 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 @@ + + + Account request sent + + + + Welcome +
    on {{ instanceName }}
    +
    +
    + +
    +

    Your account request has been sent!

    +

    Your account has been created!

    + + +

    + Check your emails to validate your account and complete your registration request. +

    + +

    + Check your emails to validate your account and complete your registration. +

    +
    + + +

    + A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected. +

    + +

    + If you need help to use PeerTube, you can have a look at the documentation. +

    +
    +
    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 @@ +import { Component, Input } from '@angular/core' + +@Component({ + selector: 'my-signup-success-before-email', + templateUrl: './signup-success-before-email.component.html', + styleUrls: [ './signup-success.component.scss' ] +}) +export class SignupSuccessBeforeEmailComponent { + @Input() requiresApproval: boolean + @Input() requiresEmailVerification: boolean + @Input() instanceName: string +} 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 @@ - - Welcome -
    on {{ instanceName }}
    -
    - -
    -

    Your account has been created!

    - -

    - Check your emails to validate your account and complete your inscription. -

    - - -

    - If you need help to use PeerTube, you can have a look at the documentation. -

    - -

    - To help moderators and other users to know who you are, don't forget to set up your account profile by adding an avatar and a description. -

    -
    -
    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 @@ -import { Component, Input } from '@angular/core' -import { ServerService } from '@app/core' - -@Component({ - selector: 'my-signup-success', - templateUrl: './signup-success.component.html', - styleUrls: [ './signup-success.component.scss' ] -}) -export class SignupSuccessComponent { - @Input() requiresEmailVerification: boolean - - constructor (private serverService: ServerService) { - - } - - get instanceName () { - return this.serverService.getHTMLConfig().instance.name - } -} diff --git a/client/src/app/+signup/shared/signup.service.ts b/client/src/app/+signup/shared/signup.service.ts new file mode 100644 index 000000000..f647298be --- /dev/null +++ b/client/src/app/+signup/shared/signup.service.ts @@ -0,0 +1,85 @@ +import { catchError, tap } from 'rxjs/operators' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, UserService } from '@app/core' +import { UserRegister, UserRegistrationRequest } from '@shared/models' + +@Injectable() +export class SignupService { + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private userService: UserService + ) { } + + directSignup (userCreate: UserRegister) { + return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) + .pipe( + tap(() => this.userService.setSignupInThisSession(true)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + requestSignup (userCreate: UserRegistrationRequest) { + return this.authHttp.post(UserService.BASE_USERS_URL + 'registrations/request', userCreate) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + // --------------------------------------------------------------------------- + + verifyUserEmail (options: { + userId: number + verificationString: string + isPendingEmail: boolean + }) { + const { userId, verificationString, isPendingEmail } = options + + const url = `${UserService.BASE_USERS_URL}${userId}/verify-email` + const body = { + verificationString, + isPendingEmail + } + + return this.authHttp.post(url, body) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + verifyRegistrationEmail (options: { + registrationId: number + verificationString: string + }) { + const { registrationId, verificationString } = options + + const url = `${UserService.BASE_USERS_URL}registrations/${registrationId}/verify-email` + const body = { verificationString } + + return this.authHttp.post(url, body) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + askSendVerifyEmail (email: string) { + const url = UserService.BASE_USERS_URL + 'ask-send-verify-email' + + return this.authHttp.post(url, { email }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + // --------------------------------------------------------------------------- + + getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { + // Don't update display name, the user seems to have changed it + if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername + + return this.displayNameToUsername(newDisplayName) + } + + private displayNameToUsername (displayName: string) { + if (!displayName) return '' + + return displayName + .toLowerCase() + .replace(/\s/g, '_') + .replace(/[^a-z0-9_.]/g, '') + } +} 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 @@ Login Login - +
    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 { return this.languageChooserModal.getCurrentLanguage() } + get requiresApproval () { + return this.serverConfig.signup.requiresApproval + } + ngOnInit () { this.htmlServerConfig = this.serverService.getHTMLConfig() 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 = { } } -export const USER_TERMS_VALIDATOR: BuildFormValidator = { - VALIDATORS: [ Validators.requiredTrue ], - MESSAGES: { - required: $localize`You must agree with the instance terms in order to register on it.` - } -} - export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = { VALIDATORS: [ 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 @@
    @@ -29,7 +29,7 @@ Reportee
    @@ -63,7 +63,7 @@
    {{ reason.label }}
    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 @@ export * from './account.model' export * from './account.service' export * from './actor.model' +export * 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 @@ +Request an account +Create an account 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 @@ +import { Component, Input } from '@angular/core' + +@Component({ + selector: 'my-signup-label', + templateUrl: './signup-label.component.html' +}) +export class SignupLabelComponent { + @Input() requiresApproval: boolean +} 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 { import { LoadingBarModule } from '@ngx-loading-bar/core' import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' import { SharedGlobalIconModule } from '../shared-icons' -import { AccountService } from './account' +import { AccountService, SignupLabelComponent } from './account' import { AutofocusDirective, BytesPipe, @@ -113,6 +113,8 @@ import { VideoChannelService } from './video-channel' UserQuotaComponent, UserNotificationsComponent, + SignupLabelComponent, + EmbedComponent, PluginPlaceholderComponent, @@ -171,6 +173,8 @@ import { VideoChannelService } from './video-channel' UserQuotaComponent, UserNotificationsComponent, + SignupLabelComponent, + EmbedComponent, 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 { latestVersion: string } + registration?: { + id: number + username: string + } + createdAt: string updatedAt: string @@ -97,6 +102,8 @@ export class UserNotification implements UserNotificationServer { accountUrl?: string + registrationsUrl?: string + videoImportIdentifier?: string videoImportUrl?: string @@ -135,6 +142,7 @@ export class UserNotification implements UserNotificationServer { this.plugin = hash.plugin this.peertube = hash.peertube + this.registration = hash.registration this.createdAt = hash.createdAt this.updatedAt = hash.updatedAt @@ -208,6 +216,10 @@ export class UserNotification implements UserNotificationServer { this.accountUrl = this.buildAccountUrl(this.account) break + case UserNotificationType.NEW_USER_REGISTRATION_REQUEST: + this.registrationsUrl = '/admin/moderation/registrations/list' + break + case UserNotificationType.NEW_FOLLOW: this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) 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 @@
    + + + +
    + User {{ notification.registration.username }} wants to register on your instance +
    +
    + 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 @@ @use '_variables' as *; @use '_mixins' as *; -.chip { - @include chip; -} - .unblock-button { @include peertube-button; @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 @@ } } -.chip { - @include chip; -} - my-action-dropdown.show { ::ng-deep .dropdown-root { 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 { .block-button { @include create-button; } - -.chip { - @include chip; -} 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 @@ export * from './user-admin.service' -export * from './user-signup.service' export * from './two-factor.service' export * 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 @@ - import { NgModule } from '@angular/core' import { SharedMainModule } from '../shared-main/shared-main.module' import { TwoFactorService } from './two-factor.service' import { UserAdminService } from './user-admin.service' -import { UserSignupService } from './user-signup.service' @NgModule({ imports: [ @@ -15,7 +13,6 @@ import { UserSignupService } from './user-signup.service' exports: [], providers: [ - UserSignupService, UserAdminService, TwoFactorService ] diff --git a/client/src/app/shared/shared-users/user-signup.service.ts b/client/src/app/shared/shared-users/user-signup.service.ts deleted file mode 100644 index 46fe34af1..000000000 --- a/client/src/app/shared/shared-users/user-signup.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { catchError, tap } from 'rxjs/operators' -import { HttpClient } from '@angular/common/http' -import { Injectable } from '@angular/core' -import { RestExtractor, UserService } from '@app/core' -import { UserRegister } from '@shared/models' - -@Injectable() -export class UserSignupService { - constructor ( - private authHttp: HttpClient, - private restExtractor: RestExtractor, - private userService: UserService - ) { } - - signup (userCreate: UserRegister) { - return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) - .pipe( - tap(() => this.userService.setSignupInThisSession(true)), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) { - const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` - const body = { - verificationString, - isPendingEmail - } - - return this.authHttp.post(url, body) - .pipe(catchError(res => this.restExtractor.handleError(res))) - } - - askSendVerifyEmail (email: string) { - const url = UserService.BASE_USERS_URL + '/ask-send-verify-email' - - return this.authHttp.post(url, { email }) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - - getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { - // Don't update display name, the user seems to have changed it - if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername - - return this.displayNameToUsername(newDisplayName) - } - - private displayNameToUsername (displayName: string) { - if (!displayName) return '' - - return displayName - .toLowerCase() - .replace(/\s/g, '_') - .replace(/[^a-z0-9_.]/g, '') - } -} 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 @@ {{ getStateLabel(video) }}
    -
    - + 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 @@ $more-button-width: 40px; -.chip { - @include chip; -} - .video-miniature { font-size: 14px; } 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 { border: 2px solid pvar(--mainColorLightest); } } + +// --------------------------------------------------------------------------- + +.chip { + @include chip; +} 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 @@ font-weight: $font-semibold; line-height: 1.1; + &.badge-fs-normal { + font-size: 100%; + } + &.badge-primary { color: pvar(--mainBackgroundColor); 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 @@ font-display: swap; src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2'); } - -@mixin muted { - color: pvar(--greyForegroundColor) !important; -} 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 @@ max-height: $font-size * $number-of-lines; } +@mixin muted { + color: pvar(--greyForegroundColor) !important; +} + @mixin fade-text ($fade-after, $background-color) { position: relative; overflow: hidden; @@ -791,51 +795,39 @@ } @mixin chip { - --chip-radius: 5rem; - --chip-padding: .2rem .4rem; - $avatar-height: 1.2rem; + --avatar-size: 1.2rem; - align-items: center; - border-radius: var(--chip-radius); display: inline-flex; - font-size: 90%; color: pvar(--mainForegroundColor); - height: $avatar-height; - line-height: 1rem; - margin: .1rem; + height: var(--avatar-size); max-width: 320px; overflow: hidden; - padding: var(--chip-padding); text-decoration: none; text-overflow: ellipsis; vertical-align: middle; white-space: nowrap; - &.rectangular { - --chip-radius: .2rem; - --chip-padding: .2rem .3rem; - } - my-actor-avatar { - @include margin-left(-.4rem); @include margin-right(.2rem); + + border-radius: 5rem; + width: var(--avatar-size); + height: var(--avatar-size); } &.two-lines { - $avatar-height: 2rem; + --avatar-size: 2rem; - height: $avatar-height; + font-size: 14px; + line-height: 1rem; my-actor-avatar { display: inline-block; } - div { - margin: 0 .1rem; - + > div { display: flex; flex-direction: column; - height: $avatar-height; justify-content: center; } } -- cgit v1.2.3