diff options
author | Josh Morel <morel.josh@hotmail.com> | 2018-11-21 02:48:29 -0500 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-11-21 08:48:29 +0100 |
commit | fc2ec87a8c4dcfbb91a1a62cf4c07a2a8e6a50fe (patch) | |
tree | a2cea5e299c5f1a731da99277949ea2f0bb35659 | |
parent | 04b8c3fba614efc3827f583096c78b08cb668470 (diff) | |
download | PeerTube-fc2ec87a8c4dcfbb91a1a62cf4c07a2a8e6a50fe.tar.gz PeerTube-fc2ec87a8c4dcfbb91a1a62cf4c07a2a8e6a50fe.tar.zst PeerTube-fc2ec87a8c4dcfbb91a1a62cf4c07a2a8e6a50fe.zip |
enable email verification by admin (#1348)
* enable email verification by admin
* rename/label to set email as verified
to be more explicit that admin is not sending
another email to confirm
* add update user emailVerified check-params test
* make user.model emailVerified property required
-rw-r--r-- | client/src/app/+admin/users/user-list/user-list.component.html | 12 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-list/user-list.component.ts | 26 | ||||
-rw-r--r-- | client/src/app/shared/moderation/user-moderation-dropdown.component.ts | 25 | ||||
-rw-r--r-- | client/src/app/shared/users/user.model.ts | 2 | ||||
-rw-r--r-- | client/src/app/shared/users/user.service.ts | 9 | ||||
-rw-r--r-- | server/controllers/api/users/index.ts | 1 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 1 | ||||
-rw-r--r-- | server/tests/api/check-params/users.ts | 9 | ||||
-rw-r--r-- | server/tests/api/users/users.ts | 2 | ||||
-rw-r--r-- | server/tests/utils/users/users.ts | 2 | ||||
-rw-r--r-- | shared/models/users/user-update.model.ts | 1 | ||||
-rw-r--r-- | shared/models/users/user.model.ts | 1 |
12 files changed, 88 insertions, 3 deletions
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index 5684004a5..556ab3c5d 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -65,7 +65,17 @@ | |||
65 | <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span> | 65 | <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span> |
66 | </a> | 66 | </a> |
67 | </td> | 67 | </td> |
68 | <td>{{ user.email }}</td> | 68 | <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td> |
69 | <ng-template #emailWithVerificationStatus> | ||
70 | <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> | ||
71 | <em>? {{ user.email }}</em> | ||
72 | </td> | ||
73 | <ng-template #emailVerifiedNotFalse> | ||
74 | <td i18n-title title="User's email is verified / User can login without email verification"> | ||
75 | ✓ {{ user.email }} | ||
76 | </td> | ||
77 | </ng-template> | ||
78 | </ng-template> | ||
69 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> | 79 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> |
70 | <td>{{ user.roleLabel }}</td> | 80 | <td>{{ user.roleLabel }}</td> |
71 | <td>{{ user.createdAt }}</td> | 81 | <td>{{ user.createdAt }}</td> |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index 31e783622..fb085c133 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { NotificationsService } from 'angular2-notifications' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/components/common/sortmeta' |
4 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService, ServerService } from '../../../core' |
5 | import { RestPagination, RestTable, UserService } from '../../../shared' | 5 | import { RestPagination, RestTable, UserService } from '../../../shared' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { User } from '../../../../../../shared' | 7 | import { User } from '../../../../../../shared' |
@@ -28,12 +28,17 @@ export class UserListComponent extends RestTable implements OnInit { | |||
28 | constructor ( | 28 | constructor ( |
29 | private notificationsService: NotificationsService, | 29 | private notificationsService: NotificationsService, |
30 | private confirmService: ConfirmService, | 30 | private confirmService: ConfirmService, |
31 | private serverService: ServerService, | ||
31 | private userService: UserService, | 32 | private userService: UserService, |
32 | private i18n: I18n | 33 | private i18n: I18n |
33 | ) { | 34 | ) { |
34 | super() | 35 | super() |
35 | } | 36 | } |
36 | 37 | ||
38 | get requiresEmailVerification () { | ||
39 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
40 | } | ||
41 | |||
37 | ngOnInit () { | 42 | ngOnInit () { |
38 | this.initialize() | 43 | this.initialize() |
39 | 44 | ||
@@ -51,6 +56,11 @@ export class UserListComponent extends RestTable implements OnInit { | |||
51 | label: this.i18n('Unban'), | 56 | label: this.i18n('Unban'), |
52 | handler: users => this.unbanUsers(users), | 57 | handler: users => this.unbanUsers(users), |
53 | isDisplayed: users => users.every(u => u.blocked === true) | 58 | isDisplayed: users => users.every(u => u.blocked === true) |
59 | }, | ||
60 | { | ||
61 | label: this.i18n('Set Email as Verified'), | ||
62 | handler: users => this.setEmailsAsVerified(users), | ||
63 | isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false) | ||
54 | } | 64 | } |
55 | ] | 65 | ] |
56 | } | 66 | } |
@@ -114,6 +124,20 @@ export class UserListComponent extends RestTable implements OnInit { | |||
114 | ) | 124 | ) |
115 | } | 125 | } |
116 | 126 | ||
127 | async setEmailsAsVerified (users: User[]) { | ||
128 | this.userService.updateUsers(users, { emailVerified: true }).subscribe( | ||
129 | () => { | ||
130 | this.notificationsService.success( | ||
131 | this.i18n('Success'), | ||
132 | this.i18n('{{num}} users email set as verified.', { num: users.length }) | ||
133 | ) | ||
134 | this.loadData() | ||
135 | }, | ||
136 | |||
137 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
138 | ) | ||
139 | } | ||
140 | |||
117 | isInSelectionMode () { | 141 | isInSelectionMode () { |
118 | return this.selectedUsers.length !== 0 | 142 | return this.selectedUsers.length !== 0 |
119 | } | 143 | } |
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts index 908f0b8e0..460750740 100644 --- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts | |||
@@ -4,7 +4,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
4 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' | 4 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' |
5 | import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' | 5 | import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' |
6 | import { UserService } from '@app/shared/users' | 6 | import { UserService } from '@app/shared/users' |
7 | import { AuthService, ConfirmService } from '@app/core' | 7 | import { AuthService, ConfirmService, ServerService } from '@app/core' |
8 | import { User, UserRight } from '../../../../../shared/models/users' | 8 | import { User, UserRight } from '../../../../../shared/models/users' |
9 | import { Account } from '@app/shared/account/account.model' | 9 | import { Account } from '@app/shared/account/account.model' |
10 | import { BlocklistService } from '@app/shared/blocklist' | 10 | import { BlocklistService } from '@app/shared/blocklist' |
@@ -32,11 +32,16 @@ export class UserModerationDropdownComponent implements OnChanges { | |||
32 | private authService: AuthService, | 32 | private authService: AuthService, |
33 | private notificationsService: NotificationsService, | 33 | private notificationsService: NotificationsService, |
34 | private confirmService: ConfirmService, | 34 | private confirmService: ConfirmService, |
35 | private serverService: ServerService, | ||
35 | private userService: UserService, | 36 | private userService: UserService, |
36 | private blocklistService: BlocklistService, | 37 | private blocklistService: BlocklistService, |
37 | private i18n: I18n | 38 | private i18n: I18n |
38 | ) { } | 39 | ) { } |
39 | 40 | ||
41 | get requiresEmailVerification () { | ||
42 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
43 | } | ||
44 | |||
40 | ngOnChanges () { | 45 | ngOnChanges () { |
41 | this.buildActions() | 46 | this.buildActions() |
42 | } | 47 | } |
@@ -97,6 +102,19 @@ export class UserModerationDropdownComponent implements OnChanges { | |||
97 | ) | 102 | ) |
98 | } | 103 | } |
99 | 104 | ||
105 | setEmailAsVerified (user: User) { | ||
106 | this.userService.updateUser(user.id, { emailVerified: true }).subscribe( | ||
107 | () => { | ||
108 | this.notificationsService.success( | ||
109 | this.i18n('Success'), | ||
110 | this.i18n('User {{username}} email set as verified', { username: user.username }) | ||
111 | ) | ||
112 | }, | ||
113 | |||
114 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
115 | ) | ||
116 | } | ||
117 | |||
100 | blockAccountByUser (account: Account) { | 118 | blockAccountByUser (account: Account) { |
101 | this.blocklistService.blockAccountByUser(account) | 119 | this.blocklistService.blockAccountByUser(account) |
102 | .subscribe( | 120 | .subscribe( |
@@ -264,6 +282,11 @@ export class UserModerationDropdownComponent implements OnChanges { | |||
264 | label: this.i18n('Unban'), | 282 | label: this.i18n('Unban'), |
265 | handler: ({ user }: { user: User }) => this.unbanUser(user), | 283 | handler: ({ user }: { user: User }) => this.unbanUser(user), |
266 | isDisplayed: ({ user }: { user: User }) => user.blocked | 284 | isDisplayed: ({ user }: { user: User }) => user.blocked |
285 | }, | ||
286 | { | ||
287 | label: this.i18n('Set Email as Verified'), | ||
288 | handler: ({ user }: { user: User }) => this.setEmailAsVerified(user), | ||
289 | isDisplayed: ({ user }: { user: User }) => this.requiresEmailVerification && !user.blocked && user.emailVerified === false | ||
267 | } | 290 | } |
268 | ]) | 291 | ]) |
269 | } | 292 | } |
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 7c840ffa7..9819829fd 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -15,6 +15,7 @@ export type UserConstructorHash = { | |||
15 | username: string, | 15 | username: string, |
16 | email: string, | 16 | email: string, |
17 | role: UserRole, | 17 | role: UserRole, |
18 | emailVerified?: boolean, | ||
18 | videoQuota?: number, | 19 | videoQuota?: number, |
19 | videoQuotaDaily?: number, | 20 | videoQuotaDaily?: number, |
20 | nsfwPolicy?: NSFWPolicyType, | 21 | nsfwPolicy?: NSFWPolicyType, |
@@ -31,6 +32,7 @@ export class User implements UserServerModel { | |||
31 | id: number | 32 | id: number |
32 | username: string | 33 | username: string |
33 | email: string | 34 | email: string |
35 | emailVerified: boolean | ||
34 | role: UserRole | 36 | role: UserRole |
35 | nsfwPolicy: NSFWPolicyType | 37 | nsfwPolicy: NSFWPolicyType |
36 | webTorrentEnabled: boolean | 38 | webTorrentEnabled: boolean |
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index 27a81f0a2..cc5c051f1 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -153,6 +153,15 @@ export class UserService { | |||
153 | ) | 153 | ) |
154 | } | 154 | } |
155 | 155 | ||
156 | updateUsers (users: User[], userUpdate: UserUpdate) { | ||
157 | return from(users) | ||
158 | .pipe( | ||
159 | concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)), | ||
160 | toArray(), | ||
161 | catchError(err => this.restExtractor.handleError(err)) | ||
162 | ) | ||
163 | } | ||
164 | |||
156 | getUser (userId: number) { | 165 | getUser (userId: number) { |
157 | return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId) | 166 | return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId) |
158 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 167 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index 9fcb8077f..87fab4a40 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -262,6 +262,7 @@ async function updateUser (req: express.Request, res: express.Response, next: ex | |||
262 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role | 262 | const roleChanged = body.role !== undefined && body.role !== userToUpdate.role |
263 | 263 | ||
264 | if (body.email !== undefined) userToUpdate.email = body.email | 264 | if (body.email !== undefined) userToUpdate.email = body.email |
265 | if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified | ||
265 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota | 266 | if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota |
266 | if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily | 267 | if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily |
267 | if (body.role !== undefined) userToUpdate.role = body.role | 268 | if (body.role !== undefined) userToUpdate.role = body.role |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 61297120a..ccaf2eeb6 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -114,6 +114,7 @@ const deleteMeValidator = [ | |||
114 | const usersUpdateValidator = [ | 114 | const usersUpdateValidator = [ |
115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 115 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
116 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 116 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
117 | body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), | ||
117 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), | 118 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), |
118 | body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), | 119 | body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), |
119 | body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), | 120 | body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index ec46609a4..273be1679 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -428,6 +428,14 @@ describe('Test users API validators', function () { | |||
428 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | 428 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) |
429 | }) | 429 | }) |
430 | 430 | ||
431 | it('Should fail with an invalid emailVerified attribute', async function () { | ||
432 | const fields = { | ||
433 | emailVerified: 'yes' | ||
434 | } | ||
435 | |||
436 | await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) | ||
437 | }) | ||
438 | |||
431 | it('Should fail with an invalid videoQuota attribute', async function () { | 439 | it('Should fail with an invalid videoQuota attribute', async function () { |
432 | const fields = { | 440 | const fields = { |
433 | videoQuota: -90 | 441 | videoQuota: -90 |
@@ -463,6 +471,7 @@ describe('Test users API validators', function () { | |||
463 | it('Should succeed with the correct params', async function () { | 471 | it('Should succeed with the correct params', async function () { |
464 | const fields = { | 472 | const fields = { |
465 | email: 'email@example.com', | 473 | email: 'email@example.com', |
474 | emailVerified: true, | ||
466 | videoQuota: 42, | 475 | videoQuota: 42, |
467 | role: UserRole.MODERATOR | 476 | role: UserRole.MODERATOR |
468 | } | 477 | } |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 513bca8a0..e7bb845b9 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -478,6 +478,7 @@ describe('Test users', function () { | |||
478 | userId, | 478 | userId, |
479 | accessToken, | 479 | accessToken, |
480 | email: 'updated2@example.com', | 480 | email: 'updated2@example.com', |
481 | emailVerified: true, | ||
481 | videoQuota: 42, | 482 | videoQuota: 42, |
482 | role: UserRole.MODERATOR | 483 | role: UserRole.MODERATOR |
483 | }) | 484 | }) |
@@ -487,6 +488,7 @@ describe('Test users', function () { | |||
487 | 488 | ||
488 | expect(user.username).to.equal('user_1') | 489 | expect(user.username).to.equal('user_1') |
489 | expect(user.email).to.equal('updated2@example.com') | 490 | expect(user.email).to.equal('updated2@example.com') |
491 | expect(user.emailVerified).to.be.true | ||
490 | expect(user.nsfwPolicy).to.equal('do_not_list') | 492 | expect(user.nsfwPolicy).to.equal('do_not_list') |
491 | expect(user.videoQuota).to.equal(42) | 493 | expect(user.videoQuota).to.equal(42) |
492 | expect(user.roleLabel).to.equal('Moderator') | 494 | expect(user.roleLabel).to.equal('Moderator') |
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index 2c21a9ecf..f12992315 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts | |||
@@ -206,6 +206,7 @@ function updateUser (options: { | |||
206 | userId: number, | 206 | userId: number, |
207 | accessToken: string, | 207 | accessToken: string, |
208 | email?: string, | 208 | email?: string, |
209 | emailVerified?: boolean, | ||
209 | videoQuota?: number, | 210 | videoQuota?: number, |
210 | videoQuotaDaily?: number, | 211 | videoQuotaDaily?: number, |
211 | role?: UserRole | 212 | role?: UserRole |
@@ -214,6 +215,7 @@ function updateUser (options: { | |||
214 | 215 | ||
215 | const toSend = {} | 216 | const toSend = {} |
216 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email | 217 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email |
218 | if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified | ||
217 | if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota | 219 | if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota |
218 | if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily | 220 | if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily |
219 | if (options.role !== undefined && options.role !== null) toSend['role'] = options.role | 221 | if (options.role !== undefined && options.role !== null) toSend['role'] = options.role |
diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts index ce866fb18..abde51321 100644 --- a/shared/models/users/user-update.model.ts +++ b/shared/models/users/user-update.model.ts | |||
@@ -2,6 +2,7 @@ import { UserRole } from './user-role' | |||
2 | 2 | ||
3 | export interface UserUpdate { | 3 | export interface UserUpdate { |
4 | email?: string | 4 | email?: string |
5 | emailVerified?: boolean | ||
5 | videoQuota?: number | 6 | videoQuota?: number |
6 | videoQuotaDaily?: number | 7 | videoQuotaDaily?: number |
7 | role?: UserRole | 8 | role?: UserRole |
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 8147dc48e..82af17516 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -7,6 +7,7 @@ export interface User { | |||
7 | id: number | 7 | id: number |
8 | username: string | 8 | username: string |
9 | email: string | 9 | email: string |
10 | emailVerified: boolean | ||
10 | nsfwPolicy: NSFWPolicyType | 11 | nsfwPolicy: NSFWPolicyType |
11 | autoPlayVideo: boolean | 12 | autoPlayVideo: boolean |
12 | role: UserRole | 13 | role: UserRole |