mirror of
https://github.com/go-gitea/gitea
synced 2025-03-08 19:54:27 +00:00
Improve "generate new access token" form (#33730)
Fix: https://github.com/go-gitea/gitea/issues/33519 As discussed in [PR #33614](https://github.com/go-gitea/gitea/pull/33614), the ScopedAccessTokenSelector Vue component is not particularly useful. This PR removes the component and reverts to using HTML templates. It also introduces some (hopefully) useful refactoring. The Vue component was causing the UX bug reported in the linked issue. Required form fields are now properly working, as expected (see screenshot).  --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
8362a41559
commit
303af554c9
@ -5,6 +5,7 @@ package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
@ -14,7 +15,7 @@ import (
|
||||
type AccessTokenScopeCategory int
|
||||
|
||||
const (
|
||||
AccessTokenScopeCategoryActivityPub = iota
|
||||
AccessTokenScopeCategoryActivityPub AccessTokenScopeCategory = iota
|
||||
AccessTokenScopeCategoryAdmin
|
||||
AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values
|
||||
AccessTokenScopeCategoryNotification
|
||||
@ -193,6 +194,14 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
|
||||
},
|
||||
}
|
||||
|
||||
func GetAccessTokenCategories() (res []string) {
|
||||
for _, cat := range accessTokenScopes[Read] {
|
||||
res = append(res, strings.TrimPrefix(string(cat), "read:"))
|
||||
}
|
||||
slices.Sort(res)
|
||||
return res
|
||||
}
|
||||
|
||||
// GetRequiredScopes gets the specific scopes for a given level and categories
|
||||
func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
|
||||
scopes := make([]AccessTokenScope, 0, len(scopeCategories))
|
||||
@ -270,6 +279,9 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
|
||||
|
||||
// StringSlice returns the AccessTokenScope as a []string
|
||||
func (s AccessTokenScope) StringSlice() []string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(string(s), ",")
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ type scopeTestNormalize struct {
|
||||
}
|
||||
|
||||
func TestAccessTokenScope_Normalize(t *testing.T) {
|
||||
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user"}, GetAccessTokenCategories())
|
||||
tests := []scopeTestNormalize{
|
||||
{"", "", nil},
|
||||
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
|
||||
@ -25,7 +26,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
|
||||
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
|
||||
}
|
||||
|
||||
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
|
||||
for _, scope := range GetAccessTokenCategories() {
|
||||
tests = append(tests,
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
|
||||
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
|
||||
@ -59,7 +60,7 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
|
||||
{"public-only", "read:issue", false, nil},
|
||||
}
|
||||
|
||||
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
|
||||
for _, scope := range GetAccessTokenCategories() {
|
||||
tests = append(tests,
|
||||
scopeTestHasScope{
|
||||
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
|
||||
|
@ -917,7 +917,6 @@ delete_token_success = The token has been deleted. Applications using it no long
|
||||
repo_and_org_access = Repository and Organization Access
|
||||
permissions_public_only = Public only
|
||||
permissions_access_all = All (public, private, and limited)
|
||||
select_permissions = Select permissions
|
||||
permission_not_set = Not set
|
||||
permission_no_access = No Access
|
||||
permission_read = Read
|
||||
|
@ -6,12 +6,14 @@ package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
@ -39,18 +41,29 @@ func ApplicationsPost(ctx *context.Context) {
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
ctx.Data["UserDisabledFeatures"] = user_model.DisabledFeaturesWithLoginType(ctx.Doer)
|
||||
|
||||
if ctx.HasError() {
|
||||
loadApplicationsData(ctx)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsApplications)
|
||||
return
|
||||
_ = ctx.Req.ParseForm()
|
||||
var scopeNames []string
|
||||
for k, v := range ctx.Req.Form {
|
||||
if strings.HasPrefix(k, "scope-") {
|
||||
scopeNames = append(scopeNames, v...)
|
||||
}
|
||||
}
|
||||
|
||||
scope, err := form.GetScope()
|
||||
scope, err := auth_model.AccessTokenScope(strings.Join(scopeNames, ",")).Normalize()
|
||||
if err != nil {
|
||||
ctx.ServerError("GetScope", err)
|
||||
return
|
||||
}
|
||||
if scope == "" || scope == auth_model.AccessTokenScopePublicOnly {
|
||||
ctx.Flash.Error(ctx.Tr("settings.at_least_one_permission"), true)
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
loadApplicationsData(ctx)
|
||||
ctx.HTML(http.StatusOK, tplSettingsApplications)
|
||||
return
|
||||
}
|
||||
|
||||
t := &auth_model.AccessToken{
|
||||
UID: ctx.Doer.ID,
|
||||
Name: form.Name,
|
||||
@ -99,7 +112,14 @@ func loadApplicationsData(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["Tokens"] = tokens
|
||||
ctx.Data["EnableOAuth2"] = setting.OAuth2.Enabled
|
||||
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
|
||||
|
||||
// Handle specific ordered token categories for admin or non-admin users
|
||||
tokenCategoryNames := auth_model.GetAccessTokenCategories()
|
||||
if !ctx.Doer.IsAdmin {
|
||||
tokenCategoryNames = util.SliceRemoveAll(tokenCategoryNames, "admin")
|
||||
}
|
||||
ctx.Data["TokenCategories"] = tokenCategoryNames
|
||||
|
||||
if setting.OAuth2.Enabled {
|
||||
ctx.Data["Applications"], err = db.Find[auth_model.OAuth2Application](ctx, auth_model.FindOAuth2ApplicationsOptions{
|
||||
OwnerID: ctx.Doer.ID,
|
||||
|
@ -213,13 +213,16 @@ func Contexter() func(next http.Handler) http.Handler {
|
||||
// Attention: this function changes ctx.Data and ctx.Flash
|
||||
// If HasError is called, then before Redirect, the error message should be stored by ctx.Flash.Error(ctx.GetErrMsg()) again.
|
||||
func (ctx *Context) HasError() bool {
|
||||
hasErr, ok := ctx.Data["HasError"]
|
||||
if !ok {
|
||||
hasErr, _ := ctx.Data["HasError"].(bool)
|
||||
hasErr = hasErr || ctx.Flash.ErrorMsg != ""
|
||||
if !hasErr {
|
||||
return false
|
||||
}
|
||||
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
|
||||
if ctx.Flash.ErrorMsg == "" {
|
||||
ctx.Flash.ErrorMsg = ctx.GetErrMsg()
|
||||
}
|
||||
ctx.Data["Flash"] = ctx.Flash
|
||||
return hasErr.(bool)
|
||||
return hasErr
|
||||
}
|
||||
|
||||
// GetErrMsg returns error message in form validation.
|
||||
|
@ -7,9 +7,7 @@ package forms
|
||||
import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
@ -347,8 +345,7 @@ func (f *EditVariableForm) Validate(req *http.Request, errs binding.Errors) bind
|
||||
|
||||
// NewAccessTokenForm form for creating access token
|
||||
type NewAccessTokenForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`
|
||||
Scope []string
|
||||
Name string `binding:"Required;MaxSize(255)" locale:"settings.token_name"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
@ -357,12 +354,6 @@ func (f *NewAccessTokenForm) Validate(req *http.Request, errs binding.Errors) bi
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) {
|
||||
scope := strings.Join(f.Scope, ",")
|
||||
s, err := auth_model.AccessTokenScope(scope).Normalize()
|
||||
return s, err
|
||||
}
|
||||
|
||||
// EditOAuth2ApplicationForm form for editing oauth2 applications
|
||||
type EditOAuth2ApplicationForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
|
||||
|
@ -4,10 +4,8 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
@ -104,28 +102,3 @@ func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) {
|
||||
assert.Equal(t, v.valid, form.IsEmailDomainAllowed())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAccessTokenForm_GetScope(t *testing.T) {
|
||||
tests := []struct {
|
||||
form NewAccessTokenForm
|
||||
scope auth_model.AccessTokenScope
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository"}},
|
||||
scope: "read:repository",
|
||||
},
|
||||
{
|
||||
form: NewAccessTokenForm{Name: "test", Scope: []string{"read:repository", "write:user"}},
|
||||
scope: "read:repository,write:user",
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
scope, err := test.form.GetScope()
|
||||
assert.Equal(t, test.expectedErr, err)
|
||||
assert.Equal(t, test.scope, scope)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -50,49 +50,41 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui bottom attached segment">
|
||||
<h5 class="ui top header">
|
||||
{{ctx.Locale.Tr "settings.generate_new_token"}}
|
||||
</h5>
|
||||
<form id="scoped-access-form" class="ui form ignore-dirty" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_Name}}error{{end}}">
|
||||
<label for="name">{{ctx.Locale.Tr "settings.token_name"}}</label>
|
||||
<input id="name" name="name" value="{{.name}}" autofocus required maxlength="255">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "settings.repo_and_org_access"}}</label>
|
||||
<label class="tw-cursor-pointer">
|
||||
<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
|
||||
{{ctx.Locale.Tr "settings.permissions_public_only"}}
|
||||
</label>
|
||||
<label class="tw-cursor-pointer">
|
||||
<input class="enable-system tw-mt-1 tw-mr-1" type="radio" name="scope" value="" checked>
|
||||
{{ctx.Locale.Tr "settings.permissions_access_all"}}
|
||||
</label>
|
||||
</div>
|
||||
<details class="ui optional field">
|
||||
<summary class="tw-pb-4 tw-pl-1">
|
||||
{{ctx.Locale.Tr "settings.select_permissions"}}
|
||||
</summary>
|
||||
<p class="activity meta">
|
||||
<i>{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</i>
|
||||
</p>
|
||||
<div id="scoped-access-token-selector"
|
||||
data-is-admin="{{if .IsAdmin}}true{{else}}false{{end}}"
|
||||
data-no-access-label="{{ctx.Locale.Tr "settings.permission_no_access"}}"
|
||||
data-read-label="{{ctx.Locale.Tr "settings.permission_read"}}"
|
||||
data-write-label="{{ctx.Locale.Tr "settings.permission_write"}}"
|
||||
data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
|
||||
>
|
||||
<details {{if or .name (not .Tokens)}}open{{end}}>
|
||||
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.generate_new_token"}}</h4></summary>
|
||||
<form class="ui form ignore-dirty" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_Name}}error{{end}}">
|
||||
<label for="name">{{ctx.Locale.Tr "settings.token_name"}}</label>
|
||||
<input id="name" name="name" value="{{.name}}" required maxlength="255">
|
||||
</div>
|
||||
</details>
|
||||
<button id="scoped-access-submit" class="ui primary button">
|
||||
{{ctx.Locale.Tr "settings.generate_token"}}
|
||||
</button>
|
||||
</form>{{/* Fomantic ".ui.form .warning.message" is hidden by default, so put the warning message out of the form*/}}
|
||||
<div id="scoped-access-warning" class="ui warning message center tw-hidden">
|
||||
{{ctx.Locale.Tr "settings.at_least_one_permission"}}
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="tw-my-2">{{ctx.Locale.Tr "settings.repo_and_org_access"}}</div>
|
||||
<label class="gt-checkbox">
|
||||
<input type="radio" name="scope-public-only" value="{{$.AccessTokenScopePublicOnly}}"> {{ctx.Locale.Tr "settings.permissions_public_only"}}
|
||||
</label>
|
||||
<label class="gt-checkbox">
|
||||
<input type="radio" name="scope-public-only" value="" checked> {{ctx.Locale.Tr "settings.permissions_access_all"}}
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tw-my-2">{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</div>
|
||||
<table class="ui table unstackable tw-my-2">
|
||||
{{range $category := .TokenCategories}}
|
||||
<tr>
|
||||
<td>{{$category}}</td>
|
||||
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="" checked> {{ctx.Locale.Tr "settings.permission_no_access"}}</label></td>
|
||||
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="read:{{$category}}"> {{ctx.Locale.Tr "settings.permission_read"}}</label></td>
|
||||
<td><label class="gt-checkbox"><input type="radio" name="scope-{{$category}}" value="write:{{$category}}"> {{ctx.Locale.Tr "settings.permission_write"}}</label></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
<button class="ui primary button">
|
||||
{{ctx.Locale.Tr "settings.generate_token"}}
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
{{if .EnableOAuth2}}
|
||||
|
@ -48,33 +48,33 @@
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached segment">
|
||||
<h5 class="ui top header">
|
||||
{{ctx.Locale.Tr "settings.create_oauth2_application"}}
|
||||
</h5>
|
||||
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_AppName}}error{{end}}">
|
||||
<label for="application-name">{{ctx.Locale.Tr "settings.oauth2_application_name"}}</label>
|
||||
<input id="application-name" name="application_name" value="{{.application_name}}" required maxlength="255">
|
||||
</div>
|
||||
<div class="field {{if .Err_RedirectURI}}error{{end}}">
|
||||
<label for="redirect-uris">{{ctx.Locale.Tr "settings.oauth2_redirect_uris"}}</label>
|
||||
<textarea name="redirect_uris" id="redirect-uris"></textarea>
|
||||
</div>
|
||||
<div class="field {{if .Err_ConfidentialClient}}error{{end}}">
|
||||
<div class="ui checkbox">
|
||||
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label>
|
||||
<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" checked>
|
||||
<details {{if .application_name}}open{{end}}>
|
||||
<summary><h4 class="ui header tw-inline-block tw-my-2">{{ctx.Locale.Tr "settings.create_oauth2_application"}}</h4></summary>
|
||||
<form class="ui form ignore-dirty" action="{{.Link}}/oauth2" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_AppName}}error{{end}}">
|
||||
<label for="application-name">{{ctx.Locale.Tr "settings.oauth2_application_name"}}</label>
|
||||
<input id="application-name" name="application_name" value="{{.application_name}}" required maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} disabled" id="skip-secondary-authorization">
|
||||
<div class="ui checkbox">
|
||||
<label>{{ctx.Locale.Tr "settings.oauth2_skip_secondary_authorization"}}</label>
|
||||
<input type="checkbox" name="skip_secondary_authorization">
|
||||
<div class="field {{if .Err_RedirectURI}}error{{end}}">
|
||||
<label for="redirect-uris">{{ctx.Locale.Tr "settings.oauth2_redirect_uris"}}</label>
|
||||
<textarea name="redirect_uris" id="redirect-uris"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button">
|
||||
{{ctx.Locale.Tr "settings.create_oauth2_application_button"}}
|
||||
</button>
|
||||
</form>
|
||||
<div class="field {{if .Err_ConfidentialClient}}error{{end}}">
|
||||
<div class="ui checkbox">
|
||||
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label>
|
||||
<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" checked>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} disabled" id="skip-secondary-authorization">
|
||||
<div class="ui checkbox">
|
||||
<label>{{ctx.Locale.Tr "settings.oauth2_skip_secondary_authorization"}}</label>
|
||||
<input type="checkbox" name="skip_secondary_authorization">
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button">
|
||||
{{ctx.Locale.Tr "settings.create_oauth2_application_button"}}
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
@ -76,7 +76,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.AccessTokenScopeAll)
|
||||
org := api.CreateOrgOption{
|
||||
UserName: "user2_org",
|
||||
FullName: "User2's organization",
|
||||
|
@ -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.AccessTokenScopeAll)
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d", adminUsername, newPublicKey.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
@ -139,7 +139,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.AccessTokenScopeAll)
|
||||
req := NewRequest(t, "GET", "/api/v1/admin/users").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
@ -33,6 +33,10 @@ type APITestContext struct {
|
||||
|
||||
func NewAPITestContext(t *testing.T, username, reponame string, scope ...auth.AccessTokenScope) APITestContext {
|
||||
session := loginUser(t, username)
|
||||
if len(scope) == 0 {
|
||||
// FIXME: legacy logic: no scope means all
|
||||
scope = []auth.AccessTokenScope{auth.AccessTokenScopeAll}
|
||||
}
|
||||
token := getTokenForLoggedInUser(t, session, scope...)
|
||||
return APITestContext{
|
||||
Session: session,
|
||||
|
@ -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.AccessTokenScopeAll)
|
||||
|
||||
// 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)
|
||||
|
@ -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.AccessTokenScopeAll)
|
||||
|
||||
// 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)
|
||||
|
@ -249,55 +249,19 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
|
||||
// token has to be unique this counter take care of
|
||||
var tokenCounter int64
|
||||
|
||||
// getTokenForLoggedInUser returns a token for a logged in user.
|
||||
// The scope is an optional list of snake_case strings like the frontend form fields,
|
||||
// but without the "scope_" prefix.
|
||||
// getTokenForLoggedInUser returns a token for a logged-in user.
|
||||
func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.AccessTokenScope) string {
|
||||
t.Helper()
|
||||
var token string
|
||||
req := NewRequest(t, "GET", "/user/settings/applications")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
var csrf string
|
||||
for _, cookie := range resp.Result().Cookies() {
|
||||
if cookie.Name != "_csrf" {
|
||||
continue
|
||||
}
|
||||
csrf = cookie.Value
|
||||
break
|
||||
}
|
||||
if csrf == "" {
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
csrf = doc.GetCSRF()
|
||||
}
|
||||
assert.NotEmpty(t, csrf)
|
||||
urlValues := url.Values{}
|
||||
urlValues.Add("_csrf", csrf)
|
||||
urlValues.Add("_csrf", GetUserCSRFToken(t, session))
|
||||
urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)))
|
||||
for _, scope := range scopes {
|
||||
urlValues.Add("scope", string(scope))
|
||||
urlValues.Add("scope-dummy", string(scope)) // it only needs to start with "scope-" to be accepted
|
||||
}
|
||||
req = NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
|
||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// Log the flash values on failure
|
||||
if !assert.Equal(t, []string{"/user/settings/applications"}, resp.Result().Header["Location"]) {
|
||||
for _, cookie := range resp.Result().Cookies() {
|
||||
if cookie.Name != gitea_context.CookieNameFlash {
|
||||
continue
|
||||
}
|
||||
flash, _ := url.ParseQuery(cookie.Value)
|
||||
for key, value := range flash {
|
||||
t.Logf("Flash %q: %q", key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req = NewRequest(t, "GET", "/user/settings/applications")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
token = htmlDoc.doc.Find(".ui.info p").Text()
|
||||
assert.NotEmpty(t, token)
|
||||
return token
|
||||
req := NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
flashes := session.GetCookieFlashMessage()
|
||||
return flashes.InfoMsg
|
||||
}
|
||||
|
||||
type RequestWrapper struct {
|
||||
|
@ -19,7 +19,6 @@
|
||||
@import "./modules/dimmer.css";
|
||||
@import "./modules/modal.css";
|
||||
|
||||
@import "./modules/select.css";
|
||||
@import "./modules/tippy.css";
|
||||
@import "./modules/breadcrumb.css";
|
||||
@import "./modules/comment.css";
|
||||
|
@ -119,3 +119,13 @@ input[type="radio"] {
|
||||
.ui.toggle.checkbox input:focus:checked ~ label::before {
|
||||
background: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
label.gt-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
.ui.form .field > label.gt-checkbox {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
.gitea-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gitea-select select {
|
||||
appearance: none; /* hide default triangle */
|
||||
}
|
||||
|
||||
/* ::before and ::after pseudo elements don't work on select elements,
|
||||
so we need to put it on the parent. */
|
||||
.gitea-select::after {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 8px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
mask-size: cover;
|
||||
-webkit-mask-size: cover;
|
||||
mask-image: var(--octicon-chevron-right);
|
||||
-webkit-mask-image: var(--octicon-chevron-right);
|
||||
transform: rotate(90deg); /* point the chevron down */
|
||||
background: currentcolor;
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import {computed, onMounted, onUnmounted} from 'vue';
|
||||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
isAdmin: boolean;
|
||||
noAccessLabel: string;
|
||||
readLabel: string;
|
||||
writeLabel: string;
|
||||
}>();
|
||||
|
||||
const categories = computed(() => {
|
||||
const categories = [
|
||||
'activitypub',
|
||||
];
|
||||
if (props.isAdmin) {
|
||||
categories.push('admin');
|
||||
}
|
||||
categories.push(
|
||||
'issue',
|
||||
'misc',
|
||||
'notification',
|
||||
'organization',
|
||||
'package',
|
||||
'repository',
|
||||
'user');
|
||||
return categories;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
document.querySelector('#scoped-access-submit').addEventListener('click', onClickSubmit);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit);
|
||||
});
|
||||
|
||||
function onClickSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const warningEl = document.querySelector('#scoped-access-warning');
|
||||
// check that at least one scope has been selected
|
||||
for (const el of document.querySelectorAll<HTMLInputElement>('.access-token-select')) {
|
||||
if (el.value) {
|
||||
// Hide the error if it was visible from previous attempt.
|
||||
hideElem(warningEl);
|
||||
// Submit the form.
|
||||
document.querySelector<HTMLFormElement>('#scoped-access-form').submit();
|
||||
// Don't show the warning.
|
||||
return;
|
||||
}
|
||||
}
|
||||
// no scopes selected, show validation error
|
||||
showElem(warningEl);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-for="category in categories" :key="category" class="field tw-pl-1 tw-pb-1 access-token-category">
|
||||
<label class="category-label" :for="'access-token-scope-' + category">
|
||||
{{ category }}
|
||||
</label>
|
||||
<div class="gitea-select">
|
||||
<select
|
||||
class="ui selection access-token-select"
|
||||
name="scope"
|
||||
:id="'access-token-scope-' + category"
|
||||
>
|
||||
<option value="">
|
||||
{{ noAccessLabel }}
|
||||
</option>
|
||||
<option :value="'read:' + category">
|
||||
{{ readLabel }}
|
||||
</option>
|
||||
<option :value="'write:' + category">
|
||||
{{ writeLabel }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
@ -1,20 +0,0 @@
|
||||
import {createApp} from 'vue';
|
||||
|
||||
export async function initScopedAccessTokenCategories() {
|
||||
const el = document.querySelector('#scoped-access-token-selector');
|
||||
if (!el) return;
|
||||
|
||||
const {default: ScopedAccessTokenSelector} = await import(/* webpackChunkName: "scoped-access-token-selector" */'../components/ScopedAccessTokenSelector.vue');
|
||||
try {
|
||||
const View = createApp(ScopedAccessTokenSelector, {
|
||||
isAdmin: JSON.parse(el.getAttribute('data-is-admin')),
|
||||
noAccessLabel: el.getAttribute('data-no-access-label'),
|
||||
readLabel: el.getAttribute('data-read-label'),
|
||||
writeLabel: el.getAttribute('data-write-label'),
|
||||
});
|
||||
View.mount(el);
|
||||
} catch (err) {
|
||||
console.error('ScopedAccessTokenSelector failed to load', err);
|
||||
el.textContent = el.getAttribute('data-locale-component-failed-to-load');
|
||||
}
|
||||
}
|
@ -68,7 +68,6 @@ import {initColorPickers} from './features/colorpicker.ts';
|
||||
import {initAdminSelfCheck} from './features/admin/selfcheck.ts';
|
||||
import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts';
|
||||
import {initGlobalFetchAction} from './features/common-fetch-action.ts';
|
||||
import {initScopedAccessTokenCategories} from './features/scoped-access-token.ts';
|
||||
import {
|
||||
initFootLanguageMenu,
|
||||
initGlobalDropdown,
|
||||
@ -209,7 +208,6 @@ onDomReady(() => {
|
||||
initUserSettings,
|
||||
initRepoDiffView,
|
||||
initPdfViewer,
|
||||
initScopedAccessTokenCategories,
|
||||
initColorPickers,
|
||||
|
||||
initOAuth2SettingsDisableCheckbox,
|
||||
|
Loading…
x
Reference in New Issue
Block a user