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 @@
(banned)
-
{{ user.email }} |
+ {{ user.email }} |
+
+
+ ? {{ user.email }}
+ |
+
+
+ ✓ {{ user.email }}
+ |
+
+
{{ user.videoQuotaUsed }} / {{ user.videoQuota }} |
{{ user.roleLabel }} |
{{ user.createdAt }} |
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 @@
import { Component, OnInit, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/components/common/sortmeta'
-import { ConfirmService } from '../../../core'
+import { ConfirmService, ServerService } from '../../../core'
import { RestPagination, RestTable, UserService } from '../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { User } from '../../../../../../shared'
@@ -28,12 +28,17 @@ export class UserListComponent extends RestTable implements OnInit {
constructor (
private notificationsService: NotificationsService,
private confirmService: ConfirmService,
+ private serverService: ServerService,
private userService: UserService,
private i18n: I18n
) {
super()
}
+ get requiresEmailVerification () {
+ return this.serverService.getConfig().signup.requiresEmailVerification
+ }
+
ngOnInit () {
this.initialize()
@@ -51,6 +56,11 @@ export class UserListComponent extends RestTable implements OnInit {
label: this.i18n('Unban'),
handler: users => this.unbanUsers(users),
isDisplayed: users => users.every(u => u.blocked === true)
+ },
+ {
+ label: this.i18n('Set Email as Verified'),
+ handler: users => this.setEmailsAsVerified(users),
+ isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false)
}
]
}
@@ -114,6 +124,20 @@ export class UserListComponent extends RestTable implements OnInit {
)
}
+ async setEmailsAsVerified (users: User[]) {
+ this.userService.updateUsers(users, { emailVerified: true }).subscribe(
+ () => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('{{num}} users email set as verified.', { num: users.length })
+ )
+ this.loadData()
+ },
+
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
+ )
+ }
+
isInSelectionMode () {
return this.selectedUsers.length !== 0
}
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'
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
import { UserService } from '@app/shared/users'
-import { AuthService, ConfirmService } from '@app/core'
+import { AuthService, ConfirmService, ServerService } from '@app/core'
import { User, UserRight } from '../../../../../shared/models/users'
import { Account } from '@app/shared/account/account.model'
import { BlocklistService } from '@app/shared/blocklist'
@@ -32,11 +32,16 @@ export class UserModerationDropdownComponent implements OnChanges {
private authService: AuthService,
private notificationsService: NotificationsService,
private confirmService: ConfirmService,
+ private serverService: ServerService,
private userService: UserService,
private blocklistService: BlocklistService,
private i18n: I18n
) { }
+ get requiresEmailVerification () {
+ return this.serverService.getConfig().signup.requiresEmailVerification
+ }
+
ngOnChanges () {
this.buildActions()
}
@@ -97,6 +102,19 @@ export class UserModerationDropdownComponent implements OnChanges {
)
}
+ setEmailAsVerified (user: User) {
+ this.userService.updateUser(user.id, { emailVerified: true }).subscribe(
+ () => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('User {{username}} email set as verified', { username: user.username })
+ )
+ },
+
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
+ )
+ }
+
blockAccountByUser (account: Account) {
this.blocklistService.blockAccountByUser(account)
.subscribe(
@@ -264,6 +282,11 @@ export class UserModerationDropdownComponent implements OnChanges {
label: this.i18n('Unban'),
handler: ({ user }: { user: User }) => this.unbanUser(user),
isDisplayed: ({ user }: { user: User }) => user.blocked
+ },
+ {
+ label: this.i18n('Set Email as Verified'),
+ handler: ({ user }: { user: User }) => this.setEmailAsVerified(user),
+ isDisplayed: ({ user }: { user: User }) => this.requiresEmailVerification && !user.blocked && user.emailVerified === false
}
])
}
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 = {
username: string,
email: string,
role: UserRole,
+ emailVerified?: boolean,
videoQuota?: number,
videoQuotaDaily?: number,
nsfwPolicy?: NSFWPolicyType,
@@ -31,6 +32,7 @@ export class User implements UserServerModel {
id: number
username: string
email: string
+ emailVerified: boolean
role: UserRole
nsfwPolicy: NSFWPolicyType
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 {
)
}
+ updateUsers (users: User[], userUpdate: UserUpdate) {
+ return from(users)
+ .pipe(
+ concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)),
+ toArray(),
+ catchError(err => this.restExtractor.handleError(err))
+ )
+ }
+
getUser (userId: number) {
return this.authHttp.get(UserService.BASE_USERS_URL + userId)
.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
const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
if (body.email !== undefined) userToUpdate.email = body.email
+ if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified
if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
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 = [
const usersUpdateValidator = [
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
+ body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
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 () {
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
})
+ it('Should fail with an invalid emailVerified attribute', async function () {
+ const fields = {
+ emailVerified: 'yes'
+ }
+
+ await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
+ })
+
it('Should fail with an invalid videoQuota attribute', async function () {
const fields = {
videoQuota: -90
@@ -463,6 +471,7 @@ describe('Test users API validators', function () {
it('Should succeed with the correct params', async function () {
const fields = {
email: 'email@example.com',
+ emailVerified: true,
videoQuota: 42,
role: UserRole.MODERATOR
}
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 () {
userId,
accessToken,
email: 'updated2@example.com',
+ emailVerified: true,
videoQuota: 42,
role: UserRole.MODERATOR
})
@@ -487,6 +488,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1')
expect(user.email).to.equal('updated2@example.com')
+ expect(user.emailVerified).to.be.true
expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(42)
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: {
userId: number,
accessToken: string,
email?: string,
+ emailVerified?: boolean,
videoQuota?: number,
videoQuotaDaily?: number,
role?: UserRole
@@ -214,6 +215,7 @@ function updateUser (options: {
const toSend = {}
if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
+ if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
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'
export interface UserUpdate {
email?: string
+ emailVerified?: boolean
videoQuota?: number
videoQuotaDaily?: number
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 {
id: number
username: string
email: string
+ emailVerified: boolean
nsfwPolicy: NSFWPolicyType
autoPlayVideo: boolean
role: UserRole