Create user
@@ -70,7 +83,7 @@
alt="Avatar"
>
-
+
{{ user.account.displayName }}
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss
index 697b2c11b..2b84dec75 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/users/user-list/user-list.component.scss
@@ -17,6 +17,12 @@ tr.banned > td {
font-weight: $font-semibold;
}
+.user-table-primary-text .glyphicon {
+ font-size: 80%;
+ color: gray;
+ margin-left: 0.1rem;
+}
+
.caption {
justify-content: space-between;
@@ -33,3 +39,14 @@ p-tableCheckbox {
.chip {
@include chip;
}
+
+.input-group {
+ @include peertube-input-group(300px);
+ input {
+ flex: 1;
+ }
+
+ .dropdown-toggle::after {
+ margin-left: 0;
+ }
+}
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts
index 8f01c7d51..0b72b07c1 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/users/user-list/user-list.component.ts
@@ -5,6 +5,7 @@ import { Actor, DropdownAction } from '@app/shared/shared-main'
import { UserBanModalComponent } from '@app/shared/shared-moderation'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ServerConfig, User } from '@shared/models'
+import { Params, Router, ActivatedRoute } from '@angular/router'
@Component({
selector: 'my-user-list',
@@ -30,6 +31,8 @@ export class UserListComponent extends RestTable implements OnInit {
private serverService: ServerService,
private userService: UserService,
private auth: AuthService,
+ private route: ActivatedRoute,
+ private router: Router,
private i18n: I18n
) {
super()
@@ -50,6 +53,14 @@ export class UserListComponent extends RestTable implements OnInit {
this.initialize()
+ this.route.queryParams
+ .subscribe(params => {
+ this.search = params.search || ''
+
+ this.setTableFilter(this.search)
+ this.loadData()
+ })
+
this.bulkUserActions = [
[
{
@@ -102,6 +113,26 @@ export class UserListComponent extends RestTable implements OnInit {
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) {
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
}
@@ -165,14 +196,17 @@ export class UserListComponent extends RestTable implements OnInit {
protected loadData () {
this.selectedUsers = []
- this.userService.getUsers(this.pagination, this.sort, this.search)
- .subscribe(
- resultList => {
- this.users = resultList.data
- this.totalRecords = resultList.total
- },
+ this.userService.getUsers({
+ pagination: this.pagination,
+ sort: this.sort,
+ search: this.search
+ }).subscribe(
+ resultList => {
+ this.users = resultList.data
+ this.totalRecords = resultList.total
+ },
- err => this.notifier.error(err.message)
- )
+ err => this.notifier.error(err.message)
+ )
}
}
diff --git a/client/src/app/core/rest/rest.service.ts b/client/src/app/core/rest/rest.service.ts
index c12b6bd41..9e32c6d58 100644
--- a/client/src/app/core/rest/rest.service.ts
+++ b/client/src/app/core/rest/rest.service.ts
@@ -9,11 +9,12 @@ interface QueryStringFilterPrefixes {
prefix: string
handler?: (v: string) => string | number
multiple?: boolean
+ isBoolean?: boolean
}
}
type ParseQueryStringFilterResult = {
- [key: string]: string | number | (string | number)[]
+ [key: string]: string | number | boolean | (string | number | boolean)[]
}
@Injectable()
@@ -96,6 +97,7 @@ export class RestService {
return t
})
.filter(t => !!t || t === 0)
+ .map(t => prefixObj.isBoolean ? t === 'true' : t)
if (matchedTokens.length === 0) continue
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts
index ab395b1f9..2c817d45e 100644
--- a/client/src/app/core/users/user.service.ts
+++ b/client/src/app/core/users/user.service.ts
@@ -290,11 +290,32 @@ export class UserService {
})
}
- getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable> {
+ getUsers (parameters: {
+ pagination: RestPagination
+ sort: SortMeta
+ search?: string
+ }): Observable> {
+ const { pagination, sort, search } = parameters
+
let params = new HttpParams()
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>(UserService.BASE_USERS_URL, { params })
.pipe(
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index c8e9eaeaa..839431afb 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -18,6 +18,7 @@ import {
setDefaultPagination,
setDefaultSort,
userAutocompleteValidator,
+ usersListValidator,
usersAddValidator,
usersGetValidator,
usersRegisterValidator,
@@ -85,6 +86,7 @@ usersRouter.get('/',
usersSortValidator,
setDefaultSort,
setDefaultPagination,
+ asyncMiddleware(usersListValidator),
asyncMiddleware(listUsers)
)
@@ -282,7 +284,13 @@ async function autocompleteUsers (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 }))
}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 4a9ed6830..6860a3bed 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -38,6 +38,21 @@ import { UserRole } from '../../../shared/models/users'
import { MUserDefault } from '@server/types/models'
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 = [
body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
@@ -444,6 +459,7 @@ const ensureCanManageUser = [
// ---------------------------------------------------------------------------
export {
+ usersListValidator,
usersAddValidator,
deleteMeValidator,
usersRegisterValidator,
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 3bde1e744..de193131a 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -412,11 +412,18 @@ export class UserModel extends Model {
return this.count()
}
- static listForApi (start: number, count: number, sort: string, search?: string) {
- let where: WhereOptions
+ static listForApi (parameters: {
+ start: number
+ count: number
+ sort: string
+ search?: string
+ blocked?: boolean
+ }) {
+ const { start, count, sort, search, blocked } = parameters
+ const where: WhereOptions = {}
if (search) {
- where = {
+ Object.assign(where, {
[Op.or]: [
{
email: {
@@ -429,7 +436,13 @@ export class UserModel extends Model {
}
}
]
- }
+ })
+ }
+
+ if (blocked !== undefined) {
+ Object.assign(where, {
+ blocked: blocked
+ })
}
const query: FindOptions = {
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index cad954fcb..0a66bd1ce 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -819,12 +819,12 @@ describe('Test users', function () {
describe('User blocking', function () {
let user16Id
let user16AccessToken
+ const user16 = {
+ username: 'user_16',
+ password: 'my super password'
+ }
- it('Should block and unblock a user', async function () {
- const user16 = {
- username: 'user_16',
- password: 'my super password'
- }
+ it('Should block a user', async function () {
const resUser = await createUser({
url: server.url,
accessToken: server.accessToken,
@@ -840,7 +840,31 @@ describe('Test users', function () {
await getMyUserInformation(server.url, user16AccessToken, 401)
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)
user16AccessToken = await userLogin(server, user16)
await getMyUserInformation(server.url, user16AccessToken, 200)
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
index 08b7743a6..9f193680d 100644
--- a/shared/extra-utils/users/users.ts
+++ b/shared/extra-utils/users/users.ts
@@ -164,14 +164,23 @@ function getUsersList (url: string, accessToken: string) {
.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 query = {
start,
count,
sort,
- search
+ search,
+ blocked
}
return request(url)
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 2fc55b832..3c22a297f 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -518,10 +518,13 @@ paths:
get:
summary: List users
security:
- - OAuth2: []
+ - OAuth2:
+ - admin
tags:
- Users
parameters:
+ - $ref: '#/components/parameters/usersSearch'
+ - $ref: '#/components/parameters/usersBlocked'
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
- $ref: '#/components/parameters/usersSort'
@@ -3148,6 +3151,13 @@ components:
schema:
type: string
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:
name: searchTarget
in: query
@@ -3224,6 +3234,20 @@ components:
- -dislikes
- -uuid
- -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:
name: sort
in: query