From aef91ab1a30dd69e742ead20b70e192165b674d4 Mon Sep 17 00:00:00 2001 From: Mathieu Fenniak Date: Wed, 18 Mar 2026 22:33:14 +0100 Subject: [PATCH] ui: move "New access token" to a separate UI page (#11659) We are updating the user's personal access token page (`/user/settings/applications`) to allow the creation of repo-specific tokens, adding a third option to "Repository and Organization Access". In preparation for this new UI, this PR moves the creation of access tokens to a new page accessed by "New access token". This also resolves a pet-peeve: the "Select permissions" dropdown on the inline edit form hides a *required* input for an access token. This section is expanded on the new dedicated page. (The Vue component used here is replaced with a JS-free alternative as well. This form component used to lose selected values when an error occurred, and it didn't make sense as a Vue component, so it has been translated into an HTML template instead.) ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Tests for JavaScript changes - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [x] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [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. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11659 Reviewed-by: Andreas Ahlenstorf Reviewed-by: Gusted Co-authored-by: Mathieu Fenniak Co-committed-by: Mathieu Fenniak --- options/locale_next/locale_en-US.json | 1 + routers/web/user/setting/access_token.go | 134 ++++++++++++++++++ routers/web/user/setting/applications.go | 85 ----------- routers/web/web.go | 15 +- .../user/settings/access_token_edit.tmpl | 63 ++++++++ templates/user/settings/applications.tmpl | 49 +------ tests/e2e/user-settings.test.e2e.ts | 22 ++- tests/integration/api_admin_org_test.go | 2 +- tests/integration/api_admin_test.go | 4 +- tests/integration/api_fork_test.go | 2 +- tests/integration/api_repo_git_blobs_test.go | 2 +- tests/integration/api_repo_git_trees_test.go | 2 +- tests/integration/integration_test.go | 4 +- tests/integration/mirror_push_test.go | 10 +- tests/integration/repo_tag_test.go | 8 +- tests/integration/signing_git_test.go | 2 +- tests/integration/user_test.go | 4 +- .../components/ScopedAccessTokenSelector.vue | 104 -------------- .../features/scoped-access-token-selector.ts | 14 -- web_src/js/index.js | 2 - 20 files changed, 255 insertions(+), 274 deletions(-) create mode 100644 routers/web/user/setting/access_token.go create mode 100644 templates/user/settings/access_token_edit.tmpl delete mode 100644 web_src/js/components/ScopedAccessTokenSelector.vue delete mode 100644 web_src/js/features/scoped-access-token-selector.ts diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json index f73ae37355..1a4036b79b 100644 --- a/options/locale_next/locale_en-US.json +++ b/options/locale_next/locale_en-US.json @@ -192,6 +192,7 @@ "settings.twofa_reenroll.description": "Re-enroll your two-factor authentication", "settings.must_enable_2fa": "This Forgejo instance requires users to enable two-factor authentication before they can access their accounts.", "settings.specific_repo_access": "Repository access", + "settings.new_access_token": "New access token", "error.must_enable_2fa": "This Forgejo instance requires users to enable two-factor authentication before they can access their accounts. Enable it at: %s", "avatar.constraints_hint": "Custom avatar may not exceed %[1]s in size or be larger than %[2]dx%[3]d pixels", "user.ghost.tooltip": "This user has been deleted, or cannot be matched.", diff --git a/routers/web/user/setting/access_token.go b/routers/web/user/setting/access_token.go new file mode 100644 index 0000000000..293bb19456 --- /dev/null +++ b/routers/web/user/setting/access_token.go @@ -0,0 +1,134 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + "slices" + + auth_model "forgejo.org/models/auth" + "forgejo.org/modules/base" + "forgejo.org/modules/log" + "forgejo.org/modules/setting" + "forgejo.org/modules/web" + "forgejo.org/services/context" + "forgejo.org/services/forms" +) + +const ( + tplAccessTokenEdit base.TplName = "user/settings/access_token_edit" +) + +func loadAccessTokenCreateData(ctx *context.Context) { + ctx.Data["AccessTokenScopePublicOnly"] = string(auth_model.AccessTokenScopePublicOnly) // note: SliceUtils.Contains won't work in the template if this is a `auth_model.AccessTokenScope`, so it's cast to a string here + + categories := []string{ + "activitypub", + "issue", + "misc", + "notification", + "organization", + "package", + "repository", + "user", + } + if ctx.Doer.IsAdmin { + categories = append(categories, "admin") + } + slices.Sort(categories) + ctx.Data["Categories"] = categories +} + +// Applications render manage access token page +func AccessTokenCreate(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings.applications") + ctx.Data["PageIsSettingsApplications"] = true + + loadAccessTokenCreateData(ctx) + + ctx.HTML(http.StatusOK, tplAccessTokenEdit) +} + +// ApplicationsPost response for add user's access token +func AccessTokenCreatePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.NewAccessTokenForm) + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsApplications"] = true + + if ctx.HasError() { + loadAccessTokenCreateData(ctx) + ctx.HTML(http.StatusOK, tplAccessTokenEdit) + return + } + + scope, err := form.GetScope() + if err != nil { + ctx.ServerError("GetScope", err) + return + } + if !scope.HasPermissionScope() { + loadAccessTokenCreateData(ctx) + ctx.RenderWithErr(ctx.Tr("settings.at_least_one_permission"), tplAccessTokenEdit, form) + return + } + t := &auth_model.AccessToken{ + UID: ctx.Doer.ID, + Name: form.Name, + Scope: scope, + + // maintain legacy behaviour until new UI options are added -- token has access to all resources, is not + // fine-grained + ResourceAllRepos: true, + } + + exist, err := auth_model.AccessTokenByNameExists(ctx, t) + if err != nil { + ctx.ServerError("AccessTokenByNameExists", err) + return + } + if exist { + loadAccessTokenCreateData(ctx) + ctx.RenderWithErr(ctx.Tr("settings.generate_token_name_duplicate", t.Name), tplAccessTokenEdit, form) + return + } + + if err := auth_model.NewAccessToken(ctx, t); err != nil { + ctx.ServerError("NewAccessToken", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.generate_token_success")) + ctx.Flash.Info(t.Token) + + ctx.Redirect(setting.AppSubURL + "/user/settings/applications") +} + +// DeleteAccessToken response for delete user access token +func DeleteAccessToken(ctx *context.Context) { + if err := auth_model.DeleteAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil { + ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.delete_token_success")) + } + + ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications") +} + +// RegenerateAccessToken response for regenerating user access token +func RegenerateAccessToken(ctx *context.Context) { + if t, err := auth_model.RegenerateAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil { + if auth_model.IsErrAccessTokenNotExist(err) { + ctx.Flash.Error(ctx.Tr("error.not_found")) + } else { + ctx.Flash.Error(ctx.Tr("error.server_internal")) + log.Error("DeleteAccessTokenByID", err) + } + } else { + ctx.Flash.Success(ctx.Tr("settings.regenerate_token_success")) + ctx.Flash.Info(t.Token) + } + + ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications") +} diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go index a5c38b8dc3..cdb610e5e2 100644 --- a/routers/web/user/setting/applications.go +++ b/routers/web/user/setting/applications.go @@ -12,11 +12,8 @@ import ( access_model "forgejo.org/models/perm/access" repo_model "forgejo.org/models/repo" "forgejo.org/modules/base" - "forgejo.org/modules/log" "forgejo.org/modules/setting" - "forgejo.org/modules/web" "forgejo.org/services/context" - "forgejo.org/services/forms" ) const ( @@ -33,94 +30,12 @@ func Applications(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsApplications) } -// ApplicationsPost response for add user's access token -func ApplicationsPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.NewAccessTokenForm) - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsApplications"] = true - - if ctx.HasError() { - loadApplicationsData(ctx) - - ctx.HTML(http.StatusOK, tplSettingsApplications) - return - } - - scope, err := form.GetScope() - if err != nil { - ctx.ServerError("GetScope", err) - return - } - if !scope.HasPermissionScope() { - ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true) - } - t := &auth_model.AccessToken{ - UID: ctx.Doer.ID, - Name: form.Name, - Scope: scope, - - // maintain legacy behaviour until new UI options are added -- token has access to all resources, is not - // fine-grained - ResourceAllRepos: true, - } - - exist, err := auth_model.AccessTokenByNameExists(ctx, t) - if err != nil { - ctx.ServerError("AccessTokenByNameExists", err) - return - } - if exist { - ctx.Flash.Error(ctx.Tr("settings.generate_token_name_duplicate", t.Name)) - ctx.Redirect(setting.AppSubURL + "/user/settings/applications") - return - } - - if err := auth_model.NewAccessToken(ctx, t); err != nil { - ctx.ServerError("NewAccessToken", err) - return - } - - ctx.Flash.Success(ctx.Tr("settings.generate_token_success")) - ctx.Flash.Info(t.Token) - - ctx.Redirect(setting.AppSubURL + "/user/settings/applications") -} - -// DeleteApplication response for delete user access token -func DeleteApplication(ctx *context.Context) { - if err := auth_model.DeleteAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil { - ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.delete_token_success")) - } - - ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications") -} - -// RegenerateApplication response for regenerating user access token -func RegenerateApplication(ctx *context.Context) { - if t, err := auth_model.RegenerateAccessTokenByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID); err != nil { - if auth_model.IsErrAccessTokenNotExist(err) { - ctx.Flash.Error(ctx.Tr("error.not_found")) - } else { - ctx.Flash.Error(ctx.Tr("error.server_internal")) - log.Error("DeleteAccessTokenByID", err) - } - } else { - ctx.Flash.Success(ctx.Tr("settings.regenerate_token_success")) - ctx.Flash.Info(t.Token) - } - - ctx.JSONRedirect(setting.AppSubURL + "/user/settings/applications") -} - type TokenWithResources struct { Token *auth_model.AccessToken Repositories []*repo_model.Repository } func loadApplicationsData(ctx *context.Context) { - ctx.Data["AccessTokenScopePublicOnly"] = auth_model.AccessTokenScopePublicOnly tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID}) if err != nil { ctx.ServerError("ListAccessTokens", err) diff --git a/routers/web/web.go b/routers/web/web.go index f8132707ca..5ea90c1eea 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -631,11 +631,16 @@ func registerRoutes(m *web.Route) { m.Post("/{id}/revoke/{grantId}", user_setting.RevokeOAuth2Grant) }, oauth2Enabled) - // access token applications - m.Combo("").Get(user_setting.Applications). - Post(web.Bind(forms.NewAccessTokenForm{}), user_setting.ApplicationsPost) - m.Post("/delete", user_setting.DeleteApplication) - m.Post("/regenerate", user_setting.RegenerateApplication) + // access token + m.Group("/tokens", func() { + m.Combo("/new"). + Get(user_setting.AccessTokenCreate). + Post(web.Bind(forms.NewAccessTokenForm{}), user_setting.AccessTokenCreatePost) + m.Post("/delete", user_setting.DeleteAccessToken) + m.Post("/regenerate", user_setting.RegenerateAccessToken) + }) + + m.Get("", user_setting.Applications) }) m.Combo("/keys").Get(user_setting.Keys). diff --git a/templates/user/settings/access_token_edit.tmpl b/templates/user/settings/access_token_edit.tmpl new file mode 100644 index 0000000000..c53fafeb1a --- /dev/null +++ b/templates/user/settings/access_token_edit.tmpl @@ -0,0 +1,63 @@ +{{template "user/settings/layout_head" (dict "ctxData" . "pageClass" "user settings applications")}} +
+

+ {{ctx.Locale.Tr "settings.manage_access_token"}} +

+ +
+
+
+ {{ctx.Locale.Tr "settings.generate_new_token"}} +
+
+ + +
+
+ + + +
+
+
+ {{ctx.Locale.Tr "settings.select_permissions"}} +
+

+

{{ctx.Locale.Tr "settings.access_token_desc" (printf "%s/api/swagger" AppSubUrl) "https://forgejo.org/docs/latest/user/token-scope/"}}

+

+ + {{range .Categories}} +
+ +
+ +
+
+ {{end}} +
+ +
+
+ +
+{{template "user/settings/layout_footer" .}} diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index 59d352eab3..eaf18f6de1 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -2,6 +2,9 @@

{{ctx.Locale.Tr "settings.manage_access_token"}} +

@@ -47,11 +50,11 @@
- - @@ -60,48 +63,6 @@ {{end}}
-
-
- {{ctx.Locale.Tr "settings.generate_new_token"}} -
-
-
- - -
-
- - - -
-
- - {{ctx.Locale.Tr "settings.select_permissions"}} - -

-

{{ctx.Locale.Tr "settings.access_token_desc" (printf "%s/api/swagger" AppSubUrl) "https://forgejo.org/docs/latest/user/token-scope/"}}

-

-
-
- -
{{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}} -
- {{ctx.Locale.Tr "settings.at_least_one_permission"}} -
-
{{if .EnableOAuth2}} {{template "user/settings/grants_oauth2" .}} diff --git a/tests/e2e/user-settings.test.e2e.ts b/tests/e2e/user-settings.test.e2e.ts index 22cec4c8cc..9a6baefef5 100644 --- a/tests/e2e/user-settings.test.e2e.ts +++ b/tests/e2e/user-settings.test.e2e.ts @@ -128,11 +128,11 @@ test('User: Canceling adding GPG key clears input', async ({browser}, workerInfo test('User: Add access token', async ({browser}, workerInfo) => { const page = await login({browser}, workerInfo); await page.goto('/user/settings/applications'); + await page.getByRole('link', {name: 'New access token'}).click(); await page.locator('#scoped-access-submit').click(); await page.locator('#name:invalid').isVisible(); - await page.locator('details.optional.field').click(); await page.selectOption('#access-token-scope-activitypub', 'read:activitypub'); await page.locator('#scoped-access-submit').click(); @@ -145,3 +145,23 @@ test('User: Add access token', async ({browser}, workerInfo) => { await page.getByText(tokenName).isVisible(); }); + +test('User: Add access token validation error', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + await page.goto('/user/settings/applications'); + await page.getByRole('link', {name: 'New access token'}).click(); + + await page.getByRole('button', {name: 'Generate token'}).click(); + await page.locator('#name:invalid').isVisible(); + + await page.getByRole('textbox', {name: 'Token name *'}).fill('Token A'); + await page.getByRole('combobox', {name: 'activitypub'}).selectOption('read:activitypub'); + await page.getByRole('radio', {name: 'Public only'}).click(); + + await page.getByRole('button', {name: 'Generate token'}).click(); + + await page.getByText('has been used as an application name already.').isVisible(); + // validate that selected options (public-only, activitypub) are still selected. + await expect(page.getByRole('radio', {name: 'Public only'})).toBeChecked(); + await expect(page.getByRole('combobox', {name: 'activitypub'})).toHaveValue('read:activitypub'); +}); diff --git a/tests/integration/api_admin_org_test.go b/tests/integration/api_admin_org_test.go index df5e961ec6..1ad1b9e55b 100644 --- a/tests/integration/api_admin_org_test.go +++ b/tests/integration/api_admin_org_test.go @@ -73,7 +73,7 @@ func TestAPIAdminOrgCreateNotAdmin(t *testing.T) { defer tests.PrepareTestEnv(t)() nonAdminUsername := "user2" session := loginUser(t, nonAdminUsername) - token := getTokenForLoggedInUser(t, session) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadAdmin) org := api.CreateOrgOption{ UserName: "user2_org", FullName: "User2's organization", diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go index ae5a5b46ae..70a921a0a4 100644 --- a/tests/integration/api_admin_test.go +++ b/tests/integration/api_admin_test.go @@ -76,7 +76,7 @@ func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) { var newPublicKey api.PublicKey DecodeJSON(t, resp, &newPublicKey) - token = getUserToken(t, normalUsername) + token = getUserToken(t, normalUsername, auth_model.AccessTokenScopeWriteAdmin) req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d", adminUsername, newPublicKey.ID). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) @@ -178,7 +178,7 @@ func TestAPIListUsersNotLoggedIn(t *testing.T) { func TestAPIListUsersNonAdmin(t *testing.T) { defer tests.PrepareTestEnv(t)() nonAdminUsername := "user2" - token := getUserToken(t, nonAdminUsername) + token := getUserToken(t, nonAdminUsername, auth_model.AccessTokenScopeReadAdmin) req := NewRequest(t, "GET", "/api/v1/admin/users"). AddTokenAuth(token) MakeRequest(t, req, http.StatusForbidden) diff --git a/tests/integration/api_fork_test.go b/tests/integration/api_fork_test.go index 0d38831b10..a030d57bc8 100644 --- a/tests/integration/api_fork_test.go +++ b/tests/integration/api_fork_test.go @@ -102,7 +102,7 @@ func TestAPIDisabledForkRepo(t *testing.T) { defer tests.PrintCurrentTest(t)() session := loginUser(t, "user5") - token := getTokenForLoggedInUser(t, session) + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{}).AddTokenAuth(token) session.MakeRequest(t, req, http.StatusNotFound) diff --git a/tests/integration/api_repo_git_blobs_test.go b/tests/integration/api_repo_git_blobs_test.go index a4424a3348..6466835602 100644 --- a/tests/integration/api_repo_git_blobs_test.go +++ b/tests/integration/api_repo_git_blobs_test.go @@ -72,7 +72,7 @@ func TestAPIReposGitBlobs(t *testing.T) { // Login as User4. session = loginUser(t, user4.Name) - token4 := getTokenForLoggedInUser(t, session) + token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) // Test using org repo "org3/repo3" where user4 is a NOT collaborator req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4) diff --git a/tests/integration/api_repo_git_trees_test.go b/tests/integration/api_repo_git_trees_test.go index f321760218..9418fc4857 100644 --- a/tests/integration/api_repo_git_trees_test.go +++ b/tests/integration/api_repo_git_trees_test.go @@ -69,7 +69,7 @@ func TestAPIReposGitTrees(t *testing.T) { // Login as User4. session = loginUser(t, user4.Name) - token4 := getTokenForLoggedInUser(t, session) + token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository) // Test using org repo "org3/repo3" where user4 is a NOT collaborator req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", org3.Name, repo3.Name, token4) diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index de915b3cbf..464008e6e4 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -487,12 +487,14 @@ func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth. // createApplicationSettingsToken creates a token with given name and scopes for the currently logged in user. // It will redirect to the application settings page. func createApplicationSettingsToken(t testing.TB, session *TestSession, name string, scopes ...auth.AccessTokenScope) { + require.NotEmpty(t, scopes, "attempted to create access token with no scopes, which is not valid") + urlValues := url.Values{} urlValues.Add("name", name) for _, scope := range scopes { urlValues.Add("scope", string(scope)) } - req := NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues) + req := NewRequestWithURLValues(t, "POST", "/user/settings/applications/tokens/new", urlValues) resp := session.MakeRequest(t, req, http.StatusSeeOther) // Log the flash values on failure diff --git a/tests/integration/mirror_push_test.go b/tests/integration/mirror_push_test.go index b02f894b58..3f5a8223c8 100644 --- a/tests/integration/mirror_push_test.go +++ b/tests/integration/mirror_push_test.go @@ -99,7 +99,7 @@ func testMirrorPush(t *testing.T, u *url.URL) { }) require.NoError(t, err) - ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name) + ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name, auth_model.AccessTokenScopeReadRepository) doCreatePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape(mirrorRepo.Name)), user.LowerName, userPassword)(t) doCreatePushMirror(ctx, fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(ctx.Username), url.PathEscape("does-not-matter")), user.LowerName, userPassword)(t) @@ -407,7 +407,7 @@ func TestPushMirrorBranchFilterWebUI(t *testing.T) { mirrorRepo, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil) defer f() - ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name) + ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name, auth_model.AccessTokenScopeReadRepository) ctx.Session = sess remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name)) @@ -506,7 +506,7 @@ func TestPushMirrorBranchFilterIntegration(t *testing.T) { sess := loginUser(t, user.Name) token := getTokenForLoggedInUser(t, sess, auth_model.AccessTokenScopeAll) - ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name) + ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name, auth_model.AccessTokenScopeReadRepository) ctx.Session = sess remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape("foo")) urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/push_mirrors", user.LowerName, srcRepo.Name) @@ -682,7 +682,7 @@ func TestPushMirrorBranchFilterSyncOperations(t *testing.T) { _, _, err = git.NewCommand(git.DefaultContext, "update-ref", "refs/heads/hotfix-123", "refs/heads/master").RunStdString(&git.RunOpts{Dir: testRepoPath}) require.NoError(t, err) - ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name) + ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name, auth_model.AccessTokenScopeReadRepository) ctx.Session = sess t.Run("Create push mirror with branch filter and trigger sync", func(t *testing.T) { @@ -907,7 +907,7 @@ func TestPushMirrorWebUIToAPIIntegration(t *testing.T) { mirrorRepo, _, f := tests.CreateDeclarativeRepo(t, user, "", []unit.Type{unit.TypeCode}, nil, nil) defer f() - ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name) + ctx := NewAPITestContext(t, user.LowerName, srcRepo.Name, auth_model.AccessTokenScopeReadRepository) ctx.Session = session remoteAddress := fmt.Sprintf("%s%s/%s", u.String(), url.PathEscape(user.Name), url.PathEscape(mirrorRepo.Name)) urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/push_mirrors", user.Name, srcRepo.Name) diff --git a/tests/integration/repo_tag_test.go b/tests/integration/repo_tag_test.go index 735cb73126..ea2ae50309 100644 --- a/tests/integration/repo_tag_test.go +++ b/tests/integration/repo_tag_test.go @@ -107,7 +107,7 @@ func TestCreateNewTagProtected(t *testing.T) { t.Run("Git", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - httpContext := NewAPITestContext(t, owner.Name, repo.Name) + httpContext := NewAPITestContext(t, owner.Name, repo.Name, auth_model.AccessTokenScopeReadRepository) dstPath := t.TempDir() @@ -127,7 +127,7 @@ func TestCreateNewTagProtected(t *testing.T) { t.Run("GitTagForce", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - httpContext := NewAPITestContext(t, owner.Name, repo.Name) + httpContext := NewAPITestContext(t, owner.Name, repo.Name, auth_model.AccessTokenScopeReadRepository) dstPath := t.TempDir() @@ -160,7 +160,7 @@ func TestSyncRepoTags(t *testing.T) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) t.Run("Git", func(t *testing.T) { - httpContext := NewAPITestContext(t, owner.Name, repo.Name) + httpContext := NewAPITestContext(t, owner.Name, repo.Name, auth_model.AccessTokenScopeReadRepository) dstPath := t.TempDir() @@ -199,7 +199,7 @@ func TestRepushTag(t *testing.T) { session := loginUser(t, owner.LowerName) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) - httpContext := NewAPITestContext(t, owner.Name, repo.Name) + httpContext := NewAPITestContext(t, owner.Name, repo.Name, auth_model.AccessTokenScopeReadRepository) dstPath := t.TempDir() diff --git a/tests/integration/signing_git_test.go b/tests/integration/signing_git_test.go index 7fbda358cf..5dee5b4801 100644 --- a/tests/integration/signing_git_test.go +++ b/tests/integration/signing_git_test.go @@ -96,7 +96,7 @@ func testCRUD(t *testing.T, u *url.URL, signingFormat string, objectFormat git.O username := "user2" user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) - baseAPITestContext := NewAPITestContext(t, username, "repo1") + baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeReadRepository) u.Path = baseAPITestContext.GitPath() suffix := "-" + signingFormat + "-" + objectFormat.Name() diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index 90a4a00029..f1acafbde8 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -256,7 +256,7 @@ func TestAccessTokenRegenerate(t *testing.T) { assert.Equal(t, "TestAccessToken", oldTokenName) - req := NewRequestWithValues(t, "POST", "/user/settings/applications/regenerate", map[string]string{ + req := NewRequestWithValues(t, "POST", "/user/settings/applications/tokens/regenerate", map[string]string{ "id": strconv.Itoa(oldTokenID), }) session.MakeRequest(t, req, http.StatusOK) @@ -268,7 +268,7 @@ func TestAccessTokenRegenerate(t *testing.T) { assert.Equal(t, oldTokenID, newTokenID) assert.Equal(t, "TestAccessToken", newTokenName) - req = NewRequestWithValues(t, "POST", "/user/settings/applications/delete", map[string]string{ + req = NewRequestWithValues(t, "POST", "/user/settings/applications/tokens/delete", map[string]string{ "id": strconv.Itoa(newTokenID), }) session.MakeRequest(t, req, http.StatusOK) diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue deleted file mode 100644 index fe79032aa9..0000000000 --- a/web_src/js/components/ScopedAccessTokenSelector.vue +++ /dev/null @@ -1,104 +0,0 @@ - - diff --git a/web_src/js/features/scoped-access-token-selector.ts b/web_src/js/features/scoped-access-token-selector.ts deleted file mode 100644 index 481ee0b57d..0000000000 --- a/web_src/js/features/scoped-access-token-selector.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {createApp} from 'vue'; - -export async function initScopedAccessTokenCategories() { - for (const el of document.getElementsByClassName('scoped-access-token')) { - const {default: ScopedAccessTokenSelector} = await import(/* webpackChunkName: "scoped-access-token-selector" */'../components/ScopedAccessTokenSelector.vue'); - const scopedAccessTokenSelector = createApp(ScopedAccessTokenSelector, { - isAdmin: el.getAttribute('data-is-admin') === 'true', - noAccessLabel: el.getAttribute('data-no-access-label'), - readLabel: el.getAttribute('data-read-label'), - writeLabel: el.getAttribute('data-write-label'), - }); - scopedAccessTokenSelector.mount(el); - } -} diff --git a/web_src/js/index.js b/web_src/js/index.js index 33c94d7e07..a3dabeacc0 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -2,7 +2,6 @@ import './bootstrap.js'; import {initRepoActivityTopAuthorsChart} from './features/repo-activity-top-authors.ts'; -import {initScopedAccessTokenCategories} from './features/scoped-access-token-selector.ts'; import {initDashboardRepoList} from './features/dashboard-repo-list.ts'; import {initGlobalCopyToClipboardListener} from './features/clipboard.js'; @@ -192,7 +191,6 @@ onDomReady(() => { initUserAuthWebAuthnRegister(); initUserAuth(); initRepoDiffView(); - initScopedAccessTokenCategories(); initColorPickers(); initModalClose();