From 829143d2636d4c0d274bf2ab4559912f472a2bc4 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:01:25 +0100 Subject: [feature] Add token review / delete to backend + settings panel (#3845) --- internal/api/client.go | 4 + internal/api/client/tokens/tokenget.go | 98 ++++++++++++++ internal/api/client/tokens/tokenget_test.go | 78 +++++++++++ internal/api/client/tokens/tokeninvalidate.go | 103 +++++++++++++++ internal/api/client/tokens/tokeninvalidate_test.go | 87 +++++++++++++ internal/api/client/tokens/tokens.go | 48 +++++++ internal/api/client/tokens/tokens_test.go | 117 +++++++++++++++++ internal/api/client/tokens/tokensget.go | 144 +++++++++++++++++++++ internal/api/client/tokens/tokensget_test.go | 69 ++++++++++ internal/api/model/token.go | 22 ++++ 10 files changed, 770 insertions(+) create mode 100644 internal/api/client/tokens/tokenget.go create mode 100644 internal/api/client/tokens/tokenget_test.go create mode 100644 internal/api/client/tokens/tokeninvalidate.go create mode 100644 internal/api/client/tokens/tokeninvalidate_test.go create mode 100644 internal/api/client/tokens/tokens.go create mode 100644 internal/api/client/tokens/tokens_test.go create mode 100644 internal/api/client/tokens/tokensget.go create mode 100644 internal/api/client/tokens/tokensget_test.go (limited to 'internal/api') diff --git a/internal/api/client.go b/internal/api/client.go index 3112aeea5..a928176de 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -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), } } diff --git a/internal/api/client/tokens/tokenget.go b/internal/api/client/tokens/tokenget.go new file mode 100644 index 000000000..c88b78743 --- /dev/null +++ b/internal/api/client/tokens/tokenget.go @@ -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 . + +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) +} diff --git a/internal/api/client/tokens/tokenget_test.go b/internal/api/client/tokens/tokenget_test.go new file mode 100644 index 000000000..c7cbf3022 --- /dev/null +++ b/internal/api/client/tokens/tokenget_test.go @@ -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 . + +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)) +} diff --git a/internal/api/client/tokens/tokeninvalidate.go b/internal/api/client/tokens/tokeninvalidate.go new file mode 100644 index 000000000..192bbf33b --- /dev/null +++ b/internal/api/client/tokens/tokeninvalidate.go @@ -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 . + +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) +} diff --git a/internal/api/client/tokens/tokeninvalidate_test.go b/internal/api/client/tokens/tokeninvalidate_test.go new file mode 100644 index 000000000..281f9b96d --- /dev/null +++ b/internal/api/client/tokens/tokeninvalidate_test.go @@ -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 . + +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)) +} diff --git a/internal/api/client/tokens/tokens.go b/internal/api/client/tokens/tokens.go new file mode 100644 index 000000000..ce00a6459 --- /dev/null +++ b/internal/api/client/tokens/tokens.go @@ -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 . + +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) +} diff --git a/internal/api/client/tokens/tokens_test.go b/internal/api/client/tokens/tokens_test.go new file mode 100644 index 000000000..bae140194 --- /dev/null +++ b/internal/api/client/tokens/tokens_test.go @@ -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 . + +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) +} diff --git a/internal/api/client/tokens/tokensget.go b/internal/api/client/tokens/tokensget.go new file mode 100644 index 000000000..2ffc2afb9 --- /dev/null +++ b/internal/api/client/tokens/tokensget.go @@ -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 . + +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: +// +// ``` +// ; rel="next", ; 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) +} diff --git a/internal/api/client/tokens/tokensget_test.go b/internal/api/client/tokens/tokensget_test.go new file mode 100644 index 000000000..0164c0379 --- /dev/null +++ b/internal/api/client/tokens/tokensget_test.go @@ -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 . + +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)) +} diff --git a/internal/api/model/token.go b/internal/api/model/token.go index 5a1abe28f..3ad45e684 100644 --- a/internal/api/model/token.go +++ b/internal/api/model/token.go @@ -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"` +} -- cgit v1.2.3