mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-01-20 11:38:59 +00:00
Permission object, display object name instead of uuid
This commit is contained in:
parent
6a573110e8
commit
707f5b6c7f
@ -311,6 +311,7 @@ import { AddRoleToGroupComponent } from './components/group-details/add-role-to-
|
|||||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
import { PermissionsFilterPipe } from './components/permissions-management/permissions-filter.pipe';
|
import { PermissionsFilterPipe } from './components/permissions-management/permissions-filter.pipe';
|
||||||
import { PermissionsTypeFilterPipe } from './components/permissions-management/permissions-type-filter.pipe';
|
import { PermissionsTypeFilterPipe } from './components/permissions-management/permissions-type-filter.pipe';
|
||||||
|
import { DisplayPathPipe } from './components/permissions-management/display-path.pipe';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -533,7 +534,9 @@ import { PermissionsTypeFilterPipe } from './components/permissions-management/p
|
|||||||
FilterCompletePipe,
|
FilterCompletePipe,
|
||||||
UserPermissionsComponent,
|
UserPermissionsComponent,
|
||||||
PermissionsFilterPipe,
|
PermissionsFilterPipe,
|
||||||
PermissionsTypeFilterPipe
|
PermissionsTypeFilterPipe,
|
||||||
|
FilterCompletePipe,
|
||||||
|
DisplayPathPipe
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@ -685,5 +688,6 @@ import { PermissionsTypeFilterPipe } from './components/permissions-management/p
|
|||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
constructor(protected _googleAnalyticsService: GoogleAnalyticsService) {}
|
constructor(protected _googleAnalyticsService: GoogleAnalyticsService) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export class AddPermissionLineComponent implements OnInit {
|
|||||||
action: PermissionActions.ALLOW,
|
action: PermissionActions.ALLOW,
|
||||||
description: "",
|
description: "",
|
||||||
methods: [],
|
methods: [],
|
||||||
path: "/",
|
path: "/"
|
||||||
};
|
};
|
||||||
edit = false;
|
edit = false;
|
||||||
|
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import {SubPath} from "./SubPath";
|
||||||
|
|
||||||
|
export class PermissionPath {
|
||||||
|
private subPath: SubPath[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
add(subPath: SubPath) {
|
||||||
|
this.subPath.push(subPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayPath() {
|
||||||
|
return this.subPath
|
||||||
|
.map((subPath) => subPath.displayValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLast() {
|
||||||
|
this.subPath.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
getPath() {
|
||||||
|
return this.subPath.map((subPath) => subPath.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
return this.subPath.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariables(): { key: string; value: string }[] {
|
||||||
|
return this.subPath
|
||||||
|
.filter((path) => path.key)
|
||||||
|
.map((path) => {
|
||||||
|
return {key: path.key, value: path.value};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
containStar() {
|
||||||
|
return this.subPath
|
||||||
|
.map(subPath => subPath.value === '*')
|
||||||
|
.reduce((previous, next) => previous || next, false);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
export class SubPath {
|
||||||
|
constructor(public value: string,
|
||||||
|
public displayValue: string,
|
||||||
|
public key?: string) {
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<div class="path">
|
<div class="path">
|
||||||
<div>Path: /</div>
|
<div>Path: /</div>
|
||||||
<div *ngFor="let p of path">{{p}}/</div>
|
<div *ngFor="let p of path.getDisplayPath()">{{p}}/</div>
|
||||||
<div class="path-edit-line">
|
<div class="path-edit-line">
|
||||||
<div>
|
<div>
|
||||||
<div *ngIf="mode === 'SELECT'">
|
<div *ngIf="mode === 'SELECT'">
|
||||||
@ -17,21 +17,21 @@
|
|||||||
[matAutocomplete]="auto">
|
[matAutocomplete]="auto">
|
||||||
<mat-autocomplete #auto="matAutocomplete">
|
<mat-autocomplete #auto="matAutocomplete">
|
||||||
<mat-option [value]="'*'">*</mat-option>
|
<mat-option [value]="'*'">*</mat-option>
|
||||||
<mat-option *ngFor="let data of completeData | filterComplete: completeField"
|
<mat-option *ngFor="let data of completeData.data | filterComplete: completeField"
|
||||||
[value]="data.id">
|
[value]="data.name">
|
||||||
<span>{{data.name}}</span> |
|
<span>{{data.name}}</span>
|
||||||
<small>{{data.id}}</small>
|
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-autocomplete>
|
</mat-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="command-button">
|
<div class="command-button">
|
||||||
<mat-icon (click)="removePrevious()" *ngIf="path.length > 0">cancel</mat-icon>
|
<mat-icon (click)="removePrevious()" *ngIf="!path.isEmpty()">cancel</mat-icon>
|
||||||
<mat-icon (click)="getNext()" *ngIf="!this.mode">add_circle_outline</mat-icon>
|
<mat-icon (click)="getNext()" *ngIf="!this.mode">add_circle_outline</mat-icon>
|
||||||
<mat-icon
|
<mat-icon
|
||||||
matTooltip="validate data"
|
matTooltip="validate data"
|
||||||
(click)="validComplete()"
|
(click)="validComplete()"
|
||||||
*ngIf="this.mode === 'COMPLETE'">check_circle</mat-icon>
|
*ngIf="this.mode === 'COMPLETE'">check_circle
|
||||||
|
</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
import {ApiInformationService, IFormatedList} from "@services/api-information.service";
|
import {ApiInformationService, IFormatedList} from "@services/api-information.service";
|
||||||
import {Server} from "@models/server";
|
import {Server} from "@models/server";
|
||||||
|
import {PermissionPath} from "@components/permissions-management/add-permission-line/path-auto-complete/PermissionPath";
|
||||||
|
import {SubPath} from "@components/permissions-management/add-permission-line/path-auto-complete/SubPath";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-path-auto-complete',
|
selector: 'app-path-auto-complete',
|
||||||
@ -24,9 +26,9 @@ export class PathAutoCompleteComponent implements OnInit {
|
|||||||
|
|
||||||
@Output() update = new EventEmitter<string>();
|
@Output() update = new EventEmitter<string>();
|
||||||
@Input() server: Server;
|
@Input() server: Server;
|
||||||
path: string[] = [];
|
path: PermissionPath = new PermissionPath();
|
||||||
values: string[] = [];
|
values: string[] = [];
|
||||||
private completeData: IFormatedList[];
|
private completeData: { data: IFormatedList[]; key: string };
|
||||||
public mode: 'SELECT' | 'COMPLETE' | undefined;
|
public mode: 'SELECT' | 'COMPLETE' | undefined;
|
||||||
completeField: string;
|
completeField: string;
|
||||||
|
|
||||||
@ -34,14 +36,14 @@ export class PathAutoCompleteComponent implements OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePath(value: string) {
|
updatePath(name: string, value: string, key?: string) {
|
||||||
this.path.push(value);
|
this.path.add(new SubPath(name, value, key));
|
||||||
this.update.emit(`/${this.path.join('/')}`);
|
this.update.emit('/' + this.path.getPath().join("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
popPath() {
|
popPath() {
|
||||||
this.path.pop();
|
this.path.removeLast();
|
||||||
this.update.emit(`/${this.path.join('/')}`);
|
this.update.emit('/' + this.path.getPath().join("/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -49,8 +51,11 @@ export class PathAutoCompleteComponent implements OnInit {
|
|||||||
|
|
||||||
getNext() {
|
getNext() {
|
||||||
this.apiInformationService
|
this.apiInformationService
|
||||||
.getPathNextElement(this.path)
|
.getPathNextElement(this.path.getPath())
|
||||||
.subscribe((next: string[]) => {
|
.subscribe((next: string[]) => {
|
||||||
|
if (this.path.containStar()) {
|
||||||
|
next = next.filter(item => !item.match(this.apiInformationService.bracketIdRegex));
|
||||||
|
}
|
||||||
this.values = next;
|
this.values = next;
|
||||||
this.mode = 'SELECT';
|
this.mode = 'SELECT';
|
||||||
});
|
});
|
||||||
@ -60,27 +65,33 @@ export class PathAutoCompleteComponent implements OnInit {
|
|||||||
if (this.mode) {
|
if (this.mode) {
|
||||||
return this.mode = undefined;
|
return this.mode = undefined;
|
||||||
}
|
}
|
||||||
if (this.path.length > 0) {
|
if (!this.path.isEmpty()) {
|
||||||
return this.popPath();
|
return this.popPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
valueChanged(value: string) {
|
valueChanged(value: string) {
|
||||||
if (value.match(this.apiInformationService.bracketIdRegex)) {
|
if (value.match(this.apiInformationService.bracketIdRegex) && !this.path.containStar()) {
|
||||||
this.apiInformationService.getListByObjectId(this.server, value)
|
this.apiInformationService.getListByObjectId(this.server, value, undefined, this.path.getVariables())
|
||||||
.subscribe((data) => {
|
.subscribe((data) => {
|
||||||
this.mode = 'COMPLETE';
|
this.mode = 'COMPLETE';
|
||||||
this.completeData = data;
|
this.completeData = {data, key: value};
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.updatePath(value);
|
this.updatePath(value, value);
|
||||||
this.mode = undefined;
|
this.mode = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validComplete() {
|
validComplete() {
|
||||||
this.updatePath(this.completeField);
|
if (this.completeField === '*') {
|
||||||
|
this.updatePath('*', '*');
|
||||||
|
} else {
|
||||||
|
const data = this.completeData.data.find((d) => this.completeField === d.name);
|
||||||
|
this.updatePath(data.id, data.name, this.completeData.key);
|
||||||
|
}
|
||||||
this.mode = undefined;
|
this.mode = undefined;
|
||||||
|
this.completeField = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import { DisplayPathPipe } from './display-path.pipe';
|
||||||
|
|
||||||
|
describe('DisplayPathPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new DisplayPathPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,42 @@
|
|||||||
|
import {Pipe, PipeTransform} from '@angular/core';
|
||||||
|
import {map, switchMap} from "rxjs/operators";
|
||||||
|
import {forkJoin, Observable, of} from "rxjs";
|
||||||
|
import {ApiInformationService} from "@services/api-information.service";
|
||||||
|
import {Server} from "@models/server";
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: 'displayPath'
|
||||||
|
})
|
||||||
|
export class DisplayPathPipe implements PipeTransform {
|
||||||
|
|
||||||
|
constructor(private apiInformation: ApiInformationService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
transform(originalPath: string, server: Server): Observable<string> {
|
||||||
|
if (!server) {
|
||||||
|
return of(originalPath);
|
||||||
|
}
|
||||||
|
return this.apiInformation
|
||||||
|
.getKeysForPath(originalPath)
|
||||||
|
.pipe(switchMap((values) => {
|
||||||
|
if (values.length === 0) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
const obs = values.map((k) => this.apiInformation.getListByObjectId(server, k.key, k.value, values));
|
||||||
|
return forkJoin(obs);
|
||||||
|
}),
|
||||||
|
map((values: { id: string; name: string }[][]) => {
|
||||||
|
let displayPath = `${originalPath}`;
|
||||||
|
values.forEach((value) => {
|
||||||
|
if (value[0].id && value[0].name) {
|
||||||
|
displayPath = displayPath.replace(value[0].id, value[0].name);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
return displayPath;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -18,9 +18,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
[matTooltip]="permission.path"
|
[matTooltip]="permission.path | displayPath: server | async"
|
||||||
matTooltipClass="custom-tooltip">
|
matTooltipClass="custom-tooltip">
|
||||||
{{permission.path}}
|
{{permission.path | displayPath: server | async}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
<div *ngFor="let permission of permissions | permissionsTypeFilter: typeFilter?.view | permissionsFilter: searchPermissions?.id | paginator: pageEvent">
|
<div *ngFor="let permission of permissions | permissionsTypeFilter: typeFilter?.view | permissionsFilter: searchPermissions?.id | paginator: pageEvent">
|
||||||
<app-permission-add-edit-line
|
<app-permission-add-edit-line
|
||||||
[permission]="permission"
|
[permission]="permission"
|
||||||
|
[server]="server"
|
||||||
(update)="refresh()"></app-permission-add-edit-line>
|
(update)="refresh()"></app-permission-add-edit-line>
|
||||||
</div>
|
</div>
|
||||||
<mat-paginator [length]="permissions.length" (page)="pageEvent = $event"
|
<mat-paginator [length]="permissions.length" (page)="pageEvent = $event"
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="center">{{permission.methods.join(",")}}</div>
|
<div class="center">{{permission.methods.join(",")}}</div>
|
||||||
<div class="center">{{permission.path}}</div>
|
<div class="center">{{permission.path | displayPath: server | async}}</div>
|
||||||
</div>
|
</div>
|
||||||
<button *ngIf="side === 'LEFT'" mat-button (click)="onClick()">
|
<button *ngIf="side === 'LEFT'" mat-button (click)="onClick()">
|
||||||
<mat-icon>keyboard_arrow_right</mat-icon>
|
<mat-icon>keyboard_arrow_right</mat-icon>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
import {Permission} from "@models/api/permission";
|
import {Permission} from "@models/api/permission";
|
||||||
|
import { Server } from '@models/server';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-editable-permission',
|
selector: 'app-editable-permission',
|
||||||
@ -21,6 +22,7 @@ import {Permission} from "@models/api/permission";
|
|||||||
export class EditablePermissionComponent implements OnInit {
|
export class EditablePermissionComponent implements OnInit {
|
||||||
|
|
||||||
@Input() permission: Permission;
|
@Input() permission: Permission;
|
||||||
|
@Input() server: Server;
|
||||||
@Input() side: 'LEFT' | 'RIGHT';
|
@Input() side: 'LEFT' | 'RIGHT';
|
||||||
@Output() click = new EventEmitter();
|
@Output() click = new EventEmitter();
|
||||||
|
|
||||||
@ -38,7 +40,7 @@ export class EditablePermissionComponent implements OnInit {
|
|||||||
return `
|
return `
|
||||||
action: ${this.permission.action}
|
action: ${this.permission.action}
|
||||||
methods: ${this.permission.methods.join(',')}
|
methods: ${this.permission.methods.join(',')}
|
||||||
path: ${this.permission.path}
|
original path: ${this.permission.path}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
<app-editable-permission
|
<app-editable-permission
|
||||||
[side]="'LEFT'"
|
[side]="'LEFT'"
|
||||||
[permission]="permission"
|
[permission]="permission"
|
||||||
|
[server]="server"
|
||||||
(click)="remove(permission)"
|
(click)="remove(permission)"
|
||||||
*ngFor="let permission of owned">
|
*ngFor="let permission of owned">
|
||||||
|
|
||||||
@ -40,6 +41,7 @@
|
|||||||
<app-editable-permission
|
<app-editable-permission
|
||||||
[side]="'RIGHT'"
|
[side]="'RIGHT'"
|
||||||
[permission]="permission"
|
[permission]="permission"
|
||||||
|
[server]="server"
|
||||||
(click)="add(permission)"
|
(click)="add(permission)"
|
||||||
*ngFor="let permission of available">
|
*ngFor="let permission of available">
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<app-editable-permission
|
<app-editable-permission
|
||||||
[permission]="permission"
|
[permission]="permission"
|
||||||
|
[server]="server"
|
||||||
*ngFor="let permission of role.permissions">
|
*ngFor="let permission of role.permissions">
|
||||||
</app-editable-permission>
|
</app-editable-permission>
|
||||||
</div>
|
</div>
|
||||||
|
@ -197,6 +197,7 @@ export class ApiInformationService {
|
|||||||
.filter(elem => !(elem === '' || elem === 'v3'));
|
.filter(elem => !(elem === '' || elem === 'v3'));
|
||||||
return paths[0].subPaths.map((elem, index) => {
|
return paths[0].subPaths.map((elem, index) => {
|
||||||
if (elem.match(this.bracketIdRegex)) {
|
if (elem.match(this.bracketIdRegex)) {
|
||||||
|
|
||||||
return {key: elem, value: splinted[index]};
|
return {key: elem, value: splinted[index]};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -205,7 +206,8 @@ export class ApiInformationService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getListByObjectId(server: Server, key: string, value?: string) {
|
getListByObjectId(server: Server, key: string, value?: string, extraParams?: { key: string; value: string }[]) {
|
||||||
|
const idName = /{([^)]+)}/.exec(key)[1];
|
||||||
function findElement(data: IApiObject[]): IApiObject {
|
function findElement(data: IApiObject[]): IApiObject {
|
||||||
const elem = data.find(d => d.name === key);
|
const elem = data.find(d => d.name === key);
|
||||||
if (!elem) {
|
if (!elem) {
|
||||||
@ -218,9 +220,16 @@ export class ApiInformationService {
|
|||||||
map(findElement),
|
map(findElement),
|
||||||
switchMap(elem => {
|
switchMap(elem => {
|
||||||
let url = `${server.protocol}//${server.host}:${server.port}${elem.path}`;
|
let url = `${server.protocol}//${server.host}:${server.port}${elem.path}`;
|
||||||
|
if (extraParams) {
|
||||||
|
extraParams.forEach((param) => {
|
||||||
|
url = url.replace(param.key, param.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
url = `${url}/${value}`;
|
url = `${url}/${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.httpClient.get<any[]>(url, {headers: {Authorization: `Bearer ${server.authToken}`}});
|
return this.httpClient.get<any[]>(url, {headers: {Authorization: `Bearer ${server.authToken}`}});
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@ -235,21 +244,19 @@ export class ApiInformationService {
|
|||||||
const nameKey = keys.find(k => k.match(/name/));
|
const nameKey = keys.find(k => k.match(/name/));
|
||||||
response = response.map(o => {
|
response = response.map(o => {
|
||||||
return {
|
return {
|
||||||
id: o[idKey],
|
id: o[idName] || o[idKey],
|
||||||
name: o[nameKey]
|
name: o[nameKey]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
response.forEach(elt => {
|
response.forEach(elt => {
|
||||||
this.cache_permissions[elt.id] = elt;
|
this.cache_permissions[elt.id] = elt;
|
||||||
})
|
});
|
||||||
console.log('b', this.cache_permissions)
|
|
||||||
|
|
||||||
return of(response);
|
return of(response);
|
||||||
} else {
|
} else {
|
||||||
const keys = Object.keys(response);
|
const keys = Object.keys(response);
|
||||||
const idKey = keys.find(k => k.match(/_id$|filename/));
|
const idKey = keys.find(k => k.match(/_id$|filename/));
|
||||||
const nameKey = keys.find(k => k.match(/name/));
|
const nameKey = keys.find(k => k.match(/name/));
|
||||||
const ret = {id: response[idKey], name: response[nameKey]};
|
const ret = {id: response[idName] || response[idKey], name: response[nameKey]};
|
||||||
this.cache_permissions[ret.id] = ret;
|
this.cache_permissions[ret.id] = ret;
|
||||||
return of([ret]);
|
return of([ret]);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,8 @@ import {Observable} from "rxjs/Rx";
|
|||||||
})
|
})
|
||||||
export class PermissionsService {
|
export class PermissionsService {
|
||||||
|
|
||||||
constructor(private httpServer: HttpServer) { }
|
constructor(private httpServer: HttpServer) {
|
||||||
|
}
|
||||||
|
|
||||||
list(server: Server) {
|
list(server: Server) {
|
||||||
return this.httpServer.get<Permission[]>(server, '/permissions');
|
return this.httpServer.get<Permission[]>(server, '/permissions');
|
||||||
|
Loading…
Reference in New Issue
Block a user