Implement signup approval in client
This commit is contained in:
parent
b379759f55
commit
9589907c89
|
@ -96,6 +96,14 @@ export class AdminComponent implements OnInit {
|
||||||
children: []
|
children: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.hasRegistrationsRight()) {
|
||||||
|
moderationItems.children.push({
|
||||||
|
label: $localize`Registrations`,
|
||||||
|
routerLink: '/admin/moderation/registrations/list',
|
||||||
|
iconName: 'user'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hasAbusesRight()) {
|
if (this.hasAbusesRight()) {
|
||||||
moderationItems.children.push({
|
moderationItems.children.push({
|
||||||
label: $localize`Reports`,
|
label: $localize`Reports`,
|
||||||
|
@ -229,4 +237,8 @@ export class AdminComponent implements OnInit {
|
||||||
private hasVideosRight () {
|
private hasVideosRight () {
|
||||||
return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
|
return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private hasRegistrationsRight () {
|
||||||
|
return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,13 @@ import { FollowersListComponent, FollowModalComponent, VideoRedundanciesListComp
|
||||||
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
||||||
import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
|
import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
|
||||||
import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.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 { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
|
||||||
import {
|
import {
|
||||||
UserCreateComponent,
|
UserCreateComponent,
|
||||||
|
@ -116,7 +122,10 @@ import { JobsComponent } from './system/jobs/jobs.component'
|
||||||
EditLiveConfigurationComponent,
|
EditLiveConfigurationComponent,
|
||||||
EditAdvancedConfigurationComponent,
|
EditAdvancedConfigurationComponent,
|
||||||
EditInstanceInformationComponent,
|
EditInstanceInformationComponent,
|
||||||
EditHomepageComponent
|
EditHomepageComponent,
|
||||||
|
|
||||||
|
RegistrationListComponent,
|
||||||
|
ProcessRegistrationModalComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -130,7 +139,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
|
||||||
ConfigService,
|
ConfigService,
|
||||||
PluginApiService,
|
PluginApiService,
|
||||||
EditConfigurationService,
|
EditConfigurationService,
|
||||||
VideoAdminService
|
VideoAdminService,
|
||||||
|
AdminRegistrationService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminModule { }
|
export class AdminModule { }
|
||||||
|
|
|
@ -171,12 +171,21 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container ngProjectAs="extra">
|
<ng-container ngProjectAs="extra">
|
||||||
|
<div class="form-group">
|
||||||
|
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
||||||
|
inputName="signupRequiresApproval" formControlName="requiresApproval"
|
||||||
|
i18n-labelText labelText="Signup requires approval by moderators"
|
||||||
|
></my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
|
||||||
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
|
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
|
||||||
i18n-labelText labelText="Signup requires email verification"
|
i18n-labelText labelText="Signup requires email verification"
|
||||||
></my-peertube-checkbox>
|
></my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div [ngClass]="getDisabledSignupClass()" class="mt-3">
|
<div [ngClass]="getDisabledSignupClass()">
|
||||||
<label i18n for="signupLimit">Signup limit</label>
|
<label i18n for="signupLimit">Signup limit</label>
|
||||||
|
|
||||||
<div class="number-with-unit">
|
<div class="number-with-unit">
|
||||||
|
|
|
@ -132,6 +132,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
signup: {
|
signup: {
|
||||||
enabled: null,
|
enabled: null,
|
||||||
limit: SIGNUP_LIMIT_VALIDATOR,
|
limit: SIGNUP_LIMIT_VALIDATOR,
|
||||||
|
requiresApproval: null,
|
||||||
requiresEmailVerification: null,
|
requiresEmailVerification: null,
|
||||||
minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
|
minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './abuse-list'
|
export * from './abuse-list'
|
||||||
export * from './instance-blocklist'
|
export * from './instance-blocklist'
|
||||||
export * from './video-block-list'
|
export * from './video-block-list'
|
||||||
|
export * from './registration-list'
|
||||||
export * from './moderation.routes'
|
export * from './moderation.routes'
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
|
||||||
import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
|
import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
|
||||||
import { UserRightGuard } from '@app/core'
|
import { UserRightGuard } from '@app/core'
|
||||||
import { UserRight } from '@shared/models'
|
import { UserRight } from '@shared/models'
|
||||||
|
import { RegistrationListComponent } from './registration-list'
|
||||||
|
|
||||||
export const ModerationRoutes: Routes = [
|
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',
|
path: 'video-comments',
|
||||||
redirectTo: 'video-comments/list',
|
redirectTo: 'video-comments/list',
|
||||||
|
|
|
@ -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<ResultList<UserRegistration>>(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)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'
|
|
@ -0,0 +1,67 @@
|
||||||
|
<ng-template #modal>
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 i18n class="modal-title">
|
||||||
|
<ng-container *ngIf="isAccept()">Accept {{ registration.username }} registration</ng-container>
|
||||||
|
<ng-container *ngIf="isReject()">Reject {{ registration.username }} registration</ng-container>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form novalidate [formGroup]="form" (ngSubmit)="processRegistration()">
|
||||||
|
<div class="modal-body mb-3">
|
||||||
|
|
||||||
|
<div i18n *ngIf="!registration.emailVerified" class="alert alert-warning">
|
||||||
|
Registration email has not been verified.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description">
|
||||||
|
<ng-container *ngIf="isAccept()">
|
||||||
|
<p i18n>
|
||||||
|
<strong>Accepting</strong> <em>{{ registration.username }}</em> registration will create the account and channel.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p *ngIf="isEmailEnabled()" i18n>
|
||||||
|
An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n>
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="isReject()">
|
||||||
|
<p i18n>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n>
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="moderationResponse" i18n>Send a message to the user</label>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
formControlName="moderationResponse" ngbAutofocus name="moderationResponse" id="moderationResponse"
|
||||||
|
[ngClass]="{ 'input-error': formErrors['moderationResponse'] }" class="form-control"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.moderationResponse" class="form-error">
|
||||||
|
{{ formErrors.moderationResponse }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer inputs">
|
||||||
|
<input
|
||||||
|
type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
|
||||||
|
(click)="hide()" (key.enter)="hide()"
|
||||||
|
>
|
||||||
|
|
||||||
|
<input type="submit" [value]="getSubmitValue()" class="peertube-button orange-button" [disabled]="!form.valid">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,3 @@
|
||||||
|
@use '_variables' as *;
|
||||||
|
@use '_mixins' as *;
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.`
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
<h1>
|
||||||
|
<my-global-icon iconName="user" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>Registration requests</ng-container>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p-table
|
||||||
|
[value]="registrations" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||||
|
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
|
||||||
|
[lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
|
||||||
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} registrations"
|
||||||
|
[expandedRowKeys]="expandedRows"
|
||||||
|
>
|
||||||
|
<ng-template pTemplate="caption">
|
||||||
|
<div class="caption">
|
||||||
|
<div class="ms-auto">
|
||||||
|
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="header">
|
||||||
|
<tr> <!-- header -->
|
||||||
|
<th style="width: 40px;"></th>
|
||||||
|
<th style="width: 150px;"></th>
|
||||||
|
<th i18n>Account</th>
|
||||||
|
<th i18n>Email</th>
|
||||||
|
<th i18n>Channel</th>
|
||||||
|
<th i18n>Registration reason</th>
|
||||||
|
<th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||||
|
<th i18n>Moderation response</th>
|
||||||
|
<th style="width: 150px;" i18n pSortableColumn="createdAt">Requested on <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="body" let-expanded="expanded" let-registration>
|
||||||
|
<tr>
|
||||||
|
<td class="expand-cell" [pRowToggler]="registration">
|
||||||
|
<my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="action-cell">
|
||||||
|
<my-action-dropdown
|
||||||
|
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
|
||||||
|
i18n-label label="Actions" [actions]="registrationActions" [entry]="registration"
|
||||||
|
></my-action-dropdown>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="chip two-lines">
|
||||||
|
<div>
|
||||||
|
<span>{{ registration.username }}</span>
|
||||||
|
<span class="muted">{{ registration.accountDisplayName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<my-user-email-info [entry]="registration" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="chip two-lines">
|
||||||
|
<div>
|
||||||
|
<span>{{ registration.channelHandle }}</span>
|
||||||
|
<span class="muted">{{ registration.channelDisplayName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td container="body" placement="left auto" [ngbTooltip]="registration.registrationReason">
|
||||||
|
{{ registration.registrationReason }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="c-hand abuse-states" [pRowToggler]="registration">
|
||||||
|
<my-global-icon *ngIf="isRegistrationAccepted(registration)" [title]="registration.state.label" iconName="tick"></my-global-icon>
|
||||||
|
<my-global-icon *ngIf="isRegistrationRejected(registration)" [title]="registration.state.label" iconName="cross"></my-global-icon>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td container="body" placement="left auto" [ngbTooltip]="registration.moderationResponse">
|
||||||
|
{{ registration.moderationResponse }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="c-hand" [pRowToggler]="registration">{{ registration.createdAt | date: 'short' }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="rowexpansion" let-registration>
|
||||||
|
<tr>
|
||||||
|
<td colspan="9">
|
||||||
|
<div class="moderation-expanded">
|
||||||
|
<div class="left">
|
||||||
|
<div class="d-flex">
|
||||||
|
<span class="moderation-expanded-label" i18n>Registration reason:</span>
|
||||||
|
<span class="moderation-expanded-text" [innerHTML]="registration.registrationReasonHTML"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="registration.moderationResponse">
|
||||||
|
<span class="moderation-expanded-label" i18n>Moderation response:</span>
|
||||||
|
<span class="moderation-expanded-text" [innerHTML]="registration.moderationResponseHTML"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template pTemplate="emptymessage">
|
||||||
|
<tr>
|
||||||
|
<td colspan="9">
|
||||||
|
<div class="no-results">
|
||||||
|
<ng-container *ngIf="search" i18n>No registrations found matching current filters.</ng-container>
|
||||||
|
<ng-container *ngIf="!search" i18n>No registrations found.</ng-container>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</p-table>
|
||||||
|
|
||||||
|
<my-process-registration-modal #processRegistrationModal (registrationProcessed)="onRegistrationProcessed()"></my-process-registration-modal>
|
|
@ -0,0 +1,7 @@
|
||||||
|
@use '_mixins' as *;
|
||||||
|
@use '_variables' as *;
|
||||||
|
|
||||||
|
my-global-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
|
@ -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<UserRegistration>[][] = []
|
||||||
|
|
||||||
|
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 })
|
||||||
|
}
|
||||||
|
}
|
|
@ -95,7 +95,7 @@
|
||||||
<div class="chip two-lines">
|
<div class="chip two-lines">
|
||||||
<my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar>
|
<my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar>
|
||||||
<div>
|
<div>
|
||||||
<span class="user-table-primary-text">{{ user.account.displayName }}</span>
|
<span>{{ user.account.displayName }}</span>
|
||||||
<span class="muted">{{ user.username }}</span>
|
<span class="muted">{{ user.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -110,23 +110,10 @@
|
||||||
<span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span>
|
<span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td *ngIf="isSelected('email')" [title]="user.email">
|
<td *ngIf="isSelected('email')">
|
||||||
<ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">
|
<my-user-email-info [entry]="user" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info>
|
||||||
<a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<ng-template #emailWithVerificationStatus>
|
|
||||||
<td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
|
|
||||||
<em>? {{ user.email }}</em>
|
|
||||||
</td>
|
|
||||||
<ng-template #emailVerifiedNotFalse>
|
|
||||||
<td i18n-title title="User's email is verified / User can login without email verification">
|
|
||||||
✓ {{ user.email }}
|
|
||||||
</td>
|
|
||||||
</ng-template>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<td *ngIf="isSelected('quota')">
|
<td *ngIf="isSelected('quota')">
|
||||||
<div class="progress" i18n-title title="Total video quota">
|
<div class="progress" i18n-title title="Total video quota">
|
||||||
<div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }"
|
<div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }"
|
||||||
|
|
|
@ -10,12 +10,6 @@ tr.banned > td {
|
||||||
background-color: lighten($color: $red, $amount: 40) !important;
|
background-color: lighten($color: $red, $amount: 40) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-email {
|
|
||||||
@include disable-default-a-behaviour;
|
|
||||||
|
|
||||||
color: pvar(--mainForegroundColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.banned-info {
|
.banned-info {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
@ -37,10 +31,6 @@ my-global-icon {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
|
||||||
@include chip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
@include progressbar($small: true);
|
@include progressbar($small: true);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { SharedMainModule } from '../../shared/shared-main/shared-main.module'
|
import { SharedMainModule } from '../../shared/shared-main/shared-main.module'
|
||||||
|
import { UserEmailInfoComponent } from './user-email-info.component'
|
||||||
import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
|
import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -8,11 +9,13 @@ import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
|
||||||
],
|
],
|
||||||
|
|
||||||
declarations: [
|
declarations: [
|
||||||
UserRealQuotaInfoComponent
|
UserRealQuotaInfoComponent,
|
||||||
|
UserEmailInfoComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
UserRealQuotaInfoComponent
|
UserRealQuotaInfoComponent,
|
||||||
|
UserEmailInfoComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: []
|
providers: []
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<ng-container>
|
||||||
|
<a [href]="'mailto:' + entry.email" [title]="getTitle()">
|
||||||
|
<ng-container *ngIf="!requiresEmailVerification">
|
||||||
|
{{ entry.email }}
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="requiresEmailVerification">
|
||||||
|
<em *ngIf="!entry.emailVerified">? {{ entry.email }}</em>
|
||||||
|
|
||||||
|
<ng-container *ngIf="entry.emailVerified === true">✓ {{ entry.email }}</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
|
@ -0,0 +1,10 @@
|
||||||
|
@use '_variables' as *;
|
||||||
|
@use '_mixins' as *;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: pvar(--mainForegroundColor);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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`
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ export class JobService {
|
||||||
private restExtractor: RestExtractor
|
private restExtractor: RestExtractor
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getJobs (options: {
|
listJobs (options: {
|
||||||
jobState?: JobStateClient
|
jobState?: JobStateClient
|
||||||
jobType: JobTypeClient
|
jobType: JobTypeClient
|
||||||
pagination: RestPagination
|
pagination: RestPagination
|
||||||
|
|
|
@ -125,7 +125,7 @@ export class JobsComponent extends RestTable implements OnInit {
|
||||||
if (this.jobState === 'all') jobState = null
|
if (this.jobState === 'all') jobState = null
|
||||||
|
|
||||||
this.jobsService
|
this.jobsService
|
||||||
.getJobs({
|
.listJobs({
|
||||||
jobState,
|
jobState,
|
||||||
jobType: this.jobType,
|
jobType: this.jobType,
|
||||||
pagination: this.pagination,
|
pagination: this.pagination,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shar
|
||||||
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
||||||
import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { getExternalAuthHref } from '@shared/core-utils'
|
import { getExternalAuthHref } from '@shared/core-utils'
|
||||||
import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
|
import { RegisteredExternalAuthConfig, ServerConfig, ServerErrorCode } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-login',
|
selector: 'my-login',
|
||||||
|
@ -197,6 +197,8 @@ The link will expire within 1 hour.`
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleError (err: any) {
|
private handleError (err: any) {
|
||||||
|
console.log(err)
|
||||||
|
|
||||||
if (this.authService.isOTPMissingError(err)) {
|
if (this.authService.isOTPMissingError(err)) {
|
||||||
this.otpStep = true
|
this.otpStep = true
|
||||||
|
|
||||||
|
@ -208,8 +210,26 @@ The link will expire within 1 hour.`
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.`
|
if (err.message.includes('credentials are invalid')) {
|
||||||
else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.`
|
this.error = $localize`Incorrect username or password.`
|
||||||
else this.error = err.message
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,6 @@
|
||||||
@use '_miniature' as *;
|
@use '_miniature' as *;
|
||||||
@use '_mixins' as *;
|
@use '_mixins' as *;
|
||||||
|
|
||||||
.chip {
|
|
||||||
@include chip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-table-video {
|
.video-table-video {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
||||||
|
|
|
@ -5,29 +5,34 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="!signupDisabled">
|
<ng-container *ngIf="!signupDisabled">
|
||||||
<h1 i18n class="title-page-v2">
|
<h1 class="title-page-v2">
|
||||||
<strong class="underline-orange">{{ instanceName }}</strong>
|
<strong class="underline-orange">{{ instanceName }}</strong>
|
||||||
>
|
>
|
||||||
Create an account
|
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="register-content">
|
<div class="register-content">
|
||||||
<my-custom-stepper linear>
|
<my-custom-stepper linear>
|
||||||
|
|
||||||
<cdk-step i18n-label label="About" [editable]="!signupSuccess">
|
<cdk-step i18n-label label="About" [editable]="!signupSuccess">
|
||||||
<my-signup-step-title mascotImageName="about" i18n>
|
<my-signup-step-title mascotImageName="about">
|
||||||
<strong>Create an account</strong>
|
<strong>
|
||||||
<div>on {{ instanceName }}</div>
|
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<div i18n>on {{ instanceName }}</div>
|
||||||
</my-signup-step-title>
|
</my-signup-step-title>
|
||||||
|
|
||||||
<my-register-step-about [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about>
|
<my-register-step-about [requiresApproval]="requiresApproval" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about>
|
||||||
|
|
||||||
<div class="step-buttons">
|
<div class="step-buttons">
|
||||||
<a i18n class="skip-step underline-orange" routerLink="/login">
|
<a i18n class="skip-step underline-orange" routerLink="/login">
|
||||||
<strong>I already have an account</strong>, I log in
|
<strong>I already have an account</strong>, I log in
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button i18n cdkStepperNext>Create an account</button>
|
<button cdkStepperNext>
|
||||||
|
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</cdk-step>
|
</cdk-step>
|
||||||
|
|
||||||
|
@ -44,8 +49,8 @@
|
||||||
></my-instance-about-accordion>
|
></my-instance-about-accordion>
|
||||||
|
|
||||||
<my-register-step-terms
|
<my-register-step-terms
|
||||||
[hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
|
[hasCodeOfConduct]="!!aboutHtml.codeOfConduct" [minimumAge]="minimumAge" [instanceName]="instanceName"
|
||||||
[minimumAge]="minimumAge"
|
[requiresApproval]="requiresApproval"
|
||||||
(formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
|
(formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
|
||||||
></my-register-step-terms>
|
></my-register-step-terms>
|
||||||
|
|
||||||
|
@ -94,14 +99,15 @@
|
||||||
<div class="skip-step-description" i18n>You will be able to create a channel later</div>
|
<div class="skip-step-description" i18n>You will be able to create a channel later</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n>
|
<button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()">
|
||||||
Create my account
|
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</cdk-step>
|
</cdk-step>
|
||||||
|
|
||||||
<cdk-step #lastStep i18n-label label="Done!" [editable]="false">
|
<cdk-step #lastStep i18n-label label="Done!" [editable]="false">
|
||||||
<div *ngIf="!signupSuccess && !signupError" class="done-loader">
|
<!-- Account creation can be a little bit long so display a loader -->
|
||||||
|
<div *ngIf="!requiresApproval && !signupSuccess && !signupError" class="done-loader">
|
||||||
<my-loader [loading]="true"></my-loader>
|
<my-loader [loading]="true"></my-loader>
|
||||||
|
|
||||||
<div i18n>PeerTube is creating your account...</div>
|
<div i18n>PeerTube is creating your account...</div>
|
||||||
|
@ -109,7 +115,10 @@
|
||||||
|
|
||||||
<div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div>
|
<div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div>
|
||||||
|
|
||||||
<my-signup-success *ngIf="signupSuccess" [requiresEmailVerification]="requiresEmailVerification"></my-signup-success>
|
<my-signup-success-before-email
|
||||||
|
*ngIf="signupSuccess"
|
||||||
|
[requiresEmailVerification]="requiresEmailVerification" [requiresApproval]="requiresApproval" [instanceName]="instanceName"
|
||||||
|
></my-signup-success-before-email>
|
||||||
|
|
||||||
<div *ngIf="signupError" class="steps-button">
|
<div *ngIf="signupError" class="steps-button">
|
||||||
<button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
|
<button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
|
||||||
|
|
|
@ -5,10 +5,10 @@ import { ActivatedRoute } from '@angular/router'
|
||||||
import { AuthService } from '@app/core'
|
import { AuthService } from '@app/core'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
||||||
import { UserSignupService } from '@app/shared/shared-users'
|
|
||||||
import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { UserRegister } from '@shared/models'
|
import { UserRegister } from '@shared/models'
|
||||||
import { ServerConfig } from '@shared/models/server'
|
import { ServerConfig } from '@shared/models/server'
|
||||||
|
import { SignupService } from '../shared/signup.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-register',
|
selector: 'my-register',
|
||||||
|
@ -53,7 +53,7 @@ export class RegisterComponent implements OnInit {
|
||||||
constructor (
|
constructor (
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private userSignupService: UserSignupService,
|
private signupService: SignupService,
|
||||||
private hooks: HooksService
|
private hooks: HooksService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
@ -61,6 +61,10 @@ export class RegisterComponent implements OnInit {
|
||||||
return this.serverConfig.signup.requiresEmailVerification
|
return this.serverConfig.signup.requiresEmailVerification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get requiresApproval () {
|
||||||
|
return this.serverConfig.signup.requiresApproval
|
||||||
|
}
|
||||||
|
|
||||||
get minimumAge () {
|
get minimumAge () {
|
||||||
return this.serverConfig.signup.minimumAge
|
return this.serverConfig.signup.minimumAge
|
||||||
}
|
}
|
||||||
|
@ -132,44 +136,64 @@ export class RegisterComponent implements OnInit {
|
||||||
skipChannelCreation () {
|
skipChannelCreation () {
|
||||||
this.formStepChannel.reset()
|
this.formStepChannel.reset()
|
||||||
this.lastStep.select()
|
this.lastStep.select()
|
||||||
|
|
||||||
this.signup()
|
this.signup()
|
||||||
}
|
}
|
||||||
|
|
||||||
async signup () {
|
async signup () {
|
||||||
this.signupError = undefined
|
this.signupError = undefined
|
||||||
|
|
||||||
const body: UserRegister = await this.hooks.wrapObject(
|
const termsForm = this.formStepTerms.value
|
||||||
{
|
const userForm = this.formStepUser.value
|
||||||
...this.formStepUser.value,
|
const channelForm = this.formStepChannel?.value
|
||||||
|
|
||||||
channel: this.formStepChannel?.value?.name
|
const channel = this.formStepChannel?.value?.name
|
||||||
? this.formStepChannel.value
|
? { name: channelForm?.name, displayName: channelForm?.displayName }
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const body = await this.hooks.wrapObject(
|
||||||
|
{
|
||||||
|
username: userForm.username,
|
||||||
|
password: userForm.password,
|
||||||
|
email: userForm.email,
|
||||||
|
displayName: userForm.displayName,
|
||||||
|
|
||||||
|
registrationReason: termsForm.registrationReason,
|
||||||
|
|
||||||
|
channel
|
||||||
},
|
},
|
||||||
'signup',
|
'signup',
|
||||||
'filter:api.signup.registration.create.params'
|
'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: () => {
|
next: () => {
|
||||||
if (this.requiresEmailVerification) {
|
if (this.requiresEmailVerification || this.requiresApproval) {
|
||||||
this.signupSuccess = true
|
this.signupSuccess = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto login
|
// Auto login
|
||||||
|
this.autoLogin(body)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => {
|
||||||
|
this.signupError = err.message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private autoLogin (body: UserRegister) {
|
||||||
this.authService.login({ username: body.username, password: body.password })
|
this.authService.login({ username: body.username, password: body.password })
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.signupSuccess = true
|
this.signupSuccess = true
|
||||||
},
|
},
|
||||||
|
|
||||||
error: err => {
|
|
||||||
this.signupError = err.message
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
error: err => {
|
error: err => {
|
||||||
this.signupError = err.message
|
this.signupError = err.message
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './register-validators'
|
|
@ -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.`
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,10 @@
|
||||||
<li i18n>Have access to your <strong>watch history</strong></li>
|
<li i18n>Have access to your <strong>watch history</strong></li>
|
||||||
<li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li>
|
<li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<p *ngIf="requiresApproval" i18n>
|
||||||
|
Moderators of {{ instanceName }} will have to approve your registration request once you have finished to fill the form.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { ServerService } from '@app/core'
|
||||||
styleUrls: [ './register-step-about.component.scss' ]
|
styleUrls: [ './register-step-about.component.scss' ]
|
||||||
})
|
})
|
||||||
export class RegisterStepAboutComponent {
|
export class RegisterStepAboutComponent {
|
||||||
|
@Input() requiresApproval: boolean
|
||||||
@Input() videoUploadDisabled: boolean
|
@Input() videoUploadDisabled: boolean
|
||||||
|
|
||||||
constructor (private serverService: ServerService) {
|
constructor (private serverService: ServerService) {
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { concat, of } from 'rxjs'
|
||||||
import { pairwise } from 'rxjs/operators'
|
import { pairwise } from 'rxjs/operators'
|
||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
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 { 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 { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
import { UserSignupService } from '@app/shared/shared-users'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-register-step-channel',
|
selector: 'my-register-step-channel',
|
||||||
|
@ -20,7 +20,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formReactiveService: FormReactiveService,
|
protected formReactiveService: FormReactiveService,
|
||||||
private userSignupService: UserSignupService
|
private signupService: SignupService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
|
||||||
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
|
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
|
||||||
const name = this.form.value['name'] || ''
|
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 })
|
this.form.patchValue({ name: newName })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,16 @@
|
||||||
<form role="form" [formGroup]="form">
|
<form role="form" [formGroup]="form">
|
||||||
|
|
||||||
|
<div *ngIf="requiresApproval" class="form-group">
|
||||||
|
<label i18n for="registrationReason">Why do you want to join {{ instanceName }}?</label>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
id="registrationReason" formControlName="registrationReason" class="form-control" rows="4"
|
||||||
|
[ngClass]="{ 'input-error': formErrors['registrationReason'] }"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.registrationReason" class="form-error">{{ formErrors.registrationReason }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<my-peertube-checkbox inputName="terms" formControlName="terms">
|
<my-peertube-checkbox inputName="terms" formControlName="terms">
|
||||||
<ng-template ptTemplate="label">
|
<ng-template ptTemplate="label">
|
||||||
|
@ -6,7 +18,7 @@
|
||||||
I am at least {{ minimumAge }} years old and agree
|
I am at least {{ minimumAge }} years old and agree
|
||||||
to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a>
|
to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a>
|
||||||
<ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
|
<ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
|
||||||
of this instance
|
of {{ instanceName }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</my-peertube-checkbox>
|
</my-peertube-checkbox>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
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 { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
|
import { REGISTER_REASON_VALIDATOR, REGISTER_TERMS_VALIDATOR } from '../shared'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-register-step-terms',
|
selector: 'my-register-step-terms',
|
||||||
|
@ -10,7 +10,9 @@ import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
})
|
})
|
||||||
export class RegisterStepTermsComponent extends FormReactive implements OnInit {
|
export class RegisterStepTermsComponent extends FormReactive implements OnInit {
|
||||||
@Input() hasCodeOfConduct = false
|
@Input() hasCodeOfConduct = false
|
||||||
|
@Input() requiresApproval: boolean
|
||||||
@Input() minimumAge = 16
|
@Input() minimumAge = 16
|
||||||
|
@Input() instanceName: string
|
||||||
|
|
||||||
@Output() formBuilt = new EventEmitter<FormGroup>()
|
@Output() formBuilt = new EventEmitter<FormGroup>()
|
||||||
@Output() termsClick = new EventEmitter<void>()
|
@Output() termsClick = new EventEmitter<void>()
|
||||||
|
@ -28,7 +30,11 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.buildForm({
|
this.buildForm({
|
||||||
terms: USER_TERMS_VALIDATOR
|
terms: REGISTER_TERMS_VALIDATOR,
|
||||||
|
|
||||||
|
registrationReason: this.requiresApproval
|
||||||
|
? REGISTER_REASON_VALIDATOR
|
||||||
|
: null
|
||||||
})
|
})
|
||||||
|
|
||||||
setTimeout(() => this.formBuilt.emit(this.form))
|
setTimeout(() => this.formBuilt.emit(this.form))
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { concat, of } from 'rxjs'
|
||||||
import { pairwise } from 'rxjs/operators'
|
import { pairwise } from 'rxjs/operators'
|
||||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||||
import { FormGroup } from '@angular/forms'
|
import { FormGroup } from '@angular/forms'
|
||||||
|
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||||
import {
|
import {
|
||||||
USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
|
USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
|
||||||
USER_EMAIL_VALIDATOR,
|
USER_EMAIL_VALIDATOR,
|
||||||
|
@ -9,7 +10,6 @@ import {
|
||||||
USER_USERNAME_VALIDATOR
|
USER_USERNAME_VALIDATOR
|
||||||
} from '@app/shared/form-validators/user-validators'
|
} from '@app/shared/form-validators/user-validators'
|
||||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
import { UserSignupService } from '@app/shared/shared-users'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-register-step-user',
|
selector: 'my-register-step-user',
|
||||||
|
@ -24,7 +24,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formReactiveService: FormReactiveService,
|
protected formReactiveService: FormReactiveService,
|
||||||
private userSignupService: UserSignupService
|
private signupService: SignupService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
|
||||||
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
|
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
|
||||||
const username = this.form.value['username'] || ''
|
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 })
|
this.form.patchValue({ username: newUsername })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||||
import { Notifier, RedirectService, ServerService } from '@app/core'
|
import { Notifier, RedirectService, ServerService } from '@app/core'
|
||||||
import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
import { UserSignupService } from '@app/shared/shared-users'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-verify-account-ask-send-email',
|
selector: 'my-verify-account-ask-send-email',
|
||||||
|
@ -15,7 +15,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formReactiveService: FormReactiveService,
|
protected formReactiveService: FormReactiveService,
|
||||||
private userSignupService: UserSignupService,
|
private signupService: SignupService,
|
||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private redirectService: RedirectService
|
private redirectService: RedirectService
|
||||||
|
@ -34,7 +34,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
|
||||||
|
|
||||||
askSendVerifyEmail () {
|
askSendVerifyEmail () {
|
||||||
const email = this.form.value['verify-email-email']
|
const email = this.form.value['verify-email-email']
|
||||||
this.userSignupService.askSendVerifyEmail(email)
|
this.signupService.askSendVerifyEmail(email)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.notifier.success($localize`An email with verification link will be sent to ${email}.`)
|
this.notifier.success($localize`An email with verification link will be sent to ${email}.`)
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
<div class="margin-content">
|
<div *ngIf="loaded" class="margin-content">
|
||||||
<h1 i18n class="title-page">Verify account email confirmation</h1>
|
<h1 i18n class="title-page">Verify email</h1>
|
||||||
|
|
||||||
<my-signup-success i18n *ngIf="!isPendingEmail && success" [requiresEmailVerification]="false">
|
<my-signup-success-after-email
|
||||||
</my-signup-success>
|
*ngIf="displaySignupSuccess()"
|
||||||
|
[requiresApproval]="isRegistrationRequest() && requiresApproval"
|
||||||
|
>
|
||||||
|
</my-signup-success-after-email>
|
||||||
|
|
||||||
<div i18n class="alert alert-success" *ngIf="isPendingEmail && success">Email updated.</div>
|
<div i18n class="alert alert-success" *ngIf="!isRegistrationRequest() && isPendingEmail && success">Email updated.</div>
|
||||||
|
|
||||||
<div class="alert alert-danger" *ngIf="failed">
|
<div class="alert alert-danger" *ngIf="failed">
|
||||||
<span i18n>An error occurred.</span>
|
<span i18n>An error occurred.</span>
|
||||||
|
|
||||||
<a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email" [queryParams]="{ isPendingEmail: isPendingEmail }">Request new verification email</a>
|
<a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email">
|
||||||
|
Request a new verification email
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { AuthService, Notifier } from '@app/core'
|
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||||
import { UserSignupService } from '@app/shared/shared-users'
|
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-verify-account-email',
|
selector: 'my-verify-account-email',
|
||||||
|
@ -13,32 +13,82 @@ export class VerifyAccountEmailComponent implements OnInit {
|
||||||
failed = false
|
failed = false
|
||||||
isPendingEmail = false
|
isPendingEmail = false
|
||||||
|
|
||||||
|
requiresApproval: boolean
|
||||||
|
loaded = false
|
||||||
|
|
||||||
private userId: number
|
private userId: number
|
||||||
|
private registrationId: number
|
||||||
private verificationString: string
|
private verificationString: string
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private userSignupService: UserSignupService,
|
private signupService: SignupService,
|
||||||
|
private server: ServerService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get instanceName () {
|
||||||
|
return this.server.getHTMLConfig().instance.name
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
const queryParams = this.route.snapshot.queryParams
|
const queryParams = this.route.snapshot.queryParams
|
||||||
|
|
||||||
|
this.server.getConfig().subscribe(config => {
|
||||||
|
this.requiresApproval = config.signup.requiresApproval
|
||||||
|
|
||||||
|
this.loaded = true
|
||||||
|
})
|
||||||
|
|
||||||
this.userId = queryParams['userId']
|
this.userId = queryParams['userId']
|
||||||
|
this.registrationId = queryParams['registrationId']
|
||||||
|
|
||||||
this.verificationString = queryParams['verificationString']
|
this.verificationString = queryParams['verificationString']
|
||||||
|
|
||||||
this.isPendingEmail = queryParams['isPendingEmail'] === 'true'
|
this.isPendingEmail = queryParams['isPendingEmail'] === 'true'
|
||||||
|
|
||||||
if (!this.userId || !this.verificationString) {
|
if (!this.verificationString) {
|
||||||
this.notifier.error($localize`Unable to find user id or verification string.`)
|
this.notifier.error($localize`Unable to find verification string in URL query.`)
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.userId && !this.registrationId) {
|
||||||
|
this.notifier.error($localize`Unable to find user id or registration id in URL query.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.verifyEmail()
|
this.verifyEmail()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isRegistrationRequest () {
|
||||||
|
return !!this.registrationId
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySignupSuccess () {
|
||||||
|
if (!this.success) return false
|
||||||
|
if (!this.isRegistrationRequest() && this.isPendingEmail) return false
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyEmail () {
|
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({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
if (this.authService.isLoggedIn()) {
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main'
|
||||||
import { SharedUsersModule } from '@app/shared/shared-users'
|
import { SharedUsersModule } from '@app/shared/shared-users'
|
||||||
import { SignupMascotComponent } from './signup-mascot.component'
|
import { SignupMascotComponent } from './signup-mascot.component'
|
||||||
import { SignupStepTitleComponent } from './signup-step-title.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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -16,7 +18,8 @@ import { SignupSuccessComponent } from './signup-success.component'
|
||||||
],
|
],
|
||||||
|
|
||||||
declarations: [
|
declarations: [
|
||||||
SignupSuccessComponent,
|
SignupSuccessBeforeEmailComponent,
|
||||||
|
SignupSuccessAfterEmailComponent,
|
||||||
SignupStepTitleComponent,
|
SignupStepTitleComponent,
|
||||||
SignupMascotComponent
|
SignupMascotComponent
|
||||||
],
|
],
|
||||||
|
@ -26,12 +29,14 @@ import { SignupSuccessComponent } from './signup-success.component'
|
||||||
SharedFormModule,
|
SharedFormModule,
|
||||||
SharedGlobalIconModule,
|
SharedGlobalIconModule,
|
||||||
|
|
||||||
SignupSuccessComponent,
|
SignupSuccessBeforeEmailComponent,
|
||||||
|
SignupSuccessAfterEmailComponent,
|
||||||
SignupStepTitleComponent,
|
SignupStepTitleComponent,
|
||||||
SignupMascotComponent
|
SignupMascotComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
|
SignupService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedSignupModule { }
|
export class SharedSignupModule { }
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<my-signup-step-title mascotImageName="success">
|
||||||
|
<strong i18n>Email verified!</strong>
|
||||||
|
</my-signup-step-title>
|
||||||
|
|
||||||
|
<div class="alert pt-alert-primary">
|
||||||
|
<ng-container *ngIf="requiresApproval">
|
||||||
|
<p i18n>Your email has been verified and your account request has been sent!</p>
|
||||||
|
|
||||||
|
<p i18n>
|
||||||
|
A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected.
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!requiresApproval">
|
||||||
|
<p i18n>Your email has been verified and your account has been created!</p>
|
||||||
|
|
||||||
|
<p i18n>
|
||||||
|
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>.
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<my-signup-step-title mascotImageName="success">
|
||||||
|
<ng-container *ngIf="requiresApproval">
|
||||||
|
<strong i18n>Account request sent</strong>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!requiresApproval" i18n>
|
||||||
|
<strong>Welcome</strong>
|
||||||
|
<div>on {{ instanceName }}</div>
|
||||||
|
</ng-container>
|
||||||
|
</my-signup-step-title>
|
||||||
|
|
||||||
|
<div class="alert pt-alert-primary">
|
||||||
|
<p *ngIf="requiresApproval" i18n>Your account request has been sent!</p>
|
||||||
|
<p *ngIf="!requiresApproval" i18n>Your account has been created!</p>
|
||||||
|
|
||||||
|
<ng-container *ngIf="requiresEmailVerification">
|
||||||
|
<p i18n *ngIf="requiresApproval">
|
||||||
|
<strong>Check your emails</strong> to validate your account and complete your registration request.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p i18n *ngIf="!requiresApproval">
|
||||||
|
<strong>Check your emails</strong> to validate your account and complete your registration.
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!requiresEmailVerification">
|
||||||
|
<p i18n *ngIf="requiresApproval">
|
||||||
|
A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p *ngIf="!requiresApproval" i18n>
|
||||||
|
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>.
|
||||||
|
</p>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
|
@ -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
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
<my-signup-step-title mascotImageName="success" i18n>
|
|
||||||
<strong>Welcome</strong>
|
|
||||||
<div>on {{ instanceName }}</div>
|
|
||||||
</my-signup-step-title>
|
|
||||||
|
|
||||||
<div class="alert pt-alert-primary">
|
|
||||||
<p i18n>Your account has been created!</p>
|
|
||||||
|
|
||||||
<p i18n *ngIf="requiresEmailVerification">
|
|
||||||
<strong>Check your emails</strong> to validate your account and complete your inscription.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ng-container *ngIf="!requiresEmailVerification">
|
|
||||||
<p i18n>
|
|
||||||
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>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p i18n>
|
|
||||||
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>.
|
|
||||||
</p>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,17 +2,18 @@ import { catchError, tap } from 'rxjs/operators'
|
||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor, UserService } from '@app/core'
|
import { RestExtractor, UserService } from '@app/core'
|
||||||
import { UserRegister } from '@shared/models'
|
import { UserRegister, UserRegistrationRequest } from '@shared/models'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserSignupService {
|
export class SignupService {
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private authHttp: HttpClient,
|
private authHttp: HttpClient,
|
||||||
private restExtractor: RestExtractor,
|
private restExtractor: RestExtractor,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
signup (userCreate: UserRegister) {
|
directSignup (userCreate: UserRegister) {
|
||||||
return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
|
return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
|
||||||
.pipe(
|
.pipe(
|
||||||
tap(() => this.userService.setSignupInThisSession(true)),
|
tap(() => this.userService.setSignupInThisSession(true)),
|
||||||
|
@ -20,8 +21,21 @@ export class UserSignupService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) {
|
requestSignup (userCreate: UserRegistrationRequest) {
|
||||||
const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
|
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 = {
|
const body = {
|
||||||
verificationString,
|
verificationString,
|
||||||
isPendingEmail
|
isPendingEmail
|
||||||
|
@ -31,13 +45,28 @@ export class UserSignupService {
|
||||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
.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) {
|
askSendVerifyEmail (email: string) {
|
||||||
const url = UserService.BASE_USERS_URL + '/ask-send-verify-email'
|
const url = UserService.BASE_USERS_URL + 'ask-send-verify-email'
|
||||||
|
|
||||||
return this.authHttp.post(url, { email })
|
return this.authHttp.post(url, { email })
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
|
getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
|
||||||
// Don't update display name, the user seems to have changed it
|
// Don't update display name, the user seems to have changed it
|
||||||
if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername
|
if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername
|
|
@ -103,7 +103,9 @@
|
||||||
<a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a>
|
<a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a>
|
||||||
<a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a>
|
<a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a>
|
||||||
|
|
||||||
<a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a>
|
<a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">
|
||||||
|
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngFor="let menuSection of menuSections" >
|
<ng-container *ngFor="let menuSection of menuSections" >
|
||||||
|
|
|
@ -92,6 +92,10 @@ export class MenuComponent implements OnInit {
|
||||||
return this.languageChooserModal.getCurrentLanguage()
|
return this.languageChooserModal.getCurrentLanguage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get requiresApproval () {
|
||||||
|
return this.serverConfig.signup.requiresApproval
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.htmlServerConfig = this.serverService.getHTMLConfig()
|
this.htmlServerConfig = this.serverService.getHTMLConfig()
|
||||||
this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
|
this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
|
||||||
|
|
|
@ -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 = {
|
export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = {
|
||||||
VALIDATORS: [
|
VALIDATORS: [
|
||||||
Validators.minLength(3),
|
Validators.minLength(3),
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<span class="moderation-expanded-text">
|
<span class="moderation-expanded-text">
|
||||||
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }"
|
||||||
class="chip"
|
class="chip me-1"
|
||||||
>
|
>
|
||||||
<my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar>
|
<my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar>
|
||||||
<div>
|
<div>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
<span class="moderation-expanded-label" i18n>Reportee</span>
|
<span class="moderation-expanded-label" i18n>Reportee</span>
|
||||||
<span class="moderation-expanded-text">
|
<span class="moderation-expanded-text">
|
||||||
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }"
|
||||||
class="chip"
|
class="chip me-1"
|
||||||
>
|
>
|
||||||
<my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar>
|
<my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar>
|
||||||
<div>
|
<div>
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
<div *ngIf="predefinedReasons" class="mt-2 d-flex">
|
<div *ngIf="predefinedReasons" class="mt-2 d-flex">
|
||||||
<span>
|
<span>
|
||||||
<a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]"
|
<a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]"
|
||||||
[queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
|
[queryParams]="{ 'search': 'tag:' + reason.id }" class="pt-badge badge-secondary"
|
||||||
>
|
>
|
||||||
<div>{{ reason.label }}</div>
|
<div>{{ reason.label }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './account.model'
|
export * from './account.model'
|
||||||
export * from './account.service'
|
export * from './account.service'
|
||||||
export * from './actor.model'
|
export * from './actor.model'
|
||||||
|
export * from './signup-label.component'
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<ng-container i18n *ngIf="requiresApproval">Request an account</ng-container>
|
||||||
|
<ng-container i18n *ngIf="!requiresApproval">Create an account</ng-container>
|
|
@ -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
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { LoadingBarModule } from '@ngx-loading-bar/core'
|
import { LoadingBarModule } from '@ngx-loading-bar/core'
|
||||||
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||||
import { SharedGlobalIconModule } from '../shared-icons'
|
import { SharedGlobalIconModule } from '../shared-icons'
|
||||||
import { AccountService } from './account'
|
import { AccountService, SignupLabelComponent } from './account'
|
||||||
import {
|
import {
|
||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
BytesPipe,
|
BytesPipe,
|
||||||
|
@ -113,6 +113,8 @@ import { VideoChannelService } from './video-channel'
|
||||||
UserQuotaComponent,
|
UserQuotaComponent,
|
||||||
UserNotificationsComponent,
|
UserNotificationsComponent,
|
||||||
|
|
||||||
|
SignupLabelComponent,
|
||||||
|
|
||||||
EmbedComponent,
|
EmbedComponent,
|
||||||
|
|
||||||
PluginPlaceholderComponent,
|
PluginPlaceholderComponent,
|
||||||
|
@ -171,6 +173,8 @@ import { VideoChannelService } from './video-channel'
|
||||||
UserQuotaComponent,
|
UserQuotaComponent,
|
||||||
UserNotificationsComponent,
|
UserNotificationsComponent,
|
||||||
|
|
||||||
|
SignupLabelComponent,
|
||||||
|
|
||||||
EmbedComponent,
|
EmbedComponent,
|
||||||
|
|
||||||
PluginPlaceholderComponent,
|
PluginPlaceholderComponent,
|
||||||
|
|
|
@ -83,6 +83,11 @@ export class UserNotification implements UserNotificationServer {
|
||||||
latestVersion: string
|
latestVersion: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registration?: {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
|
|
||||||
|
@ -97,6 +102,8 @@ export class UserNotification implements UserNotificationServer {
|
||||||
|
|
||||||
accountUrl?: string
|
accountUrl?: string
|
||||||
|
|
||||||
|
registrationsUrl?: string
|
||||||
|
|
||||||
videoImportIdentifier?: string
|
videoImportIdentifier?: string
|
||||||
videoImportUrl?: string
|
videoImportUrl?: string
|
||||||
|
|
||||||
|
@ -135,6 +142,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
|
|
||||||
this.plugin = hash.plugin
|
this.plugin = hash.plugin
|
||||||
this.peertube = hash.peertube
|
this.peertube = hash.peertube
|
||||||
|
this.registration = hash.registration
|
||||||
|
|
||||||
this.createdAt = hash.createdAt
|
this.createdAt = hash.createdAt
|
||||||
this.updatedAt = hash.updatedAt
|
this.updatedAt = hash.updatedAt
|
||||||
|
@ -208,6 +216,10 @@ export class UserNotification implements UserNotificationServer {
|
||||||
this.accountUrl = this.buildAccountUrl(this.account)
|
this.accountUrl = this.buildAccountUrl(this.account)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case UserNotificationType.NEW_USER_REGISTRATION_REQUEST:
|
||||||
|
this.registrationsUrl = '/admin/moderation/registrations/list'
|
||||||
|
break
|
||||||
|
|
||||||
case UserNotificationType.NEW_FOLLOW:
|
case UserNotificationType.NEW_FOLLOW:
|
||||||
this.accountUrl = this.buildAccountUrl(this.actorFollow.follower)
|
this.accountUrl = this.buildAccountUrl(this.actorFollow.follower)
|
||||||
break
|
break
|
||||||
|
|
|
@ -215,6 +215,14 @@
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngSwitchCase="20"> <!-- UserNotificationType.NEW_USER_REGISTRATION_REQUEST -->
|
||||||
|
<my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message" i18n>
|
||||||
|
User <a (click)="markAsRead(notification)" [routerLink]="notification.registrationsUrl">{{ notification.registration.username }}</a> wants to register on your instance
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngSwitchDefault>
|
<ng-container *ngSwitchDefault>
|
||||||
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
@use '_variables' as *;
|
@use '_variables' as *;
|
||||||
@use '_mixins' as *;
|
@use '_mixins' as *;
|
||||||
|
|
||||||
.chip {
|
|
||||||
@include chip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unblock-button {
|
.unblock-button {
|
||||||
@include peertube-button;
|
@include peertube-button;
|
||||||
@include grey-button;
|
@include grey-button;
|
||||||
|
|
|
@ -40,10 +40,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
|
||||||
@include chip;
|
|
||||||
}
|
|
||||||
|
|
||||||
my-action-dropdown.show {
|
my-action-dropdown.show {
|
||||||
::ng-deep .dropdown-root {
|
::ng-deep .dropdown-root {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
|
@ -24,7 +24,3 @@ a {
|
||||||
.block-button {
|
.block-button {
|
||||||
@include create-button;
|
@include create-button;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chip {
|
|
||||||
@include chip;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export * from './user-admin.service'
|
export * from './user-admin.service'
|
||||||
export * from './user-signup.service'
|
|
||||||
export * from './two-factor.service'
|
export * from './two-factor.service'
|
||||||
|
|
||||||
export * from './shared-users.module'
|
export * from './shared-users.module'
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
|
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
import { SharedMainModule } from '../shared-main/shared-main.module'
|
import { SharedMainModule } from '../shared-main/shared-main.module'
|
||||||
import { TwoFactorService } from './two-factor.service'
|
import { TwoFactorService } from './two-factor.service'
|
||||||
import { UserAdminService } from './user-admin.service'
|
import { UserAdminService } from './user-admin.service'
|
||||||
import { UserSignupService } from './user-signup.service'
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -15,7 +13,6 @@ import { UserSignupService } from './user-signup.service'
|
||||||
exports: [],
|
exports: [],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
UserSignupService,
|
|
||||||
UserAdminService,
|
UserAdminService,
|
||||||
TwoFactorService
|
TwoFactorService
|
||||||
]
|
]
|
||||||
|
|
|
@ -53,8 +53,8 @@
|
||||||
<ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
|
<ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="containedInPlaylists" class="video-contained-in-playlists">
|
<div *ngIf="containedInPlaylists" class="fs-6">
|
||||||
<a *ngFor="let playlist of containedInPlaylists" class="chip rectangular bg-secondary text-light" [routerLink]="['/w/p/', playlist.playlistShortUUID]">
|
<a *ngFor="let playlist of containedInPlaylists" class="pt-badge badge-secondary" [routerLink]="['/w/p/', playlist.playlistShortUUID]">
|
||||||
{{ playlist.playlistDisplayName }}
|
{{ playlist.playlistDisplayName }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
|
|
||||||
$more-button-width: 40px;
|
$more-button-width: 40px;
|
||||||
|
|
||||||
.chip {
|
|
||||||
@include chip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-miniature {
|
.video-miniature {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -284,3 +284,9 @@ label + .form-group-description {
|
||||||
border: 2px solid pvar(--mainColorLightest);
|
border: 2px solid pvar(--mainColorLightest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
.chip {
|
||||||
|
@include chip;
|
||||||
|
}
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
font-weight: $font-semibold;
|
font-weight: $font-semibold;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
|
||||||
|
&.badge-fs-normal {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&.badge-primary {
|
&.badge-primary {
|
||||||
color: pvar(--mainBackgroundColor);
|
color: pvar(--mainBackgroundColor);
|
||||||
background-color: pvar(--mainColor);
|
background-color: pvar(--mainColor);
|
||||||
|
|
|
@ -15,7 +15,3 @@
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2');
|
src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin muted {
|
|
||||||
color: pvar(--greyForegroundColor) !important;
|
|
||||||
}
|
|
||||||
|
|
|
@ -36,6 +36,10 @@
|
||||||
max-height: $font-size * $number-of-lines;
|
max-height: $font-size * $number-of-lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin muted {
|
||||||
|
color: pvar(--greyForegroundColor) !important;
|
||||||
|
}
|
||||||
|
|
||||||
@mixin fade-text ($fade-after, $background-color) {
|
@mixin fade-text ($fade-after, $background-color) {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -791,51 +795,39 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin chip {
|
@mixin chip {
|
||||||
--chip-radius: 5rem;
|
--avatar-size: 1.2rem;
|
||||||
--chip-padding: .2rem .4rem;
|
|
||||||
$avatar-height: 1.2rem;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
border-radius: var(--chip-radius);
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
font-size: 90%;
|
|
||||||
color: pvar(--mainForegroundColor);
|
color: pvar(--mainForegroundColor);
|
||||||
height: $avatar-height;
|
height: var(--avatar-size);
|
||||||
line-height: 1rem;
|
|
||||||
margin: .1rem;
|
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: var(--chip-padding);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&.rectangular {
|
|
||||||
--chip-radius: .2rem;
|
|
||||||
--chip-padding: .2rem .3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
my-actor-avatar {
|
my-actor-avatar {
|
||||||
@include margin-left(-.4rem);
|
|
||||||
@include margin-right(.2rem);
|
@include margin-right(.2rem);
|
||||||
|
|
||||||
|
border-radius: 5rem;
|
||||||
|
width: var(--avatar-size);
|
||||||
|
height: var(--avatar-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.two-lines {
|
&.two-lines {
|
||||||
$avatar-height: 2rem;
|
--avatar-size: 2rem;
|
||||||
|
|
||||||
height: $avatar-height;
|
font-size: 14px;
|
||||||
|
line-height: 1rem;
|
||||||
|
|
||||||
my-actor-avatar {
|
my-actor-avatar {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
> div {
|
||||||
margin: 0 .1rem;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: $avatar-height;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue