Implement signup approval in client

pull/5548/head
Chocobozzz 2023-01-19 09:29:47 +01:00 committed by Chocobozzz
parent b379759f55
commit 9589907c89
67 changed files with 1042 additions and 216 deletions

View File

@ -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)
}
}

View File

@ -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 { }

View File

@ -171,12 +171,21 @@
</ng-container>
<ng-container ngProjectAs="extra">
<my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
i18n-labelText labelText="Signup requires email verification"
></my-peertube-checkbox>
<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 [ngClass]="getDisabledSignupClass()" class="mt-3">
<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()">
<label i18n for="signupLimit">Signup limit</label>
<div class="number-with-unit">

View File

@ -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
},

View File

@ -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'

View File

@ -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',

View File

@ -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)))
}
}

View File

@ -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'

View File

@ -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>&nbsp;<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>

View File

@ -0,0 +1,3 @@
@use '_variables' as *;
@use '_mixins' as *;

View File

@ -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)
})
}
}

View File

@ -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.`
}
}

View File

@ -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>

View File

@ -0,0 +1,7 @@
@use '_mixins' as *;
@use '_variables' as *;
my-global-icon {
width: 24px;
height: 24px;
}

View File

@ -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 })
}
}

View File

@ -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">
&#x2713; {{ 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) + '%' }"

View File

@ -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);

View File

@ -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: []

View File

@ -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">&#x2713; {{ entry.email }}</ng-container>
</ng-container>
</a>
</ng-container>

View File

@ -0,0 +1,10 @@
@use '_variables' as *;
@use '_mixins' as *;
a {
color: pvar(--mainForegroundColor);
&:hover {
text-decoration: underline;
}
}

View File

@ -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`
}
}

View File

@ -19,7 +19,7 @@ export class JobService {
private restExtractor: RestExtractor
) {}
getJobs (options: {
listJobs (options: {
jobState?: JobStateClient
jobType: JobTypeClient
pagination: RestPagination

View File

@ -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,

View File

@ -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
}
}

View File

@ -2,10 +2,6 @@
@use '_miniature' as *;
@use '_mixins' as *;
.chip {
@include chip;
}
.video-table-video {
display: inline-flex;

View File

@ -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>

View File

@ -5,10 +5,10 @@ import { ActivatedRoute } from '@angular/router'
import { AuthService } from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
import { UserSignupService } from '@app/shared/shared-users'
import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
import { UserRegister } from '@shared/models'
import { ServerConfig } from '@shared/models/server'
import { SignupService } from '../shared/signup.service'
@Component({
selector: 'my-register',
@ -53,7 +53,7 @@ export class RegisterComponent implements OnInit {
constructor (
private route: ActivatedRoute,
private authService: AuthService,
private userSignupService: UserSignupService,
private signupService: SignupService,
private hooks: HooksService
) { }
@ -61,6 +61,10 @@ export class RegisterComponent implements OnInit {
return this.serverConfig.signup.requiresEmailVerification
}
get requiresApproval () {
return this.serverConfig.signup.requiresApproval
}
get minimumAge () {
return this.serverConfig.signup.minimumAge
}
@ -132,42 +136,49 @@ export class RegisterComponent implements OnInit {
skipChannelCreation () {
this.formStepChannel.reset()
this.lastStep.select()
this.signup()
}
async signup () {
this.signupError = undefined
const body: UserRegister = await this.hooks.wrapObject(
{
...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
: undefined
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.authService.login({ username: body.username, password: body.password })
.subscribe({
next: () => {
this.signupSuccess = true
},
error: err => {
this.signupError = err.message
}
})
this.autoLogin(body)
},
error: err => {
@ -175,4 +186,17 @@ export class RegisterComponent implements OnInit {
}
})
}
private autoLogin (body: UserRegister) {
this.authService.login({ username: body.username, password: body.password })
.subscribe({
next: () => {
this.signupSuccess = true
},
error: err => {
this.signupError = err.message
}
})
}
}

View File

@ -0,0 +1 @@
export * from './register-validators'

View File

@ -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.`
}
}

View File

@ -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>

View File

@ -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) {

View File

@ -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 })
}
}

View File

@ -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>

View File

@ -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))

View File

@ -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 })
}
}

View File

@ -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}.`)

View File

@ -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>

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AuthService, Notifier } from '@app/core'
import { UserSignupService } from '@app/shared/shared-users'
import { SignupService } from '@app/+signup/shared/signup.service'
import { AuthService, Notifier, ServerService } from '@app/core'
@Component({
selector: 'my-verify-account-email',
@ -13,32 +13,82 @@ export class VerifyAccountEmailComponent implements OnInit {
failed = false
isPendingEmail = false
requiresApproval: boolean
loaded = false
private userId: number
private registrationId: number
private verificationString: string
constructor (
private userSignupService: UserSignupService,
private signupService: SignupService,
private server: ServerService,
private authService: AuthService,
private notifier: Notifier,
private route: ActivatedRoute
) {
}
get instanceName () {
return this.server.getHTMLConfig().instance.name
}
ngOnInit () {
const queryParams = this.route.snapshot.queryParams
this.server.getConfig().subscribe(config => {
this.requiresApproval = config.signup.requiresApproval
this.loaded = true
})
this.userId = queryParams['userId']
this.registrationId = queryParams['registrationId']
this.verificationString = queryParams['verificationString']
this.isPendingEmail = queryParams['isPendingEmail'] === 'true'
if (!this.userId || !this.verificationString) {
this.notifier.error($localize`Unable to find user id or verification string.`)
} else {
this.verifyEmail()
if (!this.verificationString) {
this.notifier.error($localize`Unable to find verification string in URL query.`)
return
}
if (!this.userId && !this.registrationId) {
this.notifier.error($localize`Unable to find user id or registration id in URL query.`)
return
}
this.verifyEmail()
}
isRegistrationRequest () {
return !!this.registrationId
}
displaySignupSuccess () {
if (!this.success) return false
if (!this.isRegistrationRequest() && this.isPendingEmail) return false
return true
}
verifyEmail () {
this.userSignupService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail)
if (this.isRegistrationRequest()) {
return this.verifyRegistrationEmail()
}
return this.verifyUserEmail()
}
private verifyUserEmail () {
const options = {
userId: this.userId,
verificationString: this.verificationString,
isPendingEmail: this.isPendingEmail
}
this.signupService.verifyUserEmail(options)
.subscribe({
next: () => {
if (this.authService.isLoggedIn()) {
@ -55,4 +105,24 @@ export class VerifyAccountEmailComponent implements OnInit {
}
})
}
private verifyRegistrationEmail () {
const options = {
registrationId: this.registrationId,
verificationString: this.verificationString
}
this.signupService.verifyRegistrationEmail(options)
.subscribe({
next: () => {
this.success = true
},
error: err => {
this.failed = true
this.notifier.error(err.message)
}
})
}
}

View File

@ -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 { }

View File

@ -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>

View File

@ -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
}

View File

@ -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>

View File

@ -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
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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

View File

@ -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" >

View File

@ -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()

View File

@ -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),

View File

@ -8,7 +8,7 @@
<span class="moderation-expanded-text">
<a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
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:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
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>

View File

@ -1,3 +1,4 @@
export * from './account.model'
export * from './account.service'
export * from './actor.model'
export * from './signup-label.component'

View File

@ -0,0 +1,2 @@
<ng-container i18n *ngIf="requiresApproval">Request an account</ng-container>
<ng-container i18n *ngIf="!requiresApproval">Create an account</ng-container>

View File

@ -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
}

View File

@ -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,

View File

@ -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

View File

@ -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>

View File

@ -1,10 +1,6 @@
@use '_variables' as *;
@use '_mixins' as *;
.chip {
@include chip;
}
.unblock-button {
@include peertube-button;
@include grey-button;

View File

@ -40,10 +40,6 @@
}
}
.chip {
@include chip;
}
my-action-dropdown.show {
::ng-deep .dropdown-root {
display: block !important;

View File

@ -24,7 +24,3 @@ a {
.block-button {
@include create-button;
}
.chip {
@include chip;
}

View File

@ -1,5 +1,4 @@
export * from './user-admin.service'
export * from './user-signup.service'
export * from './two-factor.service'
export * from './shared-users.module'

View File

@ -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
]

View File

@ -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>

View File

@ -4,10 +4,6 @@
$more-button-width: 40px;
.chip {
@include chip;
}
.video-miniature {
font-size: 14px;
}

View File

@ -284,3 +284,9 @@ label + .form-group-description {
border: 2px solid pvar(--mainColorLightest);
}
}
// ---------------------------------------------------------------------------
.chip {
@include chip;
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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;
}
}