]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
enable email verification by admin (#1348)
authorJosh Morel <morel.josh@hotmail.com>
Wed, 21 Nov 2018 07:48:29 +0000 (02:48 -0500)
committerChocobozzz <me@florianbigard.com>
Wed, 21 Nov 2018 07:48:29 +0000 (08:48 +0100)
* 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

12 files changed:
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+admin/users/user-list/user-list.component.ts
client/src/app/shared/moderation/user-moderation-dropdown.component.ts
client/src/app/shared/users/user.model.ts
client/src/app/shared/users/user.service.ts
server/controllers/api/users/index.ts
server/middlewares/validators/users.ts
server/tests/api/check-params/users.ts
server/tests/api/users/users.ts
server/tests/utils/users/users.ts
shared/models/users/user-update.model.ts
shared/models/users/user.model.ts

index 5684004a5fb7ff65c47a99197573ec4f1ef41e37..556ab3c5db3a8bd9521d55e1a77392e0ba403237 100644 (file)
           <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
         </a>
       </td>
-      <td>{{ user.email }}</td>
+      <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
+      <ng-template #emailWithVerificationStatus>
+        <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
+          <em>? {{ user.email }}</em>
+        </td>
+        <ng-template #emailVerifiedNotFalse>
+          <td i18n-title title="User's email is verified / User can login without email verification">
+            &#x2713; {{ user.email }}
+          </td>
+        </ng-template>
+      </ng-template>
       <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
       <td>{{ user.roleLabel }}</td>
       <td>{{ user.createdAt }}</td>
index 31e783622913fdf2b7e12f7e6d23b4bfc043e4f3..fb085c1331f6f1a30261f175d8bffeaabdfa91da 100644 (file)
@@ -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
   }
index 908f0b8e08aee8c8bfbcc980f82739e8c4294d6f..4607507402e700937964ec311a9f06df266bfd9e 100644 (file)
@@ -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
           }
         ])
       }
index 7c840ffa7054c9109bff031e148a91a18ec988e1..9819829fd1c6e91827726c5d087d1c123ab0c0ba 100644 (file)
@@ -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
index 27a81f0a249e68bf0f1ed80672f2cade52b2e3c0..cc5c051f173b040386ba9897a6541d0154da0c04 100644 (file)
@@ -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<User>(UserService.BASE_USERS_URL + userId)
                .pipe(catchError(err => this.restExtractor.handleError(err)))
index 9fcb8077f4e451c7ff14ec34c5de8526edcb220b..87fab4a407b966b1ec068e0f06667ddefbd18cd5 100644 (file)
@@ -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
index 61297120ac80de42caaeff795f2c67f5dd05dc50..ccaf2eeb6f24bfe59f809ea77d3a0220f7fa7d64 100644 (file)
@@ -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'),
index ec46609a4c2f5f0ff0e84373cb8a6dc4463625b3..273be1679f3e2a1fc417f5d423798270266a9a70 100644 (file)
@@ -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
       }
index 513bca8a06e58f95c658a8eb84689979671e9568..e7bb845b9bf7f3117c35ebaa5994f7a680bcc24f 100644 (file)
@@ -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')
index 2c21a9ecf9c5c3ea4e41778eec9d6b5f12886c71..f129923157102b19f090478c5318181e7ee94679 100644 (file)
@@ -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
index ce866fb186a909e9496b858307d78dd8637417ef..abde513212e21f047158272e1b31b7ce416056c0 100644 (file)
@@ -2,6 +2,7 @@ import { UserRole } from './user-role'
 
 export interface UserUpdate {
   email?: string
+  emailVerified?: boolean
   videoQuota?: number
   videoQuotaDaily?: number
   role?: UserRole
index 8147dc48e8c6eedec90f89cf09eb845dc7482ab5..82af175160a787cf28df67059924b5f3ba057c35 100644 (file)
@@ -7,6 +7,7 @@ export interface User {
   id: number
   username: string
   email: string
+  emailVerified: boolean
   nsfwPolicy: NSFWPolicyType
   autoPlayVideo: boolean
   role: UserRole