Implement signup approval in client
This commit is contained in:
parent
b379759f55
commit
9589907c89
|
@ -96,6 +96,14 @@ export class AdminComponent implements OnInit {
|
|||
children: []
|
||||
}
|
||||
|
||||
if (this.hasRegistrationsRight()) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Registrations`,
|
||||
routerLink: '/admin/moderation/registrations/list',
|
||||
iconName: 'user'
|
||||
})
|
||||
}
|
||||
|
||||
if (this.hasAbusesRight()) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Reports`,
|
||||
|
@ -229,4 +237,8 @@ export class AdminComponent implements OnInit {
|
|||
private hasVideosRight () {
|
||||
return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
|
||||
}
|
||||
|
||||
private hasRegistrationsRight () {
|
||||
return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,13 @@ import { FollowersListComponent, FollowModalComponent, VideoRedundanciesListComp
|
|||
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
||||
import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
|
||||
import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
|
||||
import { AbuseListComponent, VideoBlockListComponent } from './moderation'
|
||||
import {
|
||||
AbuseListComponent,
|
||||
AdminRegistrationService,
|
||||
ProcessRegistrationModalComponent,
|
||||
RegistrationListComponent,
|
||||
VideoBlockListComponent
|
||||
} from './moderation'
|
||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
|
||||
import {
|
||||
UserCreateComponent,
|
||||
|
@ -116,7 +122,10 @@ import { JobsComponent } from './system/jobs/jobs.component'
|
|||
EditLiveConfigurationComponent,
|
||||
EditAdvancedConfigurationComponent,
|
||||
EditInstanceInformationComponent,
|
||||
EditHomepageComponent
|
||||
EditHomepageComponent,
|
||||
|
||||
RegistrationListComponent,
|
||||
ProcessRegistrationModalComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -130,7 +139,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
|
|||
ConfigService,
|
||||
PluginApiService,
|
||||
EditConfigurationService,
|
||||
VideoAdminService
|
||||
VideoAdminService,
|
||||
AdminRegistrationService
|
||||
]
|
||||
})
|
||||
export class AdminModule { }
|
||||
|
|
|
@ -171,12 +171,21 @@
|
|||
</ng-container>
|
||||
|
||||
<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()"
|
||||
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
|
||||
i18n-labelText labelText="Signup requires email verification"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div [ngClass]="getDisabledSignupClass()" class="mt-3">
|
||||
<div [ngClass]="getDisabledSignupClass()">
|
||||
<label i18n for="signupLimit">Signup limit</label>
|
||||
|
||||
<div class="number-with-unit">
|
||||
|
|
|
@ -132,6 +132,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
signup: {
|
||||
enabled: null,
|
||||
limit: SIGNUP_LIMIT_VALIDATOR,
|
||||
requiresApproval: null,
|
||||
requiresEmailVerification: null,
|
||||
minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from './abuse-list'
|
||||
export * from './instance-blocklist'
|
||||
export * from './video-block-list'
|
||||
export * from './registration-list'
|
||||
export * from './moderation.routes'
|
||||
|
|
|
@ -4,6 +4,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
|
|||
import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
|
||||
import { UserRightGuard } from '@app/core'
|
||||
import { UserRight } from '@shared/models'
|
||||
import { RegistrationListComponent } from './registration-list'
|
||||
|
||||
export const ModerationRoutes: Routes = [
|
||||
{
|
||||
|
@ -68,7 +69,19 @@ export const ModerationRoutes: Routes = [
|
|||
}
|
||||
},
|
||||
|
||||
// We move this component in admin overview pages
|
||||
{
|
||||
path: 'registrations/list',
|
||||
component: RegistrationListComponent,
|
||||
canActivate: [ UserRightGuard ],
|
||||
data: {
|
||||
userRight: UserRight.MANAGE_REGISTRATIONS,
|
||||
meta: {
|
||||
title: $localize`User registrations`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// We moved this component in admin overview pages
|
||||
{
|
||||
path: 'video-comments',
|
||||
redirectTo: 'video-comments/list',
|
||||
|
|
|
@ -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">
|
||||
<my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar>
|
||||
<div>
|
||||
<span class="user-table-primary-text">{{ user.account.displayName }}</span>
|
||||
<span>{{ user.account.displayName }}</span>
|
||||
<span class="muted">{{ user.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -110,23 +110,10 @@
|
|||
<span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span>
|
||||
</td>
|
||||
|
||||
<td *ngIf="isSelected('email')" [title]="user.email">
|
||||
<ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">
|
||||
<a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a>
|
||||
</ng-container>
|
||||
<td *ngIf="isSelected('email')">
|
||||
<my-user-email-info [entry]="user" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info>
|
||||
</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')">
|
||||
<div class="progress" i18n-title title="Total video quota">
|
||||
<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;
|
||||
}
|
||||
|
||||
.table-email {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
color: pvar(--mainForegroundColor);
|
||||
}
|
||||
|
||||
.banned-info {
|
||||
font-style: italic;
|
||||
}
|
||||
|
@ -37,10 +31,6 @@ my-global-icon {
|
|||
width: 18px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
@include chip;
|
||||
}
|
||||
|
||||
.progress {
|
||||
@include progressbar($small: true);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { NgModule } from '@angular/core'
|
||||
import { SharedMainModule } from '../../shared/shared-main/shared-main.module'
|
||||
import { UserEmailInfoComponent } from './user-email-info.component'
|
||||
import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
|
||||
|
||||
@NgModule({
|
||||
|
@ -8,11 +9,13 @@ import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
|
|||
],
|
||||
|
||||
declarations: [
|
||||
UserRealQuotaInfoComponent
|
||||
UserRealQuotaInfoComponent,
|
||||
UserEmailInfoComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
UserRealQuotaInfoComponent
|
||||
UserRealQuotaInfoComponent,
|
||||
UserEmailInfoComponent
|
||||
],
|
||||
|
||||
providers: []
|
||||
|
|
|
@ -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
|
||||
) {}
|
||||
|
||||
getJobs (options: {
|
||||
listJobs (options: {
|
||||
jobState?: JobStateClient
|
||||
jobType: JobTypeClient
|
||||
pagination: RestPagination
|
||||
|
|
|
@ -125,7 +125,7 @@ export class JobsComponent extends RestTable implements OnInit {
|
|||
if (this.jobState === 'all') jobState = null
|
||||
|
||||
this.jobsService
|
||||
.getJobs({
|
||||
.listJobs({
|
||||
jobState,
|
||||
jobType: this.jobType,
|
||||
pagination: this.pagination,
|
||||
|
|
|
@ -9,7 +9,7 @@ import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shar
|
|||
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
||||
import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { getExternalAuthHref } from '@shared/core-utils'
|
||||
import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
|
||||
import { RegisteredExternalAuthConfig, ServerConfig, ServerErrorCode } from '@shared/models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-login',
|
||||
|
@ -197,6 +197,8 @@ The link will expire within 1 hour.`
|
|||
}
|
||||
|
||||
private handleError (err: any) {
|
||||
console.log(err)
|
||||
|
||||
if (this.authService.isOTPMissingError(err)) {
|
||||
this.otpStep = true
|
||||
|
||||
|
@ -208,8 +210,26 @@ The link will expire within 1 hour.`
|
|||
return
|
||||
}
|
||||
|
||||
if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.`
|
||||
else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.`
|
||||
else this.error = err.message
|
||||
if (err.message.includes('credentials are invalid')) {
|
||||
this.error = $localize`Incorrect username or password.`
|
||||
return
|
||||
}
|
||||
|
||||
if (err.message.includes('blocked')) {
|
||||
this.error = $localize`Your account is blocked.`
|
||||
return
|
||||
}
|
||||
|
||||
if (err.body?.code === ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL) {
|
||||
this.error = $localize`This account is awaiting approval by moderators.`
|
||||
return
|
||||
}
|
||||
|
||||
if (err.body?.code === ServerErrorCode.ACCOUNT_APPROVAL_REJECTED) {
|
||||
this.error = $localize`Registration approval has been rejected for this account.`
|
||||
return
|
||||
}
|
||||
|
||||
this.error = err.message
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
@use '_miniature' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.chip {
|
||||
@include chip;
|
||||
}
|
||||
|
||||
.video-table-video {
|
||||
display: inline-flex;
|
||||
|
||||
|
|
|
@ -5,29 +5,34 @@
|
|||
</div>
|
||||
|
||||
<ng-container *ngIf="!signupDisabled">
|
||||
<h1 i18n class="title-page-v2">
|
||||
<h1 class="title-page-v2">
|
||||
<strong class="underline-orange">{{ instanceName }}</strong>
|
||||
>
|
||||
Create an account
|
||||
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||
</h1>
|
||||
|
||||
<div class="register-content">
|
||||
<my-custom-stepper linear>
|
||||
|
||||
<cdk-step i18n-label label="About" [editable]="!signupSuccess">
|
||||
<my-signup-step-title mascotImageName="about" i18n>
|
||||
<strong>Create an account</strong>
|
||||
<div>on {{ instanceName }}</div>
|
||||
<my-signup-step-title mascotImageName="about">
|
||||
<strong>
|
||||
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||
</strong>
|
||||
|
||||
<div i18n>on {{ instanceName }}</div>
|
||||
</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">
|
||||
<a i18n class="skip-step underline-orange" routerLink="/login">
|
||||
<strong>I already have an account</strong>, I log in
|
||||
</a>
|
||||
|
||||
<button i18n cdkStepperNext>Create an account</button>
|
||||
<button cdkStepperNext>
|
||||
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||
</button>
|
||||
</div>
|
||||
</cdk-step>
|
||||
|
||||
|
@ -44,8 +49,8 @@
|
|||
></my-instance-about-accordion>
|
||||
|
||||
<my-register-step-terms
|
||||
[hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
|
||||
[minimumAge]="minimumAge"
|
||||
[hasCodeOfConduct]="!!aboutHtml.codeOfConduct" [minimumAge]="minimumAge" [instanceName]="instanceName"
|
||||
[requiresApproval]="requiresApproval"
|
||||
(formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
|
||||
></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>
|
||||
|
||||
<button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n>
|
||||
Create my account
|
||||
<button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()">
|
||||
<my-signup-label [requiresApproval]="requiresApproval"></my-signup-label>
|
||||
</button>
|
||||
</div>
|
||||
</cdk-step>
|
||||
|
||||
<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>
|
||||
|
||||
<div i18n>PeerTube is creating your account...</div>
|
||||
|
@ -109,7 +115,10 @@
|
|||
|
||||
<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">
|
||||
<button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button>
|
||||
|
|
|
@ -5,10 +5,10 @@ import { ActivatedRoute } from '@angular/router'
|
|||
import { AuthService } from '@app/core'
|
||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
||||
import { UserSignupService } from '@app/shared/shared-users'
|
||||
import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { UserRegister } from '@shared/models'
|
||||
import { ServerConfig } from '@shared/models/server'
|
||||
import { SignupService } from '../shared/signup.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-register',
|
||||
|
@ -53,7 +53,7 @@ export class RegisterComponent implements OnInit {
|
|||
constructor (
|
||||
private route: ActivatedRoute,
|
||||
private authService: AuthService,
|
||||
private userSignupService: UserSignupService,
|
||||
private signupService: SignupService,
|
||||
private hooks: HooksService
|
||||
) { }
|
||||
|
||||
|
@ -61,6 +61,10 @@ export class RegisterComponent implements OnInit {
|
|||
return this.serverConfig.signup.requiresEmailVerification
|
||||
}
|
||||
|
||||
get requiresApproval () {
|
||||
return this.serverConfig.signup.requiresApproval
|
||||
}
|
||||
|
||||
get minimumAge () {
|
||||
return this.serverConfig.signup.minimumAge
|
||||
}
|
||||
|
@ -132,44 +136,64 @@ export class RegisterComponent implements OnInit {
|
|||
skipChannelCreation () {
|
||||
this.formStepChannel.reset()
|
||||
this.lastStep.select()
|
||||
|
||||
this.signup()
|
||||
}
|
||||
|
||||
async signup () {
|
||||
this.signupError = undefined
|
||||
|
||||
const body: UserRegister = await this.hooks.wrapObject(
|
||||
{
|
||||
...this.formStepUser.value,
|
||||
const termsForm = this.formStepTerms.value
|
||||
const userForm = this.formStepUser.value
|
||||
const channelForm = this.formStepChannel?.value
|
||||
|
||||
channel: this.formStepChannel?.value?.name
|
||||
? this.formStepChannel.value
|
||||
const channel = this.formStepChannel?.value?.name
|
||||
? { name: channelForm?.name, displayName: channelForm?.displayName }
|
||||
: undefined
|
||||
|
||||
const body = await this.hooks.wrapObject(
|
||||
{
|
||||
username: userForm.username,
|
||||
password: userForm.password,
|
||||
email: userForm.email,
|
||||
displayName: userForm.displayName,
|
||||
|
||||
registrationReason: termsForm.registrationReason,
|
||||
|
||||
channel
|
||||
},
|
||||
'signup',
|
||||
'filter:api.signup.registration.create.params'
|
||||
)
|
||||
|
||||
this.userSignupService.signup(body).subscribe({
|
||||
const obs = this.requiresApproval
|
||||
? this.signupService.requestSignup(body)
|
||||
: this.signupService.directSignup(body)
|
||||
|
||||
obs.subscribe({
|
||||
next: () => {
|
||||
if (this.requiresEmailVerification) {
|
||||
if (this.requiresEmailVerification || this.requiresApproval) {
|
||||
this.signupSuccess = true
|
||||
return
|
||||
}
|
||||
|
||||
// Auto login
|
||||
this.autoLogin(body)
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.signupError = err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private autoLogin (body: UserRegister) {
|
||||
this.authService.login({ username: body.username, password: body.password })
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.signupSuccess = true
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.signupError = err.message
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
error: err => {
|
||||
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 *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li>
|
||||
</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>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { ServerService } from '@app/core'
|
|||
styleUrls: [ './register-step-about.component.scss' ]
|
||||
})
|
||||
export class RegisterStepAboutComponent {
|
||||
@Input() requiresApproval: boolean
|
||||
@Input() videoUploadDisabled: boolean
|
||||
|
||||
constructor (private serverService: ServerService) {
|
||||
|
|
|
@ -2,9 +2,9 @@ import { concat, of } from 'rxjs'
|
|||
import { pairwise } from 'rxjs/operators'
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||
import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators'
|
||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||
import { UserSignupService } from '@app/shared/shared-users'
|
||||
|
||||
@Component({
|
||||
selector: 'my-register-step-channel',
|
||||
|
@ -20,7 +20,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
|
|||
|
||||
constructor (
|
||||
protected formReactiveService: FormReactiveService,
|
||||
private userSignupService: UserSignupService
|
||||
private signupService: SignupService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
|
|||
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
|
||||
const name = this.form.value['name'] || ''
|
||||
|
||||
const newName = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, name)
|
||||
const newName = this.signupService.getNewUsername(oldDisplayName, newDisplayName, name)
|
||||
this.form.patchValue({ name: newName })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
<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">
|
||||
<my-peertube-checkbox inputName="terms" formControlName="terms">
|
||||
<ng-template ptTemplate="label">
|
||||
|
@ -6,7 +18,7 @@
|
|||
I am at least {{ minimumAge }} years old and agree
|
||||
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>
|
||||
of this instance
|
||||
of {{ instanceName }}
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</my-peertube-checkbox>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||
import { REGISTER_REASON_VALIDATOR, REGISTER_TERMS_VALIDATOR } from '../shared'
|
||||
|
||||
@Component({
|
||||
selector: 'my-register-step-terms',
|
||||
|
@ -10,7 +10,9 @@ import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
|||
})
|
||||
export class RegisterStepTermsComponent extends FormReactive implements OnInit {
|
||||
@Input() hasCodeOfConduct = false
|
||||
@Input() requiresApproval: boolean
|
||||
@Input() minimumAge = 16
|
||||
@Input() instanceName: string
|
||||
|
||||
@Output() formBuilt = new EventEmitter<FormGroup>()
|
||||
@Output() termsClick = new EventEmitter<void>()
|
||||
|
@ -28,7 +30,11 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit {
|
|||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
terms: USER_TERMS_VALIDATOR
|
||||
terms: REGISTER_TERMS_VALIDATOR,
|
||||
|
||||
registrationReason: this.requiresApproval
|
||||
? REGISTER_REASON_VALIDATOR
|
||||
: null
|
||||
})
|
||||
|
||||
setTimeout(() => this.formBuilt.emit(this.form))
|
||||
|
|
|
@ -2,6 +2,7 @@ import { concat, of } from 'rxjs'
|
|||
import { pairwise } from 'rxjs/operators'
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
|
||||
import { FormGroup } from '@angular/forms'
|
||||
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||
import {
|
||||
USER_DISPLAY_NAME_REQUIRED_VALIDATOR,
|
||||
USER_EMAIL_VALIDATOR,
|
||||
|
@ -9,7 +10,6 @@ import {
|
|||
USER_USERNAME_VALIDATOR
|
||||
} from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||
import { UserSignupService } from '@app/shared/shared-users'
|
||||
|
||||
@Component({
|
||||
selector: 'my-register-step-user',
|
||||
|
@ -24,7 +24,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
|
|||
|
||||
constructor (
|
||||
protected formReactiveService: FormReactiveService,
|
||||
private userSignupService: UserSignupService
|
||||
private signupService: SignupService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
|
|||
private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
|
||||
const username = this.form.value['username'] || ''
|
||||
|
||||
const newUsername = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, username)
|
||||
const newUsername = this.signupService.getNewUsername(oldDisplayName, newDisplayName, username)
|
||||
this.form.patchValue({ username: newUsername })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||
import { Notifier, RedirectService, ServerService } from '@app/core'
|
||||
import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
|
||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||
import { UserSignupService } from '@app/shared/shared-users'
|
||||
|
||||
@Component({
|
||||
selector: 'my-verify-account-ask-send-email',
|
||||
|
@ -15,7 +15,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
|
|||
|
||||
constructor (
|
||||
protected formReactiveService: FormReactiveService,
|
||||
private userSignupService: UserSignupService,
|
||||
private signupService: SignupService,
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier,
|
||||
private redirectService: RedirectService
|
||||
|
@ -34,7 +34,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
|
|||
|
||||
askSendVerifyEmail () {
|
||||
const email = this.form.value['verify-email-email']
|
||||
this.userSignupService.askSendVerifyEmail(email)
|
||||
this.signupService.askSendVerifyEmail(email)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notifier.success($localize`An email with verification link will be sent to ${email}.`)
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
<div class="margin-content">
|
||||
<h1 i18n class="title-page">Verify account email confirmation</h1>
|
||||
<div *ngIf="loaded" class="margin-content">
|
||||
<h1 i18n class="title-page">Verify email</h1>
|
||||
|
||||
<my-signup-success i18n *ngIf="!isPendingEmail && success" [requiresEmailVerification]="false">
|
||||
</my-signup-success>
|
||||
<my-signup-success-after-email
|
||||
*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">
|
||||
<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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { AuthService, Notifier } from '@app/core'
|
||||
import { UserSignupService } from '@app/shared/shared-users'
|
||||
import { SignupService } from '@app/+signup/shared/signup.service'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-verify-account-email',
|
||||
|
@ -13,32 +13,82 @@ export class VerifyAccountEmailComponent implements OnInit {
|
|||
failed = false
|
||||
isPendingEmail = false
|
||||
|
||||
requiresApproval: boolean
|
||||
loaded = false
|
||||
|
||||
private userId: number
|
||||
private registrationId: number
|
||||
private verificationString: string
|
||||
|
||||
constructor (
|
||||
private userSignupService: UserSignupService,
|
||||
private signupService: SignupService,
|
||||
private server: ServerService,
|
||||
private authService: AuthService,
|
||||
private notifier: Notifier,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
}
|
||||
|
||||
get instanceName () {
|
||||
return this.server.getHTMLConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
const queryParams = this.route.snapshot.queryParams
|
||||
|
||||
this.server.getConfig().subscribe(config => {
|
||||
this.requiresApproval = config.signup.requiresApproval
|
||||
|
||||
this.loaded = true
|
||||
})
|
||||
|
||||
this.userId = queryParams['userId']
|
||||
this.registrationId = queryParams['registrationId']
|
||||
|
||||
this.verificationString = queryParams['verificationString']
|
||||
|
||||
this.isPendingEmail = queryParams['isPendingEmail'] === 'true'
|
||||
|
||||
if (!this.userId || !this.verificationString) {
|
||||
this.notifier.error($localize`Unable to find user id or verification string.`)
|
||||
} else {
|
||||
if (!this.verificationString) {
|
||||
this.notifier.error($localize`Unable to find verification string in URL query.`)
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.userId && !this.registrationId) {
|
||||
this.notifier.error($localize`Unable to find user id or registration id in URL query.`)
|
||||
return
|
||||
}
|
||||
|
||||
this.verifyEmail()
|
||||
}
|
||||
|
||||
isRegistrationRequest () {
|
||||
return !!this.registrationId
|
||||
}
|
||||
|
||||
displaySignupSuccess () {
|
||||
if (!this.success) return false
|
||||
if (!this.isRegistrationRequest() && this.isPendingEmail) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
verifyEmail () {
|
||||
this.userSignupService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail)
|
||||
if (this.isRegistrationRequest()) {
|
||||
return this.verifyRegistrationEmail()
|
||||
}
|
||||
|
||||
return this.verifyUserEmail()
|
||||
}
|
||||
|
||||
private verifyUserEmail () {
|
||||
const options = {
|
||||
userId: this.userId,
|
||||
verificationString: this.verificationString,
|
||||
isPendingEmail: this.isPendingEmail
|
||||
}
|
||||
|
||||
this.signupService.verifyUserEmail(options)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
if (this.authService.isLoggedIn()) {
|
||||
|
@ -55,4 +105,24 @@ export class VerifyAccountEmailComponent implements OnInit {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
private verifyRegistrationEmail () {
|
||||
const options = {
|
||||
registrationId: this.registrationId,
|
||||
verificationString: this.verificationString
|
||||
}
|
||||
|
||||
this.signupService.verifyRegistrationEmail(options)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.success = true
|
||||
},
|
||||
|
||||
error: err => {
|
||||
this.failed = true
|
||||
|
||||
this.notifier.error(err.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main'
|
|||
import { SharedUsersModule } from '@app/shared/shared-users'
|
||||
import { SignupMascotComponent } from './signup-mascot.component'
|
||||
import { SignupStepTitleComponent } from './signup-step-title.component'
|
||||
import { SignupSuccessComponent } from './signup-success.component'
|
||||
import { SignupSuccessBeforeEmailComponent } from './signup-success-before-email.component'
|
||||
import { SignupSuccessAfterEmailComponent } from './signup-success-after-email.component'
|
||||
import { SignupService } from './signup.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -16,7 +18,8 @@ import { SignupSuccessComponent } from './signup-success.component'
|
|||
],
|
||||
|
||||
declarations: [
|
||||
SignupSuccessComponent,
|
||||
SignupSuccessBeforeEmailComponent,
|
||||
SignupSuccessAfterEmailComponent,
|
||||
SignupStepTitleComponent,
|
||||
SignupMascotComponent
|
||||
],
|
||||
|
@ -26,12 +29,14 @@ import { SignupSuccessComponent } from './signup-success.component'
|
|||
SharedFormModule,
|
||||
SharedGlobalIconModule,
|
||||
|
||||
SignupSuccessComponent,
|
||||
SignupSuccessBeforeEmailComponent,
|
||||
SignupSuccessAfterEmailComponent,
|
||||
SignupStepTitleComponent,
|
||||
SignupMascotComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
SignupService
|
||||
]
|
||||
})
|
||||
export class SharedSignupModule { }
|
||||
|
|
|
@ -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 { Injectable } from '@angular/core'
|
||||
import { RestExtractor, UserService } from '@app/core'
|
||||
import { UserRegister } from '@shared/models'
|
||||
import { UserRegister, UserRegistrationRequest } from '@shared/models'
|
||||
|
||||
@Injectable()
|
||||
export class UserSignupService {
|
||||
export class SignupService {
|
||||
|
||||
constructor (
|
||||
private authHttp: HttpClient,
|
||||
private restExtractor: RestExtractor,
|
||||
private userService: UserService
|
||||
) { }
|
||||
|
||||
signup (userCreate: UserRegister) {
|
||||
directSignup (userCreate: UserRegister) {
|
||||
return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
|
||||
.pipe(
|
||||
tap(() => this.userService.setSignupInThisSession(true)),
|
||||
|
@ -20,8 +21,21 @@ export class UserSignupService {
|
|||
)
|
||||
}
|
||||
|
||||
verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) {
|
||||
const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
|
||||
requestSignup (userCreate: UserRegistrationRequest) {
|
||||
return this.authHttp.post(UserService.BASE_USERS_URL + 'registrations/request', userCreate)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
verifyUserEmail (options: {
|
||||
userId: number
|
||||
verificationString: string
|
||||
isPendingEmail: boolean
|
||||
}) {
|
||||
const { userId, verificationString, isPendingEmail } = options
|
||||
|
||||
const url = `${UserService.BASE_USERS_URL}${userId}/verify-email`
|
||||
const body = {
|
||||
verificationString,
|
||||
isPendingEmail
|
||||
|
@ -31,13 +45,28 @@ export class UserSignupService {
|
|||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
verifyRegistrationEmail (options: {
|
||||
registrationId: number
|
||||
verificationString: string
|
||||
}) {
|
||||
const { registrationId, verificationString } = options
|
||||
|
||||
const url = `${UserService.BASE_USERS_URL}registrations/${registrationId}/verify-email`
|
||||
const body = { verificationString }
|
||||
|
||||
return this.authHttp.post(url, body)
|
||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||
}
|
||||
|
||||
askSendVerifyEmail (email: string) {
|
||||
const url = UserService.BASE_USERS_URL + '/ask-send-verify-email'
|
||||
const url = UserService.BASE_USERS_URL + 'ask-send-verify-email'
|
||||
|
||||
return this.authHttp.post(url, { email })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
|
||||
// Don't update display name, the user seems to have changed it
|
||||
if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername
|
|
@ -103,7 +103,9 @@
|
|||
<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="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>
|
||||
|
||||
<ng-container *ngFor="let menuSection of menuSections" >
|
||||
|
|
|
@ -92,6 +92,10 @@ export class MenuComponent implements OnInit {
|
|||
return this.languageChooserModal.getCurrentLanguage()
|
||||
}
|
||||
|
||||
get requiresApproval () {
|
||||
return this.serverConfig.signup.requiresApproval
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.htmlServerConfig = this.serverService.getHTMLConfig()
|
||||
this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage()
|
||||
|
|
|
@ -136,13 +136,6 @@ export const USER_DESCRIPTION_VALIDATOR: BuildFormValidator = {
|
|||
}
|
||||
}
|
||||
|
||||
export const USER_TERMS_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.requiredTrue ],
|
||||
MESSAGES: {
|
||||
required: $localize`You must agree with the instance terms in order to register on it.`
|
||||
}
|
||||
}
|
||||
|
||||
export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [
|
||||
Validators.minLength(3),
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<span class="moderation-expanded-text">
|
||||
<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>
|
||||
<div>
|
||||
|
@ -29,7 +29,7 @@
|
|||
<span class="moderation-expanded-label" i18n>Reportee</span>
|
||||
<span class="moderation-expanded-text">
|
||||
<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>
|
||||
<div>
|
||||
|
@ -63,7 +63,7 @@
|
|||
<div *ngIf="predefinedReasons" class="mt-2 d-flex">
|
||||
<span>
|
||||
<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>
|
||||
</a>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './account.model'
|
||||
export * from './account.service'
|
||||
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 { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||
import { SharedGlobalIconModule } from '../shared-icons'
|
||||
import { AccountService } from './account'
|
||||
import { AccountService, SignupLabelComponent } from './account'
|
||||
import {
|
||||
AutofocusDirective,
|
||||
BytesPipe,
|
||||
|
@ -113,6 +113,8 @@ import { VideoChannelService } from './video-channel'
|
|||
UserQuotaComponent,
|
||||
UserNotificationsComponent,
|
||||
|
||||
SignupLabelComponent,
|
||||
|
||||
EmbedComponent,
|
||||
|
||||
PluginPlaceholderComponent,
|
||||
|
@ -171,6 +173,8 @@ import { VideoChannelService } from './video-channel'
|
|||
UserQuotaComponent,
|
||||
UserNotificationsComponent,
|
||||
|
||||
SignupLabelComponent,
|
||||
|
||||
EmbedComponent,
|
||||
|
||||
PluginPlaceholderComponent,
|
||||
|
|
|
@ -83,6 +83,11 @@ export class UserNotification implements UserNotificationServer {
|
|||
latestVersion: string
|
||||
}
|
||||
|
||||
registration?: {
|
||||
id: number
|
||||
username: string
|
||||
}
|
||||
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
|
||||
|
@ -97,6 +102,8 @@ export class UserNotification implements UserNotificationServer {
|
|||
|
||||
accountUrl?: string
|
||||
|
||||
registrationsUrl?: string
|
||||
|
||||
videoImportIdentifier?: string
|
||||
videoImportUrl?: string
|
||||
|
||||
|
@ -135,6 +142,7 @@ export class UserNotification implements UserNotificationServer {
|
|||
|
||||
this.plugin = hash.plugin
|
||||
this.peertube = hash.peertube
|
||||
this.registration = hash.registration
|
||||
|
||||
this.createdAt = hash.createdAt
|
||||
this.updatedAt = hash.updatedAt
|
||||
|
@ -208,6 +216,10 @@ export class UserNotification implements UserNotificationServer {
|
|||
this.accountUrl = this.buildAccountUrl(this.account)
|
||||
break
|
||||
|
||||
case UserNotificationType.NEW_USER_REGISTRATION_REQUEST:
|
||||
this.registrationsUrl = '/admin/moderation/registrations/list'
|
||||
break
|
||||
|
||||
case UserNotificationType.NEW_FOLLOW:
|
||||
this.accountUrl = this.buildAccountUrl(this.actorFollow.follower)
|
||||
break
|
||||
|
|
|
@ -215,6 +215,14 @@
|
|||
</div>
|
||||
</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>
|
||||
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.chip {
|
||||
@include chip;
|
||||
}
|
||||
|
||||
.unblock-button {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.chip {
|
||||
@include chip;
|
||||
}
|
||||
|
||||
my-action-dropdown.show {
|
||||
::ng-deep .dropdown-root {
|
||||
display: block !important;
|
||||
|
|
|
@ -24,7 +24,3 @@ a {
|
|||
.block-button {
|
||||
@include create-button;
|
||||
}
|
||||
|
||||
.chip {
|
||||
@include chip;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export * from './user-admin.service'
|
||||
export * from './user-signup.service'
|
||||
export * from './two-factor.service'
|
||||
|
||||
export * from './shared-users.module'
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
|
||||
import { NgModule } from '@angular/core'
|
||||
import { SharedMainModule } from '../shared-main/shared-main.module'
|
||||
import { TwoFactorService } from './two-factor.service'
|
||||
import { UserAdminService } from './user-admin.service'
|
||||
import { UserSignupService } from './user-signup.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -15,7 +13,6 @@ import { UserSignupService } from './user-signup.service'
|
|||
exports: [],
|
||||
|
||||
providers: [
|
||||
UserSignupService,
|
||||
UserAdminService,
|
||||
TwoFactorService
|
||||
]
|
||||
|
|
|
@ -53,8 +53,8 @@
|
|||
<ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
|
||||
</div>
|
||||
|
||||
<div *ngIf="containedInPlaylists" class="video-contained-in-playlists">
|
||||
<a *ngFor="let playlist of containedInPlaylists" class="chip rectangular bg-secondary text-light" [routerLink]="['/w/p/', playlist.playlistShortUUID]">
|
||||
<div *ngIf="containedInPlaylists" class="fs-6">
|
||||
<a *ngFor="let playlist of containedInPlaylists" class="pt-badge badge-secondary" [routerLink]="['/w/p/', playlist.playlistShortUUID]">
|
||||
{{ playlist.playlistDisplayName }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
|
||||
$more-button-width: 40px;
|
||||
|
||||
.chip {
|
||||
@include chip;
|
||||
}
|
||||
|
||||
.video-miniature {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
|
|
@ -284,3 +284,9 @@ label + .form-group-description {
|
|||
border: 2px solid pvar(--mainColorLightest);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.chip {
|
||||
@include chip;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
font-weight: $font-semibold;
|
||||
line-height: 1.1;
|
||||
|
||||
&.badge-fs-normal {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
&.badge-primary {
|
||||
color: pvar(--mainBackgroundColor);
|
||||
background-color: pvar(--mainColor);
|
||||
|
|
|
@ -15,7 +15,3 @@
|
|||
font-display: swap;
|
||||
src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@mixin muted {
|
||||
color: pvar(--greyForegroundColor) !important;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,10 @@
|
|||
max-height: $font-size * $number-of-lines;
|
||||
}
|
||||
|
||||
@mixin muted {
|
||||
color: pvar(--greyForegroundColor) !important;
|
||||
}
|
||||
|
||||
@mixin fade-text ($fade-after, $background-color) {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
@ -791,51 +795,39 @@
|
|||
}
|
||||
|
||||
@mixin chip {
|
||||
--chip-radius: 5rem;
|
||||
--chip-padding: .2rem .4rem;
|
||||
$avatar-height: 1.2rem;
|
||||
--avatar-size: 1.2rem;
|
||||
|
||||
align-items: center;
|
||||
border-radius: var(--chip-radius);
|
||||
display: inline-flex;
|
||||
font-size: 90%;
|
||||
color: pvar(--mainForegroundColor);
|
||||
height: $avatar-height;
|
||||
line-height: 1rem;
|
||||
margin: .1rem;
|
||||
height: var(--avatar-size);
|
||||
max-width: 320px;
|
||||
overflow: hidden;
|
||||
padding: var(--chip-padding);
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
|
||||
&.rectangular {
|
||||
--chip-radius: .2rem;
|
||||
--chip-padding: .2rem .3rem;
|
||||
}
|
||||
|
||||
my-actor-avatar {
|
||||
@include margin-left(-.4rem);
|
||||
@include margin-right(.2rem);
|
||||
|
||||
border-radius: 5rem;
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
}
|
||||
|
||||
&.two-lines {
|
||||
$avatar-height: 2rem;
|
||||
--avatar-size: 2rem;
|
||||
|
||||
height: $avatar-height;
|
||||
font-size: 14px;
|
||||
line-height: 1rem;
|
||||
|
||||
my-actor-avatar {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div {
|
||||
margin: 0 .1rem;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: $avatar-height;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue