Add ability to ban/unban users
This commit is contained in:
parent
63347a0ff9
commit
141b177db0
|
@ -13,6 +13,7 @@ import { JobService } from './jobs/shared/job.service'
|
|||
import { UserCreateComponent, UserListComponent, UsersComponent, UserService, UserUpdateComponent } from './users'
|
||||
import { VideoAbuseListComponent, VideoAbusesComponent } from './video-abuses'
|
||||
import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist'
|
||||
import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-modal.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -33,6 +34,7 @@ import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-bl
|
|||
UserCreateComponent,
|
||||
UserUpdateComponent,
|
||||
UserListComponent,
|
||||
UserBanModalComponent,
|
||||
|
||||
VideoBlacklistComponent,
|
||||
VideoBlacklistListComponent,
|
||||
|
|
|
@ -59,6 +59,18 @@ export class UserService {
|
|||
.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) {
|
||||
let videoQuota
|
||||
if (user.videoQuota === -1) {
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(100%, 60px);
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -9,31 +9,50 @@
|
|||
|
||||
<p-table
|
||||
[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">
|
||||
<tr>
|
||||
<th style="width: 40px"></th>
|
||||
<th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
|
||||
<th i18n>Email</th>
|
||||
<th i18n>Video quota</th>
|
||||
<th i18n>Role</th>
|
||||
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th></th>
|
||||
<th style="width: 50px;"></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-user>
|
||||
<tr>
|
||||
<td>{{ user.username }}</td>
|
||||
<ng-template pTemplate="body" let-expanded="expanded" let-user>
|
||||
|
||||
<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.videoQuota }}</td>
|
||||
<td>{{ user.roleLabel }}</td>
|
||||
<td>{{ user.createdAt }}</td>
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user"></my-action-dropdown>
|
||||
<!--<my-edit-button [routerLink]="getRouterUserEditLink(user)"></my-edit-button>-->
|
||||
<!--<my-delete-button (click)="removeUser(user)"></my-delete-button>-->
|
||||
</td>
|
||||
</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>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
|
|
@ -4,3 +4,21 @@
|
|||
.add-button {
|
||||
@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;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { ConfirmService } from '../../../core'
|
||||
|
@ -6,6 +6,8 @@ import { RestPagination, RestTable, User } from '../../../shared'
|
|||
import { UserService } from '../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
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({
|
||||
selector: 'my-user-list',
|
||||
|
@ -13,6 +15,8 @@ import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
|
|||
styleUrls: [ './user-list.component.scss' ]
|
||||
})
|
||||
export class UserListComponent extends RestTable implements OnInit {
|
||||
@ViewChild('userBanModal') userBanModal: UserBanModalComponent
|
||||
|
||||
users: User[] = []
|
||||
totalRecords = 0
|
||||
rowsPerPage = 10
|
||||
|
@ -20,6 +24,9 @@ export class UserListComponent extends RestTable implements OnInit {
|
|||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
userActions: DropdownAction<User>[] = []
|
||||
|
||||
private userToBan: User
|
||||
private openedModal: NgbModalRef
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private confirmService: ConfirmService,
|
||||
|
@ -30,12 +37,22 @@ export class UserListComponent extends RestTable implements OnInit {
|
|||
|
||||
this.userActions = [
|
||||
{
|
||||
type: 'edit',
|
||||
label: this.i18n('Edit'),
|
||||
linkBuilder: this.getRouterUserEditLink
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
label: this.i18n('Delete'),
|
||||
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()
|
||||
}
|
||||
|
||||
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) {
|
||||
if (user.username === 'root') {
|
||||
this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.'))
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<!--<ng-template #forgotPasswordModal (onShown)="onForgotPasswordModalShown()">-->
|
||||
<ng-template #forgotPasswordModal>
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Forgot your password</h4>
|
||||
|
|
|
@ -69,7 +69,7 @@ export class LoginComponent extends FormReactive implements OnInit {
|
|||
askResetPassword () {
|
||||
this.userService.askResetPassword(this.forgotPasswordEmail)
|
||||
.subscribe(
|
||||
res => {
|
||||
() => {
|
||||
const message = this.i18n(
|
||||
'An email with the reset password instructions will be sent to {{email}}.',
|
||||
{ email: this.forgotPasswordEmail }
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<div class="dropdown-root" dropdown container="body" dropup="true" placement="right" role="button">
|
||||
<div class="action-button" dropdownToggle>
|
||||
<div class="dropdown-root" ngbDropdown [placement]="placement">
|
||||
<div class="action-button" ngbDropdownToggle role="button">
|
||||
<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>
|
||||
<div ngbDropdownMenu class="dropdown-menu">
|
||||
<ng-container *ngFor="let action of actions">
|
||||
<div class="dropdown-item" *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
|
||||
<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.iconClass" class="icon" [ngClass]="action.iconClass"></span> <ng-container>{{ action.label }}</ng-container>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<span *ngIf="!action.linkBuilder" class="custom-action" class="dropdown-item" (click)="action.handler(entry)" role="button">
|
||||
{{ action.label }}
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
|
@ -5,17 +5,28 @@
|
|||
@include peertube-button;
|
||||
@include grey-button;
|
||||
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.dropdown-item {
|
||||
cursor: pointer;
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
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
|
||||
isDisplayed?: (T) => boolean
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
@ -17,4 +16,5 @@ export type DropdownAction<T> = {
|
|||
export class ActionDropdownComponent<T> {
|
||||
@Input() actions: DropdownAction<T>[] = []
|
||||
@Input() entry: T
|
||||
@Input() placement = 'left'
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ export class UserValidatorsService {
|
|||
readonly USER_DESCRIPTION: BuildFormValidator
|
||||
readonly USER_TERMS: BuildFormValidator
|
||||
|
||||
readonly USER_BAN_REASON: BuildFormValidator
|
||||
|
||||
constructor (private i18n: I18n) {
|
||||
|
||||
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.')
|
||||
}
|
||||
}
|
||||
|
||||
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.')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -310,8 +310,4 @@ table {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
bs-dropdown-container {
|
||||
z-index: 10000;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,12 @@ p-table {
|
|||
td {
|
||||
border: 1px solid #E5E5E5 !important;
|
||||
padding-left: 15px !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
|
||||
&:not(.action-cell) {
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AccessDeniedError} from 'oauth2-server'
|
||||
import { AccessDeniedError } from 'oauth2-server'
|
||||
import { logger } from '../helpers/logger'
|
||||
import { UserModel } from '../models/account/user'
|
||||
import { OAuthClientModel } from '../models/oauth/oauth-client'
|
||||
|
|
Loading…
Reference in New Issue