diff options
42 files changed, 715 insertions, 24 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index ca7890d84..a0f0abd10 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | |||
@@ -91,6 +91,11 @@ | |||
91 | i18n-labelText labelText="Signup enabled" | 91 | i18n-labelText labelText="Signup enabled" |
92 | ></my-peertube-checkbox> | 92 | ></my-peertube-checkbox> |
93 | 93 | ||
94 | <my-peertube-checkbox *ngIf="isSignupEnabled()" | ||
95 | inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification" | ||
96 | i18n-labelText labelText="Signup requires email verification" | ||
97 | ></my-peertube-checkbox> | ||
98 | |||
94 | <div *ngIf="isSignupEnabled()" class="form-group"> | 99 | <div *ngIf="isSignupEnabled()" class="form-group"> |
95 | <label i18n for="signupLimit">Signup limit</label> | 100 | <label i18n for="signupLimit">Signup limit</label> |
96 | <input | 101 | <input |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 248b0df50..ce2ea8a6c 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -90,6 +90,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
90 | cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, | 90 | cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, |
91 | signupEnabled: null, | 91 | signupEnabled: null, |
92 | signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, | 92 | signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, |
93 | signupRequiresEmailVerification: null, | ||
93 | importVideosHttpEnabled: null, | 94 | importVideosHttpEnabled: null, |
94 | importVideosTorrentEnabled: null, | 95 | importVideosTorrentEnabled: null, |
95 | adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, | 96 | adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, |
@@ -187,7 +188,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
187 | }, | 188 | }, |
188 | signup: { | 189 | signup: { |
189 | enabled: this.form.value['signupEnabled'], | 190 | enabled: this.form.value['signupEnabled'], |
190 | limit: this.form.value['signupLimit'] | 191 | limit: this.form.value['signupLimit'], |
192 | requiresEmailVerification: this.form.value['signupRequiresEmailVerification'] | ||
191 | }, | 193 | }, |
192 | admin: { | 194 | admin: { |
193 | email: this.form.value['adminEmail'] | 195 | email: this.form.value['adminEmail'] |
@@ -250,6 +252,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
250 | cacheCaptionsSize: this.customConfig.cache.captions.size, | 252 | cacheCaptionsSize: this.customConfig.cache.captions.size, |
251 | signupEnabled: this.customConfig.signup.enabled, | 253 | signupEnabled: this.customConfig.signup.enabled, |
252 | signupLimit: this.customConfig.signup.limit, | 254 | signupLimit: this.customConfig.signup.limit, |
255 | signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification, | ||
253 | adminEmail: this.customConfig.admin.email, | 256 | adminEmail: this.customConfig.admin.email, |
254 | userVideoQuota: this.customConfig.user.videoQuota, | 257 | userVideoQuota: this.customConfig.user.videoQuota, |
255 | userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily, | 258 | userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily, |
diff --git a/client/src/app/+verify-account/index.ts b/client/src/app/+verify-account/index.ts new file mode 100644 index 000000000..733f5ba77 --- /dev/null +++ b/client/src/app/+verify-account/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from '@app/+verify-account/verify-account-routing.module' | ||
2 | export * from '@app/+verify-account/verify-account.module' | ||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html new file mode 100644 index 000000000..2e4180632 --- /dev/null +++ b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.html | |||
@@ -0,0 +1,22 @@ | |||
1 | <div class="margin-content"> | ||
2 | <div i18n class="title-page title-page-single"> | ||
3 | Request email for account verification | ||
4 | </div> | ||
5 | |||
6 | <form *ngIf="requiresEmailVerification; else emailVerificationNotRequired" role="form" (ngSubmit)="askSendVerifyEmail()" [formGroup]="form"> | ||
7 | <div class="form-group"> | ||
8 | <label i18n for="verify-email-email">Email</label> | ||
9 | <input | ||
10 | type="email" id="verify-email-email" i18n-placeholder placeholder="Email address" required | ||
11 | formControlName="verify-email-email" [ngClass]="{ 'input-error': formErrors['verify-email-email'] }" | ||
12 | > | ||
13 | <div *ngIf="formErrors['verify-email-email']" class="form-error"> | ||
14 | {{ formErrors['verify-email-email'] }} | ||
15 | </div> | ||
16 | </div> | ||
17 | <input type="submit" i18n-value value="Send verification email" [disabled]="!form.valid"> | ||
18 | </form> | ||
19 | <ng-template #emailVerificationNotRequired> | ||
20 | <div i18n>This instance does not require email verification.</div> | ||
21 | </ng-template> | ||
22 | </div> | ||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss new file mode 100644 index 000000000..efec6b706 --- /dev/null +++ b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.scss | |||
@@ -0,0 +1,12 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input:not([type=submit]) { | ||
5 | @include peertube-input-text(340px); | ||
6 | display: block; | ||
7 | } | ||
8 | |||
9 | input[type=submit] { | ||
10 | @include peertube-button; | ||
11 | @include orange-button; | ||
12 | } | ||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts new file mode 100644 index 000000000..995f42ffc --- /dev/null +++ b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { ServerService } from '@app/core/server' | ||
5 | import { RedirectService } from '@app/core' | ||
6 | import { UserService, FormReactive } from '@app/shared' | ||
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-verify-account-ask-send-email', | ||
12 | templateUrl: './verify-account-ask-send-email.component.html', | ||
13 | styleUrls: [ './verify-account-ask-send-email.component.scss' ] | ||
14 | }) | ||
15 | |||
16 | export class VerifyAccountAskSendEmailComponent extends FormReactive implements OnInit { | ||
17 | |||
18 | constructor ( | ||
19 | protected formValidatorService: FormValidatorService, | ||
20 | private userValidatorsService: UserValidatorsService, | ||
21 | private userService: UserService, | ||
22 | private serverService: ServerService, | ||
23 | private notificationsService: NotificationsService, | ||
24 | private redirectService: RedirectService, | ||
25 | private i18n: I18n | ||
26 | ) { | ||
27 | super() | ||
28 | } | ||
29 | |||
30 | get requiresEmailVerification () { | ||
31 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
32 | } | ||
33 | |||
34 | ngOnInit () { | ||
35 | this.buildForm({ | ||
36 | 'verify-email-email': this.userValidatorsService.USER_EMAIL | ||
37 | }) | ||
38 | } | ||
39 | |||
40 | askSendVerifyEmail () { | ||
41 | const email = this.form.value['verify-email-email'] | ||
42 | this.userService.askSendVerifyEmail(email) | ||
43 | .subscribe( | ||
44 | () => { | ||
45 | const message = this.i18n( | ||
46 | 'An email with verification link will be sent to {{email}}.', | ||
47 | { email } | ||
48 | ) | ||
49 | this.notificationsService.success(this.i18n('Success'), message) | ||
50 | this.redirectService.redirectToHomepage() | ||
51 | }, | ||
52 | |||
53 | err => { | ||
54 | this.notificationsService.error(this.i18n('Error'), err.message) | ||
55 | } | ||
56 | ) | ||
57 | } | ||
58 | } | ||
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html new file mode 100644 index 000000000..30ace5e10 --- /dev/null +++ b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html | |||
@@ -0,0 +1,15 @@ | |||
1 | <div class="margin-content"> | ||
2 | <div i18n class="title-page title-page-single"> | ||
3 | Verify account email confirmation | ||
4 | </div> | ||
5 | |||
6 | <div i18n *ngIf="success; else verificationError"> | ||
7 | Your email has been verified and you may now login. Redirecting... | ||
8 | </div> | ||
9 | <ng-template #verificationError> | ||
10 | <div> | ||
11 | <span i18n>An error occurred. </span> | ||
12 | <a i18n routerLink="/verify-account/ask-email">Request new verification email.</a> | ||
13 | </div> | ||
14 | </ng-template> | ||
15 | </div> | ||
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts new file mode 100644 index 000000000..26b3bf4b1 --- /dev/null +++ b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { UserService } from '@app/shared' | ||
6 | |||
7 | @Component({ | ||
8 | selector: 'my-verify-account-email', | ||
9 | templateUrl: './verify-account-email.component.html' | ||
10 | }) | ||
11 | |||
12 | export class VerifyAccountEmailComponent implements OnInit { | ||
13 | success = false | ||
14 | |||
15 | private userId: number | ||
16 | private verificationString: string | ||
17 | |||
18 | constructor ( | ||
19 | private userService: UserService, | ||
20 | private notificationsService: NotificationsService, | ||
21 | private router: Router, | ||
22 | private route: ActivatedRoute, | ||
23 | private i18n: I18n | ||
24 | ) { | ||
25 | } | ||
26 | |||
27 | ngOnInit () { | ||
28 | |||
29 | this.userId = this.route.snapshot.queryParams['userId'] | ||
30 | this.verificationString = this.route.snapshot.queryParams['verificationString'] | ||
31 | |||
32 | if (!this.userId || !this.verificationString) { | ||
33 | this.notificationsService.error(this.i18n('Error'), this.i18n('Unable to find user id or verification string.')) | ||
34 | } else { | ||
35 | this.verifyEmail() | ||
36 | } | ||
37 | } | ||
38 | |||
39 | verifyEmail () { | ||
40 | this.userService.verifyEmail(this.userId, this.verificationString) | ||
41 | .subscribe( | ||
42 | () => { | ||
43 | this.success = true | ||
44 | setTimeout(() => { | ||
45 | this.router.navigate([ '/login' ]) | ||
46 | }, 2000) | ||
47 | }, | ||
48 | |||
49 | err => { | ||
50 | this.notificationsService.error(this.i18n('Error'), err.message) | ||
51 | } | ||
52 | ) | ||
53 | } | ||
54 | } | ||
diff --git a/client/src/app/+verify-account/verify-account-routing.module.ts b/client/src/app/+verify-account/verify-account-routing.module.ts new file mode 100644 index 000000000..a038f0336 --- /dev/null +++ b/client/src/app/+verify-account/verify-account-routing.module.ts | |||
@@ -0,0 +1,42 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { RouterModule, Routes } from '@angular/router' | ||
3 | |||
4 | import { MetaGuard } from '@ngx-meta/core' | ||
5 | |||
6 | import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component' | ||
7 | import { | ||
8 | VerifyAccountAskSendEmailComponent | ||
9 | } from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component' | ||
10 | |||
11 | const verifyAccountRoutes: Routes = [ | ||
12 | { | ||
13 | path: '', | ||
14 | canActivateChild: [ MetaGuard ], | ||
15 | children: [ | ||
16 | { | ||
17 | path: 'email', | ||
18 | component: VerifyAccountEmailComponent, | ||
19 | data: { | ||
20 | meta: { | ||
21 | title: 'Verify account email' | ||
22 | } | ||
23 | } | ||
24 | }, | ||
25 | { | ||
26 | path: 'ask-send-email', | ||
27 | component: VerifyAccountAskSendEmailComponent, | ||
28 | data: { | ||
29 | meta: { | ||
30 | title: 'Verify account ask send email' | ||
31 | } | ||
32 | } | ||
33 | } | ||
34 | ] | ||
35 | } | ||
36 | ] | ||
37 | |||
38 | @NgModule({ | ||
39 | imports: [ RouterModule.forChild(verifyAccountRoutes) ], | ||
40 | exports: [ RouterModule ] | ||
41 | }) | ||
42 | export class VerifyAccountRoutingModule {} | ||
diff --git a/client/src/app/+verify-account/verify-account.module.ts b/client/src/app/+verify-account/verify-account.module.ts new file mode 100644 index 000000000..9092c6b4f --- /dev/null +++ b/client/src/app/+verify-account/verify-account.module.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | |||
3 | import { VerifyAccountRoutingModule } from '@app/+verify-account/verify-account-routing.module' | ||
4 | import { VerifyAccountEmailComponent } from '@app/+verify-account/verify-account-email/verify-account-email.component' | ||
5 | import { | ||
6 | VerifyAccountAskSendEmailComponent | ||
7 | } from '@app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component' | ||
8 | import { SharedModule } from '@app/shared' | ||
9 | |||
10 | @NgModule({ | ||
11 | imports: [ | ||
12 | VerifyAccountRoutingModule, | ||
13 | SharedModule | ||
14 | ], | ||
15 | |||
16 | declarations: [ | ||
17 | VerifyAccountEmailComponent, | ||
18 | VerifyAccountAskSendEmailComponent | ||
19 | ], | ||
20 | |||
21 | exports: [ | ||
22 | ], | ||
23 | |||
24 | providers: [ | ||
25 | ] | ||
26 | }) | ||
27 | export class VerifyAccountModule { } | ||
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 30e615b3e..545d6aeda 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -14,6 +14,10 @@ const routes: Routes = [ | |||
14 | loadChildren: './+my-account/my-account.module#MyAccountModule' | 14 | loadChildren: './+my-account/my-account.module#MyAccountModule' |
15 | }, | 15 | }, |
16 | { | 16 | { |
17 | path: 'verify-account', | ||
18 | loadChildren: './+verify-account/verify-account.module#VerifyAccountModule' | ||
19 | }, | ||
20 | { | ||
17 | path: 'accounts', | 21 | path: 'accounts', |
18 | loadChildren: './+accounts/accounts.module#AccountsModule' | 22 | loadChildren: './+accounts/accounts.module#AccountsModule' |
19 | }, | 23 | }, |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index a1ce12069..e7152efa0 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -40,7 +40,8 @@ export class ServerService { | |||
40 | serverVersion: 'Unknown', | 40 | serverVersion: 'Unknown', |
41 | signup: { | 41 | signup: { |
42 | allowed: false, | 42 | allowed: false, |
43 | allowedForCurrentIP: false | 43 | allowedForCurrentIP: false, |
44 | requiresEmailVerification: false | ||
44 | }, | 45 | }, |
45 | transcoding: { | 46 | transcoding: { |
46 | enabledResolutions: [] | 47 | enabledResolutions: [] |
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html index 3a6d61327..619150ade 100644 --- a/client/src/app/login/login.component.html +++ b/client/src/app/login/login.component.html | |||
@@ -3,7 +3,9 @@ | |||
3 | Login | 3 | Login |
4 | </div> | 4 | </div> |
5 | 5 | ||
6 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | 6 | <div *ngIf="error" class="alert alert-danger">{{ error }} |
7 | <span *ngIf="error === 'User email is not verified.'"> <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a></span> | ||
8 | </div> | ||
7 | 9 | ||
8 | <form role="form" (ngSubmit)="login()" [formGroup]="form"> | 10 | <form role="form" (ngSubmit)="login()" [formGroup]="form"> |
9 | <div class="form-group"> | 11 | <div class="form-group"> |
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index e6dc3dbf8..249c589b7 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -94,4 +94,27 @@ export class UserService { | |||
94 | catchError(res => this.restExtractor.handleError(res)) | 94 | catchError(res => this.restExtractor.handleError(res)) |
95 | ) | 95 | ) |
96 | } | 96 | } |
97 | |||
98 | verifyEmail (userId: number, verificationString: string) { | ||
99 | const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` | ||
100 | const body = { | ||
101 | verificationString | ||
102 | } | ||
103 | |||
104 | return this.authHttp.post(url, body) | ||
105 | .pipe( | ||
106 | map(this.restExtractor.extractDataBool), | ||
107 | catchError(res => this.restExtractor.handleError(res)) | ||
108 | ) | ||
109 | } | ||
110 | |||
111 | askSendVerifyEmail (email: string) { | ||
112 | const url = UserService.BASE_USERS_URL + '/ask-send-verify-email' | ||
113 | |||
114 | return this.authHttp.post(url, { email }) | ||
115 | .pipe( | ||
116 | map(this.restExtractor.extractDataBool), | ||
117 | catchError(err => this.restExtractor.handleError(err)) | ||
118 | ) | ||
119 | } | ||
97 | } | 120 | } |
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts index 47f9bc6f4..16e444678 100644 --- a/client/src/app/signup/signup.component.ts +++ b/client/src/app/signup/signup.component.ts | |||
@@ -3,7 +3,7 @@ import { Router } from '@angular/router' | |||
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
4 | import { UserCreate } from '../../../../shared' | 4 | import { UserCreate } from '../../../../shared' |
5 | import { FormReactive, UserService, UserValidatorsService } from '../shared' | 5 | import { FormReactive, UserService, UserValidatorsService } from '../shared' |
6 | import { RedirectService } from '@app/core' | 6 | import { RedirectService, ServerService } from '@app/core' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 8 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
9 | 9 | ||
@@ -21,6 +21,7 @@ export class SignupComponent extends FormReactive implements OnInit { | |||
21 | private router: Router, | 21 | private router: Router, |
22 | private notificationsService: NotificationsService, | 22 | private notificationsService: NotificationsService, |
23 | private userService: UserService, | 23 | private userService: UserService, |
24 | private serverService: ServerService, | ||
24 | private redirectService: RedirectService, | 25 | private redirectService: RedirectService, |
25 | private i18n: I18n | 26 | private i18n: I18n |
26 | ) { | 27 | ) { |
@@ -31,6 +32,10 @@ export class SignupComponent extends FormReactive implements OnInit { | |||
31 | return window.location.host | 32 | return window.location.host |
32 | } | 33 | } |
33 | 34 | ||
35 | get requiresEmailVerification () { | ||
36 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
37 | } | ||
38 | |||
34 | ngOnInit () { | 39 | ngOnInit () { |
35 | this.buildForm({ | 40 | this.buildForm({ |
36 | username: this.userValidatorsService.USER_USERNAME, | 41 | username: this.userValidatorsService.USER_USERNAME, |
@@ -47,10 +52,17 @@ export class SignupComponent extends FormReactive implements OnInit { | |||
47 | 52 | ||
48 | this.userService.signup(userCreate).subscribe( | 53 | this.userService.signup(userCreate).subscribe( |
49 | () => { | 54 | () => { |
50 | this.notificationsService.success( | 55 | if (this.requiresEmailVerification) { |
51 | this.i18n('Success'), | 56 | this.notificationsService.alert( |
52 | this.i18n('Registration for {{username}} complete.', { username: userCreate.username }) | 57 | this.i18n('Welcome'), |
53 | ) | 58 | this.i18n('Please check your email to verify your account and complete signup.') |
59 | ) | ||
60 | } else { | ||
61 | this.notificationsService.success( | ||
62 | this.i18n('Success'), | ||
63 | this.i18n('Registration for {{username}} complete.', { username: userCreate.username }) | ||
64 | ) | ||
65 | } | ||
54 | this.redirectService.redirectToHomepage() | 66 | this.redirectService.redirectToHomepage() |
55 | }, | 67 | }, |
56 | 68 | ||
diff --git a/config/default.yaml b/config/default.yaml index 7799ea927..ef63fbd28 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -74,6 +74,7 @@ admin: | |||
74 | signup: | 74 | signup: |
75 | enabled: false | 75 | enabled: false |
76 | limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited | 76 | limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited |
77 | requires_email_verification: false | ||
77 | filters: | 78 | filters: |
78 | cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist | 79 | cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist |
79 | whitelist: [] | 80 | whitelist: [] |
diff --git a/config/production.yaml.example b/config/production.yaml.example index 33a26dec1..f7b153698 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -87,6 +87,7 @@ admin: | |||
87 | signup: | 87 | signup: |
88 | enabled: false | 88 | enabled: false |
89 | limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited | 89 | limit: 10 # When the limit is reached, registrations are disabled. -1 == unlimited |
90 | requires_email_verification: false | ||
90 | filters: | 91 | filters: |
91 | cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist | 92 | cidr: # You can specify CIDR ranges to whitelist (empty = no filtering) or blacklist |
92 | whitelist: [] | 93 | whitelist: [] |
diff --git a/config/test.yaml b/config/test.yaml index 879b6bdd4..6a8e47aac 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -29,6 +29,7 @@ cache: | |||
29 | 29 | ||
30 | signup: | 30 | signup: |
31 | enabled: true | 31 | enabled: true |
32 | requires_email_verification: false | ||
32 | 33 | ||
33 | transcoding: | 34 | transcoding: |
34 | enabled: true | 35 | enabled: true |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 25ddd1fa6..6edbe4820 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -60,7 +60,8 @@ async function getConfig (req: express.Request, res: express.Response, next: exp | |||
60 | serverVersion: packageJSON.version, | 60 | serverVersion: packageJSON.version, |
61 | signup: { | 61 | signup: { |
62 | allowed, | 62 | allowed, |
63 | allowedForCurrentIP | 63 | allowedForCurrentIP, |
64 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | ||
64 | }, | 65 | }, |
65 | transcoding: { | 66 | transcoding: { |
66 | enabledResolutions | 67 | enabledResolutions |
@@ -159,12 +160,20 @@ async function updateCustomConfig (req: express.Request, res: express.Response, | |||
159 | toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10) | 160 | toUpdate.transcoding.threads = parseInt('' + toUpdate.transcoding.threads, 10) |
160 | 161 | ||
161 | // camelCase to snake_case key | 162 | // camelCase to snake_case key |
162 | const toUpdateJSON = omit(toUpdate, 'user.videoQuota', 'instance.defaultClientRoute', 'instance.shortDescription', 'cache.videoCaptions') | 163 | const toUpdateJSON = omit( |
164 | toUpdate, | ||
165 | 'user.videoQuota', | ||
166 | 'instance.defaultClientRoute', | ||
167 | 'instance.shortDescription', | ||
168 | 'cache.videoCaptions', | ||
169 | 'signup.requiresEmailVerification' | ||
170 | ) | ||
163 | toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota | 171 | toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota |
164 | toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily | 172 | toUpdateJSON.user['video_quota_daily'] = toUpdate.user.videoQuotaDaily |
165 | toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute | 173 | toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute |
166 | toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription | 174 | toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription |
167 | toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy | 175 | toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy |
176 | toUpdateJSON.signup['requires_email_verification'] = toUpdate.signup.requiresEmailVerification | ||
168 | 177 | ||
169 | await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) | 178 | await writeJSON(CONFIG.CUSTOM_FILE, toUpdateJSON, { spaces: 2 }) |
170 | 179 | ||
@@ -220,7 +229,8 @@ function customConfig (): CustomConfig { | |||
220 | }, | 229 | }, |
221 | signup: { | 230 | signup: { |
222 | enabled: CONFIG.SIGNUP.ENABLED, | 231 | enabled: CONFIG.SIGNUP.ENABLED, |
223 | limit: CONFIG.SIGNUP.LIMIT | 232 | limit: CONFIG.SIGNUP.LIMIT, |
233 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | ||
224 | }, | 234 | }, |
225 | admin: { | 235 | admin: { |
226 | email: CONFIG.ADMIN.EMAIL | 236 | email: CONFIG.ADMIN.EMAIL |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 25d51ae5e..008c34ca4 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -25,7 +25,10 @@ import { | |||
25 | usersSortValidator, | 25 | usersSortValidator, |
26 | usersUpdateValidator | 26 | usersUpdateValidator |
27 | } from '../../../middlewares' | 27 | } from '../../../middlewares' |
28 | import { usersAskResetPasswordValidator, usersBlockingValidator, usersResetPasswordValidator } from '../../../middlewares/validators' | 28 | import { |
29 | usersAskResetPasswordValidator, usersBlockingValidator, usersResetPasswordValidator, | ||
30 | usersAskSendVerifyEmailValidator, usersVerifyEmailValidator | ||
31 | } from '../../../middlewares/validators' | ||
29 | import { UserModel } from '../../../models/account/user' | 32 | import { UserModel } from '../../../models/account/user' |
30 | import { OAuthTokenModel } from '../../../models/oauth/oauth-token' | 33 | import { OAuthTokenModel } from '../../../models/oauth/oauth-token' |
31 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' | 34 | import { auditLoggerFactory, UserAuditView } from '../../../helpers/audit-logger' |
@@ -110,6 +113,17 @@ usersRouter.post('/:id/reset-password', | |||
110 | asyncMiddleware(resetUserPassword) | 113 | asyncMiddleware(resetUserPassword) |
111 | ) | 114 | ) |
112 | 115 | ||
116 | usersRouter.post('/ask-send-verify-email', | ||
117 | loginRateLimiter, | ||
118 | asyncMiddleware(usersAskSendVerifyEmailValidator), | ||
119 | asyncMiddleware(askSendVerifyUserEmail) | ||
120 | ) | ||
121 | |||
122 | usersRouter.post('/:id/verify-email', | ||
123 | asyncMiddleware(usersVerifyEmailValidator), | ||
124 | asyncMiddleware(verifyUserEmail) | ||
125 | ) | ||
126 | |||
113 | usersRouter.post('/token', | 127 | usersRouter.post('/token', |
114 | loginRateLimiter, | 128 | loginRateLimiter, |
115 | token, | 129 | token, |
@@ -165,7 +179,8 @@ async function registerUser (req: express.Request, res: express.Response) { | |||
165 | autoPlayVideo: true, | 179 | autoPlayVideo: true, |
166 | role: UserRole.USER, | 180 | role: UserRole.USER, |
167 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | 181 | videoQuota: CONFIG.USER.VIDEO_QUOTA, |
168 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY | 182 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY, |
183 | emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null | ||
169 | }) | 184 | }) |
170 | 185 | ||
171 | const { user } = await createUserAccountAndChannel(userToCreate) | 186 | const { user } = await createUserAccountAndChannel(userToCreate) |
@@ -173,6 +188,10 @@ async function registerUser (req: express.Request, res: express.Response) { | |||
173 | auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) | 188 | auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) |
174 | logger.info('User %s with its channel and account registered.', body.username) | 189 | logger.info('User %s with its channel and account registered.', body.username) |
175 | 190 | ||
191 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { | ||
192 | await sendVerifyUserEmail(user) | ||
193 | } | ||
194 | |||
176 | return res.type('json').status(204).end() | 195 | return res.type('json').status(204).end() |
177 | } | 196 | } |
178 | 197 | ||
@@ -261,6 +280,30 @@ async function resetUserPassword (req: express.Request, res: express.Response, n | |||
261 | return res.status(204).end() | 280 | return res.status(204).end() |
262 | } | 281 | } |
263 | 282 | ||
283 | async function sendVerifyUserEmail (user: UserModel) { | ||
284 | const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) | ||
285 | const url = CONFIG.WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString | ||
286 | await Emailer.Instance.addVerifyEmailJob(user.email, url) | ||
287 | return | ||
288 | } | ||
289 | |||
290 | async function askSendVerifyUserEmail (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
291 | const user = res.locals.user as UserModel | ||
292 | |||
293 | await sendVerifyUserEmail(user) | ||
294 | |||
295 | return res.status(204).end() | ||
296 | } | ||
297 | |||
298 | async function verifyUserEmail (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
299 | const user = res.locals.user as UserModel | ||
300 | user.emailVerified = true | ||
301 | |||
302 | await user.save() | ||
303 | |||
304 | return res.status(204).end() | ||
305 | } | ||
306 | |||
264 | function success (req: express.Request, res: express.Response, next: express.NextFunction) { | 307 | function success (req: express.Request, res: express.Response, next: express.NextFunction) { |
265 | res.end() | 308 | res.end() |
266 | } | 309 | } |
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts index db20df20f..7db72b69c 100644 --- a/server/helpers/audit-logger.ts +++ b/server/helpers/audit-logger.ts | |||
@@ -234,6 +234,7 @@ const customConfigKeysToKeep = [ | |||
234 | 'cache-captions-size', | 234 | 'cache-captions-size', |
235 | 'signup-enabled', | 235 | 'signup-enabled', |
236 | 'signup-limit', | 236 | 'signup-limit', |
237 | 'signup-requiresEmailVerification', | ||
237 | 'admin-email', | 238 | 'admin-email', |
238 | 'user-videoQuota', | 239 | 'user-videoQuota', |
239 | 'transcoding-enabled', | 240 | 'transcoding-enabled', |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index 8d6247e41..90fc74a48 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -33,6 +33,10 @@ function isUserDescriptionValid (value: string) { | |||
33 | return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION)) | 33 | return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION)) |
34 | } | 34 | } |
35 | 35 | ||
36 | function isUserEmailVerifiedValid (value: any) { | ||
37 | return isBooleanValid(value) | ||
38 | } | ||
39 | |||
36 | const nsfwPolicies = values(NSFW_POLICY_TYPES) | 40 | const nsfwPolicies = values(NSFW_POLICY_TYPES) |
37 | function isUserNSFWPolicyValid (value: any) { | 41 | function isUserNSFWPolicyValid (value: any) { |
38 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 | 42 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 |
@@ -72,6 +76,7 @@ export { | |||
72 | isUserVideoQuotaValid, | 76 | isUserVideoQuotaValid, |
73 | isUserVideoQuotaDailyValid, | 77 | isUserVideoQuotaDailyValid, |
74 | isUserUsernameValid, | 78 | isUserUsernameValid, |
79 | isUserEmailVerifiedValid, | ||
75 | isUserNSFWPolicyValid, | 80 | isUserNSFWPolicyValid, |
76 | isUserAutoPlayVideoValid, | 81 | isUserAutoPlayVideoValid, |
77 | isUserDisplayNameValid, | 82 | isUserDisplayNameValid, |
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index 916e9067e..ee02ecf48 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts | |||
@@ -49,7 +49,8 @@ function checkMissedConfig () { | |||
49 | 'log.level', | 49 | 'log.level', |
50 | 'user.video_quota', 'user.video_quota_daily', | 50 | 'user.video_quota', 'user.video_quota_daily', |
51 | 'cache.previews.size', 'admin.email', | 51 | 'cache.previews.size', 'admin.email', |
52 | 'signup.enabled', 'signup.limit', 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', | 52 | 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', |
53 | 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', | ||
53 | 'transcoding.enabled', 'transcoding.threads', | 54 | 'transcoding.enabled', 'transcoding.threads', |
54 | 'import.videos.http.enabled', | 55 | 'import.videos.http.enabled', |
55 | 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', | 56 | 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 2d9a2e670..5d93c6b82 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -15,7 +15,7 @@ let config: IConfig = require('config') | |||
15 | 15 | ||
16 | // --------------------------------------------------------------------------- | 16 | // --------------------------------------------------------------------------- |
17 | 17 | ||
18 | const LAST_MIGRATION_VERSION = 260 | 18 | const LAST_MIGRATION_VERSION = 265 |
19 | 19 | ||
20 | // --------------------------------------------------------------------------- | 20 | // --------------------------------------------------------------------------- |
21 | 21 | ||
@@ -204,6 +204,7 @@ const CONFIG = { | |||
204 | SIGNUP: { | 204 | SIGNUP: { |
205 | get ENABLED () { return config.get<boolean>('signup.enabled') }, | 205 | get ENABLED () { return config.get<boolean>('signup.enabled') }, |
206 | get LIMIT () { return config.get<number>('signup.limit') }, | 206 | get LIMIT () { return config.get<number>('signup.limit') }, |
207 | get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') }, | ||
207 | FILTERS: { | 208 | FILTERS: { |
208 | CIDR: { | 209 | CIDR: { |
209 | get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') }, | 210 | get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') }, |
@@ -500,6 +501,8 @@ const BCRYPT_SALT_SIZE = 10 | |||
500 | 501 | ||
501 | const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes | 502 | const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes |
502 | 503 | ||
504 | const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes | ||
505 | |||
503 | const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = { | 506 | const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = { |
504 | DO_NOT_LIST: 'do_not_list', | 507 | DO_NOT_LIST: 'do_not_list', |
505 | BLUR: 'blur', | 508 | BLUR: 'blur', |
@@ -661,6 +664,7 @@ export { | |||
661 | VIDEO_ABUSE_STATES, | 664 | VIDEO_ABUSE_STATES, |
662 | JOB_REQUEST_TIMEOUT, | 665 | JOB_REQUEST_TIMEOUT, |
663 | USER_PASSWORD_RESET_LIFETIME, | 666 | USER_PASSWORD_RESET_LIFETIME, |
667 | USER_EMAIL_VERIFY_LIFETIME, | ||
664 | IMAGE_MIMETYPE_EXT, | 668 | IMAGE_MIMETYPE_EXT, |
665 | SCHEDULER_INTERVALS_MS, | 669 | SCHEDULER_INTERVALS_MS, |
666 | REPEAT_JOBS, | 670 | REPEAT_JOBS, |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index d4aaec8fe..818bb04a2 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -122,6 +122,7 @@ async function createOAuthAdminIfNotExist () { | |||
122 | email, | 122 | email, |
123 | password, | 123 | password, |
124 | role, | 124 | role, |
125 | verified: true, | ||
125 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | 126 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, |
126 | videoQuota: -1, | 127 | videoQuota: -1, |
127 | videoQuotaDaily: -1 | 128 | videoQuotaDaily: -1 |
diff --git a/server/initializers/migrations/0265-user-email-verified.ts b/server/initializers/migrations/0265-user-email-verified.ts new file mode 100644 index 000000000..59dfdad2b --- /dev/null +++ b/server/initializers/migrations/0265-user-email-verified.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<any> { | ||
8 | { | ||
9 | const data = { | ||
10 | type: Sequelize.BOOLEAN, | ||
11 | allowNull: true, | ||
12 | defaultValue: null | ||
13 | } | ||
14 | |||
15 | await utils.queryInterface.addColumn('user', 'emailVerified', data) | ||
16 | } | ||
17 | |||
18 | } | ||
19 | |||
20 | function down (options) { | ||
21 | throw new Error('Not implemented.') | ||
22 | } | ||
23 | |||
24 | export { up, down } | ||
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index bf8e5b6c3..9327792fb 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -89,6 +89,23 @@ class Emailer { | |||
89 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | 89 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) |
90 | } | 90 | } |
91 | 91 | ||
92 | addVerifyEmailJob (to: string, verifyEmailUrl: string) { | ||
93 | const text = `Welcome to PeerTube,\n\n` + | ||
94 | `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` + | ||
95 | `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + | ||
96 | `If you are not the person who initiated this request, please ignore this email.\n\n` + | ||
97 | `Cheers,\n` + | ||
98 | `PeerTube.` | ||
99 | |||
100 | const emailPayload: EmailPayload = { | ||
101 | to: [ to ], | ||
102 | subject: 'Verify your PeerTube email', | ||
103 | text | ||
104 | } | ||
105 | |||
106 | return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) | ||
107 | } | ||
108 | |||
92 | async addVideoAbuseReportJob (videoId: number) { | 109 | async addVideoAbuseReportJob (videoId: number) { |
93 | const video = await VideoModel.load(videoId) | 110 | const video = await VideoModel.load(videoId) |
94 | if (!video) throw new Error('Unknown Video id during Abuse report.') | 111 | if (!video) throw new Error('Unknown Video id during Abuse report.') |
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 09eaf75d1..2f8667e19 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts | |||
@@ -3,6 +3,7 @@ import { logger } from '../helpers/logger' | |||
3 | import { UserModel } from '../models/account/user' | 3 | import { UserModel } from '../models/account/user' |
4 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 4 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
5 | import { OAuthTokenModel } from '../models/oauth/oauth-token' | 5 | import { OAuthTokenModel } from '../models/oauth/oauth-token' |
6 | import { CONFIG } from '../initializers/constants' | ||
6 | 7 | ||
7 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } | 8 | type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } |
8 | 9 | ||
@@ -37,6 +38,10 @@ async function getUser (usernameOrEmail: string, password: string) { | |||
37 | 38 | ||
38 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 39 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
39 | 40 | ||
41 | if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION && user.emailVerified === false) { | ||
42 | throw new AccessDeniedError('User email is not verified.') | ||
43 | } | ||
44 | |||
40 | return user | 45 | return user |
41 | } | 46 | } |
42 | 47 | ||
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index 0b4b41e4e..e4e435659 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import { createClient, RedisClient } from 'redis' | 2 | import { createClient, RedisClient } from 'redis' |
3 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
4 | import { generateRandomString } from '../helpers/utils' | 4 | import { generateRandomString } from '../helpers/utils' |
5 | import { CONFIG, USER_PASSWORD_RESET_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers' | 5 | import { CONFIG, USER_PASSWORD_RESET_LIFETIME, USER_EMAIL_VERIFY_LIFETIME, VIDEO_VIEW_LIFETIME } from '../initializers' |
6 | 6 | ||
7 | type CachedRoute = { | 7 | type CachedRoute = { |
8 | body: string, | 8 | body: string, |
@@ -60,6 +60,18 @@ class Redis { | |||
60 | return this.getValue(this.generateResetPasswordKey(userId)) | 60 | return this.getValue(this.generateResetPasswordKey(userId)) |
61 | } | 61 | } |
62 | 62 | ||
63 | async setVerifyEmailVerificationString (userId: number) { | ||
64 | const generatedString = await generateRandomString(32) | ||
65 | |||
66 | await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME) | ||
67 | |||
68 | return generatedString | ||
69 | } | ||
70 | |||
71 | async getVerifyEmailLink (userId: number) { | ||
72 | return this.getValue(this.generateVerifyEmailKey(userId)) | ||
73 | } | ||
74 | |||
63 | setIPVideoView (ip: string, videoUUID: string) { | 75 | setIPVideoView (ip: string, videoUUID: string) { |
64 | return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) | 76 | return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) |
65 | } | 77 | } |
@@ -135,6 +147,10 @@ class Redis { | |||
135 | return 'reset-password-' + userId | 147 | return 'reset-password-' + userId |
136 | } | 148 | } |
137 | 149 | ||
150 | generateVerifyEmailKey (userId: number) { | ||
151 | return 'verify-email-' + userId | ||
152 | } | ||
153 | |||
138 | buildViewKey (ip: string, videoUUID: string) { | 154 | buildViewKey (ip: string, videoUUID: string) { |
139 | return videoUUID + '-' + ip | 155 | return videoUUID + '-' + ip |
140 | } | 156 | } |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 6c5e783e9..a595c39ec 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -248,6 +248,48 @@ const usersResetPasswordValidator = [ | |||
248 | } | 248 | } |
249 | ] | 249 | ] |
250 | 250 | ||
251 | const usersAskSendVerifyEmailValidator = [ | ||
252 | body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'), | ||
253 | |||
254 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
255 | logger.debug('Checking askUsersSendVerifyEmail parameters', { parameters: req.body }) | ||
256 | |||
257 | if (areValidationErrors(req, res)) return | ||
258 | const exists = await checkUserEmailExist(req.body.email, res, false) | ||
259 | if (!exists) { | ||
260 | logger.debug('User with email %s does not exist (asking verify email).', req.body.email) | ||
261 | // Do not leak our emails | ||
262 | return res.status(204).end() | ||
263 | } | ||
264 | |||
265 | return next() | ||
266 | } | ||
267 | ] | ||
268 | |||
269 | const usersVerifyEmailValidator = [ | ||
270 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | ||
271 | body('verificationString').not().isEmpty().withMessage('Should have a valid verification string'), | ||
272 | |||
273 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
274 | logger.debug('Checking usersVerifyEmail parameters', { parameters: req.params }) | ||
275 | |||
276 | if (areValidationErrors(req, res)) return | ||
277 | if (!await checkUserIdExist(req.params.id, res)) return | ||
278 | |||
279 | const user = res.locals.user as UserModel | ||
280 | const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) | ||
281 | |||
282 | if (redisVerificationString !== req.body.verificationString) { | ||
283 | return res | ||
284 | .status(403) | ||
285 | .send({ error: 'Invalid verification string.' }) | ||
286 | .end() | ||
287 | } | ||
288 | |||
289 | return next() | ||
290 | } | ||
291 | ] | ||
292 | |||
251 | // --------------------------------------------------------------------------- | 293 | // --------------------------------------------------------------------------- |
252 | 294 | ||
253 | export { | 295 | export { |
@@ -263,7 +305,9 @@ export { | |||
263 | ensureUserRegistrationAllowedForIP, | 305 | ensureUserRegistrationAllowedForIP, |
264 | usersGetValidator, | 306 | usersGetValidator, |
265 | usersAskResetPasswordValidator, | 307 | usersAskResetPasswordValidator, |
266 | usersResetPasswordValidator | 308 | usersResetPasswordValidator, |
309 | usersAskSendVerifyEmailValidator, | ||
310 | usersVerifyEmailValidator | ||
267 | } | 311 | } |
268 | 312 | ||
269 | // --------------------------------------------------------------------------- | 313 | // --------------------------------------------------------------------------- |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index bae683b12..89265774b 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -24,6 +24,7 @@ import { | |||
24 | isUserBlockedReasonValid, | 24 | isUserBlockedReasonValid, |
25 | isUserBlockedValid, | 25 | isUserBlockedValid, |
26 | isUserNSFWPolicyValid, | 26 | isUserNSFWPolicyValid, |
27 | isUserEmailVerifiedValid, | ||
27 | isUserPasswordValid, | 28 | isUserPasswordValid, |
28 | isUserRoleValid, | 29 | isUserRoleValid, |
29 | isUserUsernameValid, | 30 | isUserUsernameValid, |
@@ -92,6 +93,12 @@ export class UserModel extends Model<UserModel> { | |||
92 | @Column(DataType.STRING(400)) | 93 | @Column(DataType.STRING(400)) |
93 | email: string | 94 | email: string |
94 | 95 | ||
96 | @AllowNull(true) | ||
97 | @Default(null) | ||
98 | @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean')) | ||
99 | @Column | ||
100 | emailVerified: boolean | ||
101 | |||
95 | @AllowNull(false) | 102 | @AllowNull(false) |
96 | @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) | 103 | @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) |
97 | @Column(DataType.ENUM(values(NSFW_POLICY_TYPES))) | 104 | @Column(DataType.ENUM(values(NSFW_POLICY_TYPES))) |
@@ -304,6 +311,7 @@ export class UserModel extends Model<UserModel> { | |||
304 | id: this.id, | 311 | id: this.id, |
305 | username: this.username, | 312 | username: this.username, |
306 | email: this.email, | 313 | email: this.email, |
314 | emailVerified: this.emailVerified, | ||
307 | nsfwPolicy: this.nsfwPolicy, | 315 | nsfwPolicy: this.nsfwPolicy, |
308 | autoPlayVideo: this.autoPlayVideo, | 316 | autoPlayVideo: this.autoPlayVideo, |
309 | role: this.role, | 317 | role: this.role, |
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index ecfb76d47..d807f910b 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -42,7 +42,8 @@ describe('Test config API validators', function () { | |||
42 | }, | 42 | }, |
43 | signup: { | 43 | signup: { |
44 | enabled: false, | 44 | enabled: false, |
45 | limit: 5 | 45 | limit: 5, |
46 | requiresEmailVerification: false | ||
46 | }, | 47 | }, |
47 | admin: { | 48 | admin: { |
48 | email: 'superadmin1@example.com' | 49 | email: 'superadmin1@example.com' |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 8b2ed1b04..95903c8a5 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -737,6 +737,28 @@ describe('Test users API validators', function () { | |||
737 | }) | 737 | }) |
738 | }) | 738 | }) |
739 | 739 | ||
740 | describe('When asking for an account verification email', function () { | ||
741 | const path = '/api/v1/users/ask-send-verify-email' | ||
742 | |||
743 | it('Should fail with a missing email', async function () { | ||
744 | const fields = {} | ||
745 | |||
746 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
747 | }) | ||
748 | |||
749 | it('Should fail with an invalid email', async function () { | ||
750 | const fields = { email: 'hello' } | ||
751 | |||
752 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) | ||
753 | }) | ||
754 | |||
755 | it('Should succeed with the correct params', async function () { | ||
756 | const fields = { email: 'admin@example.com' } | ||
757 | |||
758 | await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 204 }) | ||
759 | }) | ||
760 | }) | ||
761 | |||
740 | after(async function () { | 762 | after(async function () { |
741 | killallServers([ server, serverWithRegistrationDisabled ]) | 763 | killallServers([ server, serverWithRegistrationDisabled ]) |
742 | 764 | ||
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index ece4118a6..facd1688d 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -35,6 +35,7 @@ function checkInitialConfig (data: CustomConfig) { | |||
35 | expect(data.cache.captions.size).to.equal(1) | 35 | expect(data.cache.captions.size).to.equal(1) |
36 | expect(data.signup.enabled).to.be.true | 36 | expect(data.signup.enabled).to.be.true |
37 | expect(data.signup.limit).to.equal(4) | 37 | expect(data.signup.limit).to.equal(4) |
38 | expect(data.signup.requiresEmailVerification).to.be.false | ||
38 | expect(data.admin.email).to.equal('admin1@example.com') | 39 | expect(data.admin.email).to.equal('admin1@example.com') |
39 | expect(data.user.videoQuota).to.equal(5242880) | 40 | expect(data.user.videoQuota).to.equal(5242880) |
40 | expect(data.user.videoQuotaDaily).to.equal(-1) | 41 | expect(data.user.videoQuotaDaily).to.equal(-1) |
@@ -64,6 +65,7 @@ function checkUpdatedConfig (data: CustomConfig) { | |||
64 | expect(data.cache.captions.size).to.equal(3) | 65 | expect(data.cache.captions.size).to.equal(3) |
65 | expect(data.signup.enabled).to.be.false | 66 | expect(data.signup.enabled).to.be.false |
66 | expect(data.signup.limit).to.equal(5) | 67 | expect(data.signup.limit).to.equal(5) |
68 | expect(data.signup.requiresEmailVerification).to.be.true | ||
67 | expect(data.admin.email).to.equal('superadmin1@example.com') | 69 | expect(data.admin.email).to.equal('superadmin1@example.com') |
68 | expect(data.user.videoQuota).to.equal(5242881) | 70 | expect(data.user.videoQuota).to.equal(5242881) |
69 | expect(data.user.videoQuotaDaily).to.equal(318742) | 71 | expect(data.user.videoQuotaDaily).to.equal(318742) |
@@ -148,7 +150,8 @@ describe('Test config', function () { | |||
148 | }, | 150 | }, |
149 | signup: { | 151 | signup: { |
150 | enabled: false, | 152 | enabled: false, |
151 | limit: 5 | 153 | limit: 5, |
154 | requiresEmailVerification: true | ||
152 | }, | 155 | }, |
153 | admin: { | 156 | admin: { |
154 | email: 'superadmin1@example.com' | 157 | email: 'superadmin1@example.com' |
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts index db937f288..713a27143 100644 --- a/server/tests/api/server/email.ts +++ b/server/tests/api/server/email.ts | |||
@@ -5,6 +5,7 @@ import 'mocha' | |||
5 | import { | 5 | import { |
6 | addVideoToBlacklist, | 6 | addVideoToBlacklist, |
7 | askResetPassword, | 7 | askResetPassword, |
8 | askSendVerifyEmail, | ||
8 | blockUser, | 9 | blockUser, |
9 | createUser, removeVideoFromBlacklist, | 10 | createUser, removeVideoFromBlacklist, |
10 | reportVideoAbuse, | 11 | reportVideoAbuse, |
@@ -12,7 +13,8 @@ import { | |||
12 | runServer, | 13 | runServer, |
13 | unblockUser, | 14 | unblockUser, |
14 | uploadVideo, | 15 | uploadVideo, |
15 | userLogin | 16 | userLogin, |
17 | verifyEmail | ||
16 | } from '../../utils' | 18 | } from '../../utils' |
17 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' | 19 | import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index' |
18 | import { mockSmtpServer } from '../../utils/miscs/email' | 20 | import { mockSmtpServer } from '../../utils/miscs/email' |
@@ -207,6 +209,44 @@ describe('Test emails', function () { | |||
207 | }) | 209 | }) |
208 | }) | 210 | }) |
209 | 211 | ||
212 | describe('When verifying a user email', function () { | ||
213 | |||
214 | it('Should ask to send the verification email', async function () { | ||
215 | this.timeout(10000) | ||
216 | |||
217 | await askSendVerifyEmail(server.url, 'user_1@example.com') | ||
218 | |||
219 | await waitJobs(server) | ||
220 | expect(emails).to.have.lengthOf(7) | ||
221 | |||
222 | const email = emails[6] | ||
223 | |||
224 | expect(email['from'][0]['address']).equal('test-admin@localhost') | ||
225 | expect(email['to'][0]['address']).equal('user_1@example.com') | ||
226 | expect(email['subject']).contains('Verify') | ||
227 | |||
228 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
229 | expect(verificationStringMatches).not.to.be.null | ||
230 | |||
231 | verificationString = verificationStringMatches[1] | ||
232 | expect(verificationString).to.not.be.undefined | ||
233 | expect(verificationString).to.have.length.above(2) | ||
234 | |||
235 | const userIdMatches = /userId=([0-9]+)/.exec(email['text']) | ||
236 | expect(userIdMatches).not.to.be.null | ||
237 | |||
238 | userId = parseInt(userIdMatches[1], 10) | ||
239 | }) | ||
240 | |||
241 | it('Should not verify the email with an invalid verification string', async function () { | ||
242 | await verifyEmail(server.url, userId, verificationString + 'b', 403) | ||
243 | }) | ||
244 | |||
245 | it('Should verify the email', async function () { | ||
246 | await verifyEmail(server.url, userId, verificationString) | ||
247 | }) | ||
248 | }) | ||
249 | |||
210 | after(async function () { | 250 | after(async function () { |
211 | killallServers([ server ]) | 251 | killallServers([ server ]) |
212 | }) | 252 | }) |
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index 4ce87fb91..21d75da3e 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import './user-subscriptions' | 1 | import './user-subscriptions' |
2 | import './users' | 2 | import './users' |
3 | import './users-verification' | ||
3 | import './users-multiple-servers' | 4 | import './users-multiple-servers' |
diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts new file mode 100644 index 000000000..fa5f5e371 --- /dev/null +++ b/server/tests/api/users/users-verification.ts | |||
@@ -0,0 +1,133 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | registerUser, flushTests, getUserInformation, getMyUserInformation, killallServers, | ||
7 | userLogin, login, runServer, ServerInfo, verifyEmail, updateCustomSubConfig | ||
8 | } from '../../utils' | ||
9 | import { setAccessTokensToServers } from '../../utils/users/login' | ||
10 | import { mockSmtpServer } from '../../utils/miscs/email' | ||
11 | import { waitJobs } from '../../utils/server/jobs' | ||
12 | |||
13 | const expect = chai.expect | ||
14 | |||
15 | describe('Test users account verification', function () { | ||
16 | let server: ServerInfo | ||
17 | let userId: number | ||
18 | let verificationString: string | ||
19 | let expectedEmailsLength = 0 | ||
20 | const user1 = { | ||
21 | username: 'user_1', | ||
22 | password: 'super password' | ||
23 | } | ||
24 | const user2 = { | ||
25 | username: 'user_2', | ||
26 | password: 'super password' | ||
27 | } | ||
28 | const emails: object[] = [] | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(30000) | ||
32 | |||
33 | await mockSmtpServer(emails) | ||
34 | |||
35 | await flushTests() | ||
36 | |||
37 | const overrideConfig = { | ||
38 | smtp: { | ||
39 | hostname: 'localhost' | ||
40 | } | ||
41 | } | ||
42 | server = await runServer(1, overrideConfig) | ||
43 | |||
44 | await setAccessTokensToServers([ server ]) | ||
45 | }) | ||
46 | |||
47 | it('Should register user and send verification email if verification required', async function () { | ||
48 | this.timeout(5000) | ||
49 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
50 | signup: { | ||
51 | enabled: true, | ||
52 | requiresEmailVerification: true, | ||
53 | limit: 10 | ||
54 | } | ||
55 | }) | ||
56 | |||
57 | await registerUser(server.url, user1.username, user1.password) | ||
58 | |||
59 | await waitJobs(server) | ||
60 | expectedEmailsLength++ | ||
61 | expect(emails).to.have.lengthOf(expectedEmailsLength) | ||
62 | |||
63 | const email = emails[expectedEmailsLength - 1] | ||
64 | |||
65 | const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text']) | ||
66 | expect(verificationStringMatches).not.to.be.null | ||
67 | |||
68 | verificationString = verificationStringMatches[1] | ||
69 | expect(verificationString).to.have.length.above(2) | ||
70 | |||
71 | const userIdMatches = /userId=([0-9]+)/.exec(email['text']) | ||
72 | expect(userIdMatches).not.to.be.null | ||
73 | |||
74 | userId = parseInt(userIdMatches[1], 10) | ||
75 | |||
76 | const resUserInfo = await getUserInformation(server.url, server.accessToken, userId) | ||
77 | expect(resUserInfo.body.emailVerified).to.be.false | ||
78 | }) | ||
79 | |||
80 | it('Should not allow login for user with unverified email', async function () { | ||
81 | const resLogin = await login(server.url, server.client, user1, 400) | ||
82 | expect(resLogin.body.error).to.contain('User email is not verified.') | ||
83 | }) | ||
84 | |||
85 | it('Should verify the user via email and allow login', async function () { | ||
86 | await verifyEmail(server.url, userId, verificationString) | ||
87 | await login(server.url, server.client, user1) | ||
88 | const resUserVerified = await getUserInformation(server.url, server.accessToken, userId) | ||
89 | expect(resUserVerified.body.emailVerified).to.be.true | ||
90 | }) | ||
91 | |||
92 | it('Should register user not requiring email verification if setting not enabled', async function () { | ||
93 | this.timeout(5000) | ||
94 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
95 | signup: { | ||
96 | enabled: true, | ||
97 | requiresEmailVerification: false, | ||
98 | limit: 10 | ||
99 | } | ||
100 | }) | ||
101 | |||
102 | await registerUser(server.url, user2.username, user2.password) | ||
103 | |||
104 | await waitJobs(server) | ||
105 | expect(emails).to.have.lengthOf(expectedEmailsLength) | ||
106 | |||
107 | const accessToken = await userLogin(server, user2) | ||
108 | |||
109 | const resMyUserInfo = await getMyUserInformation(server.url, accessToken) | ||
110 | expect(resMyUserInfo.body.emailVerified).to.be.null | ||
111 | }) | ||
112 | |||
113 | it('Should allow login for user with unverified email when setting later enabled', async function () { | ||
114 | await updateCustomSubConfig(server.url, server.accessToken, { | ||
115 | signup: { | ||
116 | enabled: true, | ||
117 | requiresEmailVerification: true, | ||
118 | limit: 10 | ||
119 | } | ||
120 | }) | ||
121 | |||
122 | await userLogin(server, user2) | ||
123 | }) | ||
124 | |||
125 | after(async function () { | ||
126 | killallServers([ server ]) | ||
127 | |||
128 | // Keep the logs if the test failed | ||
129 | if (this[ 'ok' ]) { | ||
130 | await flushTests() | ||
131 | } | ||
132 | }) | ||
133 | }) | ||
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 04dcc8fd1..c0dd587ee 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating, | 7 | createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating, |
8 | getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, | 8 | getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, |
9 | registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin, | 9 | registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin, |
10 | deleteMe, blockUser, unblockUser | 10 | deleteMe, blockUser, unblockUser, updateCustomSubConfig |
11 | } from '../../utils/index' | 11 | } from '../../utils/index' |
12 | import { follow } from '../../utils/server/follows' | 12 | import { follow } from '../../utils/server/follows' |
13 | import { setAccessTokensToServers } from '../../utils/users/login' | 13 | import { setAccessTokensToServers } from '../../utils/users/login' |
diff --git a/server/tests/utils/server/config.ts b/server/tests/utils/server/config.ts index 799c31ae5..b85e02ab7 100644 --- a/server/tests/utils/server/config.ts +++ b/server/tests/utils/server/config.ts | |||
@@ -74,7 +74,8 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) { | |||
74 | }, | 74 | }, |
75 | signup: { | 75 | signup: { |
76 | enabled: false, | 76 | enabled: false, |
77 | limit: 5 | 77 | limit: 5, |
78 | requiresEmailVerification: false | ||
78 | }, | 79 | }, |
79 | admin: { | 80 | admin: { |
80 | email: 'superadmin1@example.com' | 81 | email: 'superadmin1@example.com' |
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index 5dba34b69..cd1b07701 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts | |||
@@ -246,6 +246,28 @@ function resetPassword (url: string, userId: number, verificationString: string, | |||
246 | }) | 246 | }) |
247 | } | 247 | } |
248 | 248 | ||
249 | function askSendVerifyEmail (url: string, email: string) { | ||
250 | const path = '/api/v1/users/ask-send-verify-email' | ||
251 | |||
252 | return makePostBodyRequest({ | ||
253 | url, | ||
254 | path, | ||
255 | fields: { email }, | ||
256 | statusCodeExpected: 204 | ||
257 | }) | ||
258 | } | ||
259 | |||
260 | function verifyEmail (url: string, userId: number, verificationString: string, statusCodeExpected = 204) { | ||
261 | const path = '/api/v1/users/' + userId + '/verify-email' | ||
262 | |||
263 | return makePostBodyRequest({ | ||
264 | url, | ||
265 | path, | ||
266 | fields: { verificationString }, | ||
267 | statusCodeExpected | ||
268 | }) | ||
269 | } | ||
270 | |||
249 | // --------------------------------------------------------------------------- | 271 | // --------------------------------------------------------------------------- |
250 | 272 | ||
251 | export { | 273 | export { |
@@ -265,5 +287,7 @@ export { | |||
265 | unblockUser, | 287 | unblockUser, |
266 | askResetPassword, | 288 | askResetPassword, |
267 | resetPassword, | 289 | resetPassword, |
268 | updateMyAvatar | 290 | updateMyAvatar, |
291 | askSendVerifyEmail, | ||
292 | verifyEmail | ||
269 | } | 293 | } |
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index 2f5cebf7f..3afd36fcd 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts | |||
@@ -34,6 +34,7 @@ export interface CustomConfig { | |||
34 | signup: { | 34 | signup: { |
35 | enabled: boolean | 35 | enabled: boolean |
36 | limit: number | 36 | limit: number |
37 | requiresEmailVerification: boolean | ||
37 | } | 38 | } |
38 | 39 | ||
39 | admin: { | 40 | admin: { |
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index 9bbeb14d2..e0ff8c07d 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts | |||
@@ -16,7 +16,8 @@ export interface ServerConfig { | |||
16 | 16 | ||
17 | signup: { | 17 | signup: { |
18 | allowed: boolean, | 18 | allowed: boolean, |
19 | allowedForCurrentIP: boolean | 19 | allowedForCurrentIP: boolean, |
20 | requiresEmailVerification: boolean | ||
20 | } | 21 | } |
21 | 22 | ||
22 | transcoding: { | 23 | transcoding: { |