Add ability to ban/unban users

This commit is contained in:
Chocobozzz 2018-08-09 17:51:25 +02:00
parent 63347a0ff9
commit 141b177db0
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
17 changed files with 270 additions and 36 deletions

View File

@ -13,6 +13,7 @@ import { JobService } from './jobs/shared/job.service'
import { UserCreateComponent, UserListComponent, UsersComponent, UserService, UserUpdateComponent } from './users' import { UserCreateComponent, UserListComponent, UsersComponent, UserService, UserUpdateComponent } from './users'
import { VideoAbuseListComponent, VideoAbusesComponent } from './video-abuses' import { VideoAbuseListComponent, VideoAbusesComponent } from './video-abuses'
import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist' import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist'
import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-modal.component'
@NgModule({ @NgModule({
imports: [ imports: [
@ -33,6 +34,7 @@ import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-bl
UserCreateComponent, UserCreateComponent,
UserUpdateComponent, UserUpdateComponent,
UserListComponent, UserListComponent,
UserBanModalComponent,
VideoBlacklistComponent, VideoBlacklistComponent,
VideoBlacklistListComponent, VideoBlacklistListComponent,

View File

@ -59,6 +59,18 @@ export class UserService {
.pipe(catchError(err => this.restExtractor.handleError(err))) .pipe(catchError(err => this.restExtractor.handleError(err)))
} }
banUser (user: User, reason?: string) {
const body = reason ? { reason } : {}
return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/block', body)
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
unbanUser (user: User) {
return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/unblock', {})
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
private formatUser (user: User) { private formatUser (user: User) {
let videoQuota let videoQuota
if (user.videoQuota === -1) { if (user.videoQuota === -1) {

View File

@ -0,0 +1,32 @@
<ng-template #modal>
<div class="modal-header">
<h4 i18n class="modal-title">Ban {{ userToBan.username }}</h4>
<span class="close" aria-hidden="true" (click)="hideBanUserModal()"></span>
</div>
<div class="modal-body">
<form novalidate [formGroup]="form" (ngSubmit)="banUser()">
<div class="form-group">
<textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }">
</textarea>
<div *ngIf="formErrors.reason" class="form-error">
{{ formErrors.reason }}
</div>
</div>
<div i18n>
A banned user will no longer be able to login.
</div>
<div class="form-group inputs">
<span i18n class="action-button action-button-cancel" (click)="hideBanUserModal()">Cancel</span>
<input
type="submit" i18n-value value="Ban this user" class="action-button-submit"
[disabled]="!form.valid"
>
</div>
</form>
</div>
</ng-template>

View File

@ -0,0 +1,6 @@
@import 'variables';
@import 'mixins';
textarea {
@include peertube-textarea(100%, 60px);
}

View File

@ -0,0 +1,68 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { FormReactive, User, UserValidatorsService } from '../../../shared'
import { UserService } from '../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
@Component({
selector: 'my-user-ban-modal',
templateUrl: './user-ban-modal.component.html',
styleUrls: [ './user-ban-modal.component.scss' ]
})
export class UserBanModalComponent extends FormReactive implements OnInit {
@ViewChild('modal') modal: NgbModal
@Output() userBanned = new EventEmitter<User>()
private userToBan: User
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
private modalService: NgbModal,
private notificationsService: NotificationsService,
private userService: UserService,
private userValidatorsService: UserValidatorsService,
private i18n: I18n
) {
super()
}
ngOnInit () {
this.buildForm({
reason: this.userValidatorsService.USER_BAN_REASON
})
}
openModal (user: User) {
this.userToBan = user
this.openedModal = this.modalService.open(this.modal)
}
hideBanUserModal () {
this.userToBan = undefined
this.openedModal.close()
}
async banUser () {
const reason = this.form.value['reason'] || undefined
this.userService.banUser(this.userToBan, reason)
.subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('User {{username}} banned.', { username: this.userToBan.username })
)
this.userBanned.emit(this.userToBan)
this.hideBanUserModal()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
}

View File

@ -9,31 +9,50 @@
<p-table <p-table
[value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
> >
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th style="width: 40px"></th>
<th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th> <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
<th i18n>Email</th> <th i18n>Email</th>
<th i18n>Video quota</th> <th i18n>Video quota</th>
<th i18n>Role</th> <th i18n>Role</th>
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th></th> <th style="width: 50px;"></th>
</tr> </tr>
</ng-template> </ng-template>
<ng-template pTemplate="body" let-user> <ng-template pTemplate="body" let-expanded="expanded" let-user>
<tr>
<td>{{ user.username }}</td> <tr [ngClass]="{ banned: user.blocked }">
<td>
<span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user">
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
</span>
</td>
<td>
{{ user.username }}
<span *ngIf="user.blocked" class="banned-info">(banned)</span>
</td>
<td>{{ user.email }}</td> <td>{{ user.email }}</td>
<td>{{ user.videoQuota }}</td> <td>{{ user.videoQuota }}</td>
<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-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user"></my-action-dropdown> <my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user"></my-action-dropdown>
<!--<my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>--> </td>
<!--<my-delete-button (click)="removeUser(user)"></my-delete-button>--> </tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-user>
<tr class="user-blocked-reason">
<td colspan="7">
<span i18n class="ban-reason-label">Ban reason:</span>
{{ user.blockedReason }}
</td> </td>
</tr> </tr>
</ng-template> </ng-template>
</p-table> </p-table>
<my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>

View File

@ -4,3 +4,21 @@
.add-button { .add-button {
@include create-button('../../../../assets/images/global/add.svg'); @include create-button('../../../../assets/images/global/add.svg');
} }
my-action-dropdown /deep/ .icon {
&.icon-ban {
background-image: url('../../../../assets/images/global/edit-black.svg');
}
}
tr.banned {
color: red;
}
.banned-info {
font-style: italic;
}
.ban-reason-label {
font-weight: $font-semibold;
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit } 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 } from '../../../core'
@ -6,6 +6,8 @@ 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' import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-modal.component'
@Component({ @Component({
selector: 'my-user-list', selector: 'my-user-list',
@ -13,6 +15,8 @@ import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
styleUrls: [ './user-list.component.scss' ] styleUrls: [ './user-list.component.scss' ]
}) })
export class UserListComponent extends RestTable implements OnInit { export class UserListComponent extends RestTable implements OnInit {
@ViewChild('userBanModal') userBanModal: UserBanModalComponent
users: User[] = [] users: User[] = []
totalRecords = 0 totalRecords = 0
rowsPerPage = 10 rowsPerPage = 10
@ -20,6 +24,9 @@ export class UserListComponent extends RestTable implements OnInit {
pagination: RestPagination = { count: this.rowsPerPage, start: 0 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
userActions: DropdownAction<User>[] = [] userActions: DropdownAction<User>[] = []
private userToBan: User
private openedModal: NgbModalRef
constructor ( constructor (
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private confirmService: ConfirmService, private confirmService: ConfirmService,
@ -30,12 +37,22 @@ export class UserListComponent extends RestTable implements OnInit {
this.userActions = [ this.userActions = [
{ {
type: 'edit', label: this.i18n('Edit'),
linkBuilder: this.getRouterUserEditLink linkBuilder: this.getRouterUserEditLink
}, },
{ {
type: 'delete', label: this.i18n('Delete'),
handler: user => this.removeUser(user) handler: user => this.removeUser(user)
},
{
label: this.i18n('Ban'),
handler: user => this.openBanUserModal(user),
isDisplayed: user => !user.blocked
},
{
label: this.i18n('Unban'),
handler: user => this.unbanUser(user),
isDisplayed: user => user.blocked
} }
] ]
} }
@ -44,6 +61,43 @@ export class UserListComponent extends RestTable implements OnInit {
this.loadSort() this.loadSort()
} }
hideBanUserModal () {
this.userToBan = undefined
this.openedModal.close()
}
openBanUserModal (user: User) {
if (user.username === 'root') {
this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.'))
return
}
this.userBanModal.openModal(user)
}
onUserBanned () {
this.loadData()
}
async unbanUser (user: User) {
const message = this.i18n('Do you really want to unban {{username}}?', { username: user.username })
const res = await this.confirmService.confirm(message, this.i18n('Unban'))
if (res === false) return
this.userService.unbanUser(user)
.subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('User {{username}} unbanned.', { username: user.username })
)
this.loadData()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
async removeUser (user: User) { async removeUser (user: User) {
if (user.username === 'root') { if (user.username === 'root') {
this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.')) this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.'))

View File

@ -50,7 +50,6 @@
</form> </form>
</div> </div>
<!--<ng-template #forgotPasswordModal (onShown)="onForgotPasswordModalShown()">-->
<ng-template #forgotPasswordModal> <ng-template #forgotPasswordModal>
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Forgot your password</h4> <h4 i18n class="modal-title">Forgot your password</h4>

View File

@ -69,7 +69,7 @@ export class LoginComponent extends FormReactive implements OnInit {
askResetPassword () { askResetPassword () {
this.userService.askResetPassword(this.forgotPasswordEmail) this.userService.askResetPassword(this.forgotPasswordEmail)
.subscribe( .subscribe(
res => { () => {
const message = this.i18n( const message = this.i18n(
'An email with the reset password instructions will be sent to {{email}}.', 'An email with the reset password instructions will be sent to {{email}}.',
{ email: this.forgotPasswordEmail } { email: this.forgotPasswordEmail }

View File

@ -1,16 +1,17 @@
<div class="dropdown-root" dropdown container="body" dropup="true" placement="right" role="button"> <div class="dropdown-root" ngbDropdown [placement]="placement">
<div class="action-button" dropdownToggle> <div class="action-button" ngbDropdownToggle role="button">
<span class="icon icon-action"></span> <span class="icon icon-action"></span>
</div> </div>
<ul *dropdownMenu class="dropdown-menu" id="more-menu" role="menu" aria-labelledby="single-button"> <div ngbDropdownMenu class="dropdown-menu">
<li role="menuitem" *ngFor="let action of actions"> <ng-container *ngFor="let action of actions">
<my-delete-button *ngIf="action.type === 'delete'" [label]="action.label" (click)="action.handler(entry)"></my-delete-button> <div class="dropdown-item" *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
<my-edit-button *ngIf="action.type === 'edit'" [label]="action.label" [routerLink]="action.linkBuilder(entry)"></my-edit-button> <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a>
<a *ngIf="action.type === 'custom'" class="dropdown-item" href="#" (click)="action.handler(entry)"> <span *ngIf="!action.linkBuilder" class="custom-action" class="dropdown-item" (click)="action.handler(entry)" role="button">
<span *ngIf="action.iconClass" class="icon" [ngClass]="action.iconClass"></span> <ng-container>{{ action.label }}</ng-container> {{ action.label }}
</a> </span>
</li> </div>
</ul> </ng-container>
</div>
</div> </div>

View File

@ -5,13 +5,17 @@
@include peertube-button; @include peertube-button;
@include grey-button; @include grey-button;
display: inline-block;
padding: 0 10px;
&::after {
display: none;
}
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
background-color: $grey-color; background-color: $grey-color;
} }
display: inline-block;
padding: 0 10px;
.icon-action { .icon-action {
@include icon(21px); @include icon(21px);
@ -19,3 +23,10 @@
top: -1px; top: -1px;
} }
} }
.dropdown-menu {
.dropdown-item {
cursor: pointer;
color: #000 !important;
}
}

View File

@ -1,11 +1,10 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
export type DropdownAction<T> = { export type DropdownAction<T> = {
type: 'custom' | 'delete' | 'edit'
label?: string label?: string
handler?: (T) => any handler?: (T) => any
linkBuilder?: (T) => (string | number)[] linkBuilder?: (T) => (string | number)[]
iconClass?: string isDisplayed?: (T) => boolean
} }
@Component({ @Component({
@ -17,4 +16,5 @@ export type DropdownAction<T> = {
export class ActionDropdownComponent<T> { export class ActionDropdownComponent<T> {
@Input() actions: DropdownAction<T>[] = [] @Input() actions: DropdownAction<T>[] = []
@Input() entry: T @Input() entry: T
@Input() placement = 'left'
} }

View File

@ -14,6 +14,8 @@ export class UserValidatorsService {
readonly USER_DESCRIPTION: BuildFormValidator readonly USER_DESCRIPTION: BuildFormValidator
readonly USER_TERMS: BuildFormValidator readonly USER_TERMS: BuildFormValidator
readonly USER_BAN_REASON: BuildFormValidator
constructor (private i18n: I18n) { constructor (private i18n: I18n) {
this.USER_USERNAME = { this.USER_USERNAME = {
@ -99,5 +101,16 @@ export class UserValidatorsService {
'required': this.i18n('You must to agree with the instance terms in order to registering on it.') 'required': this.i18n('You must to agree with the instance terms in order to registering on it.')
} }
} }
this.USER_BAN_REASON = {
VALIDATORS: [
Validators.minLength(3),
Validators.maxLength(250)
],
MESSAGES: {
'minlength': this.i18n('Ban reason must be at least 3 characters long.'),
'maxlength': this.i18n('Ban reason cannot be more than 250 characters long.')
}
}
} }
} }

View File

@ -310,8 +310,4 @@ table {
} }
} }
} }
bs-dropdown-container {
z-index: 10000;
}
} }

View File

@ -17,10 +17,13 @@ p-table {
td { td {
border: 1px solid #E5E5E5 !important; border: 1px solid #E5E5E5 !important;
padding-left: 15px !important; padding-left: 15px !important;
&:not(.action-cell) {
overflow: hidden !important; overflow: hidden !important;
text-overflow: ellipsis !important; text-overflow: ellipsis !important;
white-space: nowrap !important; white-space: nowrap !important;
} }
}
tr { tr {
background-color: #fff !important; background-color: #fff !important;