mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-05-16 03:22:41 +00:00
Merge 88a926528c
into bfc21e4850
This commit is contained in:
commit
5a4714a605
|
@ -19,11 +19,13 @@
|
|||
//
|
||||
// View + page through known accounts according to given filters.
|
||||
//
|
||||
// Returned accounts will be ordered alphabetically (a-z) by domain + username.
|
||||
//
|
||||
// The next and previous queries can be parsed from the returned Link header.
|
||||
// Example:
|
||||
//
|
||||
// ```
|
||||
// <https://example.org/api/v1/admin/accounts?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/accounts?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
|
||||
// <https://example.org/api/v1/admin/accounts?limit=80&max_id=example.org%2F%40someone>; rel="next", <https://example.org/api/v1/admin/accounts?limit=80&min_id=example.org%2F%40someone_else>; rel="prev"
|
||||
// ````
|
||||
//
|
||||
// ---
|
||||
|
@ -117,23 +119,41 @@
|
|||
// name: max_id
|
||||
// in: query
|
||||
// type: string
|
||||
// description: All results returned will be older than the item with this ID.
|
||||
// description: >-
|
||||
// max_id in the form `[domain]/@[username]`.
|
||||
// All results returned will be later in the alphabet than `[domain]/@[username]`.
|
||||
// For example, if max_id = `example.org/@someone` then returned entries might
|
||||
// contain `example.org/@someone_else`, `later.example.org/@someone`, etc.
|
||||
// Local account IDs in this form use an empty string for the `[domain]` part,
|
||||
// for example local account with username `someone` would be `/@someone`.
|
||||
// -
|
||||
// name: since_id
|
||||
// in: query
|
||||
// type: string
|
||||
// description: All results returned will be newer than the item with this ID.
|
||||
// description: >-
|
||||
// since_id in the form `[domain]/@[username]`.
|
||||
// All results returned will be earlier in the alphabet than `[domain]/@[username]`.
|
||||
// For example, if since_id = `example.org/@someone` then returned entries might
|
||||
// contain `example.org/@earlier_account`, `earlier.example.org/@someone`, etc.
|
||||
// Local account IDs in this form use an empty string for the `[domain]` part,
|
||||
// for example local account with username `someone` would be `/@someone`.
|
||||
// -
|
||||
// name: min_id
|
||||
// in: query
|
||||
// type: string
|
||||
// description: Returns results immediately newer than the item with this ID.
|
||||
// description: >-
|
||||
// min_id in the form `[domain]/@[username]`.
|
||||
// All results returned will be earlier in the alphabet than `[domain]/@[username]`.
|
||||
// For example, if min_id = `example.org/@someone` then returned entries might
|
||||
// contain `example.org/@earlier_account`, `earlier.example.org/@someone`, etc.
|
||||
// Local account IDs in this form use an empty string for the `[domain]` part,
|
||||
// for example local account with username `someone` would be `/@someone`.
|
||||
// -
|
||||
// name: limit
|
||||
// in: query
|
||||
// type: integer
|
||||
// description: Maximum number of results to return.
|
||||
// default: 100
|
||||
// default: 50
|
||||
// maximum: 200
|
||||
// minimum: 1
|
||||
//
|
||||
|
@ -200,7 +220,7 @@ func (m *Module) AccountsGETV1Handler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 100)
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 50)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
|
|
@ -19,11 +19,13 @@
|
|||
//
|
||||
// View + page through known accounts according to given filters.
|
||||
//
|
||||
// Returned accounts will be ordered alphabetically (a-z) by domain + username.
|
||||
//
|
||||
// The next and previous queries can be parsed from the returned Link header.
|
||||
// Example:
|
||||
//
|
||||
// ```
|
||||
// <https://example.org/api/v2/admin/accounts?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v2/admin/accounts?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
|
||||
// <https://example.org/api/v2/admin/accounts?limit=80&max_id=example.org%2F%40someone>; rel="next", <https://example.org/api/v2/admin/accounts?limit=80&min_id=example.org%2F%40someone_else>; rel="prev"
|
||||
// ````
|
||||
//
|
||||
// ---
|
||||
|
@ -90,23 +92,41 @@
|
|||
// name: max_id
|
||||
// in: query
|
||||
// type: string
|
||||
// description: All results returned will be older than the item with this ID.
|
||||
// description: >-
|
||||
// max_id in the form `[domain]/@[username]`.
|
||||
// All results returned will be later in the alphabet than `[domain]/@[username]`.
|
||||
// For example, if max_id = `example.org/@someone` then returned entries might
|
||||
// contain `example.org/@someone_else`, `later.example.org/@someone`, etc.
|
||||
// Local account IDs in this form use an empty string for the `[domain]` part,
|
||||
// for example local account with username `someone` would be `/@someone`.
|
||||
// -
|
||||
// name: since_id
|
||||
// in: query
|
||||
// type: string
|
||||
// description: All results returned will be newer than the item with this ID.
|
||||
// description: >-
|
||||
// since_id in the form `[domain]/@[username]`.
|
||||
// All results returned will be earlier in the alphabet than `[domain]/@[username]`.
|
||||
// For example, if since_id = `example.org/@someone` then returned entries might
|
||||
// contain `example.org/@earlier_account`, `earlier.example.org/@someone`, etc.
|
||||
// Local account IDs in this form use an empty string for the `[domain]` part,
|
||||
// for example local account with username `someone` would be `/@someone`.
|
||||
// -
|
||||
// name: min_id
|
||||
// in: query
|
||||
// type: string
|
||||
// description: Returns results immediately newer than the item with this ID.
|
||||
// description: >-
|
||||
// min_id in the form `[domain]/@[username]`.
|
||||
// All results returned will be earlier in the alphabet than `[domain]/@[username]`.
|
||||
// For example, if min_id = `example.org/@someone` then returned entries might
|
||||
// contain `example.org/@earlier_account`, `earlier.example.org/@someone`, etc.
|
||||
// Local account IDs in this form use an empty string for the `[domain]` part,
|
||||
// for example local account with username `someone` would be `/@someone`.
|
||||
// -
|
||||
// name: limit
|
||||
// in: query
|
||||
// type: integer
|
||||
// description: Maximum number of results to return.
|
||||
// default: 100
|
||||
// default: 50
|
||||
// maximum: 200
|
||||
// minimum: 1
|
||||
//
|
||||
|
@ -173,7 +193,7 @@ func (m *Module) AccountsGETV2Handler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 100)
|
||||
page, errWithCode := paging.ParseIDPage(c, 1, 200, 50)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
|
465
internal/api/client/admin/accountsgetv2_test.go
Normal file
465
internal/api/client/admin/accountsgetv2_test.go
Normal file
|
@ -0,0 +1,465 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package admin_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
|
||||
)
|
||||
|
||||
type AccountsGetTestSuite struct {
|
||||
AdminStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
path := admin.AccountsV2Path
|
||||
ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json")
|
||||
|
||||
suite.adminModule.AccountsGETV2Handler(ctx)
|
||||
suite.Equal(http.StatusOK, recorder.Code)
|
||||
|
||||
b, err := io.ReadAll(recorder.Body)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotNil(b)
|
||||
|
||||
dst := new(bytes.Buffer)
|
||||
err = json.Indent(dst, b, "", " ")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
link := recorder.Header().Get("Link")
|
||||
suite.Equal(`<http://localhost:8080/api/v2/admin/accounts?limit=50&max_id=xn--xample-ova.org%2F%40%C3%BCser>; rel="next", <http://localhost:8080/api/v2/admin/accounts?limit=50&since_id=%2F%401happyturtle>; rel="prev"`, link)
|
||||
|
||||
suite.Equal(`[
|
||||
{
|
||||
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"username": "1happyturtle",
|
||||
"domain": null,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"email": "tortle.dude@example.org",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"name": "user"
|
||||
},
|
||||
"confirmed": true,
|
||||
"approved": true,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"username": "1happyturtle",
|
||||
"acct": "1happyturtle",
|
||||
"display_name": "happy little turtle :3",
|
||||
"locked": true,
|
||||
"discoverable": false,
|
||||
"bot": false,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"note": "<p>i post about things that concern me</p>",
|
||||
"url": "http://localhost:8080/@1happyturtle",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.png",
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28T08:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
"name": "should you follow me?",
|
||||
"value": "maybe!",
|
||||
"verified_at": null
|
||||
},
|
||||
{
|
||||
"name": "age",
|
||||
"value": "120",
|
||||
"verified_at": null
|
||||
}
|
||||
],
|
||||
"hide_collections": true,
|
||||
"role": {
|
||||
"name": "user"
|
||||
}
|
||||
},
|
||||
"created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
|
||||
},
|
||||
{
|
||||
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
"username": "admin",
|
||||
"domain": null,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"email": "admin@example.org",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"name": "admin"
|
||||
},
|
||||
"confirmed": true,
|
||||
"approved": true,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
"username": "admin",
|
||||
"acct": "admin",
|
||||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@admin",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.png",
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
"role": {
|
||||
"name": "admin"
|
||||
}
|
||||
},
|
||||
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
|
||||
},
|
||||
{
|
||||
"id": "01AY6P665V14JJR0AFVRT7311Y",
|
||||
"username": "localhost:8080",
|
||||
"domain": null,
|
||||
"created_at": "2020-05-17T13:10:59.000Z",
|
||||
"email": "",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"name": "user"
|
||||
},
|
||||
"confirmed": false,
|
||||
"approved": false,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01AY6P665V14JJR0AFVRT7311Y",
|
||||
"username": "localhost:8080",
|
||||
"acct": "localhost:8080",
|
||||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"created_at": "2020-05-17T13:10:59.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@localhost:8080",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.png",
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"last_status_at": null,
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
"username": "the_mighty_zork",
|
||||
"domain": null,
|
||||
"created_at": "2022-05-20T11:09:18.000Z",
|
||||
"email": "zork@example.org",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": "I wanna be on this damned webbed site so bad! Please! Wow",
|
||||
"role": {
|
||||
"name": "user"
|
||||
},
|
||||
"confirmed": true,
|
||||
"approved": true,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
"username": "the_mighty_zork",
|
||||
"acct": "the_mighty_zork",
|
||||
"display_name": "original zork (he/they)",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"created_at": "2022-05-20T11:09:18.000Z",
|
||||
"note": "<p>hey yo this is my profile!</p>",
|
||||
"url": "http://localhost:8080/@the_mighty_zork",
|
||||
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 7,
|
||||
"last_status_at": "2023-12-10T09:24:00.000Z",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true,
|
||||
"role": {
|
||||
"name": "user"
|
||||
}
|
||||
},
|
||||
"created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
|
||||
},
|
||||
{
|
||||
"id": "01F8MH0BBE4FHXPH513MBVFHB0",
|
||||
"username": "weed_lord420",
|
||||
"domain": null,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"email": "weed_lord420@example.org",
|
||||
"ip": "199.222.111.89",
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.",
|
||||
"role": {
|
||||
"name": "user"
|
||||
},
|
||||
"confirmed": false,
|
||||
"approved": false,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01F8MH0BBE4FHXPH513MBVFHB0",
|
||||
"username": "weed_lord420",
|
||||
"acct": "weed_lord420",
|
||||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": false,
|
||||
"bot": false,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"note": "",
|
||||
"url": "http://localhost:8080/@weed_lord420",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.png",
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"last_status_at": null,
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"role": {
|
||||
"name": "user"
|
||||
}
|
||||
},
|
||||
"created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
|
||||
},
|
||||
{
|
||||
"id": "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
"username": "Some_User",
|
||||
"domain": "example.org",
|
||||
"created_at": "2020-08-10T12:13:28.000Z",
|
||||
"email": "",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"name": "user"
|
||||
},
|
||||
"confirmed": false,
|
||||
"approved": false,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
"username": "Some_User",
|
||||
"acct": "Some_User@example.org",
|
||||
"display_name": "some user",
|
||||
"locked": true,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"created_at": "2020-08-10T12:13:28.000Z",
|
||||
"note": "i'm a real son of a gun",
|
||||
"url": "http://example.org/@Some_User",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.png",
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 1,
|
||||
"last_status_at": "2023-11-02T10:44:25.000Z",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||
"username": "foss_satan",
|
||||
"domain": "fossbros-anonymous.io",
|
||||
"created_at": "2021-09-26T10:52:36.000Z",
|
||||
"email": "",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"name": "user"
|
||||
},
|
||||
"confirmed": false,
|
||||
"approved": false,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||
"username": "foss_satan",
|
||||
"acct": "foss_satan@fossbros-anonymous.io",
|
||||
"display_name": "big gerald",
|
||||
"locked": false,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"created_at": "2021-09-26T10:52:36.000Z",
|
||||
"note": "i post about like, i dunno, stuff, or whatever!!!!",
|
||||
"url": "http://fossbros-anonymous.io/@foss_satan",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.png",
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11T09:40:37.000Z",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "062G5WYKY35KKD12EMSM3F8PJ8",
|
||||
"username": "her_fuckin_maj",
|
||||
"domain": "thequeenisstillalive.technology",
|
||||
"created_at": "2020-08-10T12:13:28.000Z",
|
||||
"email": "",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"name": "user"
|
||||
},
|
||||
"confirmed": false,
|
||||
"approved": false,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "062G5WYKY35KKD12EMSM3F8PJ8",
|
||||
"username": "her_fuckin_maj",
|
||||
"acct": "her_fuckin_maj@thequeenisstillalive.technology",
|
||||
"display_name": "lizzzieeeeeeeeeeee",
|
||||
"locked": true,
|
||||
"discoverable": true,
|
||||
"bot": false,
|
||||
"created_at": "2020-08-10T12:13:28.000Z",
|
||||
"note": "if i die blame charles don't let that fuck become king",
|
||||
"url": "http://thequeenisstillalive.technology/@her_fuckin_maj",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||
"header_static": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"last_status_at": null,
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "07GZRBAEMBNKGZ8Z9VSKSXKR98",
|
||||
"username": "üser",
|
||||
"domain": "ëxample.org",
|
||||
"created_at": "2020-08-10T12:13:28.000Z",
|
||||
"email": "",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "",
|
||||
"invite_request": null,
|
||||
"role": {
|
||||
"name": "user"
|
||||
},
|
||||
"confirmed": false,
|
||||
"approved": false,
|
||||
"disabled": false,
|
||||
"silenced": false,
|
||||
"suspended": false,
|
||||
"account": {
|
||||
"id": "07GZRBAEMBNKGZ8Z9VSKSXKR98",
|
||||
"username": "üser",
|
||||
"acct": "üser@ëxample.org",
|
||||
"display_name": "",
|
||||
"locked": false,
|
||||
"discoverable": false,
|
||||
"bot": false,
|
||||
"created_at": "2020-08-10T12:13:28.000Z",
|
||||
"note": "",
|
||||
"url": "https://xn--xample-ova.org/users/@%C3%BCser",
|
||||
"avatar": "",
|
||||
"avatar_static": "",
|
||||
"header": "http://localhost:8080/assets/default_header.png",
|
||||
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 0,
|
||||
"last_status_at": null,
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
}
|
||||
]`, dst.String())
|
||||
}
|
||||
|
||||
func TestAccountsGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, &AccountsGetTestSuite{})
|
||||
}
|
|
@ -252,6 +252,21 @@ func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gts
|
|||
return a.GetAccountByUsernameDomain(ctx, username, domain)
|
||||
}
|
||||
|
||||
// GetAccounts selects accounts using the given parameters.
|
||||
// Unlike with other functions, the paging for GetAccounts
|
||||
// is done not by ID, but by a concatenation of `[domain]/@[username]`,
|
||||
// which allows callers to page through accounts in alphabetical
|
||||
// order (much more useful for an admin overview of accounts,
|
||||
// for example, than paging by ID (which is random) or by account
|
||||
// created at date, which is not particularly interesting).
|
||||
//
|
||||
// Generated queries will look something like this
|
||||
// (SQLite example, maxID was provided so we're paging down):
|
||||
//
|
||||
// SELECT "account"."id", (COALESCE("domain", '') || '/@' || "username") AS "domain_username"
|
||||
// FROM "accounts" AS "account"
|
||||
// WHERE ("domain_username" > '/@the_mighty_zork')
|
||||
// ORDER BY "domain_username" ASC
|
||||
func (a *accountDB) GetAccounts(
|
||||
ctx context.Context,
|
||||
origin string,
|
||||
|
@ -309,32 +324,40 @@ func (a *accountDB) GetAccounts(
|
|||
// Select only IDs from table
|
||||
Column("account.id")
|
||||
|
||||
// Return only accounts OLDER
|
||||
// than account with maxID.
|
||||
if maxID != "" {
|
||||
maxIDAcct, err := a.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
maxID,
|
||||
// SQLite and Postgres use different syntax
|
||||
// for concatenation, so switch on type to
|
||||
// ensure we use the correct syntax.
|
||||
switch d := a.db.Dialect().Name(); d {
|
||||
case dialect.SQLite:
|
||||
q = q.ColumnExpr(
|
||||
"(COALESCE(?, ?) || ? || ?) AS ?",
|
||||
bun.Ident("domain"), "",
|
||||
"/@",
|
||||
bun.Ident("username"),
|
||||
bun.Ident("domain_username"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting maxID account %s: %w", maxID, err)
|
||||
}
|
||||
|
||||
q = q.Where("? < ?", bun.Ident("account.created_at"), maxIDAcct.CreatedAt)
|
||||
case dialect.PG:
|
||||
q = q.ColumnExpr(
|
||||
"(CONCAT(COALESCE(?, ?), ?, ?)) AS ?",
|
||||
bun.Ident("domain"), "",
|
||||
"/@",
|
||||
bun.Ident("username"),
|
||||
bun.Ident("domain_username"),
|
||||
)
|
||||
default:
|
||||
log.Panicf(ctx, "dialect %s was neither postgres nor sqlite", d)
|
||||
}
|
||||
|
||||
// Return only accounts NEWER
|
||||
// than account with minID.
|
||||
if minID != "" {
|
||||
minIDAcct, err := a.GetAccountByID(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
minID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting minID account %s: %w", minID, err)
|
||||
}
|
||||
// Return only accounts with `[domain]/@[username]`
|
||||
// later in the alphabet (a-z) than provided maxID.
|
||||
if maxID != "" {
|
||||
q = q.Where("? > ?", bun.Ident("domain_username"), maxID)
|
||||
}
|
||||
|
||||
q = q.Where("? > ?", bun.Ident("account.created_at"), minIDAcct.CreatedAt)
|
||||
// Return only accounts with `[domain]/@[username]`
|
||||
// earlier in the alphabet (a-z) than provided minID.
|
||||
if minID != "" {
|
||||
q = q.Where("? < ?", bun.Ident("domain_username"), minID)
|
||||
}
|
||||
|
||||
switch status {
|
||||
|
@ -479,13 +502,21 @@ func (a *accountDB) GetAccounts(
|
|||
|
||||
if order == paging.OrderAscending {
|
||||
// Page up.
|
||||
q = q.Order("account.created_at ASC")
|
||||
// It's counterintuitive because it
|
||||
// says DESC in the query, but we're
|
||||
// going backwards in the alphabet,
|
||||
// and a < z in a string comparison.
|
||||
q = q.OrderExpr("? DESC", bun.Ident("domain_username"))
|
||||
} else {
|
||||
// Page down.
|
||||
q = q.Order("account.created_at DESC")
|
||||
// It's counterintuitive because it
|
||||
// says ASC in the query, but we're
|
||||
// going forwards in the alphabet,
|
||||
// and z > a in a string comparison.
|
||||
q = q.OrderExpr("? ASC", bun.Ident("domain_username"))
|
||||
}
|
||||
|
||||
if err := q.Scan(ctx, &accountIDs); err != nil {
|
||||
if err := q.Scan(ctx, &accountIDs, new([]string)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -502,6 +502,80 @@ func (suite *AccountTestSuite) TestGetAccountsAll() {
|
|||
suite.Len(accounts, 9)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountsMaxID() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
origin = ""
|
||||
status = ""
|
||||
mods = false
|
||||
invitedBy = ""
|
||||
username = ""
|
||||
displayName = ""
|
||||
domain = ""
|
||||
email = ""
|
||||
ip netip.Addr
|
||||
// Get accounts with `[domain]/@[username]`
|
||||
// later in the alphabet than `/@the_mighty_zork`.
|
||||
page = &paging.Page{Max: paging.MaxID("/@the_mighty_zork")}
|
||||
)
|
||||
|
||||
accounts, err := suite.db.GetAccounts(
|
||||
ctx,
|
||||
origin,
|
||||
status,
|
||||
mods,
|
||||
invitedBy,
|
||||
username,
|
||||
displayName,
|
||||
domain,
|
||||
email,
|
||||
ip,
|
||||
page,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Len(accounts, 5)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountsMinID() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
origin = ""
|
||||
status = ""
|
||||
mods = false
|
||||
invitedBy = ""
|
||||
username = ""
|
||||
displayName = ""
|
||||
domain = ""
|
||||
email = ""
|
||||
ip netip.Addr
|
||||
// Get accounts with `[domain]/@[username]`
|
||||
// earlier in the alphabet than `/@the_mighty_zork`.
|
||||
page = &paging.Page{Min: paging.MinID("/@the_mighty_zork")}
|
||||
)
|
||||
|
||||
accounts, err := suite.db.GetAccounts(
|
||||
ctx,
|
||||
origin,
|
||||
status,
|
||||
mods,
|
||||
invitedBy,
|
||||
username,
|
||||
displayName,
|
||||
domain,
|
||||
email,
|
||||
ip,
|
||||
page,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Len(accounts, 3)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountsModsOnly() {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
log.Info(ctx, "reindexing accounts (accounts_paging_idx); this may take a few minutes, please don't interrupt this migration!")
|
||||
|
||||
q := db.NewCreateIndex().
|
||||
TableExpr("accounts").
|
||||
Index("accounts_paging_idx").
|
||||
IfNotExists()
|
||||
|
||||
switch d := db.Dialect().Name(); d {
|
||||
case dialect.SQLite:
|
||||
q = q.ColumnExpr(
|
||||
"COALESCE(?, ?) || ? || ?",
|
||||
bun.Ident("domain"), "",
|
||||
"/@",
|
||||
bun.Ident("username"),
|
||||
)
|
||||
|
||||
case dialect.PG:
|
||||
q = q.ColumnExpr(
|
||||
"CONCAT(COALESCE(?, ?), ?, ?)",
|
||||
bun.Ident("domain"), "",
|
||||
"/@",
|
||||
bun.Ident("username"),
|
||||
)
|
||||
|
||||
default:
|
||||
log.Panicf(ctx, "dialect %s was neither postgres nor sqlite", d)
|
||||
}
|
||||
|
||||
if _, err := q.Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -115,8 +115,12 @@ func (p *Processor) AccountsGet(
|
|||
return paging.EmptyResponse(), nil
|
||||
}
|
||||
|
||||
hi := accounts[count-1].ID
|
||||
lo := accounts[0].ID
|
||||
var (
|
||||
loAcct = accounts[count-1]
|
||||
hiAcct = accounts[0]
|
||||
lo = loAcct.Domain + "/@" + loAcct.Username
|
||||
hi = hiAcct.Domain + "/@" + hiAcct.Username
|
||||
)
|
||||
|
||||
items := make([]interface{}, 0, count)
|
||||
for _, account := range accounts {
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"nanoid": "^4.0.0",
|
||||
"object-to-formdata": "^4.4.2",
|
||||
"papaparse": "^5.3.2",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"photoswipe": "^5.3.3",
|
||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||
"plyr": "^3.7.8",
|
||||
|
@ -44,6 +45,7 @@
|
|||
"@joepie91/eslint-config": "^1.1.1",
|
||||
"@types/is-valid-domain": "^0.0.2",
|
||||
"@types/papaparse": "^5.3.9",
|
||||
"@types/parse-link-header": "^2.0.3",
|
||||
"@types/psl": "^1.1.1",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Link } from "wouter";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { Error } from "./error";
|
||||
import { AdminAccount } from "../lib/types/account";
|
||||
import { SerializedError } from "@reduxjs/toolkit";
|
||||
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
|
||||
import { Links } from "parse-link-header";
|
||||
|
||||
export interface AccountListProps {
|
||||
isSuccess: boolean,
|
||||
|
@ -31,6 +32,7 @@ export interface AccountListProps {
|
|||
isError: boolean,
|
||||
error: FetchBaseQueryError | SerializedError | undefined,
|
||||
emptyMessage: string,
|
||||
links?: Links | null | undefined,
|
||||
}
|
||||
|
||||
export function AccountList({
|
||||
|
@ -40,7 +42,10 @@ export function AccountList({
|
|||
isError,
|
||||
error,
|
||||
emptyMessage,
|
||||
links,
|
||||
}: AccountListProps) {
|
||||
const [ location, setLocation ] = useLocation();
|
||||
|
||||
if (!(isSuccess || isError)) {
|
||||
// Hasn't been called yet.
|
||||
return null;
|
||||
|
@ -58,25 +63,63 @@ export function AccountList({
|
|||
return <Error error={error} />;
|
||||
}
|
||||
|
||||
// Map response to entries if possible.
|
||||
let content: ReactNode;
|
||||
if (data == undefined || data.length == 0) {
|
||||
return <b>{emptyMessage}</b>;
|
||||
content = <b>{emptyMessage}</b>;
|
||||
} else {
|
||||
content = (
|
||||
<div className="entries">
|
||||
{data.map(({ account: acc }) => (
|
||||
<Link
|
||||
key={acc.acct}
|
||||
className="account entry"
|
||||
href={`/${acc.id}`}
|
||||
>
|
||||
{acc.display_name?.length > 0
|
||||
? acc.display_name
|
||||
: acc.username
|
||||
}
|
||||
<span id="username">(@{acc.acct})</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If it's possible to page to next and previous
|
||||
// pages, instantiate button handlers for this.
|
||||
let prevClick: (() => void) | undefined;
|
||||
let nextClick: (() => void) | undefined;
|
||||
if (links) {
|
||||
const prev = links["prev"];
|
||||
if (prev) {
|
||||
const prevUrl = new URL(prev.url);
|
||||
const prevParams = prevUrl.search;
|
||||
prevClick = () => {
|
||||
setLocation(location + prevParams.toString());
|
||||
};
|
||||
}
|
||||
|
||||
const next = links["next"];
|
||||
if (next) {
|
||||
const nextUrl = new URL(next.url);
|
||||
const nextParams = nextUrl.search;
|
||||
nextClick = () => {
|
||||
setLocation(location + nextParams.toString());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="list">
|
||||
{data.map(({ account: acc }) => (
|
||||
<Link
|
||||
key={acc.acct}
|
||||
className="account entry"
|
||||
href={`/${acc.id}`}
|
||||
>
|
||||
{acc.display_name?.length > 0
|
||||
? acc.display_name
|
||||
: acc.username
|
||||
}
|
||||
<span id="username">(@{acc.acct})</span>
|
||||
</Link>
|
||||
))}
|
||||
{ content }
|
||||
{ links &&
|
||||
<div className="prev-next">
|
||||
{ prevClick && <button onClick={prevClick}>Previous page</button> }
|
||||
{ nextClick && <button onClick={nextClick}>Next page</button> }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -20,8 +20,9 @@
|
|||
import { replaceCacheOnMutation, removeFromCacheOnMutation } from "../query-modifiers";
|
||||
import { gtsApi } from "../gts-api";
|
||||
import { listToKeyedObject } from "../transforms";
|
||||
import { AdminAccount, HandleSignupParams, SearchAccountParams } from "../../types/account";
|
||||
import { AdminAccount, HandleSignupParams, SearchAccountParams, SearchAccountResp } from "../../types/account";
|
||||
import { InstanceRule, MappedRules } from "../../types/rules";
|
||||
import parse from "parse-link-header";
|
||||
|
||||
const extended = gtsApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
|
@ -65,7 +66,7 @@ const extended = gtsApi.injectEndpoints({
|
|||
],
|
||||
}),
|
||||
|
||||
searchAccounts: build.query<AdminAccount[], SearchAccountParams>({
|
||||
searchAccounts: build.query<SearchAccountResp, SearchAccountParams>({
|
||||
query: (form) => {
|
||||
const params = new(URLSearchParams);
|
||||
Object.entries(form).forEach(([k, v]) => {
|
||||
|
@ -83,10 +84,16 @@ const extended = gtsApi.injectEndpoints({
|
|||
url: `/api/v2/admin/accounts${query}`
|
||||
};
|
||||
},
|
||||
transformResponse: (apiResp: AdminAccount[], meta) => {
|
||||
const accounts = apiResp;
|
||||
const linksStr = meta?.response?.headers.get("Link");
|
||||
const links = parse(linksStr);
|
||||
return { accounts, links };
|
||||
},
|
||||
providesTags: (res) =>
|
||||
res
|
||||
? [
|
||||
...res.map(({ id }) => ({ type: 'Account' as const, id })),
|
||||
...res.accounts.map(({ id }) => ({ type: 'Account' as const, id })),
|
||||
{ type: 'Account', id: 'LIST' },
|
||||
]
|
||||
: [{ type: 'Account', id: 'LIST' }],
|
||||
|
|
|
@ -24,7 +24,7 @@ import type {
|
|||
FetchBaseQueryError,
|
||||
} from '@reduxjs/toolkit/query/react';
|
||||
import { serialize as serializeForm } from "object-to-formdata";
|
||||
|
||||
import type { FetchBaseQueryMeta } from "@reduxjs/toolkit/dist/query/fetchBaseQuery";
|
||||
import type { RootState } from '../../redux/store';
|
||||
import { InstanceV1 } from '../types/instance';
|
||||
|
||||
|
@ -65,7 +65,9 @@ export interface GTSFetchArgs extends FetchArgs {
|
|||
const gtsBaseQuery: BaseQueryFn<
|
||||
string | GTSFetchArgs,
|
||||
any,
|
||||
FetchBaseQueryError
|
||||
FetchBaseQueryError,
|
||||
{},
|
||||
FetchBaseQueryMeta
|
||||
> = async (args, api, extraOptions) => {
|
||||
// Retrieve state at the moment
|
||||
// this function was called.
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Links } from "parse-link-header";
|
||||
import { CustomEmoji } from "./custom-emoji";
|
||||
|
||||
export interface AdminAccount {
|
||||
|
@ -79,6 +80,11 @@ export interface SearchAccountParams {
|
|||
limit?: number,
|
||||
}
|
||||
|
||||
export interface SearchAccountResp {
|
||||
accounts: AdminAccount[];
|
||||
links: Links | null;
|
||||
}
|
||||
|
||||
export interface HandleSignupParams {
|
||||
id: string,
|
||||
approve_or_reject: "approve" | "reject",
|
||||
|
|
|
@ -1132,17 +1132,26 @@ button.with-padding {
|
|||
}
|
||||
|
||||
.list {
|
||||
margin: 0.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
a {
|
||||
color: $fg;
|
||||
text-decoration: none;
|
||||
|
||||
#username {
|
||||
color: $link-fg;
|
||||
margin-left: 0.5em;
|
||||
.entries {
|
||||
a {
|
||||
color: $fg;
|
||||
text-decoration: none;
|
||||
|
||||
#username {
|
||||
color: $link-fg;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prev-next {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,10 +20,10 @@
|
|||
import React from "react";
|
||||
import { AccountSearchForm } from "./search";
|
||||
|
||||
export default function AccountsOverview({ }) {
|
||||
export default function AccountsSearch({ }) {
|
||||
return (
|
||||
<div className="accounts-view">
|
||||
<h1>Accounts Overview</h1>
|
||||
<h1>Accounts Search</h1>
|
||||
<span>
|
||||
You can perform actions on an account by clicking
|
||||
its name in a report, or by searching for the account
|
||||
|
|
|
@ -30,7 +30,7 @@ export default function AccountsPending() {
|
|||
<AccountList
|
||||
isLoading={searchRes.isLoading}
|
||||
isSuccess={searchRes.isSuccess}
|
||||
data={searchRes.data}
|
||||
data={searchRes.data?.accounts}
|
||||
isError={searchRes.isError}
|
||||
error={searchRes.error}
|
||||
emptyMessage="No pending account sign-ups."
|
||||
|
|
|
@ -17,28 +17,51 @@
|
|||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
import { useLazySearchAccountsQuery } from "../../../../lib/query/admin";
|
||||
import { useTextInput } from "../../../../lib/form";
|
||||
import { AccountList } from "../../../../components/account-list";
|
||||
import { SearchAccountParams } from "../../../../lib/types/account";
|
||||
import { Select, TextInput } from "../../../../components/form/inputs";
|
||||
import MutationButton from "../../../../components/form/mutation-button";
|
||||
import { useLocation, useSearch } from "wouter";
|
||||
|
||||
export function AccountSearchForm() {
|
||||
const [ location, setLocation ] = useLocation();
|
||||
const search = useSearch();
|
||||
const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||
const [ searchAcct, searchRes ] = useLazySearchAccountsQuery();
|
||||
|
||||
// Populate search form using values from
|
||||
// urlQueryParams, to allow paging.
|
||||
const form = {
|
||||
origin: useTextInput("origin"),
|
||||
status: useTextInput("status"),
|
||||
permissions: useTextInput("permissions"),
|
||||
username: useTextInput("username"),
|
||||
display_name: useTextInput("display_name"),
|
||||
by_domain: useTextInput("by_domain"),
|
||||
email: useTextInput("email"),
|
||||
ip: useTextInput("ip"),
|
||||
origin: useTextInput("origin", { defaultValue: urlQueryParams.get("origin") ?? ""}),
|
||||
status: useTextInput("status", { defaultValue: urlQueryParams.get("status") ?? ""}),
|
||||
permissions: useTextInput("permissions", { defaultValue: urlQueryParams.get("permissions") ?? ""}),
|
||||
username: useTextInput("username", { defaultValue: urlQueryParams.get("username") ?? ""}),
|
||||
display_name: useTextInput("display_name", { defaultValue: urlQueryParams.get("display_name") ?? ""}),
|
||||
by_domain: useTextInput("by_domain", { defaultValue: urlQueryParams.get("by_domain") ?? ""}),
|
||||
email: useTextInput("email", { defaultValue: urlQueryParams.get("email") ?? ""}),
|
||||
ip: useTextInput("ip", { defaultValue: urlQueryParams.get("ip") ?? ""}),
|
||||
limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "50"})
|
||||
};
|
||||
|
||||
function submitSearch(e) {
|
||||
// On mount, if urlQueryParams were provided,
|
||||
// trigger the search. For example, if page
|
||||
// was accessed at /search?origin=local&limit=20,
|
||||
// then run a search with origin=local and
|
||||
// limit=20 and immediately render the results.
|
||||
useEffect(() => {
|
||||
if (urlQueryParams.size > 0) {
|
||||
searchAcct(Object.fromEntries(urlQueryParams), true);
|
||||
}
|
||||
}, [urlQueryParams, searchAcct]);
|
||||
|
||||
// Rather than triggering the search directly,
|
||||
// the "submit" button changes the location
|
||||
// based on form field params, and lets the
|
||||
// useEffect hook above actually do the search.
|
||||
function submitQuery(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Parse query parameters.
|
||||
|
@ -52,16 +75,15 @@ export function AccountSearchForm() {
|
|||
// Remove any nulls.
|
||||
return kv || [];
|
||||
});
|
||||
const params: SearchAccountParams = Object.fromEntries(entries);
|
||||
searchAcct(params);
|
||||
}
|
||||
|
||||
const [ searchAcct, searchRes ] = useLazySearchAccountsQuery();
|
||||
const searchParams = new URLSearchParams(entries);
|
||||
setLocation(location + "?" + searchParams.toString());
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
onSubmit={submitSearch}
|
||||
onSubmit={submitQuery}
|
||||
// Prevent password managers trying
|
||||
// to fill in username/email fields.
|
||||
autoComplete="off"
|
||||
|
@ -120,10 +142,11 @@ export function AccountSearchForm() {
|
|||
<AccountList
|
||||
isLoading={searchRes.isLoading}
|
||||
isSuccess={searchRes.isSuccess}
|
||||
data={searchRes.data}
|
||||
data={searchRes.data?.accounts}
|
||||
isError={searchRes.isError}
|
||||
error={searchRes.error}
|
||||
emptyMessage="No accounts found that match your query"
|
||||
links={searchRes.data?.links}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -28,7 +28,7 @@ import { useHasPermission } from "../../lib/navigation/util";
|
|||
/**
|
||||
* - /settings/moderation/reports/overview
|
||||
* - /settings/moderation/reports/:reportId
|
||||
* - /settings/moderation/accounts/overview
|
||||
* - /settings/moderation/accounts/search
|
||||
* - /settings/moderation/accounts/pending
|
||||
* - /settings/moderation/accounts/:accountID
|
||||
* - /settings/moderation/domain-permissions/:permType
|
||||
|
@ -76,12 +76,12 @@ function ModerationAccountsMenu() {
|
|||
<MenuItem
|
||||
name="Accounts"
|
||||
itemUrl="accounts"
|
||||
defaultChild="overview"
|
||||
defaultChild="search"
|
||||
icon="fa-users"
|
||||
>
|
||||
<MenuItem
|
||||
name="Overview"
|
||||
itemUrl="overview"
|
||||
name="Search"
|
||||
itemUrl="search"
|
||||
icon="fa-list"
|
||||
/>
|
||||
<MenuItem
|
||||
|
|
|
@ -26,7 +26,7 @@ import { ErrorBoundary } from "../../lib/navigation/error";
|
|||
import ImportExport from "./domain-permissions/import-export";
|
||||
import DomainPermissionsOverview from "./domain-permissions/overview";
|
||||
import DomainPermDetail from "./domain-permissions/detail";
|
||||
import AccountsOverview from "./accounts";
|
||||
import AccountsSearch from "./accounts";
|
||||
import AccountsPending from "./accounts/pending";
|
||||
import AccountDetail from "./accounts/detail";
|
||||
|
||||
|
@ -37,7 +37,7 @@ import AccountDetail from "./accounts/detail";
|
|||
/**
|
||||
* - /settings/moderation/reports/overview
|
||||
* - /settings/moderation/reports/:reportId
|
||||
* - /settings/moderation/accounts/overview
|
||||
* - /settings/moderation/accounts/search
|
||||
* - /settings/moderation/accounts/pending
|
||||
* - /settings/moderation/accounts/:accountID
|
||||
* - /settings/moderation/domain-permissions/:permType
|
||||
|
@ -95,7 +95,7 @@ function ModerationReportsRouter() {
|
|||
}
|
||||
|
||||
/**
|
||||
* - /settings/moderation/accounts/overview
|
||||
* - /settings/moderation/accounts/search
|
||||
* - /settings/moderation/accounts/pending
|
||||
* - /settings/moderation/accounts/:accountID
|
||||
*/
|
||||
|
@ -109,10 +109,10 @@ function ModerationAccountsRouter() {
|
|||
<Router base={thisBase}>
|
||||
<ErrorBoundary>
|
||||
<Switch>
|
||||
<Route path="/overview" component={AccountsOverview}/>
|
||||
<Route path="/search" component={AccountsSearch}/>
|
||||
<Route path="/pending" component={AccountsPending}/>
|
||||
<Route path="/:accountID" component={AccountDetail}/>
|
||||
<Route><Redirect to="/overview"/></Route>
|
||||
<Route><Redirect to="/search"/></Route>
|
||||
</Switch>
|
||||
</ErrorBoundary>
|
||||
</Router>
|
||||
|
|
|
@ -1468,6 +1468,11 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/parse-link-header@^2.0.3":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-2.0.3.tgz#37ad650d12aecb055b64c2d43ddb1534e356ad33"
|
||||
integrity sha512-ffLAxD6Xqcf2gSbtEJehj8yJ5R/2OZqD4liodQvQQ+hhO4kg1mk9ToEZQPMtNTm/zIQj2GNleQbsjPp9+UQm4Q==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3"
|
||||
|
@ -5182,6 +5187,13 @@ parse-json@^2.2.0:
|
|||
dependencies:
|
||||
error-ex "^1.2.0"
|
||||
|
||||
parse-link-header@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-2.0.0.tgz#949353e284f8aa01f2ac857a98f692b57733f6b7"
|
||||
integrity sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==
|
||||
dependencies:
|
||||
xtend "~4.0.1"
|
||||
|
||||
parse-ms@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
|
||||
|
|
Loading…
Reference in a new issue