From fc2ec87a8c4dcfbb91a1a62cf4c07a2a8e6a50fe Mon Sep 17 00:00:00 2001 From: Josh Morel Date: Wed, 21 Nov 2018 02:48:29 -0500 Subject: [PATCH] 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 --- .../users/user-list/user-list.component.html | 12 ++++++++- .../users/user-list/user-list.component.ts | 26 ++++++++++++++++++- .../user-moderation-dropdown.component.ts | 25 +++++++++++++++++- client/src/app/shared/users/user.model.ts | 2 ++ client/src/app/shared/users/user.service.ts | 9 +++++++ server/controllers/api/users/index.ts | 1 + server/middlewares/validators/users.ts | 1 + server/tests/api/check-params/users.ts | 9 +++++++ server/tests/api/users/users.ts | 2 ++ server/tests/utils/users/users.ts | 2 ++ shared/models/users/user-update.model.ts | 1 + 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 @@ (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