aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html12
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts26
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts25
-rw-r--r--client/src/app/shared/users/user.model.ts2
-rw-r--r--client/src/app/shared/users/user.service.ts9
-rw-r--r--server/controllers/api/users/index.ts1
-rw-r--r--server/middlewares/validators/users.ts1
-rw-r--r--server/tests/api/check-params/users.ts9
-rw-r--r--server/tests/api/users/users.ts2
-rw-r--r--server/tests/utils/users/users.ts2
-rw-r--r--shared/models/users/user-update.model.ts1
-rw-r--r--shared/models/users/user.model.ts1
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 &#x2713; {{ 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 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { NotificationsService } from 'angular2-notifications'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { ConfirmService } from '../../../core' 4import { ConfirmService, ServerService } from '../../../core'
5import { RestPagination, RestTable, UserService } from '../../../shared' 5import { RestPagination, RestTable, UserService } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { User } from '../../../../../../shared' 7import { 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'
4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' 4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
5import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' 5import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
6import { UserService } from '@app/shared/users' 6import { UserService } from '@app/shared/users'
7import { AuthService, ConfirmService } from '@app/core' 7import { AuthService, ConfirmService, ServerService } from '@app/core'
8import { User, UserRight } from '../../../../../shared/models/users' 8import { User, UserRight } from '../../../../../shared/models/users'
9import { Account } from '@app/shared/account/account.model' 9import { Account } from '@app/shared/account/account.model'
10import { BlocklistService } from '@app/shared/blocklist' 10import { 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 = [
114const usersUpdateValidator = [ 114const 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
3export interface UserUpdate { 3export 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