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
This commit is contained in:
parent
04b8c3fba6
commit
fc2ec87a8c
|
@ -65,7 +65,17 @@
|
||||||
<span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
|
<span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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">
|
||||||
|
✓ {{ user.email }}
|
||||||
|
</td>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
<td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
|
<td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
|
||||||
<td>{{ user.roleLabel }}</td>
|
<td>{{ user.roleLabel }}</td>
|
||||||
<td>{{ user.createdAt }}</td>
|
<td>{{ user.createdAt }}</td>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||||
import { ConfirmService } from '../../../core'
|
import { ConfirmService, ServerService } from '../../../core'
|
||||||
import { RestPagination, RestTable, UserService } from '../../../shared'
|
import { RestPagination, RestTable, UserService } from '../../../shared'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { User } from '../../../../../../shared'
|
import { User } from '../../../../../../shared'
|
||||||
|
@ -28,12 +28,17 @@ export class UserListComponent extends RestTable implements OnInit {
|
||||||
constructor (
|
constructor (
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
|
private serverService: ServerService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get requiresEmailVerification () {
|
||||||
|
return this.serverService.getConfig().signup.requiresEmailVerification
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.initialize()
|
this.initialize()
|
||||||
|
|
||||||
|
@ -51,6 +56,11 @@ export class UserListComponent extends RestTable implements OnInit {
|
||||||
label: this.i18n('Unban'),
|
label: this.i18n('Unban'),
|
||||||
handler: users => this.unbanUsers(users),
|
handler: users => this.unbanUsers(users),
|
||||||
isDisplayed: users => users.every(u => u.blocked === true)
|
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 () {
|
isInSelectionMode () {
|
||||||
return this.selectedUsers.length !== 0
|
return this.selectedUsers.length !== 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
|
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
|
||||||
import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
|
import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
|
||||||
import { UserService } from '@app/shared/users'
|
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 { User, UserRight } from '../../../../../shared/models/users'
|
||||||
import { Account } from '@app/shared/account/account.model'
|
import { Account } from '@app/shared/account/account.model'
|
||||||
import { BlocklistService } from '@app/shared/blocklist'
|
import { BlocklistService } from '@app/shared/blocklist'
|
||||||
|
@ -32,11 +32,16 @@ export class UserModerationDropdownComponent implements OnChanges {
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
|
private serverService: ServerService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private blocklistService: BlocklistService,
|
private blocklistService: BlocklistService,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
get requiresEmailVerification () {
|
||||||
|
return this.serverService.getConfig().signup.requiresEmailVerification
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges () {
|
ngOnChanges () {
|
||||||
this.buildActions()
|
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) {
|
blockAccountByUser (account: Account) {
|
||||||
this.blocklistService.blockAccountByUser(account)
|
this.blocklistService.blockAccountByUser(account)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -264,6 +282,11 @@ export class UserModerationDropdownComponent implements OnChanges {
|
||||||
label: this.i18n('Unban'),
|
label: this.i18n('Unban'),
|
||||||
handler: ({ user }: { user: User }) => this.unbanUser(user),
|
handler: ({ user }: { user: User }) => this.unbanUser(user),
|
||||||
isDisplayed: ({ user }: { user: User }) => user.blocked
|
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
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export type UserConstructorHash = {
|
||||||
username: string,
|
username: string,
|
||||||
email: string,
|
email: string,
|
||||||
role: UserRole,
|
role: UserRole,
|
||||||
|
emailVerified?: boolean,
|
||||||
videoQuota?: number,
|
videoQuota?: number,
|
||||||
videoQuotaDaily?: number,
|
videoQuotaDaily?: number,
|
||||||
nsfwPolicy?: NSFWPolicyType,
|
nsfwPolicy?: NSFWPolicyType,
|
||||||
|
@ -31,6 +32,7 @@ export class User implements UserServerModel {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
|
emailVerified: boolean
|
||||||
role: UserRole
|
role: UserRole
|
||||||
nsfwPolicy: NSFWPolicyType
|
nsfwPolicy: NSFWPolicyType
|
||||||
webTorrentEnabled: boolean
|
webTorrentEnabled: boolean
|
||||||
|
|
|
@ -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) {
|
getUser (userId: number) {
|
||||||
return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId)
|
return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId)
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
|
|
|
@ -262,6 +262,7 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
|
||||||
const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
|
const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
|
||||||
|
|
||||||
if (body.email !== undefined) userToUpdate.email = body.email
|
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.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
|
||||||
if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
|
if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
|
||||||
if (body.role !== undefined) userToUpdate.role = body.role
|
if (body.role !== undefined) userToUpdate.role = body.role
|
||||||
|
|
|
@ -114,6 +114,7 @@ const deleteMeValidator = [
|
||||||
const usersUpdateValidator = [
|
const usersUpdateValidator = [
|
||||||
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
|
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||||
body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
|
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('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('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
|
||||||
body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'),
|
body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'),
|
||||||
|
|
|
@ -428,6 +428,14 @@ describe('Test users API validators', function () {
|
||||||
await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
|
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 () {
|
it('Should fail with an invalid videoQuota attribute', async function () {
|
||||||
const fields = {
|
const fields = {
|
||||||
videoQuota: -90
|
videoQuota: -90
|
||||||
|
@ -463,6 +471,7 @@ describe('Test users API validators', function () {
|
||||||
it('Should succeed with the correct params', async function () {
|
it('Should succeed with the correct params', async function () {
|
||||||
const fields = {
|
const fields = {
|
||||||
email: 'email@example.com',
|
email: 'email@example.com',
|
||||||
|
emailVerified: true,
|
||||||
videoQuota: 42,
|
videoQuota: 42,
|
||||||
role: UserRole.MODERATOR
|
role: UserRole.MODERATOR
|
||||||
}
|
}
|
||||||
|
|
|
@ -478,6 +478,7 @@ describe('Test users', function () {
|
||||||
userId,
|
userId,
|
||||||
accessToken,
|
accessToken,
|
||||||
email: 'updated2@example.com',
|
email: 'updated2@example.com',
|
||||||
|
emailVerified: true,
|
||||||
videoQuota: 42,
|
videoQuota: 42,
|
||||||
role: UserRole.MODERATOR
|
role: UserRole.MODERATOR
|
||||||
})
|
})
|
||||||
|
@ -487,6 +488,7 @@ describe('Test users', function () {
|
||||||
|
|
||||||
expect(user.username).to.equal('user_1')
|
expect(user.username).to.equal('user_1')
|
||||||
expect(user.email).to.equal('updated2@example.com')
|
expect(user.email).to.equal('updated2@example.com')
|
||||||
|
expect(user.emailVerified).to.be.true
|
||||||
expect(user.nsfwPolicy).to.equal('do_not_list')
|
expect(user.nsfwPolicy).to.equal('do_not_list')
|
||||||
expect(user.videoQuota).to.equal(42)
|
expect(user.videoQuota).to.equal(42)
|
||||||
expect(user.roleLabel).to.equal('Moderator')
|
expect(user.roleLabel).to.equal('Moderator')
|
||||||
|
|
|
@ -206,6 +206,7 @@ function updateUser (options: {
|
||||||
userId: number,
|
userId: number,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
email?: string,
|
email?: string,
|
||||||
|
emailVerified?: boolean,
|
||||||
videoQuota?: number,
|
videoQuota?: number,
|
||||||
videoQuotaDaily?: number,
|
videoQuotaDaily?: number,
|
||||||
role?: UserRole
|
role?: UserRole
|
||||||
|
@ -214,6 +215,7 @@ function updateUser (options: {
|
||||||
|
|
||||||
const toSend = {}
|
const toSend = {}
|
||||||
if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
|
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.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
|
||||||
if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
|
if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
|
||||||
if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
|
if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { UserRole } from './user-role'
|
||||||
|
|
||||||
export interface UserUpdate {
|
export interface UserUpdate {
|
||||||
email?: string
|
email?: string
|
||||||
|
emailVerified?: boolean
|
||||||
videoQuota?: number
|
videoQuota?: number
|
||||||
videoQuotaDaily?: number
|
videoQuotaDaily?: number
|
||||||
role?: UserRole
|
role?: UserRole
|
||||||
|
|
|
@ -7,6 +7,7 @@ export interface User {
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
|
emailVerified: boolean
|
||||||
nsfwPolicy: NSFWPolicyType
|
nsfwPolicy: NSFWPolicyType
|
||||||
autoPlayVideo: boolean
|
autoPlayVideo: boolean
|
||||||
role: UserRole
|
role: UserRole
|
||||||
|
|
Loading…
Reference in New Issue