feat: expose access token creation date in API responses (#12620)

## Checklist

Following the previous contribution that added admin-level management of user access tokens (particularly useful for bot/service accounts), this change exposes the created_at field in the API response when listing or retrieving access tokens.

This information is needed to implement token rotation policies for these users — knowing when a token was created allows administrators to identify and revoke stale tokens.

### Tests for Go changes

- I added test coverage for Go changes...
  - [X] in their respective `*_test.go` for unit tests.
  - [X] `make pr-go` before pushing

### Documentation

- [X] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [X] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/12620): <!--number 12620 --><!--line 0 --><!--description ZXhwb3NlIGFjY2VzcyB0b2tlbiBjcmVhdGlvbiBkYXRlIGluIEFQSSByZXNwb25zZXM=-->expose access token creation date in API responses<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12620
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
This commit is contained in:
steven.guiheux 2026-05-20 18:45:38 +02:00 committed by Mathieu Fenniak
parent aec047c7b5
commit 0ef80f6b0f
5 changed files with 17 additions and 5 deletions

View file

@ -11,11 +11,12 @@ import (
// AccessToken represents an API access token.
// swagger:response AccessToken
type AccessToken struct {
ID int64 `json:"id"`
Name string `json:"name"`
Token string `json:"sha1"`
TokenLastEight string `json:"token_last_eight"`
Scopes []string `json:"scopes"`
ID int64 `json:"id"`
Name string `json:"name"`
Token string `json:"sha1"`
TokenLastEight string `json:"token_last_eight"`
Scopes []string `json:"scopes"`
Created time.Time `json:"created_at"`
// Indicates that an access token only has access to the specified repositories. Will be null if the access token
// is not limited to a set of specified repositories.
Repositories []*RepositoryMeta `json:"repositories"`

View file

@ -117,6 +117,7 @@ func ListAccessTokens(ctx *context.APIContext) {
Name: tokens[i].Name,
TokenLastEight: tokens[i].TokenLastEight,
Scopes: tokens[i].Scope.StringSlice(),
Created: tokens[i].CreatedUnix.AsTime(),
Repositories: reposByTokenID[tokens[i].ID],
}
// Provide a consistent sort order on repositories, helpful for test consistency. Hard to do any earlier
@ -229,6 +230,7 @@ func CreateAccessToken(ctx *context.APIContext) {
ID: t.ID,
TokenLastEight: t.TokenLastEight,
Scopes: t.Scope.StringSlice(),
Created: t.CreatedUnix.AsTime(),
Repositories: tokenRepositories,
})
}

View file

@ -22792,6 +22792,11 @@
"type": "object",
"title": "AccessToken represents an API access token.",
"properties": {
"created_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Created"
},
"id": {
"type": "integer",
"format": "int64",

View file

@ -43,6 +43,7 @@ func TestAPIAdminCreateUserAccessToken(t *testing.T) {
assert.NotEmpty(t, newToken.Token)
assert.NotEmpty(t, newToken.TokenLastEight)
assert.Contains(t, newToken.Scopes, "all")
assert.NotZero(t, newToken.Created)
// Verify the token exists in DB
unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{
@ -143,6 +144,7 @@ func TestAPIAdminListUserAccessTokens(t *testing.T) {
if tk.Name == "list-test-token" {
found = true
assert.NotEmpty(t, tk.TokenLastEight)
assert.NotZero(t, tk.Created)
break
}
}

View file

@ -57,6 +57,7 @@ func TestAPIGetTokens(t *testing.T) {
assert.Equal(t, []string{""}, at.Scopes)
assert.Empty(t, at.Token)
assert.Equal(t, "69d28c91", at.TokenLastEight)
assert.NotZero(t, at.Created)
assert.Nil(t, at.Repositories) // not repo-specific access token - nil expected, not an empty array
})
@ -753,6 +754,7 @@ func TestAPITokenCreation(t *testing.T) {
resp := MakeRequest(t, req, http.StatusCreated)
var token api.AccessToken
DecodeJSON(t, resp, &token)
assert.NotZero(t, token.Created)
})
t.Run("repo-specific", func(t *testing.T) {