Add reason when banning a user
This commit is contained in:
parent
a6ce68673a
commit
eacb25c436
|
@ -30,8 +30,9 @@
|
||||||
<td>{{ user.roleLabel }}</td>
|
<td>{{ user.roleLabel }}</td>
|
||||||
<td>{{ user.createdAt }}</td>
|
<td>{{ user.createdAt }}</td>
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>
|
<my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user"></my-action-dropdown>
|
||||||
<my-delete-button (click)="removeUser(user)"></my-delete-button>
|
<!--<my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>-->
|
||||||
|
<!--<my-delete-button (click)="removeUser(user)"></my-delete-button>-->
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ConfirmService } from '../../../core'
|
||||||
import { RestPagination, RestTable, User } from '../../../shared'
|
import { RestPagination, RestTable, User } from '../../../shared'
|
||||||
import { UserService } from '../shared'
|
import { UserService } from '../shared'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-user-list',
|
selector: 'my-user-list',
|
||||||
|
@ -17,6 +18,7 @@ export class UserListComponent extends RestTable implements OnInit {
|
||||||
rowsPerPage = 10
|
rowsPerPage = 10
|
||||||
sort: SortMeta = { field: 'createdAt', order: 1 }
|
sort: SortMeta = { field: 'createdAt', order: 1 }
|
||||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||||
|
userActions: DropdownAction<User>[] = []
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
|
@ -25,6 +27,17 @@ export class UserListComponent extends RestTable implements OnInit {
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
this.userActions = [
|
||||||
|
{
|
||||||
|
type: 'edit',
|
||||||
|
linkBuilder: this.getRouterUserEditLink
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'delete',
|
||||||
|
handler: user => this.removeUser(user)
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<div class="dropdown-root" dropdown container="body" dropup="true" placement="right" role="button">
|
||||||
|
<div class="action-button" dropdownToggle>
|
||||||
|
<span class="icon icon-action"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button">
|
||||||
|
<li role="menuitem" *ngFor="let action of actions">
|
||||||
|
<my-delete-button *ngIf="action.type === 'delete'" [label]="action.label" (click)="action.handler(entry)"></my-delete-button>
|
||||||
|
<my-edit-button *ngIf="action.type === 'edit'" [label]="action.label" [routerLink]="action.linkBuilder(entry)"></my-edit-button>
|
||||||
|
|
||||||
|
<a *ngIf="action.type === 'custom'" class="dropdown-item" href="#" (click)="action.handler(entry)">
|
||||||
|
<span *ngIf="action.iconClass" class="icon" [ngClass]="action.iconClass"></span> <ng-container>{{ action.label }}</ng-container>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
|
@ -0,0 +1,21 @@
|
||||||
|
@import '_variables';
|
||||||
|
@import '_mixins';
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
@include peertube-button;
|
||||||
|
@include grey-button;
|
||||||
|
|
||||||
|
&:hover, &:active, &:focus {
|
||||||
|
background-color: $grey-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
.icon-action {
|
||||||
|
@include icon(21px);
|
||||||
|
|
||||||
|
background-image: url('../../../assets/images/video/more.svg');
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
|
||||||
|
export type DropdownAction<T> = {
|
||||||
|
type: 'custom' | 'delete' | 'edit'
|
||||||
|
label?: string
|
||||||
|
handler?: (T) => any
|
||||||
|
linkBuilder?: (T) => (string | number)[]
|
||||||
|
iconClass?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-action-dropdown',
|
||||||
|
styleUrls: [ './action-dropdown.component.scss' ],
|
||||||
|
templateUrl: './action-dropdown.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
export class ActionDropdownComponent<T> {
|
||||||
|
@Input() actions: DropdownAction<T>[] = []
|
||||||
|
@Input() entry: T
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<span class="action-button action-button-delete" [title]="label" role="button">
|
||||||
|
<span class="icon icon-delete-grey"></span>
|
||||||
|
|
||||||
|
<span class="button-label" *ngIf="label">{{ label }}</span>
|
||||||
|
<span class="button-label" i18n *ngIf="!label">Delete</span>
|
||||||
|
</span>
|
|
@ -7,5 +7,5 @@ import { Component, Input } from '@angular/core'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class DeleteButtonComponent {
|
export class DeleteButtonComponent {
|
||||||
@Input() label = 'Delete'
|
@Input() label: string
|
||||||
}
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
<a class="action-button action-button-edit" [routerLink]="routerLink" title="Edit">
|
<a class="action-button action-button-edit" [routerLink]="routerLink" title="Edit">
|
||||||
<span class="icon icon-edit"></span>
|
<span class="icon icon-edit"></span>
|
||||||
<span i18n class="button-label">Edit</span>
|
|
||||||
|
<span class="button-label" *ngIf="label">{{ label }}</span>
|
||||||
|
<span i18n class="button-label" *ngIf="!label">Edit</span>
|
||||||
</a>
|
</a>
|
|
@ -7,5 +7,6 @@ import { Component, Input } from '@angular/core'
|
||||||
})
|
})
|
||||||
|
|
||||||
export class EditButtonComponent {
|
export class EditButtonComponent {
|
||||||
|
@Input() label: string
|
||||||
@Input() routerLink = []
|
@Input() routerLink = []
|
||||||
}
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
<span class="action-button action-button-delete" [title]="label">
|
|
||||||
<span class="icon icon-delete-grey"></span>
|
|
||||||
<span class="button-label">{{ label }}</span>
|
|
||||||
</span>
|
|
|
@ -17,8 +17,8 @@ import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
|
||||||
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
|
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
|
||||||
|
|
||||||
import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
|
import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
|
||||||
import { DeleteButtonComponent } from './misc/delete-button.component'
|
import { DeleteButtonComponent } from './buttons/delete-button.component'
|
||||||
import { EditButtonComponent } from './misc/edit-button.component'
|
import { EditButtonComponent } from './buttons/edit-button.component'
|
||||||
import { FromNowPipe } from './misc/from-now.pipe'
|
import { FromNowPipe } from './misc/from-now.pipe'
|
||||||
import { LoaderComponent } from './misc/loader.component'
|
import { LoaderComponent } from './misc/loader.component'
|
||||||
import { NumberFormatterPipe } from './misc/number-formatter.pipe'
|
import { NumberFormatterPipe } from './misc/number-formatter.pipe'
|
||||||
|
@ -52,6 +52,7 @@ import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validator
|
||||||
import { VideoCaptionService } from '@app/shared/video-caption'
|
import { VideoCaptionService } from '@app/shared/video-caption'
|
||||||
import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component'
|
import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component'
|
||||||
import { VideoImportService } from '@app/shared/video-import/video-import.service'
|
import { VideoImportService } from '@app/shared/video-import/video-import.service'
|
||||||
|
import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -78,6 +79,7 @@ import { VideoImportService } from '@app/shared/video-import/video-import.servic
|
||||||
VideoFeedComponent,
|
VideoFeedComponent,
|
||||||
DeleteButtonComponent,
|
DeleteButtonComponent,
|
||||||
EditButtonComponent,
|
EditButtonComponent,
|
||||||
|
ActionDropdownComponent,
|
||||||
NumberFormatterPipe,
|
NumberFormatterPipe,
|
||||||
ObjectLengthPipe,
|
ObjectLengthPipe,
|
||||||
FromNowPipe,
|
FromNowPipe,
|
||||||
|
@ -110,6 +112,7 @@ import { VideoImportService } from '@app/shared/video-import/video-import.servic
|
||||||
VideoFeedComponent,
|
VideoFeedComponent,
|
||||||
DeleteButtonComponent,
|
DeleteButtonComponent,
|
||||||
EditButtonComponent,
|
EditButtonComponent,
|
||||||
|
ActionDropdownComponent,
|
||||||
MarkdownTextareaComponent,
|
MarkdownTextareaComponent,
|
||||||
InfiniteScrollerDirective,
|
InfiniteScrollerDirective,
|
||||||
HelpComponent,
|
HelpComponent,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
VideoChannel
|
VideoChannel
|
||||||
} from '../../../../../shared'
|
} from '../../../../../shared'
|
||||||
import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
|
import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
|
||||||
import { Actor } from '@app/shared/actor/actor.model'
|
|
||||||
import { Account } from '@app/shared/account/account.model'
|
import { Account } from '@app/shared/account/account.model'
|
||||||
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
|
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
|
||||||
|
|
||||||
|
@ -22,6 +21,9 @@ export type UserConstructorHash = {
|
||||||
createdAt?: Date,
|
createdAt?: Date,
|
||||||
account?: AccountServerModel,
|
account?: AccountServerModel,
|
||||||
videoChannels?: VideoChannel[]
|
videoChannels?: VideoChannel[]
|
||||||
|
|
||||||
|
blocked?: boolean
|
||||||
|
blockedReason?: string
|
||||||
}
|
}
|
||||||
export class User implements UserServerModel {
|
export class User implements UserServerModel {
|
||||||
id: number
|
id: number
|
||||||
|
@ -35,35 +37,26 @@ export class User implements UserServerModel {
|
||||||
videoChannels: VideoChannel[]
|
videoChannels: VideoChannel[]
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
blocked: boolean
|
||||||
|
blockedReason?: string
|
||||||
|
|
||||||
constructor (hash: UserConstructorHash) {
|
constructor (hash: UserConstructorHash) {
|
||||||
this.id = hash.id
|
this.id = hash.id
|
||||||
this.username = hash.username
|
this.username = hash.username
|
||||||
this.email = hash.email
|
this.email = hash.email
|
||||||
this.role = hash.role
|
this.role = hash.role
|
||||||
|
|
||||||
|
this.videoChannels = hash.videoChannels
|
||||||
|
this.videoQuota = hash.videoQuota
|
||||||
|
this.nsfwPolicy = hash.nsfwPolicy
|
||||||
|
this.autoPlayVideo = hash.autoPlayVideo
|
||||||
|
this.createdAt = hash.createdAt
|
||||||
|
this.blocked = hash.blocked
|
||||||
|
this.blockedReason = hash.blockedReason
|
||||||
|
|
||||||
if (hash.account !== undefined) {
|
if (hash.account !== undefined) {
|
||||||
this.account = new Account(hash.account)
|
this.account = new Account(hash.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hash.videoChannels !== undefined) {
|
|
||||||
this.videoChannels = hash.videoChannels
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hash.videoQuota !== undefined) {
|
|
||||||
this.videoQuota = hash.videoQuota
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hash.nsfwPolicy !== undefined) {
|
|
||||||
this.nsfwPolicy = hash.nsfwPolicy
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hash.autoPlayVideo !== undefined) {
|
|
||||||
this.autoPlayVideo = hash.autoPlayVideo
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hash.createdAt !== undefined) {
|
|
||||||
this.createdAt = hash.createdAt
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get accountAvatarUrl () {
|
get accountAvatarUrl () {
|
||||||
|
|
|
@ -302,8 +302,9 @@ async function unblockUser (req: express.Request, res: express.Response, next: e
|
||||||
|
|
||||||
async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const user: UserModel = res.locals.user
|
const user: UserModel = res.locals.user
|
||||||
|
const reason = req.body.reason
|
||||||
|
|
||||||
await changeUserBlock(res, user, true)
|
await changeUserBlock(res, user, true, reason)
|
||||||
|
|
||||||
return res.status(204).end()
|
return res.status(204).end()
|
||||||
}
|
}
|
||||||
|
@ -454,10 +455,11 @@ function success (req: express.Request, res: express.Response, next: express.Nex
|
||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeUserBlock (res: express.Response, user: UserModel, block: boolean) {
|
async function changeUserBlock (res: express.Response, user: UserModel, block: boolean, reason?: string) {
|
||||||
const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
|
const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
|
||||||
|
|
||||||
user.blocked = block
|
user.blocked = block
|
||||||
|
user.blockedReason = reason || null
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
await OAuthTokenModel.deleteUserToken(user.id, t)
|
await OAuthTokenModel.deleteUserToken(user.id, t)
|
||||||
|
@ -465,6 +467,8 @@ async function changeUserBlock (res: express.Response, user: UserModel, block: b
|
||||||
await user.save({ transaction: t })
|
await user.save({ transaction: t })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await Emailer.Instance.addUserBlockJob(user, block, reason)
|
||||||
|
|
||||||
auditLogger.update(
|
auditLogger.update(
|
||||||
res.locals.oauth.token.User.Account.Actor.getIdentifier(),
|
res.locals.oauth.token.User.Account.Actor.getIdentifier(),
|
||||||
new UserAuditView(user.toFormattedJSON()),
|
new UserAuditView(user.toFormattedJSON()),
|
||||||
|
|
|
@ -42,6 +42,10 @@ function isUserBlockedValid (value: any) {
|
||||||
return isBooleanValid(value)
|
return isBooleanValid(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUserBlockedReasonValid (value: any) {
|
||||||
|
return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
|
||||||
|
}
|
||||||
|
|
||||||
function isUserRoleValid (value: any) {
|
function isUserRoleValid (value: any) {
|
||||||
return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
|
return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
|
||||||
}
|
}
|
||||||
|
@ -59,6 +63,7 @@ function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } |
|
||||||
export {
|
export {
|
||||||
isUserBlockedValid,
|
isUserBlockedValid,
|
||||||
isUserPasswordValid,
|
isUserPasswordValid,
|
||||||
|
isUserBlockedReasonValid,
|
||||||
isUserRoleValid,
|
isUserRoleValid,
|
||||||
isUserVideoQuotaValid,
|
isUserVideoQuotaValid,
|
||||||
isUserUsernameValid,
|
isUserUsernameValid,
|
||||||
|
|
|
@ -254,7 +254,8 @@ const CONSTRAINTS_FIELDS = {
|
||||||
DESCRIPTION: { min: 3, max: 250 }, // Length
|
DESCRIPTION: { min: 3, max: 250 }, // Length
|
||||||
USERNAME: { min: 3, max: 20 }, // Length
|
USERNAME: { min: 3, max: 20 }, // Length
|
||||||
PASSWORD: { min: 6, max: 255 }, // Length
|
PASSWORD: { min: 6, max: 255 }, // Length
|
||||||
VIDEO_QUOTA: { min: -1 }
|
VIDEO_QUOTA: { min: -1 },
|
||||||
|
BLOCKED_REASON: { min: 3, max: 250 } // Length
|
||||||
},
|
},
|
||||||
VIDEO_ABUSES: {
|
VIDEO_ABUSES: {
|
||||||
REASON: { min: 2, max: 300 } // Length
|
REASON: { min: 2, max: 300 } // Length
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import { createClient } from 'redis'
|
import { CONSTRAINTS_FIELDS } from '../constants'
|
||||||
import { CONFIG } from '../constants'
|
|
||||||
import { JobQueue } from '../../lib/job-queue'
|
|
||||||
import { initDatabaseModels } from '../database'
|
|
||||||
|
|
||||||
async function up (utils: {
|
async function up (utils: {
|
||||||
transaction: Sequelize.Transaction
|
transaction: Sequelize.Transaction
|
||||||
|
@ -31,6 +28,15 @@ async function up (utils: {
|
||||||
}
|
}
|
||||||
await utils.queryInterface.changeColumn('user', 'blocked', data)
|
await utils.queryInterface.changeColumn('user', 'blocked', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.STRING(CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON.max),
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
}
|
||||||
|
await utils.queryInterface.addColumn('user', 'blockedReason', data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function down (options) {
|
function down (options) {
|
||||||
|
|
|
@ -89,7 +89,7 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||||
}
|
}
|
||||||
|
|
||||||
async addVideoAbuseReport (videoId: number) {
|
async addVideoAbuseReportJob (videoId: number) {
|
||||||
const video = await VideoModel.load(videoId)
|
const video = await VideoModel.load(videoId)
|
||||||
if (!video) throw new Error('Unknown Video id during Abuse report.')
|
if (!video) throw new Error('Unknown Video id during Abuse report.')
|
||||||
|
|
||||||
|
@ -108,6 +108,27 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
|
||||||
|
const reasonString = reason ? ` for the following reason: ${reason}` : ''
|
||||||
|
const blockedWord = blocked ? 'blocked' : 'unblocked'
|
||||||
|
const blockedString = `Your account ${user.username} on ${CONFIG.WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
|
||||||
|
|
||||||
|
const text = 'Hi,\n\n' +
|
||||||
|
blockedString +
|
||||||
|
'\n\n' +
|
||||||
|
'Cheers,\n' +
|
||||||
|
`PeerTube.`
|
||||||
|
|
||||||
|
const to = user.email
|
||||||
|
const emailPayload: EmailPayload = {
|
||||||
|
to: [ to ],
|
||||||
|
subject: '[PeerTube] Account ' + blockedWord,
|
||||||
|
text
|
||||||
|
}
|
||||||
|
|
||||||
|
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||||
|
}
|
||||||
|
|
||||||
sendMail (to: string[], subject: string, text: string) {
|
sendMail (to: string[], subject: string, text: string) {
|
||||||
if (!this.transporter) {
|
if (!this.transporter) {
|
||||||
throw new Error('Cannot send mail because SMTP is not configured.')
|
throw new Error('Cannot send mail because SMTP is not configured.')
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { body, param } from 'express-validator/check'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
|
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
|
||||||
import {
|
import {
|
||||||
isUserAutoPlayVideoValid,
|
isUserAutoPlayVideoValid, isUserBlockedReasonValid,
|
||||||
isUserDescriptionValid,
|
isUserDescriptionValid,
|
||||||
isUserDisplayNameValid,
|
isUserDisplayNameValid,
|
||||||
isUserNSFWPolicyValid,
|
isUserNSFWPolicyValid,
|
||||||
|
@ -76,9 +76,10 @@ const usersRemoveValidator = [
|
||||||
|
|
||||||
const usersBlockingValidator = [
|
const usersBlockingValidator = [
|
||||||
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
|
param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
body('reason').optional().custom(isUserBlockedReasonValid).withMessage('Should have a valid blocking reason'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking usersRemove parameters', { parameters: req.params })
|
logger.debug('Checking usersBlocking parameters', { parameters: req.params })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
if (!await checkUserIdExist(req.params.id, res)) return
|
if (!await checkUserIdExist(req.params.id, res)) return
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
|
||||||
import { User, UserRole } from '../../../shared/models/users'
|
import { User, UserRole } from '../../../shared/models/users'
|
||||||
import {
|
import {
|
||||||
isUserAutoPlayVideoValid,
|
isUserAutoPlayVideoValid,
|
||||||
|
isUserBlockedReasonValid,
|
||||||
isUserBlockedValid,
|
isUserBlockedValid,
|
||||||
isUserNSFWPolicyValid,
|
isUserNSFWPolicyValid,
|
||||||
isUserPasswordValid,
|
isUserPasswordValid,
|
||||||
|
@ -107,6 +108,12 @@ export class UserModel extends Model<UserModel> {
|
||||||
@Column
|
@Column
|
||||||
blocked: boolean
|
blocked: boolean
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Default(null)
|
||||||
|
@Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason'))
|
||||||
|
@Column
|
||||||
|
blockedReason: string
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
|
@Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
|
||||||
@Column
|
@Column
|
||||||
|
@ -284,6 +291,8 @@ export class UserModel extends Model<UserModel> {
|
||||||
roleLabel: USER_ROLE_LABELS[ this.role ],
|
roleLabel: USER_ROLE_LABELS[ this.role ],
|
||||||
videoQuota: this.videoQuota,
|
videoQuota: this.videoQuota,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
|
blocked: this.blocked,
|
||||||
|
blockedReason: this.blockedReason,
|
||||||
account: this.Account.toFormattedJSON(),
|
account: this.Account.toFormattedJSON(),
|
||||||
videoChannels: []
|
videoChannels: []
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
|
|
||||||
@AfterCreate
|
@AfterCreate
|
||||||
static sendEmailNotification (instance: VideoAbuseModel) {
|
static sendEmailNotification (instance: VideoAbuseModel) {
|
||||||
return Emailer.Instance.addVideoAbuseReport(instance.videoId)
|
return Emailer.Instance.addVideoAbuseReportJob(instance.videoId)
|
||||||
}
|
}
|
||||||
|
|
||||||
static listForApi (start: number, count: number, sort: string) {
|
static listForApi (start: number, count: number, sort: string) {
|
||||||
|
|
|
@ -2,7 +2,17 @@
|
||||||
|
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { askResetPassword, createUser, reportVideoAbuse, resetPassword, runServer, uploadVideo, userLogin, wait } from '../../utils'
|
import {
|
||||||
|
askResetPassword,
|
||||||
|
blockUser,
|
||||||
|
createUser,
|
||||||
|
reportVideoAbuse,
|
||||||
|
resetPassword,
|
||||||
|
runServer,
|
||||||
|
unblockUser,
|
||||||
|
uploadVideo,
|
||||||
|
userLogin
|
||||||
|
} from '../../utils'
|
||||||
import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
|
import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../utils/index'
|
||||||
import { mockSmtpServer } from '../../utils/miscs/email'
|
import { mockSmtpServer } from '../../utils/miscs/email'
|
||||||
import { waitJobs } from '../../utils/server/jobs'
|
import { waitJobs } from '../../utils/server/jobs'
|
||||||
|
@ -112,6 +122,42 @@ describe('Test emails', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('When blocking/unblocking user', async function () {
|
||||||
|
it('Should send the notification email when blocking a user', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
|
||||||
|
const reason = 'my super bad reason'
|
||||||
|
await blockUser(server.url, userId, server.accessToken, 204, reason)
|
||||||
|
|
||||||
|
await waitJobs(server)
|
||||||
|
expect(emails).to.have.lengthOf(3)
|
||||||
|
|
||||||
|
const email = emails[2]
|
||||||
|
|
||||||
|
expect(email['from'][0]['address']).equal('test-admin@localhost')
|
||||||
|
expect(email['to'][0]['address']).equal('user_1@example.com')
|
||||||
|
expect(email['subject']).contains(' blocked')
|
||||||
|
expect(email['text']).contains(' blocked')
|
||||||
|
expect(email['text']).contains(reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should send the notification email when unblocking a user', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
|
||||||
|
await unblockUser(server.url, userId, server.accessToken, 204)
|
||||||
|
|
||||||
|
await waitJobs(server)
|
||||||
|
expect(emails).to.have.lengthOf(4)
|
||||||
|
|
||||||
|
const email = emails[3]
|
||||||
|
|
||||||
|
expect(email['from'][0]['address']).equal('test-admin@localhost')
|
||||||
|
expect(email['to'][0]['address']).equal('user_1@example.com')
|
||||||
|
expect(email['subject']).contains(' unblocked')
|
||||||
|
expect(email['text']).contains(' unblocked')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
killallServers([ server ])
|
killallServers([ server ])
|
||||||
})
|
})
|
||||||
|
|
|
@ -134,11 +134,14 @@ function removeUser (url: string, userId: number | string, accessToken: string,
|
||||||
.expect(expectedStatus)
|
.expect(expectedStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
|
function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204, reason?: string) {
|
||||||
const path = '/api/v1/users'
|
const path = '/api/v1/users'
|
||||||
|
let body: any
|
||||||
|
if (reason) body = { reason }
|
||||||
|
|
||||||
return request(url)
|
return request(url)
|
||||||
.post(path + '/' + userId + '/block')
|
.post(path + '/' + userId + '/block')
|
||||||
|
.send(body)
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', 'Bearer ' + accessToken)
|
.set('Authorization', 'Bearer ' + accessToken)
|
||||||
.expect(expectedStatus)
|
.expect(expectedStatus)
|
||||||
|
|
|
@ -14,4 +14,7 @@ export interface User {
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
account: Account
|
account: Account
|
||||||
videoChannels?: VideoChannel[]
|
videoChannels?: VideoChannel[]
|
||||||
|
|
||||||
|
blocked: boolean
|
||||||
|
blockedReason?: string
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue