mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Add token review / delete to backend + settings panel (#3845)
This commit is contained in:
@@ -54,6 +54,7 @@ import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/streaming"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/tags"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/timelines"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/tokens"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
||||
@@ -99,6 +100,7 @@ type Client struct {
|
||||
streaming *streaming.Module // api/v1/streaming
|
||||
tags *tags.Module // api/v1/tags
|
||||
timelines *timelines.Module // api/v1/timelines
|
||||
tokens *tokens.Module // api/v1/tokens
|
||||
user *user.Module // api/v1/user
|
||||
}
|
||||
|
||||
@@ -152,6 +154,7 @@ func (c *Client) Route(r *router.Router, m ...gin.HandlerFunc) {
|
||||
c.streaming.Route(h)
|
||||
c.tags.Route(h)
|
||||
c.timelines.Route(h)
|
||||
c.tokens.Route(h)
|
||||
c.user.Route(h)
|
||||
}
|
||||
|
||||
@@ -193,6 +196,7 @@ func NewClient(state *state.State, p *processing.Processor) *Client {
|
||||
streaming: streaming.New(p, time.Second*30, 4096),
|
||||
tags: tags.New(p),
|
||||
timelines: timelines.New(p),
|
||||
tokens: tokens.New(p),
|
||||
user: user.New(p),
|
||||
}
|
||||
}
|
||||
|
98
internal/api/client/tokens/tokenget.go
Normal file
98
internal/api/client/tokens/tokenget.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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 tokens
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// TokenInfoGETHandler swagger:operation GET /api/v1/tokens/{id} tokenInfoGet
|
||||
//
|
||||
// Get information about a single token.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - tokens
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: The id of the requested token.
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:accounts
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: The requested token.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/tokenInfo"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) TokenInfoGETHandler(c *gin.Context) {
|
||||
authed, errWithCode := apiutil.TokenAuth(c,
|
||||
true, true, true, true,
|
||||
apiutil.ScopeReadAccounts,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
tokenID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
tokenInfo, errWithCode := m.processor.Account().TokenGet(
|
||||
c.Request.Context(),
|
||||
authed.User.ID,
|
||||
tokenID,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, tokenInfo)
|
||||
}
|
78
internal/api/client/tokens/tokenget_test.go
Normal file
78
internal/api/client/tokens/tokenget_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 tokens_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/tokens"
|
||||
)
|
||||
|
||||
type TokenGetTestSuite struct {
|
||||
TokensStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *TokenGetTestSuite) TestTokenGet() {
|
||||
var (
|
||||
testToken = suite.testTokens["local_account_1"]
|
||||
testPath = "/api" + tokens.BasePath + "/" + testToken.ID
|
||||
)
|
||||
|
||||
out, code := suite.req(
|
||||
http.MethodGet,
|
||||
testPath,
|
||||
suite.tokens.TokenInfoGETHandler,
|
||||
map[string]string{"id": testToken.ID},
|
||||
)
|
||||
|
||||
suite.Equal(http.StatusOK, code)
|
||||
suite.Equal(`{
|
||||
"id": "01F8MGTQW4DKTDF8SW5CT9HYGA",
|
||||
"created_at": "2021-06-20T10:53:00.164Z",
|
||||
"scope": "read write push",
|
||||
"application": {
|
||||
"name": "really cool gts application",
|
||||
"website": "https://reallycool.app"
|
||||
}
|
||||
}`, out)
|
||||
}
|
||||
|
||||
func (suite *TokenGetTestSuite) TestTokenGetNotOurs() {
|
||||
var (
|
||||
testToken = suite.testTokens["admin_account"]
|
||||
testPath = "/api" + tokens.BasePath + "/" + testToken.ID
|
||||
)
|
||||
|
||||
out, code := suite.req(
|
||||
http.MethodGet,
|
||||
testPath,
|
||||
suite.tokens.TokenInfoGETHandler,
|
||||
map[string]string{"id": testToken.ID},
|
||||
)
|
||||
|
||||
suite.Equal(http.StatusNotFound, code)
|
||||
suite.Equal(`{
|
||||
"error": "Not Found"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
func TestTokenGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(TokenGetTestSuite))
|
||||
}
|
103
internal/api/client/tokens/tokeninvalidate.go
Normal file
103
internal/api/client/tokens/tokeninvalidate.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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 tokens
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// TokenInvalidatePOSTHandler swagger:operation POST /api/v1/tokens/{id}/invalidate tokenInvalidatePost
|
||||
//
|
||||
// Invalidate the target token, removing it from the database and making it unusable.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - tokens
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: The id of the target token.
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:accounts
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: Info about the invalidated token.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/tokenInfo"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) TokenInvalidatePOSTHandler(c *gin.Context) {
|
||||
authed, errWithCode := apiutil.TokenAuth(c,
|
||||
true, true, true, true,
|
||||
apiutil.ScopeWriteAccounts,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
tokenID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
tokenInfo, errWithCode := m.processor.Account().TokenInvalidate(
|
||||
c.Request.Context(),
|
||||
authed.User.ID,
|
||||
tokenID,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, tokenInfo)
|
||||
}
|
87
internal/api/client/tokens/tokeninvalidate_test.go
Normal file
87
internal/api/client/tokens/tokeninvalidate_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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 tokens_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/tokens"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
)
|
||||
|
||||
type TokenInvalidateTestSuite struct {
|
||||
TokensStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *TokenInvalidateTestSuite) TestTokenInvalidate() {
|
||||
var (
|
||||
testToken = suite.testTokens["local_account_1"]
|
||||
testPath = "/api" + tokens.BasePath + "/" + testToken.ID + "/invalidate"
|
||||
)
|
||||
|
||||
out, code := suite.req(
|
||||
http.MethodPost,
|
||||
testPath,
|
||||
suite.tokens.TokenInvalidatePOSTHandler,
|
||||
map[string]string{"id": testToken.ID},
|
||||
)
|
||||
|
||||
suite.Equal(http.StatusOK, code)
|
||||
suite.Equal(`{
|
||||
"id": "01F8MGTQW4DKTDF8SW5CT9HYGA",
|
||||
"created_at": "2021-06-20T10:53:00.164Z",
|
||||
"scope": "read write push",
|
||||
"application": {
|
||||
"name": "really cool gts application",
|
||||
"website": "https://reallycool.app"
|
||||
}
|
||||
}`, out)
|
||||
|
||||
// Check database for token we
|
||||
// just invalidated, should be gone.
|
||||
_, err := suite.testStructs.State.DB.GetTokenByID(
|
||||
context.Background(), testToken.ID,
|
||||
)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
}
|
||||
|
||||
func (suite *TokenInvalidateTestSuite) TestTokenInvalidateNotOurs() {
|
||||
var (
|
||||
testToken = suite.testTokens["admin_account"]
|
||||
testPath = "/api" + tokens.BasePath + "/" + testToken.ID + "/invalidate"
|
||||
)
|
||||
|
||||
out, code := suite.req(
|
||||
http.MethodGet,
|
||||
testPath,
|
||||
suite.tokens.TokenInfoGETHandler,
|
||||
map[string]string{"id": testToken.ID},
|
||||
)
|
||||
|
||||
suite.Equal(http.StatusNotFound, code)
|
||||
suite.Equal(`{
|
||||
"error": "Not Found"
|
||||
}`, out)
|
||||
}
|
||||
|
||||
func TestTokenInvalidateTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(TokenInvalidateTestSuite))
|
||||
}
|
48
internal/api/client/tokens/tokens.go
Normal file
48
internal/api/client/tokens/tokens.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 tokens
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
)
|
||||
|
||||
const (
|
||||
BasePath = "/v1/tokens"
|
||||
BasePathWithID = BasePath + "/:" + apiutil.IDKey
|
||||
InvalidateTokenPath = BasePathWithID + "/invalidate"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
processor *processing.Processor
|
||||
}
|
||||
|
||||
func New(processor *processing.Processor) *Module {
|
||||
return &Module{
|
||||
processor: processor,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
attachHandler(http.MethodGet, BasePath, m.TokensInfoGETHandler)
|
||||
attachHandler(http.MethodGet, BasePathWithID, m.TokensInfoGETHandler)
|
||||
attachHandler(http.MethodPost, InvalidateTokenPath, m.TokenInvalidatePOSTHandler)
|
||||
}
|
117
internal/api/client/tokens/tokens_test.go
Normal file
117
internal/api/client/tokens/tokens_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// 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 tokens_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/tokens"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type TokensStandardTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
// standard suite models
|
||||
testTokens map[string]*gtsmodel.Token
|
||||
testApplications map[string]*gtsmodel.Application
|
||||
testUsers map[string]*gtsmodel.User
|
||||
testAccounts map[string]*gtsmodel.Account
|
||||
testStructs *testrig.TestStructs
|
||||
|
||||
// module being tested
|
||||
tokens *tokens.Module
|
||||
}
|
||||
|
||||
func (suite *TokensStandardTestSuite) req(
|
||||
httpMethod string,
|
||||
requestPath string,
|
||||
handler gin.HandlerFunc,
|
||||
pathParams map[string]string,
|
||||
) (string, int) {
|
||||
var (
|
||||
recorder = httptest.NewRecorder()
|
||||
ctx, _ = testrig.CreateGinTestContext(recorder, nil)
|
||||
)
|
||||
|
||||
// Prepare test context.
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// Prepare test context request.
|
||||
request := httptest.NewRequest(httpMethod, requestPath, nil)
|
||||
request.Header.Set("accept", "application/json")
|
||||
ctx.Request = request
|
||||
|
||||
// Inject path parameters.
|
||||
if pathParams != nil {
|
||||
for k, v := range pathParams {
|
||||
ctx.AddParam(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger the handler
|
||||
handler(ctx)
|
||||
|
||||
// Read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Format as nice indented json.
|
||||
dst := &bytes.Buffer{}
|
||||
if err := json.Indent(dst, b, "", " "); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
return dst.String(), recorder.Code
|
||||
}
|
||||
|
||||
func (suite *TokensStandardTestSuite) SetupSuite() {
|
||||
testrig.InitTestConfig()
|
||||
testrig.InitTestLog()
|
||||
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testApplications = testrig.NewTestApplications()
|
||||
suite.testUsers = testrig.NewTestUsers()
|
||||
suite.testAccounts = testrig.NewTestAccounts()
|
||||
}
|
||||
|
||||
func (suite *TokensStandardTestSuite) SetupTest() {
|
||||
suite.testStructs = testrig.SetupTestStructs(
|
||||
"../../../../testrig/media",
|
||||
"../../../../web/template",
|
||||
)
|
||||
suite.tokens = tokens.New(suite.testStructs.Processor)
|
||||
}
|
||||
|
||||
func (suite *TokensStandardTestSuite) TearDownTest() {
|
||||
testrig.TearDownTestStructs(suite.testStructs)
|
||||
}
|
144
internal/api/client/tokens/tokensget.go
Normal file
144
internal/api/client/tokens/tokensget.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// 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 tokens
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||
)
|
||||
|
||||
// TokensInfoGETHandler swagger:operation GET /api/v1/tokens tokensInfoGet
|
||||
//
|
||||
// See info about tokens created for/by your account.
|
||||
//
|
||||
// The items will be returned in descending chronological order (newest first), with sequential IDs (bigger = newer).
|
||||
//
|
||||
// The returned Link header can be used to generate the previous and next queries when paging up or down.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ```
|
||||
// <https://example.org/api/v1/tokens?limit=20&max_id=01FC3GSQ8A3MMJ43BPZSGEG29M>; rel="next", <https://example.org/api/v1/tokens?limit=20&min_id=01FC3KJW2GYXSDDRA6RWNDM46M>; rel="prev"
|
||||
// ````
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - tokens
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: max_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items *OLDER* than the given max status ID.
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// required: false
|
||||
// -
|
||||
// name: since_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items *newer* than the given since status ID.
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// -
|
||||
// name: min_id
|
||||
// type: string
|
||||
// description: >-
|
||||
// Return only items *immediately newer* than the given since status ID.
|
||||
// The item with the specified ID will not be included in the response.
|
||||
// in: query
|
||||
// required: false
|
||||
// -
|
||||
// name: limit
|
||||
// type: integer
|
||||
// description: Number of items to return.
|
||||
// default: 20
|
||||
// in: query
|
||||
// required: false
|
||||
// max: 80
|
||||
// min: 0
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:accounts
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: tokens
|
||||
// description: Array of token info entries.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/tokenInfo"
|
||||
// headers:
|
||||
// Link:
|
||||
// type: string
|
||||
// description: Links to the next and previous queries.
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '400':
|
||||
// description: bad request
|
||||
func (m *Module) TokensInfoGETHandler(c *gin.Context) {
|
||||
authed, errWithCode := apiutil.TokenAuth(c,
|
||||
true, true, true, true,
|
||||
apiutil.ScopeReadAccounts,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
page, errWithCode := paging.ParseIDPage(c,
|
||||
0, // min limit
|
||||
80, // max limit
|
||||
20, // default limit
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Account().TokensGet(
|
||||
c.Request.Context(),
|
||||
authed.User.ID,
|
||||
page,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.LinkHeader != "" {
|
||||
c.Header("Link", resp.LinkHeader)
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, resp.Items)
|
||||
}
|
69
internal/api/client/tokens/tokensget_test.go
Normal file
69
internal/api/client/tokens/tokensget_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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 tokens_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/tokens"
|
||||
)
|
||||
|
||||
type TokensGetTestSuite struct {
|
||||
TokensStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *TokensGetTestSuite) TestTokensGet() {
|
||||
var (
|
||||
testPath = "/api" + tokens.BasePath
|
||||
)
|
||||
|
||||
out, code := suite.req(
|
||||
http.MethodGet,
|
||||
testPath,
|
||||
suite.tokens.TokensInfoGETHandler,
|
||||
nil,
|
||||
)
|
||||
|
||||
suite.Equal(http.StatusOK, code)
|
||||
suite.Equal(`[
|
||||
{
|
||||
"id": "01JN0X2D9GJTZQ5KYPYFWN16QW",
|
||||
"created_at": "2025-02-26T10:33:04.560Z",
|
||||
"scope": "push",
|
||||
"application": {
|
||||
"name": "really cool gts application",
|
||||
"website": "https://reallycool.app"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "01F8MGTQW4DKTDF8SW5CT9HYGA",
|
||||
"created_at": "2021-06-20T10:53:00.164Z",
|
||||
"scope": "read write push",
|
||||
"application": {
|
||||
"name": "really cool gts application",
|
||||
"website": "https://reallycool.app"
|
||||
}
|
||||
}
|
||||
]`, out)
|
||||
}
|
||||
|
||||
func TestTokensGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(TokensGetTestSuite))
|
||||
}
|
@@ -33,3 +33,25 @@ type Token struct {
|
||||
// example: 1627644520
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
// TokenInfo represents metadata about one user-level access token.
|
||||
// The actual access token itself will never be sent via the API.
|
||||
//
|
||||
// swagger:model tokenInfo
|
||||
type TokenInfo struct {
|
||||
// Database ID of this token.
|
||||
// example: 01JMW7QBAZYZ8T8H73PCEX12XG
|
||||
ID string `json:"id"`
|
||||
// When the token was created (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
CreatedAt string `json:"created_at"`
|
||||
// Approximate time (accurate to within an hour) when the token was last used (ISO 8601 Datetime).
|
||||
// Omitted if token has never been used, or it is not known when it was last used (eg., it was last used before tracking "last_used" became a thing).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
LastUsed string `json:"last_used,omitempty"`
|
||||
// OAuth scopes granted by the token, space-separated.
|
||||
// example: read write admin
|
||||
Scope string `json:"scope"`
|
||||
// Application used to create this token.
|
||||
Application *Application `json:"application"`
|
||||
}
|
||||
|
Reference in New Issue
Block a user