add user account email verificiation (#977)
* add user account email verificiation includes server and client code to: * enable verificationRequired via custom config * send verification email with registration * ask for verification email * verify via email * prevent login if not verified and required * conditional client links to ask for new verification email * allow login for verified=null these are users created when verification not required should still be able to login when verification is enabled * refactor email verifcation pr * change naming from verified to emailVerified * change naming from askVerifyEmail to askSendVerifyEmail * undo unrelated automatic prettier formatting on api/config * use redirectService for home * remove redundant success notification on email verified * revert test.yaml smpt host
This commit is contained in:
parent
04291e1ba4
commit
d9eaee3939
|
@ -91,6 +91,11 @@
|
||||||
i18n-labelText labelText="Signup enabled"
|
i18n-labelText labelText="Signup enabled"
|
||||||
></my-peertube-checkbox>
|
></my-peertube-checkbox>
|
||||||
|
|
||||||
|
<my-peertube-checkbox *ngIf="isSignupEnabled()"
|
||||||
|
inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification"
|
||||||
|
i18n-labelText labelText="Signup requires email verification"
|
||||||
|
></my-peertube-checkbox>
|
||||||
|
|
||||||
<div *ngIf="isSignupEnabled()" class="form-group">
|
<div *ngIf="isSignupEnabled()" class="form-group">
|
||||||
<label i18n for="signupLimit">Signup limit</label>
|
<label i18n for="signupLimit">Signup limit</label>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -90,6 +90,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE,
|
cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE,
|
||||||
signupEnabled: null,
|
signupEnabled: null,
|
||||||
signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT,
|
signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT,
|
||||||
|
signupRequiresEmailVerification: null,
|
||||||
importVideosHttpEnabled: null,
|
importVideosHttpEnabled: null,
|
||||||
importVideosTorrentEnabled: null,
|
importVideosTorrentEnabled: null,
|
||||||
adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
|
adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
|
||||||
|
@ -187,7 +188,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
},
|
},
|
||||||
signup: {
|
signup: {
|
||||||
enabled: this.form.value['signupEnabled'],
|
enabled: this.form.value['signupEnabled'],
|
||||||
limit: this.form.value['signupLimit']
|
limit: this.form.value['signupLimit'],
|
||||||
|
requiresEmailVerification: this.form.value['signupRequiresEmailVerification']
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
email: this.form.value['adminEmail']
|
email: this.form.value['adminEmail']
|
||||||
|
@ -250,6 +252,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
cacheCaptionsSize: this.customConfig.cache.captions.size,
|
cacheCaptionsSize: this.customConfig.cache.captions.size,
|
||||||
signupEnabled: this.customConfig.signup.enabled,
|
signupEnabled: this.customConfig.signup.enabled,
|
||||||
signupLimit: this.customConfig.signup.limit,
|
signupLimit: this.customConfig.signup.limit,
|
||||||
|
signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
|
||||||
adminEmail: this.customConfig.admin.email,
|
adminEmail: this.customConfig.admin.email,
|
||||||
userVideoQuota: this.customConfig.user.videoQuota,
|
userVideoQuota: this.customConfig.user.videoQuota,
|
||||||
userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
|
userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from '@app/+verify-account/verify-account-routing.module'
|
||||||
|
export * from '@app/+verify-account/verify-account.module'
|
|
@ -0,0 +1,22 @@
|
||||||
|
<div class="margin-content">
|
||||||
|
<div i18n class="title-page title-page-single">
|
||||||
|
Request email for account verification
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form *ngIf="requiresEmailVerification; else emailVerificationNotRequired" role="form" (ngSubmit)="askSendVerifyEmail()" [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="verify-email-email">Email</label>
|
||||||
|
<input
|
||||||
|
type="email" id="verify-email-email" i18n-placeholder placeholder="Email address" required
|
||||||
|
formControlName="verify-email-email" [ngClass]="{ 'input-error': formErrors['verify-email-email'] }"
|
||||||
|
>
|
||||||
|
<div *ngIf="formErrors['verify-email-email']" class="form-error">
|
||||||
|
{{ formErrors['verify-email-email'] }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="submit" i18n-value value="Send verification email" [disabled]="!form.valid">
|
||||||
|
</form>
|
||||||
|
<ng-template #emailVerificationNotRequired>
|
||||||
|
<div i18n>This instance does not require email verification.</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
|
@ -0,0 +1,12 @@
|
||||||
|
@import '_variables';
|
||||||
|
@import '_mixins';
|
||||||
|
|
||||||
|
input:not([type=submit]) {
|
||||||
|
@include peertube-input-text(340px);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit] {
|
||||||
|
@include peertube-button;
|
||||||
|
@include orange-button;
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
|
import { ServerService } from '@app/core/server'
|
||||||
|
import { RedirectService } from '@app/core'
|
||||||
|
import { UserService, FormReactive } from '@app/shared'
|
||||||
|
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||||
|
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-verify-account-ask-send-email',
|
||||||
|
templateUrl: './verify-account-ask-send-email.component.html',
|
||||||
|
styleUrls: [ './verify-account-ask-send-email.component.scss' ]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class VerifyAccountAskSendEmailComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
protected formValidatorService: FormValidatorService,
|
||||||
|
private userValidatorsService: UserValidatorsService,
|
||||||
|
private userService: UserService,
|
||||||
|
private serverService: ServerService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private redirectService: RedirectService,
|
||||||
|
private i18n: I18n
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
get requiresEmailVerification () {
|
||||||
|
return this.serverService.getConfig().signup.requiresEmailVerification
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm({
|
||||||
|
'verify-email-email': this.userValidatorsService.USER_EMAIL
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
askSendVerifyEmail () {
|
||||||
|
const email = this.form.value['verify-email-email']
|
||||||
|
this.userService.askSendVerifyEmail(email)
|
||||||
|
.subscribe(
|
||||||
|
() => {
|
||||||
|
const message = this.i18n(
|
||||||
|
'An email with verification link will be sent to {{email}}.',
|
||||||
|
{ email }
|
||||||
|
)
|
||||||
|
this.notificationsService.success(this.i18n('Success'), message)
|
||||||
|
this.redirectService.redirectToHomepage()
|
||||||
|
},
|
||||||
|
|
||||||
|
err => {
|
||||||
|
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<div class="margin-content">
|
||||||
|
<div i18n class="title-page title-page-single">
|
||||||
|
Verify account email confirmation
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div i18n *ngIf="success; else verificationError">
|
||||||
|
Your email has been verified and you may now login. Redirecting...
|
||||||
|
</div>
|
||||||
|
<ng-template #verificationError>
|
||||||
|
<div>
|
||||||
|
<span i18n>An error occurred. </span>
|
||||||
|
<a i18n routerLink="/verify-account/ask-email">Request new verification email.</a>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { Component, OnInit } from '@angular/core'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
|
import { UserService } from '@app/shared'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-verify-account-email',
|
||||||
|
templateUrl: './verify-account-email.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class VerifyAccountEmailComponent implements OnInit {
|
||||||
|
success = false
|
||||||
|
|
||||||
|
private userId: number
|
||||||
|
private verificationString: string
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private userService: UserService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private router: Router,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private i18n: I18n
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
|
||||||
|
this.userId = this.route.snapshot.queryParams['userId']
|
||||||
|
this.verificationString = this.route.snapshot.queryParams['verificationString']
|
||||||
|
|
||||||
|
if (!this.userId || !this.verificationString) {
|
||||||
|
this.notificationsService.error(this.i18n('Error'), this.i18n('Unable to find user id or verification string.'))
|
||||||
|
} else {
|
||||||
|
this.verifyEmail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyEmail () {
|
||||||
|
this.userService.verifyEmail(this.userId, this.verificationString)
|
||||||
|
.subscribe(
|
||||||
|
() => {
|
||||||
|
this.success = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.router.navigate([ '/login' ])
|
||||||
|
}, 2000)
|
||||||
|
},
|
||||||
|
|
||||||
|
err => {
|
||||||
|
this.notificationsService.error(this.i18n('Error'), err.message)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
import { RouterModule, Routes } from '@angular/router'
|
||||||
|
|
||||||
|
import { MetaGuard } from '@ngx-meta/core'
|
||||||
|
|
||||||
|
import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component'
|
||||||
|
import {
|
||||||
|
VerifyAccountAskSendEmailComponent
|
||||||
|
} from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component'
|
||||||
|
|
||||||
|
const verifyAccountRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
canActivateChild: [ MetaGuard ],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'email',
|
||||||
|
component: VerifyAccountEmailComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: 'Verify account email'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'ask-send-email',
|
||||||
|
component: VerifyAccountAskSendEmailComponent,
|
||||||
|
data: {
|
||||||
|
meta: {
|
||||||
|
title: 'Verify account ask send email'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [ RouterModule.forChild(verifyAccountRoutes) ],
|
||||||
|
exports: [ RouterModule ]
|
||||||
|
})
|
||||||
|
export class VerifyAccountRoutingModule {}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { NgModule } from '@angular/core'
|
||||||
|
|
||||||
|
import { VerifyAccountRoutingModule } from '@app/+verify-account/verify-account-routing.module'
|
||||||
|
import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component'
|
||||||
|
import {
|
||||||
|
VerifyAccountAskSendEmailComponent
|
||||||
|
} from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component'
|
||||||
|
import { SharedModule } from '@app/shared'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
VerifyAccountRoutingModule,
|
||||||
|
SharedModule
|
||||||
|
],
|
||||||
|
|
||||||
|
declarations: [
|
||||||
|
VerifyAccountEmailComponent,
|
||||||
|
VerifyAccountAskSendEmailComponent
|
||||||
|
],
|
||||||
|
|
||||||
|
exports: [
|
||||||
|
],
|
||||||
|
|
||||||
|
providers: [
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class VerifyAccountModule { }
|
|
@ -13,6 +13,10 @@ const routes: Routes = [
|
||||||
path: 'my-account',
|
path: 'my-account',
|
||||||
loadChildren: './+my-account/my-account.module#MyAccountModule'
|
loadChildren: './+my-account/my-account.module#MyAccountModule'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'verify-account',
|
||||||
|
loadChildren: './+verify-account/verify-account.module#VerifyAccountModule'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'accounts',
|
path: 'accounts',
|
||||||
loadChildren: './+accounts/accounts.module#AccountsModule'
|
loadChildren: './+accounts/accounts.module#AccountsModule'
|
||||||
|
|
|
@ -40,7 +40,8 @@ export class ServerService {
|
||||||
serverVersion: 'Unknown',
|
serverVersion: 'Unknown',
|
||||||
signup: {
|
signup: {
|
||||||
allowed: false,
|
allowed: false,
|
||||||
allowedForCurrentIP: false
|
allowedForCurrentIP: false,
|
||||||
|
requiresEmailVerification: false
|
||||||
},
|
},
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabledResolutions: []
|
enabledResolutions: []
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
Login
|
Login
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
<div *ngIf="error" class="alert alert-danger">{{ error }}
|
||||||
|
<span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form role="form" (ngSubmit)="login()" [formGroup]="form">
|
<form role="form" (ngSubmit)="login()" [formGroup]="form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
@ -94,4 +94,27 @@ export class UserService {
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verifyEmail (userId: number, verificationString: string) {
|
||||||
|
const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
|
||||||
|
const body = {
|
||||||
|
verificationString
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.authHttp.post(url, body)
|
||||||
|
.pipe(
|
||||||
|
map(this.restExtractor.extractDataBool),
|
||||||
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
askSendVerifyEmail (email: string) {
|
||||||
|
const url = UserService.BASE_USERS_URL + '/ask-send-verify-email'
|
||||||
|
|
||||||
|
return this.authHttp.post(url, { email })
|
||||||
|
.pipe(
|
||||||
|
map(this.restExtractor.extractDataBool),
|
||||||
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Router } from '@angular/router'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { UserCreate } from '../../../../shared'
|
import { UserCreate } from '../../../../shared'
|
||||||
import { FormReactive, UserService, UserValidatorsService } from '../shared'
|
import { FormReactive, UserService, UserValidatorsService } from '../shared'
|
||||||
import { RedirectService } from '@app/core'
|
import { RedirectService, ServerService } from '@app/core'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ export class SignupComponent extends FormReactive implements OnInit {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
|
private serverService: ServerService,
|
||||||
private redirectService: RedirectService,
|
private redirectService: RedirectService,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {
|
) {
|
||||||
|
@ -31,6 +32,10 @@ export class SignupComponent extends FormReactive implements OnInit {
|
||||||
return window.location.host
|
return window.location.host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get requiresEmailVerification () {
|
||||||
|
return this.serverService.getConfig().signup.requiresEmailVerification
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.buildForm({
|
this.buildForm({
|
||||||
username: this.userValidatorsService.USER_USERNAME,
|
username: this.userValidatorsService.USER_USERNAME,
|
||||||
|
@ -47,10 +52,17 @@ export class SignupComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
this.userService.signup(userCreate).subscribe(
|
this.userService.signup(userCreate).subscribe(
|
||||||
() => {
|
() => {
|
||||||
|
if (this.requiresEmailVerification) {
|
||||||
|
this.notificationsService.alert(
|
||||||
|
this.i18n('Welcome'),
|
||||||
|
this.i18n('Please check your email to verify your account and complete signup.')
|
||||||
|
)
|
||||||
|
} else {
|
||||||
this.notificationsService.success(
|
this.notificationsService.success(
|
||||||
this.i18n('Success'),
|
this.i18n('Success'),
|
||||||
this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
|
this.i18n('Registration for {{username}} complete.', { username: userCreate.username })
|
||||||
)
|
)
|
||||||
|
}
|
||||||
this.redirectService.redirectToHomepage()
|
this.redirectService.redirectToHomepage()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,7 @@ admin:
|
||||||
signup:
|
signup:
|
||||||
enabled: false
|
enabled: false
|
||||||
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
||||||
|
requires_email_verification: false
|
||||||
filters:
|
filters:
|
||||||
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
|
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
|
||||||
whitelist: []
|
whitelist: []
|
||||||
|
|
|
@ -87,6 +87,7 @@ admin:
|
||||||
signup:
|
signup:
|
||||||
enabled: false
|
enabled: false
|
||||||
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited
|
||||||
|
requires_email_verification: false
|
||||||
filters:
|
filters:
|
||||||
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
|
cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist
|
||||||
whitelist: []
|
whitelist: []
|
||||||
|
|
|
@ -29,6 +29,7 @@ cache:
|
||||||
|
|
||||||
signup:
|
signup:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
requires_email_verification: false
|
||||||
|
|
||||||
transcoding:
|
transcoding:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -60,7 +60,8 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
|
||||||
serverVersion: packageJSON.version,
|
serverVersion: packageJSON.version,
|
||||||
signup: {
|
signup: {
|
||||||
allowed,
|
allowed,
|
||||||
allowedForCurrentIP
|
allowedForCurrentIP,
|
||||||
|
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
|
||||||
},
|
},
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabledResolutions
|
enabledResolutions
|
||||||
|
@ -159,12 +160,20 @@ async function updateCustomConfig (req: express.Request, res: express.Response,
|
||||||
toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10)
|
toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10)
|
||||||
|
|
||||||
// camelCase to snake_case key
|
// camelCase to snake_case key
|
||||||
const toUpdateJSON = omit(toUpdate, 'user.videoQuota', 'instance.defaultClientRoute', 'instance.shortDescription', 'cache.videoCaptions')
|
const toUpdateJSON = omit(
|
||||||
|
toUpdate,
|
||||||
|
'user.videoQuota',
|
||||||
|
'instance.defaultClientRoute',
|
||||||
|
'instance.shortDescription',
|
||||||
|
'cache.videoCaptions',
|
||||||
|
'signup.requiresEmailVerification'
|
||||||
|
)
|
||||||
toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
|
toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
|
||||||
toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily
|
toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily
|
||||||
toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
|
toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
|
||||||
toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
|
toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
|
||||||
toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
|
toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
|
||||||
|
toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification
|
||||||
|
|
||||||
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
|
await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 })
|
||||||
|
|
||||||
|
@ -220,7 +229,8 @@ function customConfig (): CustomConfig {
|
||||||
},
|
},
|
||||||
signup: {
|
signup: {
|
||||||
enabled: CONFIG.SIGNUP.ENABLED,
|
enabled: CONFIG.SIGNUP.ENABLED,
|
||||||
limit: CONFIG.SIGNUP.LIMIT
|
limit: CONFIG.SIGNUP.LIMIT,
|
||||||
|
requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
email: CONFIG.ADMIN.EMAIL
|
email: CONFIG.ADMIN.EMAIL
|
||||||
|
|
|
@ -25,7 +25,10 @@ import {
|
||||||
usersSortValidator,
|
usersSortValidator,
|
||||||
usersUpdateValidator
|
usersUpdateValidator
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
import { usersAskResetPasswordValidator, usersBlockingValidator, usersResetPasswordValidator } from '../../../middlewares/validators'
|
import {
|
||||||
|
usersAskResetPasswordValidator, usersBlockingValidator, usersResetPasswordValidator,
|
||||||
|
usersAskSendVerifyEmailValidator, usersVerifyEmailValidator
|
||||||
|
} from '../../../middlewares/validators'
|
||||||
import { UserModel } from '../../../models/account/user'
|
import { UserModel } from '../../../models/account/user'
|
||||||
import { OAuthTokenModel } from '../../../models/oauth/oauth-token'
|
import { OAuthTokenModel } from '../../../models/oauth/oauth-token'
|
||||||
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger'
|
||||||
|
@ -110,6 +113,17 @@ usersRouter.post('/:id/reset-password',
|
||||||
asyncMiddleware(resetUserPassword)
|
asyncMiddleware(resetUserPassword)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
usersRouter.post('/ask-send-verify-email',
|
||||||
|
loginRateLimiter,
|
||||||
|
asyncMiddleware(usersAskSendVerifyEmailValidator),
|
||||||
|
asyncMiddleware(askSendVerifyUserEmail)
|
||||||
|
)
|
||||||
|
|
||||||
|
usersRouter.post('/:id/verify-email',
|
||||||
|
asyncMiddleware(usersVerifyEmailValidator),
|
||||||
|
asyncMiddleware(verifyUserEmail)
|
||||||
|
)
|
||||||
|
|
||||||
usersRouter.post('/token',
|
usersRouter.post('/token',
|
||||||
loginRateLimiter,
|
loginRateLimiter,
|
||||||
token,
|
token,
|
||||||
|
@ -165,7 +179,8 @@ async function registerUser (req: express.Request, res: express.Response) {
|
||||||
autoPlayVideo: true,
|
autoPlayVideo: true,
|
||||||
role: UserRole.USER,
|
role: UserRole.USER,
|
||||||
videoQuota: CONFIG.USER.VIDEO_QUOTA,
|
videoQuota: CONFIG.USER.VIDEO_QUOTA,
|
||||||
videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY
|
videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY,
|
||||||
|
emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
|
||||||
})
|
})
|
||||||
|
|
||||||
const { user } = await createUserAccountAndChannel(userToCreate)
|
const { user } = await createUserAccountAndChannel(userToCreate)
|
||||||
|
@ -173,6 +188,10 @@ async function registerUser (req: express.Request, res: express.Response) {
|
||||||
auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
|
auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
|
||||||
logger.info('User %s with its channel and account registered.', body.username)
|
logger.info('User %s with its channel and account registered.', body.username)
|
||||||
|
|
||||||
|
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
|
||||||
|
await sendVerifyUserEmail(user)
|
||||||
|
}
|
||||||
|
|
||||||
return res.type('json').status(204).end()
|
return res.type('json').status(204).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,6 +280,30 @@ async function resetUserPassword (req: express.Request, res: express.Response, n
|
||||||
return res.status(204).end()
|
return res.status(204).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendVerifyUserEmail (user: UserModel) {
|
||||||
|
const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
|
||||||
|
const url = CONFIG.WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
|
||||||
|
await Emailer.Instance.addVerifyEmailJob(user.email, url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askSendVerifyUserEmail (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
const user = res.locals.user as UserModel
|
||||||
|
|
||||||
|
await sendVerifyUserEmail(user)
|
||||||
|
|
||||||
|
return res.status(204).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyUserEmail (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
const user = res.locals.user as UserModel
|
||||||
|
user.emailVerified = true
|
||||||
|
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
return res.status(204).end()
|
||||||
|
}
|
||||||
|
|
||||||
function success (req: express.Request, res: express.Response, next: express.NextFunction) {
|
function success (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -234,6 +234,7 @@ const customConfigKeysToKeep = [
|
||||||
'cache-captions-size',
|
'cache-captions-size',
|
||||||
'signup-enabled',
|
'signup-enabled',
|
||||||
'signup-limit',
|
'signup-limit',
|
||||||
|
'signup-requiresEmailVerification',
|
||||||
'admin-email',
|
'admin-email',
|
||||||
'user-videoQuota',
|
'user-videoQuota',
|
||||||
'transcoding-enabled',
|
'transcoding-enabled',
|
||||||
|
|
|
@ -33,6 +33,10 @@ function isUserDescriptionValid (value: string) {
|
||||||
return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
|
return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUserEmailVerifiedValid (value: any) {
|
||||||
|
return isBooleanValid(value)
|
||||||
|
}
|
||||||
|
|
||||||
const nsfwPolicies = values(NSFW_POLICY_TYPES)
|
const nsfwPolicies = values(NSFW_POLICY_TYPES)
|
||||||
function isUserNSFWPolicyValid (value: any) {
|
function isUserNSFWPolicyValid (value: any) {
|
||||||
return exists(value) && nsfwPolicies.indexOf(value) !== -1
|
return exists(value) && nsfwPolicies.indexOf(value) !== -1
|
||||||
|
@ -72,6 +76,7 @@ export {
|
||||||
isUserVideoQuotaValid,
|
isUserVideoQuotaValid,
|
||||||
isUserVideoQuotaDailyValid,
|
isUserVideoQuotaDailyValid,
|
||||||
isUserUsernameValid,
|
isUserUsernameValid,
|
||||||
|
isUserEmailVerifiedValid,
|
||||||
isUserNSFWPolicyValid,
|
isUserNSFWPolicyValid,
|
||||||
isUserAutoPlayVideoValid,
|
isUserAutoPlayVideoValid,
|
||||||
isUserDisplayNameValid,
|
isUserDisplayNameValid,
|
||||||
|
|
|
@ -49,7 +49,8 @@ function checkMissedConfig () {
|
||||||
'log.level',
|
'log.level',
|
||||||
'user.video_quota', 'user.video_quota_daily',
|
'user.video_quota', 'user.video_quota_daily',
|
||||||
'cache.previews.size', 'admin.email',
|
'cache.previews.size', 'admin.email',
|
||||||
'signup.enabled', 'signup.limit', 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
|
'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
|
||||||
|
'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
|
||||||
'transcoding.enabled', 'transcoding.threads',
|
'transcoding.enabled', 'transcoding.threads',
|
||||||
'import.videos.http.enabled',
|
'import.videos.http.enabled',
|
||||||
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
|
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
|
||||||
|
|
|
@ -15,7 +15,7 @@ let config: IConfig = require('config')
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 260
|
const LAST_MIGRATION_VERSION = 265
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -204,6 +204,7 @@ const CONFIG = {
|
||||||
SIGNUP: {
|
SIGNUP: {
|
||||||
get ENABLED () { return config.get<boolean>('signup.enabled') },
|
get ENABLED () { return config.get<boolean>('signup.enabled') },
|
||||||
get LIMIT () { return config.get<number>('signup.limit') },
|
get LIMIT () { return config.get<number>('signup.limit') },
|
||||||
|
get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') },
|
||||||
FILTERS: {
|
FILTERS: {
|
||||||
CIDR: {
|
CIDR: {
|
||||||
get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') },
|
get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') },
|
||||||
|
@ -500,6 +501,8 @@ const BCRYPT_SALT_SIZE = 10
|
||||||
|
|
||||||
const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes
|
const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes
|
||||||
|
|
||||||
|
const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
|
||||||
|
|
||||||
const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = {
|
const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = {
|
||||||
DO_NOT_LIST: 'do_not_list',
|
DO_NOT_LIST: 'do_not_list',
|
||||||
BLUR: 'blur',
|
BLUR: 'blur',
|
||||||
|
@ -661,6 +664,7 @@ export {
|
||||||
VIDEO_ABUSE_STATES,
|
VIDEO_ABUSE_STATES,
|
||||||
JOB_REQUEST_TIMEOUT,
|
JOB_REQUEST_TIMEOUT,
|
||||||
USER_PASSWORD_RESET_LIFETIME,
|
USER_PASSWORD_RESET_LIFETIME,
|
||||||
|
USER_EMAIL_VERIFY_LIFETIME,
|
||||||
IMAGE_MIMETYPE_EXT,
|
IMAGE_MIMETYPE_EXT,
|
||||||
SCHEDULER_INTERVALS_MS,
|
SCHEDULER_INTERVALS_MS,
|
||||||
REPEAT_JOBS,
|
REPEAT_JOBS,
|
||||||
|
|
|
@ -122,6 +122,7 @@ async function createOAuthAdminIfNotExist () {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
role,
|
role,
|
||||||
|
verified: true,
|
||||||
nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
|
nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
|
||||||
videoQuota: -1,
|
videoQuota: -1,
|
||||||
videoQuotaDaily: -1
|
videoQuotaDaily: -1
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
}): Promise<any> {
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.queryInterface.addColumn('user', 'emailVerified', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export { up, down }
|
|
@ -89,6 +89,23 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addVerifyEmailJob (to: string, verifyEmailUrl: string) {
|
||||||
|
const text = `Welcome to PeerTube,\n\n` +
|
||||||
|
`To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` +
|
||||||
|
`Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
|
||||||
|
`If you are not the person who initiated this request, please ignore this email.\n\n` +
|
||||||
|
`Cheers,\n` +
|
||||||
|
`PeerTube.`
|
||||||
|
|
||||||
|
const emailPayload: EmailPayload = {
|
||||||
|
to: [ to ],
|
||||||
|
subject: 'Verify your PeerTube email',
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||||
|
}
|
||||||
|
|
||||||
async addVideoAbuseReportJob (videoId: number) {
|
async addVideoAbuseReportJob (videoId: number) {
|
||||||
const video = await VideoModel.load(videoId)
|
const video = await VideoModel.load(videoId)
|
||||||
if (!video) throw new Error('Unknown Video id during Abuse report.')
|
if (!video) throw new Error('Unknown Video id during Abuse report.')
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { logger } from '../helpers/logger'
|
||||||
import { UserModel } from '../models/account/user'
|
import { UserModel } from '../models/account/user'
|
||||||
import { OAuthClientModel } from '../models/oauth/oauth-client'
|
import { OAuthClientModel } from '../models/oauth/oauth-client'
|
||||||
import { OAuthTokenModel } from '../models/oauth/oauth-token'
|
import { OAuthTokenModel } from '../models/oauth/oauth-token'
|
||||||
|
import { CONFIG } from '../initializers/constants'
|
||||||
|
|
||||||
type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
|
type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
|
||||||
|
|
||||||
|
@ -37,6 +38,10 @@ async function getUser (usernameOrEmail: string, password: string) {
|
||||||
|
|
||||||
if (user.blocked) throw new AccessDeniedError('User is blocked.')
|
if (user.blocked) throw new AccessDeniedError('User is blocked.')
|
||||||
|
|
||||||
|
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION && user.emailVerified === false) {
|
||||||
|
throw new AccessDeniedError('User email is not verified.')
|
||||||
|
}
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as express from 'express'
|
||||||
import { createClient, RedisClient } from 'redis'
|
import { createClient, RedisClient } from 'redis'
|
||||||
import { logger } from '../helpers/logger'
|
import { logger } from '../helpers/logger'
|
||||||
import { generateRandomString } from '../helpers/utils'
|
import { generateRandomString } from '../helpers/utils'
|
||||||
import { CONFIG, USER_PASSWORD_RESET_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers'
|
import { CONFIG, USER_PASSWORD_RESET_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers'
|
||||||
|
|
||||||
type CachedRoute = {
|
type CachedRoute = {
|
||||||
body: string,
|
body: string,
|
||||||
|
@ -60,6 +60,18 @@ class Redis {
|
||||||
return this.getValue(this.generateResetPasswordKey(userId))
|
return this.getValue(this.generateResetPasswordKey(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setVerifyEmailVerificationString (userId: number) {
|
||||||
|
const generatedString = await generateRandomString(32)
|
||||||
|
|
||||||
|
await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
|
||||||
|
|
||||||
|
return generatedString
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVerifyEmailLink (userId: number) {
|
||||||
|
return this.getValue(this.generateVerifyEmailKey(userId))
|
||||||
|
}
|
||||||
|
|
||||||
setIPVideoView (ip: string, videoUUID: string) {
|
setIPVideoView (ip: string, videoUUID: string) {
|
||||||
return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
|
return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
|
||||||
}
|
}
|
||||||
|
@ -135,6 +147,10 @@ class Redis {
|
||||||
return 'reset-password-' + userId
|
return 'reset-password-' + userId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateVerifyEmailKey (userId: number) {
|
||||||
|
return 'verify-email-' + userId
|
||||||
|
}
|
||||||
|
|
||||||
buildViewKey (ip: string, videoUUID: string) {
|
buildViewKey (ip: string, videoUUID: string) {
|
||||||
return videoUUID + '-' + ip
|
return videoUUID + '-' + ip
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,6 +248,48 @@ const usersResetPasswordValidator = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const usersAskSendVerifyEmailValidator = [
|
||||||
|
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
const exists = await checkUserEmailExist(req.body.email, res, false)
|
||||||
|
if (!exists) {
|
||||||
|
logger.debug('User with email %s does not exist (asking verify email).', req.body.email)
|
||||||
|
// Do not leak our emails
|
||||||
|
return res.status(204).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const usersVerifyEmailValidator = [
|
||||||
|
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
if (!await checkUserIdExist(req.params.id, res)) return
|
||||||
|
|
||||||
|
const user = res.locals.user as UserModel
|
||||||
|
const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
|
||||||
|
|
||||||
|
if (redisVerificationString !== req.body.verificationString) {
|
||||||
|
return res
|
||||||
|
.status(403)
|
||||||
|
.send({ error: 'Invalid verification string.' })
|
||||||
|
.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -263,7 +305,9 @@ export {
|
||||||
ensureUserRegistrationAllowedForIP,
|
ensureUserRegistrationAllowedForIP,
|
||||||
usersGetValidator,
|
usersGetValidator,
|
||||||
usersAskResetPasswordValidator,
|
usersAskResetPasswordValidator,
|
||||||
usersResetPasswordValidator
|
usersResetPasswordValidator,
|
||||||
|
usersAskSendVerifyEmailValidator,
|
||||||
|
usersVerifyEmailValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
isUserBlockedReasonValid,
|
isUserBlockedReasonValid,
|
||||||
isUserBlockedValid,
|
isUserBlockedValid,
|
||||||
isUserNSFWPolicyValid,
|
isUserNSFWPolicyValid,
|
||||||
|
isUserEmailVerifiedValid,
|
||||||
isUserPasswordValid,
|
isUserPasswordValid,
|
||||||
isUserRoleValid,
|
isUserRoleValid,
|
||||||
isUserUsernameValid,
|
isUserUsernameValid,
|
||||||
|
@ -92,6 +93,12 @@ export class UserModel extends Model<UserModel> {
|
||||||
@Column(DataType.STRING(400))
|
@Column(DataType.STRING(400))
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Default(null)
|
||||||
|
@Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean'))
|
||||||
|
@Column
|
||||||
|
emailVerified: boolean
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
|
@Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
|
||||||
@Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
|
@Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
|
||||||
|
@ -304,6 +311,7 @@ export class UserModel extends Model<UserModel> {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
username: this.username,
|
username: this.username,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
|
emailVerified: this.emailVerified,
|
||||||
nsfwPolicy: this.nsfwPolicy,
|
nsfwPolicy: this.nsfwPolicy,
|
||||||
autoPlayVideo: this.autoPlayVideo,
|
autoPlayVideo: this.autoPlayVideo,
|
||||||
role: this.role,
|
role: this.role,
|
||||||
|
|
|
@ -42,7 +42,8 @@ describe('Test config API validators', function () {
|
||||||
},
|
},
|
||||||
signup: {
|
signup: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
limit: 5
|
limit: 5,
|
||||||
|
requiresEmailVerification: false
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
email: 'superadmin1@example.com'
|
email: 'superadmin1@example.com'
|
||||||
|
|
|
@ -737,6 +737,28 @@ describe('Test users API validators', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When asking for an account verification email', function () {
|
||||||
|
const path = '/api/v1/users/ask-send-verify-email'
|
||||||
|
|
||||||
|
it('Should fail with a missing email', async function () {
|
||||||
|
const fields = {}
|
||||||
|
|
||||||
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid email', async function () {
|
||||||
|
const fields = { email: 'hello' }
|
||||||
|
|
||||||
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
const fields = { email: 'admin@example.com' }
|
||||||
|
|
||||||
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
killallServers([ server, serverWithRegistrationDisabled ])
|
killallServers([ server, serverWithRegistrationDisabled ])
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ function checkInitialConfig (data: CustomConfig) {
|
||||||
expect(data.cache.captions.size).to.equal(1)
|
expect(data.cache.captions.size).to.equal(1)
|
||||||
expect(data.signup.enabled).to.be.true
|
expect(data.signup.enabled).to.be.true
|
||||||
expect(data.signup.limit).to.equal(4)
|
expect(data.signup.limit).to.equal(4)
|
||||||
|
expect(data.signup.requiresEmailVerification).to.be.false
|
||||||
expect(data.admin.email).to.equal('admin1@example.com')
|
expect(data.admin.email).to.equal('admin1@example.com')
|
||||||
expect(data.user.videoQuota).to.equal(5242880)
|
expect(data.user.videoQuota).to.equal(5242880)
|
||||||
expect(data.user.videoQuotaDaily).to.equal(-1)
|
expect(data.user.videoQuotaDaily).to.equal(-1)
|
||||||
|
@ -64,6 +65,7 @@ function checkUpdatedConfig (data: CustomConfig) {
|
||||||
expect(data.cache.captions.size).to.equal(3)
|
expect(data.cache.captions.size).to.equal(3)
|
||||||
expect(data.signup.enabled).to.be.false
|
expect(data.signup.enabled).to.be.false
|
||||||
expect(data.signup.limit).to.equal(5)
|
expect(data.signup.limit).to.equal(5)
|
||||||
|
expect(data.signup.requiresEmailVerification).to.be.true
|
||||||
expect(data.admin.email).to.equal('superadmin1@example.com')
|
expect(data.admin.email).to.equal('superadmin1@example.com')
|
||||||
expect(data.user.videoQuota).to.equal(5242881)
|
expect(data.user.videoQuota).to.equal(5242881)
|
||||||
expect(data.user.videoQuotaDaily).to.equal(318742)
|
expect(data.user.videoQuotaDaily).to.equal(318742)
|
||||||
|
@ -148,7 +150,8 @@ describe('Test config', function () {
|
||||||
},
|
},
|
||||||
signup: {
|
signup: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
limit: 5
|
limit: 5,
|
||||||
|
requiresEmailVerification: true
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
email: 'superadmin1@example.com'
|
email: 'superadmin1@example.com'
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'mocha'
|
||||||
import {
|
import {
|
||||||
addVideoToBlacklist,
|
addVideoToBlacklist,
|
||||||
askResetPassword,
|
askResetPassword,
|
||||||
|
askSendVerifyEmail,
|
||||||
blockUser,
|
blockUser,
|
||||||
createUser, removeVideoFromBlacklist,
|
createUser, removeVideoFromBlacklist,
|
||||||
reportVideoAbuse,
|
reportVideoAbuse,
|
||||||
|
@ -12,7 +13,8 @@ import {
|
||||||
runServer,
|
runServer,
|
||||||
unblockUser,
|
unblockUser,
|
||||||
uploadVideo,
|
uploadVideo,
|
||||||
userLogin
|
userLogin,
|
||||||
|
verifyEmail
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
|
import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
|
||||||
import { mockSmtpServer } from '../../utils/miscs/email'
|
import { mockSmtpServer } from '../../utils/miscs/email'
|
||||||
|
@ -207,6 +209,44 @@ describe('Test emails', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When verifying a user email', function () {
|
||||||
|
|
||||||
|
it('Should ask to send the verification email', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
|
||||||
|
await askSendVerifyEmail(server.url, 'user_1@example.com')
|
||||||
|
|
||||||
|
await waitJobs(server)
|
||||||
|
expect(emails).to.have.lengthOf(7)
|
||||||
|
|
||||||
|
const email = emails[6]
|
||||||
|
|
||||||
|
expect(email['from'][0]['address']).equal('test-admin@localhost')
|
||||||
|
expect(email['to'][0]['address']).equal('user_1@example.com')
|
||||||
|
expect(email['subject']).contains('Verify')
|
||||||
|
|
||||||
|
const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
|
||||||
|
expect(verificationStringMatches).not.to.be.null
|
||||||
|
|
||||||
|
verificationString = verificationStringMatches[1]
|
||||||
|
expect(verificationString).to.not.be.undefined
|
||||||
|
expect(verificationString).to.have.length.above(2)
|
||||||
|
|
||||||
|
const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
|
||||||
|
expect(userIdMatches).not.to.be.null
|
||||||
|
|
||||||
|
userId = parseInt(userIdMatches[1], 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not verify the email with an invalid verification string', async function () {
|
||||||
|
await verifyEmail(server.url, userId, verificationString + 'b', 403)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should verify the email', async function () {
|
||||||
|
await verifyEmail(server.url, userId, verificationString)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
killallServers([ server ])
|
killallServers([ server ])
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
import './user-subscriptions'
|
import './user-subscriptions'
|
||||||
import './users'
|
import './users'
|
||||||
|
import './users-verification'
|
||||||
import './users-multiple-servers'
|
import './users-multiple-servers'
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import {
|
||||||
|
registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers,
|
||||||
|
userLogin, login, runServer, ServerInfo, verifyEmail, updateCustomSubConfig
|
||||||
|
} from '../../utils'
|
||||||
|
import { setAccessTokensToServers } from '../../utils/users/login'
|
||||||
|
import { mockSmtpServer } from '../../utils/miscs/email'
|
||||||
|
import { waitJobs } from '../../utils/server/jobs'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test users account verification', function () {
|
||||||
|
let server: ServerInfo
|
||||||
|
let userId: number
|
||||||
|
let verificationString: string
|
||||||
|
let expectedEmailsLength = 0
|
||||||
|
const user1 = {
|
||||||
|
username: 'user_1',
|
||||||
|
password: 'super password'
|
||||||
|
}
|
||||||
|
const user2 = {
|
||||||
|
username: 'user_2',
|
||||||
|
password: 'super password'
|
||||||
|
}
|
||||||
|
const emails: object[] = []
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
await mockSmtpServer(emails)
|
||||||
|
|
||||||
|
await flushTests()
|
||||||
|
|
||||||
|
const overrideConfig = {
|
||||||
|
smtp: {
|
||||||
|
hostname: 'localhost'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server = await runServer(1, overrideConfig)
|
||||||
|
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should register user and send verification email if verification required', async function () {
|
||||||
|
this.timeout(5000)
|
||||||
|
await updateCustomSubConfig(server.url, server.accessToken, {
|
||||||
|
signup: {
|
||||||
|
enabled: true,
|
||||||
|
requiresEmailVerification: true,
|
||||||
|
limit: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await registerUser(server.url, user1.username, user1.password)
|
||||||
|
|
||||||
|
await waitJobs(server)
|
||||||
|
expectedEmailsLength++
|
||||||
|
expect(emails).to.have.lengthOf(expectedEmailsLength)
|
||||||
|
|
||||||
|
const email = emails[expectedEmailsLength - 1]
|
||||||
|
|
||||||
|
const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
|
||||||
|
expect(verificationStringMatches).not.to.be.null
|
||||||
|
|
||||||
|
verificationString = verificationStringMatches[1]
|
||||||
|
expect(verificationString).to.have.length.above(2)
|
||||||
|
|
||||||
|
const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
|
||||||
|
expect(userIdMatches).not.to.be.null
|
||||||
|
|
||||||
|
userId = parseInt(userIdMatches[1], 10)
|
||||||
|
|
||||||
|
const resUserInfo = await getUserInformation(server.url, server.accessToken, userId)
|
||||||
|
expect(resUserInfo.body.emailVerified).to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not allow login for user with unverified email', async function () {
|
||||||
|
const resLogin = await login(server.url, server.client, user1, 400)
|
||||||
|
expect(resLogin.body.error).to.contain('User email is not verified.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should verify the user via email and allow login', async function () {
|
||||||
|
await verifyEmail(server.url, userId, verificationString)
|
||||||
|
await login(server.url, server.client, user1)
|
||||||
|
const resUserVerified = await getUserInformation(server.url, server.accessToken, userId)
|
||||||
|
expect(resUserVerified.body.emailVerified).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should register user not requiring email verification if setting not enabled', async function () {
|
||||||
|
this.timeout(5000)
|
||||||
|
await updateCustomSubConfig(server.url, server.accessToken, {
|
||||||
|
signup: {
|
||||||
|
enabled: true,
|
||||||
|
requiresEmailVerification: false,
|
||||||
|
limit: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await registerUser(server.url, user2.username, user2.password)
|
||||||
|
|
||||||
|
await waitJobs(server)
|
||||||
|
expect(emails).to.have.lengthOf(expectedEmailsLength)
|
||||||
|
|
||||||
|
const accessToken = await userLogin(server, user2)
|
||||||
|
|
||||||
|
const resMyUserInfo = await getMyUserInformation(server.url, accessToken)
|
||||||
|
expect(resMyUserInfo.body.emailVerified).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should allow login for user with unverified email when setting later enabled', async function () {
|
||||||
|
await updateCustomSubConfig(server.url, server.accessToken, {
|
||||||
|
signup: {
|
||||||
|
enabled: true,
|
||||||
|
requiresEmailVerification: true,
|
||||||
|
limit: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await userLogin(server, user2)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
killallServers([ server ])
|
||||||
|
|
||||||
|
// Keep the logs if the test failed
|
||||||
|
if (this[ 'ok' ]) {
|
||||||
|
await flushTests()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -7,7 +7,7 @@ import {
|
||||||
createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating,
|
createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating,
|
||||||
getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo,
|
getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo,
|
||||||
registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin,
|
registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin,
|
||||||
deleteMe, blockUser, unblockUser
|
deleteMe, blockUser, unblockUser, updateCustomSubConfig
|
||||||
} from '../../utils/index'
|
} from '../../utils/index'
|
||||||
import { follow } from '../../utils/server/follows'
|
import { follow } from '../../utils/server/follows'
|
||||||
import { setAccessTokensToServers } from '../../utils/users/login'
|
import { setAccessTokensToServers } from '../../utils/users/login'
|
||||||
|
|
|
@ -74,7 +74,8 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
|
||||||
},
|
},
|
||||||
signup: {
|
signup: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
limit: 5
|
limit: 5,
|
||||||
|
requiresEmailVerification: false
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
email: 'superadmin1@example.com'
|
email: 'superadmin1@example.com'
|
||||||
|
|
|
@ -246,6 +246,28 @@ function resetPassword (url: string, userId: number, verificationString: string,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function askSendVerifyEmail (url: string, email: string) {
|
||||||
|
const path = '/api/v1/users/ask-send-verify-email'
|
||||||
|
|
||||||
|
return makePostBodyRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
fields: { email },
|
||||||
|
statusCodeExpected: 204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) {
|
||||||
|
const path = '/api/v1/users/' + userId + '/verify-email'
|
||||||
|
|
||||||
|
return makePostBodyRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
fields: { verificationString },
|
||||||
|
statusCodeExpected
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -265,5 +287,7 @@ export {
|
||||||
unblockUser,
|
unblockUser,
|
||||||
askResetPassword,
|
askResetPassword,
|
||||||
resetPassword,
|
resetPassword,
|
||||||
updateMyAvatar
|
updateMyAvatar,
|
||||||
|
askSendVerifyEmail,
|
||||||
|
verifyEmail
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ export interface CustomConfig {
|
||||||
signup: {
|
signup: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
limit: number
|
limit: number
|
||||||
|
requiresEmailVerification: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
admin: {
|
admin: {
|
||||||
|
|
|
@ -16,7 +16,8 @@ export interface ServerConfig {
|
||||||
|
|
||||||
signup: {
|
signup: {
|
||||||
allowed: boolean,
|
allowed: boolean,
|
||||||
allowedForCurrentIP: boolean
|
allowedForCurrentIP: boolean,
|
||||||
|
requiresEmailVerification: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
transcoding: {
|
transcoding: {
|
||||||
|
|
Loading…
Reference in New Issue