aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html29
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss17
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts50
-rw-r--r--client/src/app/core/rest/rest.service.ts4
-rw-r--r--client/src/app/core/users/user.service.ts25
-rw-r--r--server/controllers/api/users/index.ts10
-rw-r--r--server/middlewares/validators/users.ts16
-rw-r--r--server/models/account/user.ts21
-rw-r--r--server/tests/api/users/users.ts34
-rw-r--r--shared/extra-utils/users/users.ts13
-rw-r--r--support/doc/api/openapi.yaml26
11 files changed, 213 insertions, 32 deletions
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 27d4a5787..9580a3c8a 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -16,14 +16,27 @@
16 </my-action-dropdown> 16 </my-action-dropdown>
17 </div> 17 </div>
18 18
19 <div class="ml-auto has-feedback has-clear"> 19 <div class="ml-auto">
20 <input 20 <div class="input-group has-feedback has-clear">
21 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." 21 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
22 (keyup)="onSearch($event)" 22 <div class="input-group-text" ngbDropdownToggle>
23 > 23 <span class="caret" aria-haspopup="menu" role="button"></span>
24 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> 24 </div>
25 <span class="sr-only" i18n>Clear filters</span> 25
26 <div role="menu" ngbDropdownMenu>
27 <h6 class="dropdown-header" i18n>Advanced user filters</h6>
28 <a [routerLink]="[ '/admin/users/list' ]" [queryParams]="{ 'search': 'banned:true' }" class="dropdown-item" i18n>Banned users</a>
29 </div>
30 </div>
31 <input
32 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
33 (keyup)="onUserSearch($event)"
34 >
35 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
36 <span class="sr-only" i18n>Clear filters</span>
37 </div>
26 </div> 38 </div>
39
27 <a class="ml-2 add-button" routerLink="/admin/users/create"> 40 <a class="ml-2 add-button" routerLink="/admin/users/create">
28 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon> 41 <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
29 <ng-container i18n>Create user</ng-container> 42 <ng-container i18n>Create user</ng-container>
@@ -70,7 +83,7 @@
70 alt="Avatar" 83 alt="Avatar"
71 > 84 >
72 <div> 85 <div>
73 <span> 86 <span class="user-table-primary-text">
74 <span *ngIf="user.blocked" i18n-title title="The user was banned" class="glyphicon glyphicon-ban-circle"></span> 87 <span *ngIf="user.blocked" i18n-title title="The user was banned" class="glyphicon glyphicon-ban-circle"></span>
75 {{ user.account.displayName }} 88 {{ user.account.displayName }}
76 </span> 89 </span>
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 {
17 font-weight: $font-semibold; 17 font-weight: $font-semibold;
18} 18}
19 19
20.user-table-primary-text .glyphicon {
21 font-size: 80%;
22 color: gray;
23 margin-left: 0.1rem;
24}
25
20.caption { 26.caption {
21 justify-content: space-between; 27 justify-content: space-between;
22 28
@@ -33,3 +39,14 @@ p-tableCheckbox {
33.chip { 39.chip {
34 @include chip; 40 @include chip;
35} 41}
42
43.input-group {
44 @include peertube-input-group(300px);
45 input {
46 flex: 1;
47 }
48
49 .dropdown-toggle::after {
50 margin-left: 0;
51 }
52}
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'
5import { UserBanModalComponent } from '@app/shared/shared-moderation' 5import { UserBanModalComponent } from '@app/shared/shared-moderation'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { ServerConfig, User } from '@shared/models' 7import { ServerConfig, User } from '@shared/models'
8import { Params, Router, ActivatedRoute } from '@angular/router'
8 9
9@Component({ 10@Component({
10 selector: 'my-user-list', 11 selector: 'my-user-list',
@@ -30,6 +31,8 @@ export class UserListComponent extends RestTable implements OnInit {
30 private serverService: ServerService, 31 private serverService: ServerService,
31 private userService: UserService, 32 private userService: UserService,
32 private auth: AuthService, 33 private auth: AuthService,
34 private route: ActivatedRoute,
35 private router: Router,
33 private i18n: I18n 36 private i18n: I18n
34 ) { 37 ) {
35 super() 38 super()
@@ -50,6 +53,14 @@ export class UserListComponent extends RestTable implements OnInit {
50 53
51 this.initialize() 54 this.initialize()
52 55
56 this.route.queryParams
57 .subscribe(params => {
58 this.search = params.search || ''
59
60 this.setTableFilter(this.search)
61 this.loadData()
62 })
63
53 this.bulkUserActions = [ 64 this.bulkUserActions = [
54 [ 65 [
55 { 66 {
@@ -102,6 +113,26 @@ export class UserListComponent extends RestTable implements OnInit {
102 this.loadData() 113 this.loadData()
103 } 114 }
104 115
116 /* Table filter functions */
117 onUserSearch (event: Event) {
118 this.onSearch(event)
119 this.setQueryParams((event.target as HTMLInputElement).value)
120 }
121
122 setQueryParams (search: string) {
123 const queryParams: Params = {}
124 if (search) Object.assign(queryParams, { search })
125
126 this.router.navigate([ '/admin/users/list' ], { queryParams })
127 }
128
129 resetTableFilter () {
130 this.setTableFilter('')
131 this.setQueryParams('')
132 this.resetSearch()
133 }
134 /* END Table filter functions */
135
105 switchToDefaultAvatar ($event: Event) { 136 switchToDefaultAvatar ($event: Event) {
106 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL() 137 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
107 } 138 }
@@ -165,14 +196,17 @@ export class UserListComponent extends RestTable implements OnInit {
165 protected loadData () { 196 protected loadData () {
166 this.selectedUsers = [] 197 this.selectedUsers = []
167 198
168 this.userService.getUsers(this.pagination, this.sort, this.search) 199 this.userService.getUsers({
169 .subscribe( 200 pagination: this.pagination,
170 resultList => { 201 sort: this.sort,
171 this.users = resultList.data 202 search: this.search
172 this.totalRecords = resultList.total 203 }).subscribe(
173 }, 204 resultList => {
205 this.users = resultList.data
206 this.totalRecords = resultList.total
207 },
174 208
175 err => this.notifier.error(err.message) 209 err => this.notifier.error(err.message)
176 ) 210 )
177 } 211 }
178} 212}
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 {
9 prefix: string 9 prefix: string
10 handler?: (v: string) => string | number 10 handler?: (v: string) => string | number
11 multiple?: boolean 11 multiple?: boolean
12 isBoolean?: boolean
12 } 13 }
13} 14}
14 15
15type ParseQueryStringFilterResult = { 16type ParseQueryStringFilterResult = {
16 [key: string]: string | number | (string | number)[] 17 [key: string]: string | number | boolean | (string | number | boolean)[]
17} 18}
18 19
19@Injectable() 20@Injectable()
@@ -96,6 +97,7 @@ export class RestService {
96 return t 97 return t
97 }) 98 })
98 .filter(t => !!t || t === 0) 99 .filter(t => !!t || t === 0)
100 .map(t => prefixObj.isBoolean ? t === 'true' : t)
99 101
100 if (matchedTokens.length === 0) continue 102 if (matchedTokens.length === 0) continue
101 103
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 {
290 }) 290 })
291 } 291 }
292 292
293 getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<UserServerModel>> { 293 getUsers (parameters: {
294 pagination: RestPagination
295 sort: SortMeta
296 search?: string
297 }): Observable<ResultList<UserServerModel>> {
298 const { pagination, sort, search } = parameters
299
294 let params = new HttpParams() 300 let params = new HttpParams()
295 params = this.restService.addRestGetParams(params, pagination, sort) 301 params = this.restService.addRestGetParams(params, pagination, sort)
296 302
297 if (search) params = params.append('search', search) 303 if (search) {
304 const filters = this.restService.parseQueryStringFilter(search, {
305 blocked: {
306 prefix: 'banned:',
307 isBoolean: true,
308 handler: v => {
309 if (v === 'true') return v
310 if (v === 'false') return v
311
312 return undefined
313 }
314 }
315 })
316
317 params = this.restService.addObjectParams(params, filters)
318 }
298 319
299 return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params }) 320 return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params })
300 .pipe( 321 .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 {
18 setDefaultPagination, 18 setDefaultPagination,
19 setDefaultSort, 19 setDefaultSort,
20 userAutocompleteValidator, 20 userAutocompleteValidator,
21 usersListValidator,
21 usersAddValidator, 22 usersAddValidator,
22 usersGetValidator, 23 usersGetValidator,
23 usersRegisterValidator, 24 usersRegisterValidator,
@@ -85,6 +86,7 @@ usersRouter.get('/',
85 usersSortValidator, 86 usersSortValidator,
86 setDefaultSort, 87 setDefaultSort,
87 setDefaultPagination, 88 setDefaultPagination,
89 asyncMiddleware(usersListValidator),
88 asyncMiddleware(listUsers) 90 asyncMiddleware(listUsers)
89) 91)
90 92
@@ -282,7 +284,13 @@ async function autocompleteUsers (req: express.Request, res: express.Response) {
282} 284}
283 285
284async function listUsers (req: express.Request, res: express.Response) { 286async function listUsers (req: express.Request, res: express.Response) {
285 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) 287 const resultList = await UserModel.listForApi({
288 start: req.query.start,
289 count: req.query.count,
290 sort: req.query.sort,
291 search: req.query.search,
292 blocked: req.query.blocked
293 })
286 294
287 return res.json(getFormattedObjects(resultList.data, resultList.total, { withAdminFlags: true })) 295 return res.json(getFormattedObjects(resultList.data, resultList.total, { withAdminFlags: true }))
288} 296}
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'
38import { MUserDefault } from '@server/types/models' 38import { MUserDefault } from '@server/types/models'
39import { Hooks } from '@server/lib/plugins/hooks' 39import { Hooks } from '@server/lib/plugins/hooks'
40 40
41const usersListValidator = [
42 query('blocked')
43 .optional()
44 .customSanitizer(toBooleanOrNull)
45 .isBoolean().withMessage('Should be a valid boolean banned state'),
46
47 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
48 logger.debug('Checking usersList parameters', { parameters: req.query })
49
50 if (areValidationErrors(req, res)) return
51
52 return next()
53 }
54]
55
41const usersAddValidator = [ 56const usersAddValidator = [
42 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), 57 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
43 body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'), 58 body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
@@ -444,6 +459,7 @@ const ensureCanManageUser = [
444// --------------------------------------------------------------------------- 459// ---------------------------------------------------------------------------
445 460
446export { 461export {
462 usersListValidator,
447 usersAddValidator, 463 usersAddValidator,
448 deleteMeValidator, 464 deleteMeValidator,
449 usersRegisterValidator, 465 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<UserModel> {
412 return this.count() 412 return this.count()
413 } 413 }
414 414
415 static listForApi (start: number, count: number, sort: string, search?: string) { 415 static listForApi (parameters: {
416 let where: WhereOptions 416 start: number
417 count: number
418 sort: string
419 search?: string
420 blocked?: boolean
421 }) {
422 const { start, count, sort, search, blocked } = parameters
423 const where: WhereOptions = {}
417 424
418 if (search) { 425 if (search) {
419 where = { 426 Object.assign(where, {
420 [Op.or]: [ 427 [Op.or]: [
421 { 428 {
422 email: { 429 email: {
@@ -429,7 +436,13 @@ export class UserModel extends Model<UserModel> {
429 } 436 }
430 } 437 }
431 ] 438 ]
432 } 439 })
440 }
441
442 if (blocked !== undefined) {
443 Object.assign(where, {
444 blocked: blocked
445 })
433 } 446 }
434 447
435 const query: FindOptions = { 448 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 () {
819 describe('User blocking', function () { 819 describe('User blocking', function () {
820 let user16Id 820 let user16Id
821 let user16AccessToken 821 let user16AccessToken
822 const user16 = {
823 username: 'user_16',
824 password: 'my super password'
825 }
822 826
823 it('Should block and unblock a user', async function () { 827 it('Should block a user', async function () {
824 const user16 = {
825 username: 'user_16',
826 password: 'my super password'
827 }
828 const resUser = await createUser({ 828 const resUser = await createUser({
829 url: server.url, 829 url: server.url,
830 accessToken: server.accessToken, 830 accessToken: server.accessToken,
@@ -840,7 +840,31 @@ describe('Test users', function () {
840 840
841 await getMyUserInformation(server.url, user16AccessToken, 401) 841 await getMyUserInformation(server.url, user16AccessToken, 401)
842 await userLogin(server, user16, 400) 842 await userLogin(server, user16, 400)
843 })
844
845 it('Should search user by banned status', async function () {
846 {
847 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', undefined, true)
848 const users = res.body.data as User[]
849
850 expect(res.body.total).to.equal(1)
851 expect(users.length).to.equal(1)
852
853 expect(users[0].username).to.equal(user16.username)
854 }
855
856 {
857 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', undefined, false)
858 const users = res.body.data as User[]
859
860 expect(res.body.total).to.equal(1)
861 expect(users.length).to.equal(1)
862
863 expect(users[0].username).to.not.equal(user16.username)
864 }
865 })
843 866
867 it('Should unblock a user', async function () {
844 await unblockUser(server.url, user16Id, server.accessToken) 868 await unblockUser(server.url, user16Id, server.accessToken)
845 user16AccessToken = await userLogin(server, user16) 869 user16AccessToken = await userLogin(server, user16)
846 await getMyUserInformation(server.url, user16AccessToken, 200) 870 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) {
164 .expect('Content-Type', /json/) 164 .expect('Content-Type', /json/)
165} 165}
166 166
167function getUsersListPaginationAndSort (url: string, accessToken: string, start: number, count: number, sort: string, search?: string) { 167function getUsersListPaginationAndSort (
168 url: string,
169 accessToken: string,
170 start: number,
171 count: number,
172 sort: string,
173 search?: string,
174 blocked?: boolean
175) {
168 const path = '/api/v1/users' 176 const path = '/api/v1/users'
169 177
170 const query = { 178 const query = {
171 start, 179 start,
172 count, 180 count,
173 sort, 181 sort,
174 search 182 search,
183 blocked
175 } 184 }
176 185
177 return request(url) 186 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:
518 get: 518 get:
519 summary: List users 519 summary: List users
520 security: 520 security:
521 - OAuth2: [] 521 - OAuth2:
522 - admin
522 tags: 523 tags:
523 - Users 524 - Users
524 parameters: 525 parameters:
526 - $ref: '#/components/parameters/usersSearch'
527 - $ref: '#/components/parameters/usersBlocked'
525 - $ref: '#/components/parameters/start' 528 - $ref: '#/components/parameters/start'
526 - $ref: '#/components/parameters/count' 529 - $ref: '#/components/parameters/count'
527 - $ref: '#/components/parameters/usersSort' 530 - $ref: '#/components/parameters/usersSort'
@@ -3148,6 +3151,13 @@ components:
3148 schema: 3151 schema:
3149 type: string 3152 type: string
3150 example: -createdAt 3153 example: -createdAt
3154 search:
3155 name: search
3156 in: query
3157 required: false
3158 description: Plain text search, applied to various parts of the model depending on endpoint
3159 schema:
3160 type: string
3151 searchTarget: 3161 searchTarget:
3152 name: searchTarget 3162 name: searchTarget
3153 in: query 3163 in: query
@@ -3224,6 +3234,20 @@ components:
3224 - -dislikes 3234 - -dislikes
3225 - -uuid 3235 - -uuid
3226 - -createdAt 3236 - -createdAt
3237 usersSearch:
3238 name: search
3239 in: query
3240 required: false
3241 description: Plain text search that will match with user usernames or emails
3242 schema:
3243 type: string
3244 usersBlocked:
3245 name: blocked
3246 in: query
3247 required: false
3248 description: Filter results down to (un)banned users
3249 schema:
3250 type: boolean
3227 usersSort: 3251 usersSort:
3228 name: sort 3252 name: sort
3229 in: query 3253 in: query