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: [
UserCreateComponent,
UserUpdateComponent,
UserListComponent,
+ UserBanModalComponent,
VideoBlacklistComponent,
VideoBlacklistListComponent,
.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) {
--- /dev/null
+<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>
\ No newline at end of file
--- /dev/null
+@import 'variables';
+@import 'mixins';
+
+textarea {
+ @include peertube-textarea(100%, 60px);
+}
--- /dev/null
+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)
+ )
+ }
+
+}
<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>
\ No newline at end of file
.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;
+}
\ No newline at end of file
-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'
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',
styleUrls: [ './user-list.component.scss' ]
})
export class UserListComponent extends RestTable implements OnInit {
+ @ViewChild('userBanModal') userBanModal: UserBanModalComponent
+
users: User[] = []
totalRecords = 0
rowsPerPage = 10
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
userActions: DropdownAction<User>[] = []
+ private userToBan: User
+ private openedModal: NgbModalRef
+
constructor (
private notificationsService: NotificationsService,
private confirmService: ConfirmService,
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
}
]
}
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.'))
</form>
</div>
-<!--<ng-template #forgotPasswordModal (onShown)="onForgotPasswordModalShown()">-->
<ng-template #forgotPasswordModal>
<div class="modal-header">
<h4 i18n class="modal-title">Forgot your password</h4>
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 }
-<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>
\ No newline at end of file
@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;
+ }
}
\ No newline at end of file
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({
export class ActionDropdownComponent<T> {
@Input() actions: DropdownAction<T>[] = []
@Input() entry: T
+ @Input() placement = 'left'
}
readonly USER_DESCRIPTION: BuildFormValidator
readonly USER_TERMS: BuildFormValidator
+ readonly USER_BAN_REASON: BuildFormValidator
+
constructor (private i18n: I18n) {
this.USER_USERNAME = {
'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.')
+ }
+ }
}
}
}
}
}
-
- bs-dropdown-container {
- z-index: 10000;
- }
}
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 {
-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'