parent
2b587cad93
commit
8491293b02
|
@ -16,14 +16,27 @@
|
||||||
</my-action-dropdown>
|
</my-action-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-auto has-feedback has-clear">
|
<div class="ml-auto">
|
||||||
|
<div class="input-group has-feedback has-clear">
|
||||||
|
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
|
||||||
|
<div class="input-group-text" ngbDropdownToggle>
|
||||||
|
<span class="caret" aria-haspopup="menu" role="button"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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
|
<input
|
||||||
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||||
(keyup)="onSearch($event)"
|
(keyup)="onUserSearch($event)"
|
||||||
>
|
>
|
||||||
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
|
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
|
||||||
<span class="sr-only" i18n>Clear filters</span>
|
<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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,8 +196,11 @@ 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,
|
||||||
|
sort: this.sort,
|
||||||
|
search: this.search
|
||||||
|
}).subscribe(
|
||||||
resultList => {
|
resultList => {
|
||||||
this.users = resultList.data
|
this.users = resultList.data
|
||||||
this.totalRecords = resultList.total
|
this.totalRecords = resultList.total
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 }))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -819,12 +819,12 @@ describe('Test users', function () {
|
||||||
describe('User blocking', function () {
|
describe('User blocking', function () {
|
||||||
let user16Id
|
let user16Id
|
||||||
let user16AccessToken
|
let user16AccessToken
|
||||||
|
|
||||||
it('Should block and unblock a user', async function () {
|
|
||||||
const user16 = {
|
const user16 = {
|
||||||
username: 'user_16',
|
username: 'user_16',
|
||||||
password: 'my super password'
|
password: 'my super password'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it('Should block a user', async function () {
|
||||||
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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue