1
0
Fork 0

add blocked filter in users list to filter banned users

fixes #2914
This commit is contained in:
Rigel Kent 2020-07-02 22:49:51 +02:00
parent 2b587cad93
commit 8491293b02
No known key found for this signature in database
GPG Key ID: 5E53E96A494E452F
11 changed files with 213 additions and 32 deletions

View File

@ -16,14 +16,27 @@
</my-action-dropdown> </my-action-dropdown>
</div> </div>
<div class="ml-auto has-feedback has-clear"> <div class="ml-auto">
<input <div class="input-group has-feedback has-clear">
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
(keyup)="onSearch($event)" <div class="input-group-text" ngbDropdownToggle>
> <span class="caret" aria-haspopup="menu" role="button"></span>
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> </div>
<span class="sr-only" i18n>Clear filters</span>
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced user filters</h6>
<a [routerLink]="[ '/admin/users/list' ]" [queryParams]="{ 'search': 'banned:true' }" class="dropdown-item" i18n>Banned users</a>
</div>
</div>
<input
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
(keyup)="onUserSearch($event)"
>
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
<span class="sr-only" i18n>Clear filters</span>
</div>
</div> </div>
<a class="ml-2 add-button" routerLink="/admin/users/create"> <a class="ml-2 add-button" routerLink="/admin/users/create">
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon> <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
<ng-container i18n>Create user</ng-container> <ng-container i18n>Create user</ng-container>
@ -70,7 +83,7 @@
alt="Avatar" alt="Avatar"
> >
<div> <div>
<span> <span class="user-table-primary-text">
<span *ngIf="user.blocked" i18n-title title="The user was banned" class="glyphicon glyphicon-ban-circle"></span> <span *ngIf="user.blocked" i18n-title title="The user was banned" class="glyphicon glyphicon-ban-circle"></span>
{{ user.account.displayName }} {{ user.account.displayName }}
</span> </span>

View File

@ -17,6 +17,12 @@ tr.banned > td {
font-weight: $font-semibold; font-weight: $font-semibold;
} }
.user-table-primary-text .glyphicon {
font-size: 80%;
color: gray;
margin-left: 0.1rem;
}
.caption { .caption {
justify-content: space-between; justify-content: space-between;
@ -33,3 +39,14 @@ p-tableCheckbox {
.chip { .chip {
@include chip; @include chip;
} }
.input-group {
@include peertube-input-group(300px);
input {
flex: 1;
}
.dropdown-toggle::after {
margin-left: 0;
}
}

View File

@ -5,6 +5,7 @@ import { Actor, DropdownAction } from '@app/shared/shared-main'
import { UserBanModalComponent } from '@app/shared/shared-moderation' import { UserBanModalComponent } from '@app/shared/shared-moderation'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { ServerConfig, User } from '@shared/models' import { ServerConfig, User } from '@shared/models'
import { Params, Router, ActivatedRoute } from '@angular/router'
@Component({ @Component({
selector: 'my-user-list', selector: 'my-user-list',
@ -30,6 +31,8 @@ export class UserListComponent extends RestTable implements OnInit {
private serverService: ServerService, private serverService: ServerService,
private userService: UserService, private userService: UserService,
private auth: AuthService, private auth: AuthService,
private route: ActivatedRoute,
private router: Router,
private i18n: I18n private i18n: I18n
) { ) {
super() super()
@ -50,6 +53,14 @@ export class UserListComponent extends RestTable implements OnInit {
this.initialize() this.initialize()
this.route.queryParams
.subscribe(params => {
this.search = params.search || ''
this.setTableFilter(this.search)
this.loadData()
})
this.bulkUserActions = [ this.bulkUserActions = [
[ [
{ {
@ -102,6 +113,26 @@ export class UserListComponent extends RestTable implements OnInit {
this.loadData() this.loadData()
} }
/* Table filter functions */
onUserSearch (event: Event) {
this.onSearch(event)
this.setQueryParams((event.target as HTMLInputElement).value)
}
setQueryParams (search: string) {
const queryParams: Params = {}
if (search) Object.assign(queryParams, { search })
this.router.navigate([ '/admin/users/list' ], { queryParams })
}
resetTableFilter () {
this.setTableFilter('')
this.setQueryParams('')
this.resetSearch()
}
/* END Table filter functions */
switchToDefaultAvatar ($event: Event) { switchToDefaultAvatar ($event: Event) {
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
} }
@ -165,14 +196,17 @@ export class UserListComponent extends RestTable implements OnInit {
protected loadData () { protected loadData () {
this.selectedUsers = [] this.selectedUsers = []
this.userService.getUsers(this.pagination, this.sort, this.search) this.userService.getUsers({
.subscribe( pagination: this.pagination,
resultList => { sort: this.sort,
this.users = resultList.data search: this.search
this.totalRecords = resultList.total }).subscribe(
}, resultList => {
this.users = resultList.data
this.totalRecords = resultList.total
},
err => this.notifier.error(err.message) err => this.notifier.error(err.message)
) )
} }
} }

View File

@ -9,11 +9,12 @@ interface QueryStringFilterPrefixes {
prefix: string prefix: string
handler?: (v: string) => string | number handler?: (v: string) => string | number
multiple?: boolean multiple?: boolean
isBoolean?: boolean
} }
} }
type ParseQueryStringFilterResult = { type ParseQueryStringFilterResult = {
[key: string]: string | number | (string | number)[] [key: string]: string | number | boolean | (string | number | boolean)[]
} }
@Injectable() @Injectable()
@ -96,6 +97,7 @@ export class RestService {
return t return t
}) })
.filter(t => !!t || t === 0) .filter(t => !!t || t === 0)
.map(t => prefixObj.isBoolean ? t === 'true' : t)
if (matchedTokens.length === 0) continue if (matchedTokens.length === 0) continue

View File

@ -290,11 +290,32 @@ export class UserService {
}) })
} }
getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<UserServerModel>> { getUsers (parameters: {
pagination: RestPagination
sort: SortMeta
search?: string
}): Observable<ResultList<UserServerModel>> {
const { pagination, sort, search } = parameters
let params = new HttpParams() let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort) params = this.restService.addRestGetParams(params, pagination, sort)
if (search) params = params.append('search', search) if (search) {
const filters = this.restService.parseQueryStringFilter(search, {
blocked: {
prefix: 'banned:',
isBoolean: true,
handler: v => {
if (v === 'true') return v
if (v === 'false') return v
return undefined
}
}
})
params = this.restService.addObjectParams(params, filters)
}
return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params }) return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params })
.pipe( .pipe(

View File

@ -18,6 +18,7 @@ import {
setDefaultPagination, setDefaultPagination,
setDefaultSort, setDefaultSort,
userAutocompleteValidator, userAutocompleteValidator,
usersListValidator,
usersAddValidator, usersAddValidator,
usersGetValidator, usersGetValidator,
usersRegisterValidator, usersRegisterValidator,
@ -85,6 +86,7 @@ usersRouter.get('/',
usersSortValidator, usersSortValidator,
setDefaultSort, setDefaultSort,
setDefaultPagination, setDefaultPagination,
asyncMiddleware(usersListValidator),
asyncMiddleware(listUsers) asyncMiddleware(listUsers)
) )
@ -282,7 +284,13 @@ async function autocompleteUsers (req: express.Request, res: express.Response) {
} }
async function listUsers (req: express.Request, res: express.Response) { async function listUsers (req: express.Request, res: express.Response) {
const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) const resultList = await UserModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
search: req.query.search,
blocked: req.query.blocked
})
return res.json(getFormattedObjects(resultList.data, resultList.total, { withAdminFlags: true })) return res.json(getFormattedObjects(resultList.data, resultList.total, { withAdminFlags: true }))
} }

View File

@ -38,6 +38,21 @@ import { UserRole } from '../../../shared/models/users'
import { MUserDefault } from '@server/types/models' import { MUserDefault } from '@server/types/models'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
const usersListValidator = [
query('blocked')
.optional()
.customSanitizer(toBooleanOrNull)
.isBoolean().withMessage('Should be a valid boolean banned state'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking usersList parameters', { parameters: req.query })
if (areValidationErrors(req, res)) return
return next()
}
]
const usersAddValidator = [ const usersAddValidator = [
body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'), body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
@ -444,6 +459,7 @@ const ensureCanManageUser = [
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
usersListValidator,
usersAddValidator, usersAddValidator,
deleteMeValidator, deleteMeValidator,
usersRegisterValidator, usersRegisterValidator,

View File

@ -412,11 +412,18 @@ export class UserModel extends Model<UserModel> {
return this.count() return this.count()
} }
static listForApi (start: number, count: number, sort: string, search?: string) { static listForApi (parameters: {
let where: WhereOptions start: number
count: number
sort: string
search?: string
blocked?: boolean
}) {
const { start, count, sort, search, blocked } = parameters
const where: WhereOptions = {}
if (search) { if (search) {
where = { Object.assign(where, {
[Op.or]: [ [Op.or]: [
{ {
email: { email: {
@ -429,7 +436,13 @@ export class UserModel extends Model<UserModel> {
} }
} }
] ]
} })
}
if (blocked !== undefined) {
Object.assign(where, {
blocked: blocked
})
} }
const query: FindOptions = { const query: FindOptions = {

View File

@ -819,12 +819,12 @@ describe('Test users', function () {
describe('User blocking', function () { describe('User blocking', function () {
let user16Id let user16Id
let user16AccessToken let user16AccessToken
const user16 = {
username: 'user_16',
password: 'my super password'
}
it('Should block and unblock a user', async function () { it('Should block a user', async function () {
const user16 = {
username: 'user_16',
password: 'my super password'
}
const resUser = await createUser({ const resUser = await createUser({
url: server.url, url: server.url,
accessToken: server.accessToken, accessToken: server.accessToken,
@ -840,7 +840,31 @@ describe('Test users', function () {
await getMyUserInformation(server.url, user16AccessToken, 401) await getMyUserInformation(server.url, user16AccessToken, 401)
await userLogin(server, user16, 400) await userLogin(server, user16, 400)
})
it('Should search user by banned status', async function () {
{
const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', undefined, true)
const users = res.body.data as User[]
expect(res.body.total).to.equal(1)
expect(users.length).to.equal(1)
expect(users[0].username).to.equal(user16.username)
}
{
const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', undefined, false)
const users = res.body.data as User[]
expect(res.body.total).to.equal(1)
expect(users.length).to.equal(1)
expect(users[0].username).to.not.equal(user16.username)
}
})
it('Should unblock a user', async function () {
await unblockUser(server.url, user16Id, server.accessToken) await unblockUser(server.url, user16Id, server.accessToken)
user16AccessToken = await userLogin(server, user16) user16AccessToken = await userLogin(server, user16)
await getMyUserInformation(server.url, user16AccessToken, 200) await getMyUserInformation(server.url, user16AccessToken, 200)

View File

@ -164,14 +164,23 @@ function getUsersList (url: string, accessToken: string) {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) { function getUsersListPaginationAndSort (
url: string,
accessToken: string,
start: number,
count: number,
sort: string,
search?: string,
blocked?: boolean
) {
const path = '/api/v1/users' const path = '/api/v1/users'
const query = { const query = {
start, start,
count, count,
sort, sort,
search search,
blocked
} }
return request(url) return request(url)

View File

@ -518,10 +518,13 @@ paths:
get: get:
summary: List users summary: List users
security: security:
- OAuth2: [] - OAuth2:
- admin
tags: tags:
- Users - Users
parameters: parameters:
- $ref: '#/components/parameters/usersSearch'
- $ref: '#/components/parameters/usersBlocked'
- $ref: '#/components/parameters/start' - $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count' - $ref: '#/components/parameters/count'
- $ref: '#/components/parameters/usersSort' - $ref: '#/components/parameters/usersSort'
@ -3148,6 +3151,13 @@ components:
schema: schema:
type: string type: string
example: -createdAt example: -createdAt
search:
name: search
in: query
required: false
description: Plain text search, applied to various parts of the model depending on endpoint
schema:
type: string
searchTarget: searchTarget:
name: searchTarget name: searchTarget
in: query in: query
@ -3224,6 +3234,20 @@ components:
- -dislikes - -dislikes
- -uuid - -uuid
- -createdAt - -createdAt
usersSearch:
name: search
in: query
required: false
description: Plain text search that will match with user usernames or emails
schema:
type: string
usersBlocked:
name: blocked
in: query
required: false
description: Filter results down to (un)banned users
schema:
type: boolean
usersSort: usersSort:
name: sort name: sort
in: query in: query