mirror of
https://github.com/go-gitea/gitea
synced 2024-07-27 17:17:08 +02:00
Merge branch 'main' into bugfix/migrate_lfs
This commit is contained in:
commit
c1f7ad456e
|
@ -512,7 +512,7 @@ rules:
|
|||
no-jquery/no-box-model: [2]
|
||||
no-jquery/no-browser: [2]
|
||||
no-jquery/no-camel-case: [2]
|
||||
no-jquery/no-class-state: [0]
|
||||
no-jquery/no-class-state: [2]
|
||||
no-jquery/no-class: [0]
|
||||
no-jquery/no-clone: [2]
|
||||
no-jquery/no-closest: [0]
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -108,6 +108,9 @@ prime/
|
|||
*_source.tar.bz2
|
||||
.DS_Store
|
||||
|
||||
# nix-direnv generated files
|
||||
.direnv/
|
||||
|
||||
# Make evidence files
|
||||
/.make_evidence
|
||||
|
||||
|
|
2
Makefile
2
Makefile
|
@ -887,6 +887,8 @@ update-js: node-check | node_modules
|
|||
npx updates -u -f package.json
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install --package-lock
|
||||
npx nolyfill install
|
||||
npm install --package-lock
|
||||
@touch node_modules
|
||||
|
||||
.PHONY: update-py
|
||||
|
|
|
@ -1676,6 +1676,10 @@ LEVEL = Info
|
|||
;; Sometimes it is helpful to use a different address on the envelope. Set this to use ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address.
|
||||
;ENVELOPE_FROM =
|
||||
;;
|
||||
;; If gitea sends mails on behave of users, it will just use the name also displayed in the WebUI. If you want e.g. `Mister X (by CodeIt) <gitea@codeit.net>`,
|
||||
;; set it to `{{ .DisplayName }} (by {{ .AppName }})`. Available Variables: `.DisplayName`, `.AppName` and `.Domain`.
|
||||
;FROM_DISPLAY_NAME_FORMAT = {{ .DisplayName }}
|
||||
;;
|
||||
;; Mailer user name and password, if required by provider.
|
||||
;USER =
|
||||
;;
|
||||
|
|
|
@ -20,11 +20,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1717974879,
|
||||
"narHash": "sha256-GTO3C88+5DX171F/gVS3Qga/hOs/eRMxPFpiHq2t+D8=",
|
||||
"lastModified": 1720542800,
|
||||
"narHash": "sha256-ZgnNHuKV6h2+fQ5LuqnUaqZey1Lqqt5dTUAiAnqH0QQ=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "c7b821ba2e1e635ba5a76d299af62821cbcb09f3",
|
||||
"rev": "feb2849fdeb70028c70d73b848214b00d324a497",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
4
go.mod
4
go.mod
|
@ -91,7 +91,7 @@ require (
|
|||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/quasoft/websspi v1.1.2
|
||||
github.com/redis/go-redis/v9 v9.5.3
|
||||
github.com/redis/go-redis/v9 v9.6.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/sassoftware/go-rpmutils v0.4.0
|
||||
|
@ -123,7 +123,7 @@ require (
|
|||
mvdan.cc/xurls/v2 v2.5.0
|
||||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
|
||||
xorm.io/builder v0.3.13
|
||||
xorm.io/xorm v1.3.8
|
||||
xorm.io/xorm v1.3.9
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
8
go.sum
8
go.sum
|
@ -668,8 +668,8 @@ github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43Z
|
|||
github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw=
|
||||
github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
|
||||
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/redis/go-redis/v9 v9.6.0 h1:NLck+Rab3AOTHw21CGRpvQpgTrAU4sgdCswqGtlhGRA=
|
||||
github.com/redis/go-redis/v9 v9.6.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rhysd/actionlint v1.7.1 h1:WJaDzyT1StBWVKGSsZPYnbV0HF9Y9/vD6KFdZQL42qE=
|
||||
|
@ -1064,5 +1064,5 @@ strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3
|
|||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
|
||||
xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
|
||||
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
||||
xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
|
||||
xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
|
||||
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
|
||||
|
|
|
@ -37,10 +37,11 @@ type OAuth2Application struct {
|
|||
// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1
|
||||
// "Authorization servers MUST record the client type in the client registration details"
|
||||
// https://datatracker.ietf.org/doc/html/rfc8252#section-8.4
|
||||
ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"`
|
||||
RedirectURIs []string `xorm:"redirect_uris JSON TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
ConfidentialClient bool `xorm:"NOT NULL DEFAULT TRUE"`
|
||||
SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
RedirectURIs []string `xorm:"redirect_uris JSON TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -251,21 +252,23 @@ func GetOAuth2ApplicationByID(ctx context.Context, id int64) (app *OAuth2Applica
|
|||
|
||||
// CreateOAuth2ApplicationOptions holds options to create an oauth2 application
|
||||
type CreateOAuth2ApplicationOptions struct {
|
||||
Name string
|
||||
UserID int64
|
||||
ConfidentialClient bool
|
||||
RedirectURIs []string
|
||||
Name string
|
||||
UserID int64
|
||||
ConfidentialClient bool
|
||||
SkipSecondaryAuthorization bool
|
||||
RedirectURIs []string
|
||||
}
|
||||
|
||||
// CreateOAuth2Application inserts a new oauth2 application
|
||||
func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOptions) (*OAuth2Application, error) {
|
||||
clientID := uuid.New().String()
|
||||
app := &OAuth2Application{
|
||||
UID: opts.UserID,
|
||||
Name: opts.Name,
|
||||
ClientID: clientID,
|
||||
RedirectURIs: opts.RedirectURIs,
|
||||
ConfidentialClient: opts.ConfidentialClient,
|
||||
UID: opts.UserID,
|
||||
Name: opts.Name,
|
||||
ClientID: clientID,
|
||||
RedirectURIs: opts.RedirectURIs,
|
||||
ConfidentialClient: opts.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: opts.SkipSecondaryAuthorization,
|
||||
}
|
||||
if err := db.Insert(ctx, app); err != nil {
|
||||
return nil, err
|
||||
|
@ -275,11 +278,12 @@ func CreateOAuth2Application(ctx context.Context, opts CreateOAuth2ApplicationOp
|
|||
|
||||
// UpdateOAuth2ApplicationOptions holds options to update an oauth2 application
|
||||
type UpdateOAuth2ApplicationOptions struct {
|
||||
ID int64
|
||||
Name string
|
||||
UserID int64
|
||||
ConfidentialClient bool
|
||||
RedirectURIs []string
|
||||
ID int64
|
||||
Name string
|
||||
UserID int64
|
||||
ConfidentialClient bool
|
||||
SkipSecondaryAuthorization bool
|
||||
RedirectURIs []string
|
||||
}
|
||||
|
||||
// UpdateOAuth2Application updates an oauth2 application
|
||||
|
@ -305,6 +309,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp
|
|||
app.Name = opts.Name
|
||||
app.RedirectURIs = opts.RedirectURIs
|
||||
app.ConfidentialClient = opts.ConfidentialClient
|
||||
app.SkipSecondaryAuthorization = opts.SkipSecondaryAuthorization
|
||||
|
||||
if err = updateOAuth2Application(ctx, app); err != nil {
|
||||
return nil, err
|
||||
|
@ -315,7 +320,7 @@ func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOp
|
|||
}
|
||||
|
||||
func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error {
|
||||
if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client").Update(app); err != nil {
|
||||
if _, err := db.GetEngine(ctx).ID(app.ID).UseBool("confidential_client", "skip_secondary_authorization").Update(app); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -210,7 +210,7 @@ func CreateSource(ctx context.Context, source *Source) error {
|
|||
return ErrSourceAlreadyExist{source.Name}
|
||||
}
|
||||
// Synchronization is only available with LDAP for now
|
||||
if !source.IsLDAP() {
|
||||
if !source.IsLDAP() && !source.IsOAuth2() {
|
||||
source.IsSyncEnabled = false
|
||||
}
|
||||
|
||||
|
|
|
@ -70,8 +70,10 @@ func (opts FindMilestoneOptions) ToOrders() string {
|
|||
return "num_issues DESC"
|
||||
case "id":
|
||||
return "id ASC"
|
||||
case "name":
|
||||
return "name DESC"
|
||||
default:
|
||||
return "deadline_unix ASC, id ASC"
|
||||
return "deadline_unix ASC, name ASC"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -593,6 +593,8 @@ var migrations = []Migration{
|
|||
NewMigration("Add content version to issue and comment table", v1_23.AddContentVersionToIssueAndComment),
|
||||
// v300 -> v301
|
||||
NewMigration("Add force-push branch protection support", v1_23.AddForcePushBranchProtection),
|
||||
// v301 -> v302
|
||||
NewMigration("Add skip_secondary_authorization option to oauth2 application table", v1_23.AddSkipSecondaryAuthColumnToOAuth2ApplicationTable),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
|
@ -12,5 +12,9 @@ func AddIndexToActionUserID(x *xorm.Engine) error {
|
|||
UserID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Action))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(Action))
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -10,5 +10,9 @@ func AddIgnoreStaleApprovalsColumnToProtectedBranchTable(x *xorm.Engine) error {
|
|||
type ProtectedBranch struct {
|
||||
IgnoreStaleApprovals bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(ProtectedBranch))
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(ProtectedBranch))
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -14,5 +14,9 @@ func AddPreviousDurationToActionRun(x *xorm.Engine) error {
|
|||
PreviousDuration time.Duration
|
||||
}
|
||||
|
||||
return x.Sync(&ActionRun{})
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, &ActionRun{})
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -86,7 +86,10 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
|
|||
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
|
||||
}
|
||||
|
||||
if err := x.Sync(new(Repository)); err != nil {
|
||||
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(Repository)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,10 @@ func AddDefaultWikiBranch(x *xorm.Engine) error {
|
|||
ID int64
|
||||
DefaultWikiBranch string
|
||||
}
|
||||
if err := x.Sync(&Repository{}); err != nil {
|
||||
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, &Repository{}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := x.Exec("UPDATE `repository` SET default_wiki_branch = 'master' WHERE (default_wiki_branch IS NULL) OR (default_wiki_branch = '')")
|
||||
|
|
|
@ -13,5 +13,12 @@ type HookTask struct {
|
|||
|
||||
func AddPayloadVersionToHookTaskTable(x *xorm.Engine) error {
|
||||
// create missing column
|
||||
return x.Sync(new(HookTask))
|
||||
if _, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, new(HookTask)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := x.Exec("UPDATE hook_task SET payload_version = 1 WHERE payload_version IS NULL")
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -10,5 +10,9 @@ func AddCommentIDIndexofAttachment(x *xorm.Engine) error {
|
|||
CommentID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
return x.Sync(&Attachment{})
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||
IgnoreDropIndices: true,
|
||||
IgnoreConstrains: true,
|
||||
}, &Attachment{})
|
||||
return err
|
||||
}
|
||||
|
|
14
models/migrations/v1_23/v301.go
Normal file
14
models/migrations/v1_23/v301.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //nolint
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
// AddSkipSeconderyAuthToOAuth2ApplicationTable: add SkipSecondaryAuthorization column, setting existing rows to false
|
||||
func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error {
|
||||
type oauth2Application struct {
|
||||
SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||
}
|
||||
return x.Sync(new(oauth2Application))
|
||||
}
|
|
@ -160,12 +160,34 @@ func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLogin
|
|||
return err
|
||||
}
|
||||
|
||||
// EnsureLinkExternalToUser link the external user to the user
|
||||
func EnsureLinkExternalToUser(ctx context.Context, external *ExternalLoginUser) error {
|
||||
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
|
||||
"external_id": external.ExternalID,
|
||||
"login_source_id": external.LoginSourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if has {
|
||||
_, err = db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).AllCols().Update(external)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).Insert(external)
|
||||
return err
|
||||
}
|
||||
|
||||
// FindExternalUserOptions represents an options to find external users
|
||||
type FindExternalUserOptions struct {
|
||||
db.ListOptions
|
||||
Provider string
|
||||
UserID int64
|
||||
OrderBy string
|
||||
Provider string
|
||||
UserID int64
|
||||
LoginSourceID int64
|
||||
HasRefreshToken bool
|
||||
Expired bool
|
||||
OrderBy string
|
||||
}
|
||||
|
||||
func (opts FindExternalUserOptions) ToConds() builder.Cond {
|
||||
|
@ -176,9 +198,22 @@ func (opts FindExternalUserOptions) ToConds() builder.Cond {
|
|||
if opts.UserID > 0 {
|
||||
cond = cond.And(builder.Eq{"user_id": opts.UserID})
|
||||
}
|
||||
if opts.Expired {
|
||||
cond = cond.And(builder.Lt{"expires_at": time.Now()})
|
||||
}
|
||||
if opts.HasRefreshToken {
|
||||
cond = cond.And(builder.Neq{"refresh_token": ""})
|
||||
}
|
||||
if opts.LoginSourceID != 0 {
|
||||
cond = cond.And(builder.Eq{"login_source_id": opts.LoginSourceID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts FindExternalUserOptions) ToOrders() string {
|
||||
return opts.OrderBy
|
||||
}
|
||||
|
||||
func IterateExternalLogin(ctx context.Context, opts FindExternalUserOptions, f func(ctx context.Context, u *ExternalLoginUser) error) error {
|
||||
return db.Iterate(ctx, opts.ToConds(), f)
|
||||
}
|
||||
|
|
|
@ -48,13 +48,10 @@ func BasicAuthDecode(encoded string) (string, string, error) {
|
|||
return "", "", err
|
||||
}
|
||||
|
||||
auth := strings.SplitN(string(s), ":", 2)
|
||||
|
||||
if len(auth) != 2 {
|
||||
return "", "", errors.New("invalid basic authentication")
|
||||
if username, password, ok := strings.Cut(string(s), ":"); ok {
|
||||
return username, password, nil
|
||||
}
|
||||
|
||||
return auth[0], auth[1], nil
|
||||
return "", "", errors.New("invalid basic authentication")
|
||||
}
|
||||
|
||||
// VerifyTimeLimitCode verify time limit code
|
||||
|
|
|
@ -41,6 +41,9 @@ func TestBasicAuthDecode(t *testing.T) {
|
|||
|
||||
_, _, err = BasicAuthDecode("invalid")
|
||||
assert.Error(t, err)
|
||||
|
||||
_, _, err = BasicAuthDecode("YWxpY2U=") // "alice", no colon
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestVerifyTimeLimitCode(t *testing.T) {
|
||||
|
|
|
@ -71,6 +71,12 @@ func (i *Indexer) Search(ctx context.Context, options *internal.SearchOptions) (
|
|||
)),
|
||||
),
|
||||
)
|
||||
|
||||
if options.IsKeywordNumeric() {
|
||||
cond = cond.Or(
|
||||
builder.Eq{"`index`": options.Keyword},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
opt, err := ToDBOptions(ctx, options)
|
||||
|
|
|
@ -44,6 +44,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
||||
}
|
||||
|
||||
if opts.AssigneeID > 0 {
|
||||
searchOpt.AssigneeID = optional.Some(opts.AssigneeID)
|
||||
} else if opts.AssigneeID == -1 { // FIXME: this is inconsistent from other places
|
||||
searchOpt.AssigneeID = optional.Some[int64](0)
|
||||
}
|
||||
|
||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||
convertID := func(id int64) optional.Option[int64] {
|
||||
if id > 0 {
|
||||
|
@ -57,7 +63,6 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||
|
||||
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
|
||||
searchOpt.PosterID = convertID(opts.PosterID)
|
||||
searchOpt.AssigneeID = convertID(opts.AssigneeID)
|
||||
searchOpt.MentionID = convertID(opts.MentionedID)
|
||||
searchOpt.ReviewedID = convertID(opts.ReviewedID)
|
||||
searchOpt.ReviewRequestedID = convertID(opts.ReviewRequestedID)
|
||||
|
|
|
@ -283,9 +283,9 @@ const (
|
|||
func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) {
|
||||
indexer := *globalIndexer.Load()
|
||||
|
||||
if opts.Keyword == "" {
|
||||
if opts.Keyword == "" || opts.IsKeywordNumeric() {
|
||||
// This is a conservative shortcut.
|
||||
// If the keyword is empty, db has better (at least not worse) performance to filter issues.
|
||||
// If the keyword is empty or an integer, db has better (at least not worse) performance to filter issues.
|
||||
// When the keyword is empty, it tends to listing rather than searching issues.
|
||||
// So if the user creates an issue and list issues immediately, the issue may not be listed because the indexer needs time to index the issue.
|
||||
// Even worse, the external indexer like elastic search may not be available for a while,
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
|
@ -31,6 +32,7 @@ func TestDBSearchIssues(t *testing.T) {
|
|||
InitIssueIndexer(true)
|
||||
|
||||
t.Run("search issues with keyword", searchIssueWithKeyword)
|
||||
t.Run("search issues by index", searchIssueByIndex)
|
||||
t.Run("search issues in repo", searchIssueInRepo)
|
||||
t.Run("search issues by ID", searchIssueByID)
|
||||
t.Run("search issues is pr", searchIssueIsPull)
|
||||
|
@ -87,6 +89,43 @@ func searchIssueWithKeyword(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func searchIssueByIndex(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts SearchOptions
|
||||
expectedIDs []int64
|
||||
}{
|
||||
{
|
||||
SearchOptions{
|
||||
Keyword: "1000",
|
||||
RepoIDs: []int64{1},
|
||||
},
|
||||
[]int64{},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
Keyword: "2",
|
||||
RepoIDs: []int64{1, 2, 3, 32},
|
||||
},
|
||||
[]int64{17, 12, 7, 2},
|
||||
},
|
||||
{
|
||||
SearchOptions{
|
||||
Keyword: "1",
|
||||
RepoIDs: []int64{58},
|
||||
},
|
||||
[]int64{19},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
issueIDs, _, err := SearchIssues(context.TODO(), &test.opts)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, test.expectedIDs, issueIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func searchIssueInRepo(t *testing.T) {
|
||||
tests := []struct {
|
||||
opts SearchOptions
|
||||
|
@ -150,6 +189,11 @@ func searchIssueByID(t *testing.T) {
|
|||
},
|
||||
expectedIDs: []int64{6, 1},
|
||||
},
|
||||
{
|
||||
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1.
|
||||
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: -1}),
|
||||
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2},
|
||||
},
|
||||
{
|
||||
opts: SearchOptions{
|
||||
MentionID: optional.Some(int64(4)),
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
@ -124,6 +126,12 @@ func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOption
|
|||
return &v
|
||||
}
|
||||
|
||||
// used for optimized issue index based search
|
||||
func (o *SearchOptions) IsKeywordNumeric() bool {
|
||||
_, err := strconv.Atoi(o.Keyword)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
type SortBy string
|
||||
|
||||
const (
|
||||
|
|
|
@ -88,6 +88,9 @@ func validateYaml(template *api.IssueTemplate) error {
|
|||
if err := validateBoolItem(position, field.Attributes, "multiple"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateBoolItem(position, field.Attributes, "list"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateOptions(field, idx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -340,7 +343,13 @@ func (f *valuedField) WriteTo(builder *strings.Builder) {
|
|||
}
|
||||
}
|
||||
if len(checkeds) > 0 {
|
||||
_, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", "))
|
||||
if list, ok := f.Attributes["list"].(bool); ok && list {
|
||||
for _, check := range checkeds {
|
||||
_, _ = fmt.Fprintf(builder, "- %s\n", check)
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(builder, "%s\n", strings.Join(checkeds, ", "))
|
||||
}
|
||||
} else {
|
||||
_, _ = fmt.Fprint(builder, blankPlaceholder)
|
||||
}
|
||||
|
|
|
@ -216,6 +216,20 @@ body:
|
|||
`,
|
||||
wantErr: "body[0](dropdown): 'multiple' should be a bool",
|
||||
},
|
||||
{
|
||||
name: "dropdown invalid list",
|
||||
content: `
|
||||
name: "test"
|
||||
about: "this is about"
|
||||
body:
|
||||
- type: "dropdown"
|
||||
id: "1"
|
||||
attributes:
|
||||
label: "a"
|
||||
list: "on"
|
||||
`,
|
||||
wantErr: "body[0](dropdown): 'list' should be a bool",
|
||||
},
|
||||
{
|
||||
name: "checkboxes invalid description",
|
||||
content: `
|
||||
|
@ -807,7 +821,7 @@ body:
|
|||
- type: dropdown
|
||||
id: id5
|
||||
attributes:
|
||||
label: Label of dropdown
|
||||
label: Label of dropdown (one line)
|
||||
description: Description of dropdown
|
||||
multiple: true
|
||||
options:
|
||||
|
@ -816,8 +830,21 @@ body:
|
|||
- Option 3 of dropdown
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
id: id6
|
||||
attributes:
|
||||
label: Label of dropdown (list)
|
||||
description: Description of dropdown
|
||||
multiple: true
|
||||
list: true
|
||||
options:
|
||||
- Option 1 of dropdown
|
||||
- Option 2 of dropdown
|
||||
- Option 3 of dropdown
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: id7
|
||||
attributes:
|
||||
label: Label of checkboxes
|
||||
description: Description of checkboxes
|
||||
|
@ -836,8 +863,9 @@ body:
|
|||
"form-field-id3": {"Value of id3"},
|
||||
"form-field-id4": {"Value of id4"},
|
||||
"form-field-id5": {"0,1"},
|
||||
"form-field-id6-0": {"on"},
|
||||
"form-field-id6-2": {"on"},
|
||||
"form-field-id6": {"1,2"},
|
||||
"form-field-id7-0": {"on"},
|
||||
"form-field-id7-2": {"on"},
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -849,10 +877,15 @@ body:
|
|||
|
||||
Value of id4
|
||||
|
||||
### Label of dropdown
|
||||
### Label of dropdown (one line)
|
||||
|
||||
Option 1 of dropdown, Option 2 of dropdown
|
||||
|
||||
### Label of dropdown (list)
|
||||
|
||||
- Option 2 of dropdown
|
||||
- Option 3 of dropdown
|
||||
|
||||
### Label of checkboxes
|
||||
|
||||
- [x] Option 1 of checkboxes
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"net"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -46,6 +47,10 @@ type Mailer struct {
|
|||
SendmailArgs []string `ini:"-"`
|
||||
SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"`
|
||||
SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"`
|
||||
|
||||
// Customization
|
||||
FromDisplayNameFormat string `ini:"FROM_DISPLAY_NAME_FORMAT"`
|
||||
FromDisplayNameFormatTemplate *template.Template `ini:"-"`
|
||||
}
|
||||
|
||||
// MailService the global mailer
|
||||
|
@ -226,6 +231,16 @@ func loadMailerFrom(rootCfg ConfigProvider) {
|
|||
log.Error("no mailer.FROM provided, email system may not work.")
|
||||
}
|
||||
|
||||
MailService.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}")
|
||||
if MailService.FromDisplayNameFormat != "" {
|
||||
template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat)
|
||||
if err != nil {
|
||||
log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err)
|
||||
} else {
|
||||
MailService.FromDisplayNameFormatTemplate = template
|
||||
}
|
||||
}
|
||||
|
||||
switch MailService.EnvelopeFrom {
|
||||
case "":
|
||||
MailService.OverrideEnvelopeFrom = false
|
||||
|
|
|
@ -31,21 +31,23 @@ type CreateAccessTokenOption struct {
|
|||
|
||||
// CreateOAuth2ApplicationOptions holds options to create an oauth2 application
|
||||
type CreateOAuth2ApplicationOptions struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
ConfidentialClient bool `json:"confidential_client"`
|
||||
RedirectURIs []string `json:"redirect_uris" binding:"Required"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
ConfidentialClient bool `json:"confidential_client"`
|
||||
SkipSecondaryAuthorization bool `json:"skip_secondary_authorization"`
|
||||
RedirectURIs []string `json:"redirect_uris" binding:"Required"`
|
||||
}
|
||||
|
||||
// OAuth2Application represents an OAuth2 application.
|
||||
// swagger:response OAuth2Application
|
||||
type OAuth2Application struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
ConfidentialClient bool `json:"confidential_client"`
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
Created time.Time `json:"created"`
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
ConfidentialClient bool `json:"confidential_client"`
|
||||
SkipSecondaryAuthorization bool `json:"skip_secondary_authorization"`
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
// OAuth2ApplicationList represents a list of OAuth2 applications.
|
||||
|
|
|
@ -1927,6 +1927,7 @@ milestones.edit_success=Milník „%s“ byl aktualizován.
|
|||
milestones.deletion=Smazat milník
|
||||
milestones.deletion_desc=Odstranění milníku jej smaže ze všech souvisejících úkolů. Pokračovat?
|
||||
milestones.deletion_success=Milník byl odstraněn.
|
||||
milestones.filter_sort.name=Název
|
||||
milestones.filter_sort.earliest_due_data=Nejstarší datum dokončení
|
||||
milestones.filter_sort.latest_due_date=Nejnovější datum dokončení
|
||||
milestones.filter_sort.least_complete=Nejméně dokončené
|
||||
|
|
|
@ -1907,6 +1907,7 @@ milestones.edit_success=Meilenstein "%s" wurde aktualisiert.
|
|||
milestones.deletion=Meilenstein löschen
|
||||
milestones.deletion_desc=Das Löschen des Meilensteins entfernt ihn von allen Issues. Fortfahren?
|
||||
milestones.deletion_success=Der Meilenstein wurde gelöscht.
|
||||
milestones.filter_sort.name=Name
|
||||
milestones.filter_sort.earliest_due_data=Frühestes Fälligkeitsdatum
|
||||
milestones.filter_sort.latest_due_date=Spätestes Fälligkeitsdatum
|
||||
milestones.filter_sort.least_complete=Am wenigsten vollständig
|
||||
|
|
|
@ -1826,6 +1826,7 @@ milestones.edit_success=Το ορόσημο "%s" ενημερώθηκε.
|
|||
milestones.deletion=Διαγραφή Ορόσημου
|
||||
milestones.deletion_desc=Η διαγραφή ενός ορόσημου το αφαιρεί από όλα τα συναφή ζητήματα. Συνέχεια;
|
||||
milestones.deletion_success=Το ορόσημο έχει διαγραφεί.
|
||||
milestones.filter_sort.name=Όνομα
|
||||
milestones.filter_sort.earliest_due_data=Πλησιέστερη παράδοση
|
||||
milestones.filter_sort.latest_due_date=Απώτερη παράδοση
|
||||
milestones.filter_sort.least_complete=Λιγότερο πλήρη
|
||||
|
|
|
@ -914,6 +914,7 @@ create_oauth2_application_success = You have successfully created a new OAuth2 a
|
|||
update_oauth2_application_success = You have successfully updated the OAuth2 application.
|
||||
oauth2_application_name = Application Name
|
||||
oauth2_confidential_client = Confidential Client. Select for apps that keep the secret confidential, such as web apps. Do not select for native apps including desktop and mobile apps.
|
||||
oauth2_skip_secondary_authorization = Skip authorization for public clients after granting access once. <strong>May pose a security risk.</strong>
|
||||
oauth2_redirect_uris = Redirect URIs. Please use a new line for every URI.
|
||||
save_application = Save
|
||||
oauth2_client_id = Client ID
|
||||
|
@ -1935,6 +1936,7 @@ milestones.edit_success = Milestone "%s" has been updated.
|
|||
milestones.deletion = Delete Milestone
|
||||
milestones.deletion_desc = Deleting a milestone removes it from all related issues. Continue?
|
||||
milestones.deletion_success = The milestone has been deleted.
|
||||
milestones.filter_sort.name = Name
|
||||
milestones.filter_sort.earliest_due_data = Earliest due date
|
||||
milestones.filter_sort.latest_due_date = Latest due date
|
||||
milestones.filter_sort.least_complete = Least complete
|
||||
|
|
|
@ -1812,6 +1812,7 @@ milestones.edit_success=Se ha actualizado el hito "%s".
|
|||
milestones.deletion=Eliminar hito
|
||||
milestones.deletion_desc=Eliminando un hito lo elimina de todos los problemas relacionados. ¿Continuar?
|
||||
milestones.deletion_success=El hito se ha eliminado.
|
||||
milestones.filter_sort.name=Nombre
|
||||
milestones.filter_sort.earliest_due_data=Fecha de vencimiento más temprana
|
||||
milestones.filter_sort.latest_due_date=Fecha de vencimiento más lejana
|
||||
milestones.filter_sort.least_complete=Menos completa
|
||||
|
|
|
@ -1389,6 +1389,7 @@ milestones.modify=به روزرسانی نقطه عطف
|
|||
milestones.deletion=حذف نقطه عطف
|
||||
milestones.deletion_desc=نقاط عطف از تمام مسائل مرتبط حذف میشوند. آیا ادامه میدهید؟
|
||||
milestones.deletion_success=نقطه عطف حذف شد.
|
||||
milestones.filter_sort.name=نام
|
||||
milestones.filter_sort.least_complete=حداقل کامل شده
|
||||
milestones.filter_sort.most_complete=بیشترین کامل شده
|
||||
milestones.filter_sort.most_issues=بیشترین مسائل
|
||||
|
|
|
@ -1009,6 +1009,7 @@ milestones.clear=Tyhjennä
|
|||
milestones.edit=Muokkaa merkkipaalua
|
||||
milestones.cancel=Peruuta
|
||||
milestones.modify=Päivitä merkkipaalu
|
||||
milestones.filter_sort.name=Nimi
|
||||
milestones.filter_sort.most_issues=Eniten ongelmia
|
||||
milestones.filter_sort.least_issues=Vähiten ongelmia
|
||||
|
||||
|
|
|
@ -387,6 +387,8 @@ relevant_repositories=Seuls les dépôts pertinents sont affichés, <a href="%s"
|
|||
|
||||
[auth]
|
||||
create_new_account=Créer un compte
|
||||
already_have_account=Avez-vous déjà un compte ?
|
||||
sign_in_now=Connectez-vous!
|
||||
disable_register_prompt=Les inscriptions sont désactivées. Veuillez contacter l'administrateur du site.
|
||||
disable_register_mail=La confirmation par courriel à l’inscription est désactivée.
|
||||
manual_activation_only=Contactez l'administrateur de votre site pour terminer l'activation.
|
||||
|
@ -394,6 +396,8 @@ remember_me=Mémoriser cet appareil
|
|||
remember_me.compromised=Le jeton de connexion n’est plus valide, ce qui peut indiquer un compte compromis. Veuillez inspecter les activités inhabituelles de votre compte.
|
||||
forgot_password_title=Mot de passe oublié
|
||||
forgot_password=Mot de passe oublié ?
|
||||
need_account=Besoin d‘un compte ?
|
||||
sign_up_now=Inscrivez-vous dès maintenant !
|
||||
sign_up_successful=Le compte a été créé avec succès. Bienvenue !
|
||||
confirmation_mail_sent_prompt_ex=Un nouveau courriel de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans la prochaine %s pour terminer le processus d’inscription. Si votre adresse courriel est incorrecte, vous pouvez vous reconnecter et la modifier.
|
||||
must_change_password=Réinitialisez votre mot de passe
|
||||
|
@ -455,6 +459,8 @@ sspi_auth_failed=Échec de l'authentification SSPI
|
|||
password_pwned=Le mot de passe que vous avez choisi se trouve sur la liste <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">des mots de passe ayant fuité</a> sur internet. Veuillez réessayer avec un mot de passe différent et considérer remplacer ce mot de passe si vous l'utilisez ailleurs.
|
||||
password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned
|
||||
last_admin=Vous ne pouvez pas supprimer ce compte car au moins un administrateur est requis.
|
||||
signin_passkey=Se connecter avec une clé d’identification (passkey)
|
||||
back_to_sign_in=Revenir à la page de connexion
|
||||
|
||||
[mail]
|
||||
view_it_on=Voir sur %s
|
||||
|
@ -471,6 +477,7 @@ activate_email=Veuillez vérifier votre adresse courriel
|
|||
activate_email.title=%s, veuillez vérifier votre adresse courriel
|
||||
activate_email.text=Veuillez cliquer sur le lien suivant pour vérifier votre adresse courriel dans <b>%s</b>:
|
||||
|
||||
register_notify=Bienvenue sur %s
|
||||
register_notify.title=%[1]s, bienvenue à %[2]s
|
||||
register_notify.text_1=ceci est votre courriel de confirmation d'inscription pour %s!
|
||||
register_notify.text_2=Vous pouvez maintenant vous connecter avec le nom d'utilisateur : %s.
|
||||
|
@ -907,6 +914,7 @@ create_oauth2_application_success=Vous avez créé une nouvelle application OAut
|
|||
update_oauth2_application_success=Vous avez mis à jour l'application OAuth2 avec succès.
|
||||
oauth2_application_name=Nom de l'Application
|
||||
oauth2_confidential_client=Client confidentiel. Sélectionnez cette option pour les applications qui préservent la confidentialité du secret, telles que les applications web. Ne la sélectionnez pas pour les applications natives, y compris les applications de bureau et les applications mobiles.
|
||||
oauth2_skip_secondary_authorization=Ne plus demander d’autorisation pour les clients publics après la première fois. <strong>Introduit un risque de sécurité.</strong>
|
||||
oauth2_redirect_uris=URI de redirection. Veuillez utiliser une nouvelle ligne pour chaque URI.
|
||||
save_application=Enregistrer
|
||||
oauth2_client_id=ID du client
|
||||
|
@ -1927,6 +1935,7 @@ milestones.edit_success=Le jalon "%s" a été mis à jour.
|
|||
milestones.deletion=Supprimer un Jalon
|
||||
milestones.deletion_desc=Supprimer un jalon le retire de tous les tickets. Continuer ?
|
||||
milestones.deletion_success=Le jalon a été supprimé.
|
||||
milestones.filter_sort.name=Nom
|
||||
milestones.filter_sort.earliest_due_data=Date d’échéance la plus ancienne
|
||||
milestones.filter_sort.latest_due_date=Date d’échéance la plus récente
|
||||
milestones.filter_sort.least_complete=Le moins complété
|
||||
|
@ -2272,6 +2281,7 @@ settings.event_wiki_desc=Page wiki créée, renommée, modifiée ou supprimée.
|
|||
settings.event_release=Publication
|
||||
settings.event_release_desc=Publication publiée, mise à jour ou supprimée.
|
||||
settings.event_push=Soumission
|
||||
settings.event_force_push=Poussée forcée
|
||||
settings.event_push_desc=Soumission Git.
|
||||
settings.event_repository=Dépôt
|
||||
settings.event_repository_desc=Dépôt créé ou supprimé.
|
||||
|
@ -2365,10 +2375,28 @@ settings.protect_this_branch=Activer la protection de branche
|
|||
settings.protect_this_branch_desc=Empêche les suppressions et limite les poussées et fusions sur cette branche.
|
||||
settings.protect_disable_push=Désactiver la soumission
|
||||
settings.protect_disable_push_desc=Aucune soumission ne sera possible sur cette branche.
|
||||
settings.protect_disable_force_push=Désactiver les poussés forcées
|
||||
settings.protect_disable_force_push_desc=Aucune poussée forcée ne sera possible sur cette branche.
|
||||
settings.protect_enable_push=Activer la soumission
|
||||
settings.protect_enable_push_desc=Toute personne ayant un accès en écriture sera autorisée à soumettre sur cette branche (sans forcer).
|
||||
settings.protect_enable_force_push_all=Activer les poussées forcées
|
||||
settings.protect_enable_force_push_all_desc=Toute personne pouvant pousser pourra forcer sur cette branche.
|
||||
settings.protect_enable_force_push_allowlist=Soumission forcée sur autorisation uniquement
|
||||
settings.protect_enable_force_push_allowlist_desc=Seuls les utilisateurs ou équipes autorisés ayants un droit de pousser seront autorisés à pousser en force sur cette branche.
|
||||
settings.protect_enable_merge=Activer la fusion
|
||||
settings.protect_enable_merge_desc=Toute personne ayant un accès en écriture sera autorisée à fusionner les demandes d'ajout dans cette branche.
|
||||
settings.protect_whitelist_committers=Soumissions sur autorisation uniquement
|
||||
settings.protect_whitelist_committers_desc=Seuls les utilisateurs ou les équipes autorisés pourront pousser sur cette branche (sans forcer).
|
||||
settings.protect_whitelist_deploy_keys=Clés de déploiement pouvant écrire autorisées à pousser.
|
||||
settings.protect_whitelist_users=Utilisateurs autorisés à pousser :
|
||||
settings.protect_whitelist_teams=Équipes autorisées à pousser :
|
||||
settings.protect_force_push_allowlist_users=Utilisateurs autorisés à pousser en force :
|
||||
settings.protect_force_push_allowlist_teams=Équipes autorisées à pousser en force :
|
||||
settings.protect_force_push_allowlist_deploy_keys=Clés de déploiement pouvant pousser autorisées à pousser en force.
|
||||
settings.protect_merge_whitelist_committers=Activer la liste d’autorisés pour la fusion
|
||||
settings.protect_merge_whitelist_committers_desc=N’autoriser que les utilisateurs et les équipes listés à appliquer les demandes de fusion sur cette branche.
|
||||
settings.protect_merge_whitelist_users=Utilisateurs autorisés à fusionner :
|
||||
settings.protect_merge_whitelist_teams=Équipes autorisées à fusionner :
|
||||
settings.protect_check_status_contexts=Activer le Contrôle Qualité
|
||||
settings.protect_status_check_patterns=Motifs de vérification des statuts :
|
||||
settings.protect_status_check_patterns_desc=Entrez des motifs pour spécifier quelles vérifications doivent réussir avant que des branches puissent être fusionnées. Un motif par ligne. Un motif ne peut être vide.
|
||||
|
@ -2379,6 +2407,10 @@ settings.protect_invalid_status_check_pattern=Motif de vérification des statuts
|
|||
settings.protect_no_valid_status_check_patterns=Aucun motif de vérification des statuts valide.
|
||||
settings.protect_required_approvals=Minimum d'approbations requis :
|
||||
settings.protect_required_approvals_desc=Permet de fusionner les demandes d’ajout lorsque suffisamment d’évaluation sont positives.
|
||||
settings.protect_approvals_whitelist_enabled=Restreindre les approbations aux utilisateurs ou aux équipes sur liste d’autorisés
|
||||
settings.protect_approvals_whitelist_enabled_desc=Seuls les évaluations des utilisateurs ou des équipes suivantes compteront dans les approbations requises. Si laissé vide, les évaluations de toute personne ayant un accès en écriture seront comptabilisées à la place.
|
||||
settings.protect_approvals_whitelist_users=Évaluateurs autorisés :
|
||||
settings.protect_approvals_whitelist_teams=Équipes d’évaluateurs autorisés :
|
||||
settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées
|
||||
settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande d’ajout, les approbations existantes sont révoquées.
|
||||
settings.ignore_stale_approvals=Ignorer les approbations obsolètes
|
||||
|
|
|
@ -962,6 +962,7 @@ milestones.modify=Mérföldkő frissítése
|
|||
milestones.deletion=Mérföldkő törlése
|
||||
milestones.deletion_desc=A mérföldkő törlése eltávolítja az összes hozzárendelt hibajegyet. Biztosan folytatja?
|
||||
milestones.deletion_success=A mérföldkő törölve.
|
||||
milestones.filter_sort.name=Név
|
||||
milestones.filter_sort.least_complete=Legkevésbé befejezve
|
||||
milestones.filter_sort.most_complete=Leginkább befejezve
|
||||
milestones.filter_sort.most_issues=Legtöbb hibajegy
|
||||
|
|
|
@ -770,6 +770,7 @@ milestones.due_date=Jatuh Tempo (opsional)
|
|||
milestones.clear=Bersihkan
|
||||
milestones.edit=Ubah Milestone
|
||||
milestones.cancel=Batal
|
||||
milestones.filter_sort.name=Nama
|
||||
milestones.filter_sort.least_complete=Paling tidak lengkap
|
||||
milestones.filter_sort.most_complete=Paling lengkap
|
||||
milestones.filter_sort.most_issues=Paling banyak masalah
|
||||
|
|
|
@ -916,6 +916,7 @@ milestones.desc=Lýsing
|
|||
milestones.due_date=Eindagi (valfrjálst)
|
||||
milestones.clear=Hreinsa
|
||||
milestones.cancel=Hætta við
|
||||
milestones.filter_sort.name=Heiti
|
||||
milestones.filter_sort.most_issues=Flest vandamál
|
||||
milestones.filter_sort.least_issues=Fæst vandamál
|
||||
|
||||
|
|
|
@ -1512,6 +1512,7 @@ milestones.modify=Aggiorna pietra miliare
|
|||
milestones.deletion=Elimina pietra miliare
|
||||
milestones.deletion_desc=Eliminare una pietra miliare la rimuove da tutte le relative issue. Continuare?
|
||||
milestones.deletion_success=La pietra miliare è stata eliminata.
|
||||
milestones.filter_sort.name=Nome
|
||||
milestones.filter_sort.least_complete=Meno completato
|
||||
milestones.filter_sort.most_complete=Più completato
|
||||
milestones.filter_sort.most_issues=Più problemi
|
||||
|
|
|
@ -1933,6 +1933,7 @@ milestones.edit_success=マイルストーン "%s" を更新しました。
|
|||
milestones.deletion=マイルストーンの削除
|
||||
milestones.deletion_desc=マイルストーンを削除すると、関連するすべてのイシューから除去されます。 続行しますか?
|
||||
milestones.deletion_success=マイルストーンを削除しました。
|
||||
milestones.filter_sort.name=名称
|
||||
milestones.filter_sort.earliest_due_data=期日が早い順
|
||||
milestones.filter_sort.latest_due_date=期日が遅い順
|
||||
milestones.filter_sort.least_complete=消化率の低い順
|
||||
|
|
|
@ -875,6 +875,7 @@ milestones.modify=마일스톤 갱신
|
|||
milestones.deletion=마일스톤 삭제
|
||||
milestones.deletion_desc=마일스톤을 삭제하면 연관된 모든 이슈에서 삭제됩니다. 계속 하시겠습니까?
|
||||
milestones.deletion_success=마일스톤이 삭제되었습니다.
|
||||
milestones.filter_sort.name=이름
|
||||
milestones.filter_sort.least_complete=완료율이 낮은 순
|
||||
milestones.filter_sort.most_complete=완료율이 높은 순
|
||||
milestones.filter_sort.most_issues=이슈 많은 순
|
||||
|
|
|
@ -1828,6 +1828,7 @@ milestones.edit_success=Izmaiņas atskaites punktā "%s" tika veiksmīgi saglab
|
|||
milestones.deletion=Dzēst atskaites punktu
|
||||
milestones.deletion_desc=Dzēšot šo atskaites punktu, tas tiks noņemts no visām saistītajām problēmām un izmaiņu pieprasījumiem. Vai turpināt?
|
||||
milestones.deletion_success=Atskaites punkts tika veiksmīgi izdzēsts.
|
||||
milestones.filter_sort.name=Nosaukums
|
||||
milestones.filter_sort.earliest_due_data=Agrākais izpildes laiks
|
||||
milestones.filter_sort.latest_due_date=Vēlākais izpildes laiks
|
||||
milestones.filter_sort.least_complete=Vismazāk pabeigtais
|
||||
|
|
|
@ -1507,6 +1507,7 @@ milestones.modify=Mijlpaal bijwerken
|
|||
milestones.deletion=Mijlpaal verwijderen
|
||||
milestones.deletion_desc=Als je een mijlpaal verwijdert, wordt hij van alle gerelateerde kwesties verwijderd. Doorgaan?
|
||||
milestones.deletion_success=De mijlpaal is verwijderd.
|
||||
milestones.filter_sort.name=Naam
|
||||
milestones.filter_sort.least_complete=Minst compleet
|
||||
milestones.filter_sort.most_complete=Meest compleet
|
||||
milestones.filter_sort.most_issues=Meeste problemen
|
||||
|
|
|
@ -1360,6 +1360,7 @@ milestones.modify=Zaktualizuj cel
|
|||
milestones.deletion=Usuń kamień milowy
|
||||
milestones.deletion_desc=Usunięcie celu usuwa go z wszystkich pozostałych zagadnień. Kontynuować?
|
||||
milestones.deletion_success=Cel został usunięty.
|
||||
milestones.filter_sort.name=Nazwa
|
||||
milestones.filter_sort.least_complete=Najmniej kompletne
|
||||
milestones.filter_sort.most_complete=Najbardziej kompletne
|
||||
milestones.filter_sort.most_issues=Najwięcej zgłoszeń
|
||||
|
|
|
@ -1820,6 +1820,7 @@ milestones.edit_success=O marco "%s" foi atualizado.
|
|||
milestones.deletion=Excluir marco
|
||||
milestones.deletion_desc=A exclusão deste marco irá removê-lo de todas as issues. Tem certeza que deseja continuar?
|
||||
milestones.deletion_success=O marco foi excluído.
|
||||
milestones.filter_sort.name=Nome
|
||||
milestones.filter_sort.earliest_due_data=Data limite mais próxima
|
||||
milestones.filter_sort.latest_due_date=Data limite mais distante
|
||||
milestones.filter_sort.least_complete=Menos completo
|
||||
|
|
|
@ -477,6 +477,7 @@ activate_email=Valide o seu endereço de email
|
|||
activate_email.title=%s, por favor valide o seu endereço de email
|
||||
activate_email.text=Por favor clique na seguinte ligação para validar o seu endereço de email dentro de <b>%s</b>:
|
||||
|
||||
register_notify=Bem-vindo(a) a %s
|
||||
register_notify.title=%[1]s, bem-vindo(a) a %[2]s
|
||||
register_notify.text_1=este é o seu email de confirmação de registo para %s!
|
||||
register_notify.text_2=Agora pode iniciar a sessão com o nome de utilizador: %s.
|
||||
|
@ -913,6 +914,7 @@ create_oauth2_application_success=Criou com sucesso uma nova aplicação OAuth2.
|
|||
update_oauth2_application_success=Modificou com sucesso a aplicação OAuth2.
|
||||
oauth2_application_name=Nome da aplicação
|
||||
oauth2_confidential_client=Cliente confidencial. Escolha esta opção para aplicações que mantêm o segredo confidencial, tais como aplicações web. Não escolha esta opção para aplicações nativas, incluindo aplicações para computador e aplicações móveis.
|
||||
oauth2_skip_secondary_authorization=Saltar a autorização para clientes públicos depois de lhes ceder acesso uma vez. <strong>Pode representar um risco de segurança.</strong>
|
||||
oauth2_redirect_uris=URIs de reencaminhamento. Use uma linha por URI.
|
||||
save_application=Guardar
|
||||
oauth2_client_id=ID do cliente
|
||||
|
@ -1933,6 +1935,7 @@ milestones.edit_success=A etapa "%s" foi modificada.
|
|||
milestones.deletion=Eliminar etapa
|
||||
milestones.deletion_desc=Se eliminar uma etapa, irá removê-la de todas as questões relacionadas. Quer continuar?
|
||||
milestones.deletion_success=A etapa foi eliminada.
|
||||
milestones.filter_sort.name=Nome
|
||||
milestones.filter_sort.earliest_due_data=Data de vencimento mais próxima
|
||||
milestones.filter_sort.latest_due_date=Data de vencimento mais distante
|
||||
milestones.filter_sort.least_complete=Menos completo
|
||||
|
@ -2384,6 +2387,7 @@ settings.protect_enable_merge=Habilitar integração
|
|||
settings.protect_enable_merge_desc=Qualquer pessoa com permissão de escrita tem autorização para realizar neste ramo as integrações constantes nos pedidos.
|
||||
settings.protect_whitelist_committers=Lista de permissões para restringir os envios
|
||||
settings.protect_whitelist_committers_desc=Apenas os utilizadores ou equipas constantes na lista terão permissão para enviar para este ramo (mas não poderão fazer envios forçados).
|
||||
settings.protect_whitelist_deploy_keys=Lista de permissão de chaves de instalação com acesso de escrita para enviar.
|
||||
settings.protect_whitelist_users=Utilizadores com permissão para enviar:
|
||||
settings.protect_whitelist_teams=Equipas com permissão para enviar:
|
||||
settings.protect_force_push_allowlist_users=Utilizadores na lista de permissão para enviar forçadamente:
|
||||
|
|
|
@ -1789,6 +1789,7 @@ milestones.edit_success=Этап «%s» обновлён.
|
|||
milestones.deletion=Удалить этап
|
||||
milestones.deletion_desc=Удаление этапа приведет к его удалению из всех связанных задач. Продолжить?
|
||||
milestones.deletion_success=Этап успешно удалён.
|
||||
milestones.filter_sort.name=Название
|
||||
milestones.filter_sort.earliest_due_data=По возрастанию даты завершения
|
||||
milestones.filter_sort.latest_due_date=По убыванию даты завершения
|
||||
milestones.filter_sort.least_complete=Менее полное
|
||||
|
|
|
@ -1352,6 +1352,7 @@ milestones.modify=සන්ධිස්ථානයක් යාවත්කා
|
|||
milestones.deletion=සන්ධිස්ථානය මකන්න
|
||||
milestones.deletion_desc=සන්ධිස්ථානයක් මකා දැමීම සම්බන්ධ සියලු ගැටළු වලින් එය ඉවත් කරයි. දිගටම?
|
||||
milestones.deletion_success=සන්ධිස්ථානය මකා දමා ඇත.
|
||||
milestones.filter_sort.name=නම
|
||||
milestones.filter_sort.least_complete=අවම වශයෙන් සම්පූර්ණයි
|
||||
milestones.filter_sort.most_complete=වඩාත්ම සම්පූර්ණයි
|
||||
milestones.filter_sort.most_issues=බොහෝ ප්රශ්න
|
||||
|
|
|
@ -1132,6 +1132,7 @@ milestones.modify=Uppdatera milstolpe
|
|||
milestones.deletion=Ta bort milstolpe
|
||||
milestones.deletion_desc=Borttagning av en milstolpe tar bort den från samtliga relaterade ärende. Fortsätta?
|
||||
milestones.deletion_success=Milstolpen har blivit borttagen.
|
||||
milestones.filter_sort.name=Namn
|
||||
milestones.filter_sort.least_complete=Minst klar
|
||||
milestones.filter_sort.most_complete=Mest klar
|
||||
milestones.filter_sort.most_issues=Mest ärenden
|
||||
|
|
|
@ -1920,6 +1920,7 @@ milestones.edit_success=`"%s" dönüm noktası güncellendi.`
|
|||
milestones.deletion=Kilometre Taşını Sil
|
||||
milestones.deletion_desc=Bir kilometre taşını silmek, onu ilgili tüm sorunlardan kaldırır. Devam edilsin mi?
|
||||
milestones.deletion_success=Kilometre taşı silindi.
|
||||
milestones.filter_sort.name=İsim
|
||||
milestones.filter_sort.earliest_due_data=En erken bitiş tarihi
|
||||
milestones.filter_sort.latest_due_date=En uzak bitiş tarihi
|
||||
milestones.filter_sort.least_complete=En az tamamlama
|
||||
|
|
|
@ -1399,6 +1399,7 @@ milestones.modify=Оновити етап
|
|||
milestones.deletion=Видалити етап
|
||||
milestones.deletion_desc=Видалення етапу призведе до його видалення з усіх пов'язаних задач. Продовжити?
|
||||
milestones.deletion_success=Етап успішно видалено.
|
||||
milestones.filter_sort.name=Назва
|
||||
milestones.filter_sort.least_complete=Менш повне
|
||||
milestones.filter_sort.most_complete=Більш повне
|
||||
milestones.filter_sort.most_issues=Найбільш задач
|
||||
|
|
|
@ -1923,6 +1923,7 @@ milestones.edit_success=里程碑 %s 已经更新。
|
|||
milestones.deletion=删除里程碑
|
||||
milestones.deletion_desc=删除该里程碑将会移除所有工单中相关的信息。是否继续?
|
||||
milestones.deletion_success=里程碑已被删除。
|
||||
milestones.filter_sort.name=名称
|
||||
milestones.filter_sort.earliest_due_data=到期日从远到近
|
||||
milestones.filter_sort.latest_due_date=到期日从近到远
|
||||
milestones.filter_sort.least_complete=完成度从低到高
|
||||
|
|
|
@ -511,6 +511,7 @@ milestones.due_date=截止日期(可選)
|
|||
milestones.clear=清除
|
||||
milestones.edit=編輯里程碑
|
||||
milestones.cancel=取消
|
||||
milestones.filter_sort.name=組織名稱
|
||||
milestones.filter_sort.least_complete=完成度由低到高
|
||||
milestones.filter_sort.most_complete=完成度由高到低
|
||||
milestones.filter_sort.most_issues=問題由多到少
|
||||
|
|
|
@ -1656,6 +1656,7 @@ milestones.edit_success=已更新里程碑「%s」。
|
|||
milestones.deletion=刪除里程碑
|
||||
milestones.deletion_desc=刪除里程碑會從所有相關的問題移除它。是否繼續?
|
||||
milestones.deletion_success=里程碑已刪除
|
||||
milestones.filter_sort.name=名稱
|
||||
milestones.filter_sort.least_complete=完成度由低到高
|
||||
milestones.filter_sort.most_complete=完成度由高到低
|
||||
milestones.filter_sort.most_issues=問題由多到少
|
||||
|
|
1934
package-lock.json
generated
1934
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
|
@ -100,6 +100,7 @@
|
|||
"eslint-plugin-wc": "2.1.0",
|
||||
"happy-dom": "14.12.3",
|
||||
"markdownlint-cli": "0.41.0",
|
||||
"nolyfill": "1.0.39",
|
||||
"postcss-html": "1.7.0",
|
||||
"stylelint": "16.6.1",
|
||||
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
|
||||
|
@ -112,5 +113,23 @@
|
|||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
]
|
||||
],
|
||||
"overrides": {
|
||||
"array-includes": "npm:@nolyfill/array-includes@^1",
|
||||
"array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@^1",
|
||||
"array.prototype.flat": "npm:@nolyfill/array.prototype.flat@^1",
|
||||
"array.prototype.flatmap": "npm:@nolyfill/array.prototype.flatmap@^1",
|
||||
"deep-equal": "npm:@nolyfill/deep-equal@^1",
|
||||
"es-aggregate-error": "npm:@nolyfill/es-aggregate-error@^1",
|
||||
"es-iterator-helpers": "npm:@nolyfill/es-iterator-helpers@^1",
|
||||
"hasown": "npm:@nolyfill/hasown@^1",
|
||||
"object.assign": "npm:@nolyfill/object.assign@^1",
|
||||
"object.entries": "npm:@nolyfill/object.entries@^1",
|
||||
"object.fromentries": "npm:@nolyfill/object.fromentries@^1",
|
||||
"object.groupby": "npm:@nolyfill/object.groupby@^1",
|
||||
"object.values": "npm:@nolyfill/object.values@^1",
|
||||
"safe-regex-test": "npm:@nolyfill/safe-regex-test@^1",
|
||||
"string.prototype.includes": "npm:@nolyfill/string.prototype.includes@^1",
|
||||
"is-core-module": "npm:@nolyfill/is-core-module@^1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -223,10 +223,11 @@ func CreateOauth2Application(ctx *context.APIContext) {
|
|||
data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions)
|
||||
|
||||
app, err := auth_model.CreateOAuth2Application(ctx, auth_model.CreateOAuth2ApplicationOptions{
|
||||
Name: data.Name,
|
||||
UserID: ctx.Doer.ID,
|
||||
RedirectURIs: data.RedirectURIs,
|
||||
ConfidentialClient: data.ConfidentialClient,
|
||||
Name: data.Name,
|
||||
UserID: ctx.Doer.ID,
|
||||
RedirectURIs: data.RedirectURIs,
|
||||
ConfidentialClient: data.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: data.SkipSecondaryAuthorization,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusBadRequest, "", "error creating oauth2 application")
|
||||
|
@ -381,11 +382,12 @@ func UpdateOauth2Application(ctx *context.APIContext) {
|
|||
data := web.GetForm(ctx).(*api.CreateOAuth2ApplicationOptions)
|
||||
|
||||
app, err := auth_model.UpdateOAuth2Application(ctx, auth_model.UpdateOAuth2ApplicationOptions{
|
||||
Name: data.Name,
|
||||
UserID: ctx.Doer.ID,
|
||||
ID: appID,
|
||||
RedirectURIs: data.RedirectURIs,
|
||||
ConfidentialClient: data.ConfidentialClient,
|
||||
Name: data.Name,
|
||||
UserID: ctx.Doer.ID,
|
||||
ID: appID,
|
||||
RedirectURIs: data.RedirectURIs,
|
||||
ConfidentialClient: data.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: data.SkipSecondaryAuthorization,
|
||||
})
|
||||
if err != nil {
|
||||
if auth_model.IsErrOauthClientIDInvalid(err) || auth_model.IsErrOAuthApplicationNotFound(err) {
|
||||
|
|
|
@ -622,10 +622,8 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
|
|||
|
||||
// update external user information
|
||||
if gothUser != nil {
|
||||
if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
log.Error("UpdateExternalUser failed: %v", err)
|
||||
}
|
||||
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, *gothUser); err != nil {
|
||||
log.Error("EnsureLinkExternalToUser failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ package auth
|
|||
|
||||
import (
|
||||
go_context "context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
|
@ -27,7 +26,6 @@ import (
|
|||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
|
@ -327,10 +325,29 @@ func getOAuthGroupsForUser(ctx go_context.Context, user *user_model.User) ([]str
|
|||
return groups, nil
|
||||
}
|
||||
|
||||
func parseBasicAuth(ctx *context.Context) (username, password string, err error) {
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
if authType, authData, ok := strings.Cut(authHeader, " "); ok && authType == "Basic" {
|
||||
return base.BasicAuthDecode(authData)
|
||||
}
|
||||
return "", "", errors.New("invalid basic authentication")
|
||||
}
|
||||
|
||||
// IntrospectOAuth introspects an oauth token
|
||||
func IntrospectOAuth(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm=""`)
|
||||
clientIDValid := false
|
||||
if clientID, clientSecret, err := parseBasicAuth(ctx); err == nil {
|
||||
app, err := auth.GetOAuth2ApplicationByClientID(ctx, clientID)
|
||||
if err != nil && !auth.IsErrOauthClientIDInvalid(err) {
|
||||
// this is likely a database error; log it and respond without details
|
||||
log.Error("Error retrieving client_id: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret))
|
||||
}
|
||||
if !clientIDValid {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm=""`)
|
||||
ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
|
||||
return
|
||||
}
|
||||
|
@ -470,9 +487,9 @@ func AuthorizeOAuth(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Redirect if user already granted access and the application is confidential.
|
||||
// I.e. always require authorization for public clients as recommended by RFC 6749 Section 10.2
|
||||
if app.ConfidentialClient && grant != nil {
|
||||
// Redirect if user already granted access and the application is confidential or trusted otherwise
|
||||
// I.e. always require authorization for untrusted public clients as recommended by RFC 6749 Section 10.2
|
||||
if (app.ConfidentialClient || app.SkipSecondaryAuthorization) && grant != nil {
|
||||
code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
|
||||
if err != nil {
|
||||
handleServerError(ctx, form.State, form.RedirectURI)
|
||||
|
@ -640,9 +657,8 @@ func AccessTokenOAuth(ctx *context.Context) {
|
|||
// if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
|
||||
if form.ClientID == "" || form.ClientSecret == "" {
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
authContent := strings.SplitN(authHeader, " ", 2)
|
||||
if len(authContent) == 2 && authContent[0] == "Basic" {
|
||||
payload, err := base64.StdEncoding.DecodeString(authContent[1])
|
||||
if authType, authData, ok := strings.Cut(authHeader, " "); ok && authType == "Basic" {
|
||||
clientID, clientSecret, err := base.BasicAuthDecode(authData)
|
||||
if err != nil {
|
||||
handleAccessTokenError(ctx, AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
|
@ -650,30 +666,23 @@ func AccessTokenOAuth(ctx *context.Context) {
|
|||
})
|
||||
return
|
||||
}
|
||||
pair := strings.SplitN(string(payload), ":", 2)
|
||||
if len(pair) != 2 {
|
||||
handleAccessTokenError(ctx, AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "cannot parse basic auth header",
|
||||
})
|
||||
return
|
||||
}
|
||||
if form.ClientID != "" && form.ClientID != pair[0] {
|
||||
// validate that any fields present in the form match the Basic auth header
|
||||
if form.ClientID != "" && form.ClientID != clientID {
|
||||
handleAccessTokenError(ctx, AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "client_id in request body inconsistent with Authorization header",
|
||||
})
|
||||
return
|
||||
}
|
||||
form.ClientID = pair[0]
|
||||
if form.ClientSecret != "" && form.ClientSecret != pair[1] {
|
||||
form.ClientID = clientID
|
||||
if form.ClientSecret != "" && form.ClientSecret != clientSecret {
|
||||
handleAccessTokenError(ctx, AccessTokenError{
|
||||
ErrorCode: AccessTokenErrorCodeInvalidRequest,
|
||||
ErrorDescription: "client_secret in request body inconsistent with Authorization header",
|
||||
})
|
||||
return
|
||||
}
|
||||
form.ClientSecret = pair[1]
|
||||
form.ClientSecret = clientSecret
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1148,9 +1157,39 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
|||
|
||||
groups := getClaimedGroups(oauth2Source, &gothUser)
|
||||
|
||||
opts := &user_service.UpdateOptions{}
|
||||
|
||||
// Reactivate user if they are deactivated
|
||||
if !u.IsActive {
|
||||
opts.IsActive = optional.Some(true)
|
||||
}
|
||||
|
||||
// Update GroupClaims
|
||||
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
|
||||
|
||||
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
|
||||
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
|
||||
ctx.ServerError("SyncGroupsToTeams", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := externalaccount.EnsureLinkExternalToUser(ctx, u, gothUser); err != nil {
|
||||
ctx.ServerError("EnsureLinkExternalToUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If this user is enrolled in 2FA and this source doesn't override it,
|
||||
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
|
||||
if !needs2FA {
|
||||
// Register last login
|
||||
opts.SetLastLogin = true
|
||||
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
"uid": u.ID,
|
||||
"uname": u.Name,
|
||||
|
@ -1162,29 +1201,6 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
|||
// Clear whatever CSRF cookie has right now, force to generate a new one
|
||||
ctx.Csrf.DeleteCookie(ctx)
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
SetLastLogin: true,
|
||||
}
|
||||
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
|
||||
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
|
||||
ctx.ServerError("SyncGroupsToTeams", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// update external user information
|
||||
if err := externalaccount.UpdateExternalUser(ctx, u, gothUser); err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
log.Error("UpdateExternalUser failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := resetLocale(ctx, u); err != nil {
|
||||
ctx.ServerError("resetLocale", err)
|
||||
return
|
||||
|
@ -1200,22 +1216,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
|||
return
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{}
|
||||
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
|
||||
if opts.IsAdmin.Has() || opts.IsRestricted.Has() {
|
||||
if opts.IsActive.Has() || opts.IsAdmin.Has() || opts.IsRestricted.Has() {
|
||||
if err := user_service.UpdateUser(ctx, u, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
|
||||
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
|
||||
ctx.ServerError("SyncGroupsToTeams", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
// User needs to use 2FA, save data and redirect to 2FA page.
|
||||
"twofaUid": u.ID,
|
||||
|
|
|
@ -49,10 +49,11 @@ func (oa *OAuth2CommonHandlers) AddApp(ctx *context.Context) {
|
|||
|
||||
// TODO validate redirect URI
|
||||
app, err := auth.CreateOAuth2Application(ctx, auth.CreateOAuth2ApplicationOptions{
|
||||
Name: form.Name,
|
||||
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
|
||||
UserID: oa.OwnerID,
|
||||
ConfidentialClient: form.ConfidentialClient,
|
||||
Name: form.Name,
|
||||
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
|
||||
UserID: oa.OwnerID,
|
||||
ConfidentialClient: form.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: form.SkipSecondaryAuthorization,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CreateOAuth2Application", err)
|
||||
|
@ -102,11 +103,12 @@ func (oa *OAuth2CommonHandlers) EditSave(ctx *context.Context) {
|
|||
// TODO validate redirect URI
|
||||
var err error
|
||||
if ctx.Data["App"], err = auth.UpdateOAuth2Application(ctx, auth.UpdateOAuth2ApplicationOptions{
|
||||
ID: ctx.PathParamInt64("id"),
|
||||
Name: form.Name,
|
||||
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
|
||||
UserID: oa.OwnerID,
|
||||
ConfidentialClient: form.ConfidentialClient,
|
||||
ID: ctx.PathParamInt64("id"),
|
||||
Name: form.Name,
|
||||
RedirectURIs: util.SplitTrimSpace(form.RedirectURIs, "\n"),
|
||||
UserID: oa.OwnerID,
|
||||
ConfidentialClient: form.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: form.SkipSecondaryAuthorization,
|
||||
}); err != nil {
|
||||
ctx.ServerError("UpdateOAuth2Application", err)
|
||||
return
|
||||
|
|
14
services/auth/source/oauth2/main_test.go
Normal file
14
services/auth/source/oauth2/main_test.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{})
|
||||
}
|
62
services/auth/source/oauth2/providers_test.go
Normal file
62
services/auth/source/oauth2/providers_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type fakeProvider struct{}
|
||||
|
||||
func (p *fakeProvider) Name() string {
|
||||
return "fake"
|
||||
}
|
||||
|
||||
func (p *fakeProvider) SetName(name string) {}
|
||||
|
||||
func (p *fakeProvider) BeginAuth(state string) (goth.Session, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *fakeProvider) UnmarshalSession(string) (goth.Session, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *fakeProvider) FetchUser(goth.Session) (goth.User, error) {
|
||||
return goth.User{}, nil
|
||||
}
|
||||
|
||||
func (p *fakeProvider) Debug(bool) {
|
||||
}
|
||||
|
||||
func (p *fakeProvider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
|
||||
switch refreshToken {
|
||||
case "expired":
|
||||
return nil, &oauth2.RetrieveError{
|
||||
ErrorCode: "invalid_grant",
|
||||
}
|
||||
default:
|
||||
return &oauth2.Token{
|
||||
AccessToken: "token",
|
||||
TokenType: "Bearer",
|
||||
RefreshToken: "refresh",
|
||||
Expiry: time.Now().Add(time.Hour),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *fakeProvider) RefreshTokenAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterGothProvider(
|
||||
NewSimpleProvider("fake", "Fake", []string{"account"},
|
||||
func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider {
|
||||
return &fakeProvider{}
|
||||
}))
|
||||
}
|
|
@ -36,7 +36,7 @@ func (source *Source) FromDB(bs []byte) error {
|
|||
return json.UnmarshalHandleDoubleEncode(bs, &source)
|
||||
}
|
||||
|
||||
// ToDB exports an SMTPConfig to a serialized format.
|
||||
// ToDB exports an OAuth2Config to a serialized format.
|
||||
func (source *Source) ToDB() ([]byte, error) {
|
||||
return json.Marshal(source)
|
||||
}
|
||||
|
|
114
services/auth/source/oauth2/source_sync.go
Normal file
114
services/auth/source/oauth2/source_sync.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"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/log"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Sync causes this OAuth2 source to synchronize its users with the db.
|
||||
func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
log.Trace("Doing: SyncExternalUsers[%s] %d", source.authSource.Name, source.authSource.ID)
|
||||
|
||||
if !updateExisting {
|
||||
log.Info("SyncExternalUsers[%s] not running since updateExisting is false", source.authSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
provider, err := createProvider(source.authSource.Name, source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !provider.RefreshTokenAvailable() {
|
||||
log.Trace("SyncExternalUsers[%s] provider doesn't support refresh tokens, can't synchronize", source.authSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
opts := user_model.FindExternalUserOptions{
|
||||
HasRefreshToken: true,
|
||||
Expired: true,
|
||||
LoginSourceID: source.authSource.ID,
|
||||
}
|
||||
|
||||
return user_model.IterateExternalLogin(ctx, opts, func(ctx context.Context, u *user_model.ExternalLoginUser) error {
|
||||
return source.refresh(ctx, provider, u)
|
||||
})
|
||||
}
|
||||
|
||||
func (source *Source) refresh(ctx context.Context, provider goth.Provider, u *user_model.ExternalLoginUser) error {
|
||||
log.Trace("Syncing login_source_id=%d external_id=%s expiration=%s", u.LoginSourceID, u.ExternalID, u.ExpiresAt)
|
||||
|
||||
shouldDisable := false
|
||||
|
||||
token, err := provider.RefreshToken(u.RefreshToken)
|
||||
if err != nil {
|
||||
if err, ok := err.(*oauth2.RetrieveError); ok && err.ErrorCode == "invalid_grant" {
|
||||
// this signals that the token is not valid and the user should be disabled
|
||||
shouldDisable = true
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
user := &user_model.User{
|
||||
LoginName: u.ExternalID,
|
||||
LoginType: auth.OAuth2,
|
||||
LoginSource: u.LoginSourceID,
|
||||
}
|
||||
|
||||
hasUser, err := user_model.GetUser(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the grant is no longer valid, disable the user and
|
||||
// delete local tokens. If the OAuth2 provider still
|
||||
// recognizes them as a valid user, they will be able to login
|
||||
// via their provider and reactivate their account.
|
||||
if shouldDisable {
|
||||
log.Info("SyncExternalUsers[%s] disabling user %d", source.authSource.Name, user.ID)
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if hasUser {
|
||||
user.IsActive = false
|
||||
err := user_model.UpdateUserCols(ctx, user, "is_active")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete stored tokens, since they are invalid. This
|
||||
// also provents us from checking this in subsequent runs.
|
||||
u.AccessToken = ""
|
||||
u.RefreshToken = ""
|
||||
u.ExpiresAt = time.Time{}
|
||||
|
||||
return user_model.UpdateExternalUserByExternalID(ctx, u)
|
||||
})
|
||||
}
|
||||
|
||||
// Otherwise, update the tokens
|
||||
u.AccessToken = token.AccessToken
|
||||
u.ExpiresAt = token.Expiry
|
||||
|
||||
// Some providers only update access tokens provide a new
|
||||
// refresh token, so avoid updating it if it's empty
|
||||
if token.RefreshToken != "" {
|
||||
u.RefreshToken = token.RefreshToken
|
||||
}
|
||||
|
||||
err = user_model.UpdateExternalUserByExternalID(ctx, u)
|
||||
|
||||
return err
|
||||
}
|
100
services/auth/source/oauth2/source_sync_test.go
Normal file
100
services/auth/source/oauth2/source_sync_test.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSource(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
source := &Source{
|
||||
Provider: "fake",
|
||||
authSource: &auth.Source{
|
||||
ID: 12,
|
||||
Type: auth.OAuth2,
|
||||
Name: "fake",
|
||||
IsActive: true,
|
||||
IsSyncEnabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
user := &user_model.User{
|
||||
LoginName: "external",
|
||||
LoginType: auth.OAuth2,
|
||||
LoginSource: source.authSource.ID,
|
||||
Name: "test",
|
||||
Email: "external@example.com",
|
||||
}
|
||||
|
||||
err := user_model.CreateUser(context.Background(), user, &user_model.CreateUserOverwriteOptions{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
e := &user_model.ExternalLoginUser{
|
||||
ExternalID: "external",
|
||||
UserID: user.ID,
|
||||
LoginSourceID: user.LoginSource,
|
||||
RefreshToken: "valid",
|
||||
}
|
||||
err = user_model.LinkExternalToUser(context.Background(), user, e)
|
||||
assert.NoError(t, err)
|
||||
|
||||
provider, err := createProvider(source.authSource.Name, source)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("refresh", func(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
err := source.refresh(context.Background(), provider, e)
|
||||
assert.NoError(t, err)
|
||||
|
||||
e := &user_model.ExternalLoginUser{
|
||||
ExternalID: e.ExternalID,
|
||||
LoginSourceID: e.LoginSourceID,
|
||||
}
|
||||
|
||||
ok, err := user_model.GetExternalLogin(context.Background(), e)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, e.RefreshToken, "refresh")
|
||||
assert.Equal(t, e.AccessToken, "token")
|
||||
|
||||
u, err := user_model.GetUserByID(context.Background(), user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, u.IsActive)
|
||||
})
|
||||
|
||||
t.Run("expired", func(t *testing.T) {
|
||||
err := source.refresh(context.Background(), provider, &user_model.ExternalLoginUser{
|
||||
ExternalID: "external",
|
||||
UserID: user.ID,
|
||||
LoginSourceID: user.LoginSource,
|
||||
RefreshToken: "expired",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
e := &user_model.ExternalLoginUser{
|
||||
ExternalID: e.ExternalID,
|
||||
LoginSourceID: e.LoginSourceID,
|
||||
}
|
||||
|
||||
ok, err := user_model.GetExternalLogin(context.Background(), e)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, e.RefreshToken, "")
|
||||
assert.Equal(t, e.AccessToken, "")
|
||||
|
||||
u, err := user_model.GetUserByID(context.Background(), user.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, u.IsActive)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -455,13 +455,14 @@ func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse {
|
|||
// ToOAuth2Application convert from auth.OAuth2Application to api.OAuth2Application
|
||||
func ToOAuth2Application(app *auth.OAuth2Application) *api.OAuth2Application {
|
||||
return &api.OAuth2Application{
|
||||
ID: app.ID,
|
||||
Name: app.Name,
|
||||
ClientID: app.ClientID,
|
||||
ClientSecret: app.ClientSecret,
|
||||
ConfidentialClient: app.ConfidentialClient,
|
||||
RedirectURIs: app.RedirectURIs,
|
||||
Created: app.CreatedUnix.AsTime(),
|
||||
ID: app.ID,
|
||||
Name: app.Name,
|
||||
ClientID: app.ClientID,
|
||||
ClientSecret: app.ClientSecret,
|
||||
ConfidentialClient: app.ConfidentialClient,
|
||||
SkipSecondaryAuthorization: app.SkipSecondaryAuthorization,
|
||||
RedirectURIs: app.RedirectURIs,
|
||||
Created: app.CreatedUnix.AsTime(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,14 +71,14 @@ func LinkAccountToUser(ctx context.Context, user *user_model.User, gothUser goth
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateExternalUser updates external user's information
|
||||
func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
|
||||
// EnsureLinkExternalToUser link the gothUser to the user
|
||||
func EnsureLinkExternalToUser(ctx context.Context, user *user_model.User, gothUser goth.User) error {
|
||||
externalLoginUser, err := toExternalLoginUser(ctx, user, gothUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return user_model.UpdateExternalUserByExternalID(ctx, externalLoginUser)
|
||||
return user_model.EnsureLinkExternalToUser(ctx, externalLoginUser)
|
||||
}
|
||||
|
||||
// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID
|
||||
|
|
|
@ -365,9 +365,10 @@ func (f *NewAccessTokenForm) GetScope() (auth_model.AccessTokenScope, error) {
|
|||
|
||||
// EditOAuth2ApplicationForm form for editing oauth2 applications
|
||||
type EditOAuth2ApplicationForm struct {
|
||||
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
|
||||
RedirectURIs string `binding:"Required" form:"redirect_uris"`
|
||||
ConfidentialClient bool `form:"confidential_client"`
|
||||
Name string `binding:"Required;MaxSize(255)" form:"application_name"`
|
||||
RedirectURIs string `binding:"Required" form:"redirect_uris"`
|
||||
ConfidentialClient bool `form:"confidential_client"`
|
||||
SkipSecondaryAuthorization bool `form:"skip_secondary_authorization"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
|
|
@ -314,7 +314,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
|
|||
for _, recipient := range recipients {
|
||||
msg := NewMessageFrom(
|
||||
recipient.Email,
|
||||
ctx.Doer.GetCompleteName(),
|
||||
fromDisplayName(ctx.Doer),
|
||||
setting.MailService.FromEmail,
|
||||
subject,
|
||||
mailBody.String(),
|
||||
|
@ -536,3 +536,19 @@ func actionToTemplate(issue *issues_model.Issue, actionType activities_model.Act
|
|||
}
|
||||
return typeName, name, template
|
||||
}
|
||||
|
||||
func fromDisplayName(u *user_model.User) string {
|
||||
if setting.MailService.FromDisplayNameFormatTemplate != nil {
|
||||
var ctx bytes.Buffer
|
||||
err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{
|
||||
"DisplayName": u.DisplayName(),
|
||||
"AppName": setting.AppName,
|
||||
"Domain": setting.Domain,
|
||||
})
|
||||
if err == nil {
|
||||
return mime.QEncoding.Encode("utf-8", ctx.String())
|
||||
}
|
||||
log.Error("fromDisplayName: %w", err)
|
||||
}
|
||||
return u.GetCompleteName()
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re
|
|||
}
|
||||
|
||||
msgs := make([]*Message, 0, len(tos))
|
||||
publisherName := rel.Publisher.DisplayName()
|
||||
publisherName := fromDisplayName(rel.Publisher)
|
||||
msgID := generateMessageIDForRelease(rel)
|
||||
for _, to := range tos {
|
||||
msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String())
|
||||
|
|
|
@ -79,7 +79,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
|
|||
}
|
||||
|
||||
for _, to := range emailTos {
|
||||
msg := NewMessage(to.EmailTo(), subject, content.String())
|
||||
msg := NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String())
|
||||
msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID)
|
||||
|
||||
SendAsync(msg)
|
||||
|
|
|
@ -403,3 +403,51 @@ func TestGenerateMessageIDForRelease(t *testing.T) {
|
|||
})
|
||||
assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID)
|
||||
}
|
||||
|
||||
func TestFromDisplayName(t *testing.T) {
|
||||
template, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
|
||||
assert.NoError(t, err)
|
||||
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
|
||||
defer func() { setting.MailService = nil }()
|
||||
|
||||
tests := []struct {
|
||||
userDisplayName string
|
||||
fromDisplayName string
|
||||
}{{
|
||||
userDisplayName: "test",
|
||||
fromDisplayName: "test",
|
||||
}, {
|
||||
userDisplayName: "Hi Its <Mee>",
|
||||
fromDisplayName: "Hi Its <Mee>",
|
||||
}, {
|
||||
userDisplayName: "Æsir",
|
||||
fromDisplayName: "=?utf-8?q?=C3=86sir?=",
|
||||
}, {
|
||||
userDisplayName: "new😀user",
|
||||
fromDisplayName: "=?utf-8?q?new=F0=9F=98=80user?=",
|
||||
}}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.userDisplayName, func(t *testing.T) {
|
||||
user := &user_model.User{FullName: tc.userDisplayName, Name: "tmp"}
|
||||
got := fromDisplayName(user)
|
||||
assert.EqualValues(t, tc.fromDisplayName, got)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("template with all available vars", func(t *testing.T) {
|
||||
template, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])")
|
||||
assert.NoError(t, err)
|
||||
setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template}
|
||||
oldAppName := setting.AppName
|
||||
setting.AppName = "Code IT"
|
||||
oldDomain := setting.Domain
|
||||
setting.Domain = "code.it"
|
||||
defer func() {
|
||||
setting.AppName = oldAppName
|
||||
setting.Domain = oldDomain
|
||||
}()
|
||||
|
||||
assert.EqualValues(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -147,6 +147,23 @@ func DelDivergenceFromCache(repoID int64, branchName string) error {
|
|||
return cache.GetCache().Delete(getDivergenceCacheKey(repoID, branchName))
|
||||
}
|
||||
|
||||
// DelRepoDivergenceFromCache deletes all divergence caches of a repository
|
||||
func DelRepoDivergenceFromCache(ctx context.Context, repoID int64) error {
|
||||
dbBranches, err := db.Find[git_model.Branch](ctx, git_model.FindBranchOptions{
|
||||
RepoID: repoID,
|
||||
ListOptions: db.ListOptionsAll,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range dbBranches {
|
||||
if err := DelDivergenceFromCache(repoID, dbBranches[i].Name); err != nil {
|
||||
log.Error("DelDivergenceFromCache: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *git_model.Branch, protectedBranches *git_model.ProtectedBranchRules,
|
||||
repoIDToRepo map[int64]*repo_model.Repository,
|
||||
repoIDToGitRepo map[int64]*git.Repository,
|
||||
|
|
|
@ -221,8 +221,14 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
|
|||
}
|
||||
|
||||
// delete cache for divergence
|
||||
if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
|
||||
log.Error("DelDivergenceFromCache: %v", err)
|
||||
if branch == repo.DefaultBranch {
|
||||
if err := DelRepoDivergenceFromCache(ctx, repo.ID); err != nil {
|
||||
log.Error("DelRepoDivergenceFromCache: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
|
||||
log.Error("DelDivergenceFromCache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
commits := repo_module.GitToPushCommits(l)
|
||||
|
|
|
@ -412,7 +412,7 @@
|
|||
<p class="help">{{ctx.Locale.Tr "admin.auths.sspi_default_language_helper"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Source.IsLDAP}}
|
||||
{{if (or .Source.IsLDAP .Source.IsOAuth2)}}
|
||||
<div class="inline field">
|
||||
<div class="ui checkbox">
|
||||
<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<input name="attributes_in_bind" type="checkbox" {{if .attributes_in_bind}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ldap inline field {{if not (eq .type 2)}}tw-hidden{{end}}">
|
||||
<div class="oauth2 ldap inline field {{if not (or (eq .type 2) (eq .type 6))}}tw-hidden{{end}}">
|
||||
<div class="ui checkbox">
|
||||
<label><strong>{{ctx.Locale.Tr "admin.auths.syncenabled"}}</strong></label>
|
||||
<input name="is_sync_enabled" type="checkbox" {{if .is_sync_enabled}}checked{{end}}>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="ui bottom attached segment tw-p-0">
|
||||
<div class="ui active tab tw-rounded" data-tab="write">
|
||||
<div class="ui active tab tw-rounded-b" data-tab="write">
|
||||
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
|
||||
data-url="{{.Repository.Link}}/markup"
|
||||
data-context="{{.RepoLink}}"
|
||||
|
|
|
@ -11,5 +11,6 @@
|
|||
<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
|
||||
<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
|
||||
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
|
||||
<a class="{{if eq .SortType "name"}}active {{end}}item" href="{{$.Link}}?sort=name&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.name"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
8
templates/swagger/v1_json.tmpl
generated
8
templates/swagger/v1_json.tmpl
generated
|
@ -19875,6 +19875,10 @@
|
|||
"type": "string"
|
||||
},
|
||||
"x-go-name": "RedirectURIs"
|
||||
},
|
||||
"skip_secondary_authorization": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "SkipSecondaryAuthorization"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
@ -23002,6 +23006,10 @@
|
|||
"type": "string"
|
||||
},
|
||||
"x-go-name": "RedirectURIs"
|
||||
},
|
||||
"skip_secondary_authorization": {
|
||||
"type": "boolean",
|
||||
"x-go-name": "SkipSecondaryAuthorization"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
|
|
|
@ -65,6 +65,7 @@
|
|||
<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
|
||||
<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
|
||||
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
|
||||
<a class="{{if eq .SortType "name"}}active {{end}}item" href="{{$.Link}}?sort=name&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.name"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -44,7 +44,13 @@
|
|||
<div class="field {{if .Err_ConfidentialClient}}error{{end}}">
|
||||
<div class="ui checkbox">
|
||||
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label>
|
||||
<input type="checkbox" name="confidential_client" {{if .App.ConfidentialClient}}checked{{end}}>
|
||||
<input class="disable-setting" type="checkbox" name="confidential_client" data-target="#skip-secondary-authorization" {{if .App.ConfidentialClient}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field {{if .Err_SkipSecondaryAuthorization}}error{{end}} {{if .App.ConfidentialClient}}disabled{{end}}" 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" {{if .App.SkipSecondaryAuthorization}}checked{{end}}>
|
||||
</div>
|
||||
</div>
|
||||
<button class="ui primary button">
|
||||
|
|
|
@ -64,7 +64,13 @@
|
|||
<div class="field {{if .Err_ConfidentialClient}}error{{end}}">
|
||||
<div class="ui checkbox">
|
||||
<label>{{ctx.Locale.Tr "settings.oauth2_confidential_client"}}</label>
|
||||
<input type="checkbox" name="confidential_client" checked>
|
||||
<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">
|
||||
|
|
|
@ -224,7 +224,8 @@ func TestLDAPUserSync(t *testing.T) {
|
|||
}
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
addAuthSourceLDAP(t, "", "")
|
||||
auth.SyncExternalUsers(context.Background(), true)
|
||||
err := auth.SyncExternalUsers(context.Background(), true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if users exists
|
||||
for _, gitLDAPUser := range gitLDAPUsers {
|
||||
|
|
|
@ -419,3 +419,59 @@ func TestRefreshTokenInvalidation(t *testing.T) {
|
|||
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
|
||||
assert.Equal(t, "token was already used", parsedError.ErrorDescription)
|
||||
}
|
||||
|
||||
func TestOAuthIntrospection(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
|
||||
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
|
||||
"redirect_uri": "a",
|
||||
"code": "authcode",
|
||||
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
type response struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
parsed := new(response)
|
||||
|
||||
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
|
||||
assert.True(t, len(parsed.AccessToken) > 10)
|
||||
assert.True(t, len(parsed.RefreshToken) > 10)
|
||||
|
||||
// successful request with a valid client_id/client_secret and a valid token
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
|
||||
"token": parsed.AccessToken,
|
||||
})
|
||||
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
type introspectResponse struct {
|
||||
Active bool `json:"active"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
introspectParsed := new(introspectResponse)
|
||||
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), introspectParsed))
|
||||
assert.True(t, introspectParsed.Active)
|
||||
|
||||
// successful request with a valid client_id/client_secret, but an invalid token
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
|
||||
"token": "xyzzy",
|
||||
})
|
||||
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
introspectParsed = new(introspectResponse)
|
||||
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), introspectParsed))
|
||||
assert.False(t, introspectParsed.Active)
|
||||
|
||||
// unsuccessful request with an invalid client_id/client_secret
|
||||
req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
|
||||
"token": parsed.AccessToken,
|
||||
})
|
||||
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpK")
|
||||
resp = MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.Contains(t, resp.Body.String(), "no valid authorization")
|
||||
}
|
||||
|
|
|
@ -23,18 +23,7 @@
|
|||
|
||||
.monaco-editor,
|
||||
.monaco-editor .overflow-guard {
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* these seem unthemeable */
|
||||
.monaco-scrollable-element > .scrollbar > .slider {
|
||||
background: var(--color-primary) !important;
|
||||
}
|
||||
.monaco-scrollable-element > .scrollbar > .slider:hover {
|
||||
background: var(--color-primary-dark-1) !important;
|
||||
}
|
||||
.monaco-scrollable-element > .scrollbar > .slider:active {
|
||||
background: var(--color-primary-dark-2) !important;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
/* fomantic styles destroy this element only visible on IOS, restore it */
|
||||
|
|
|
@ -96,7 +96,6 @@
|
|||
.page-content.organization #org-info {
|
||||
overflow-wrap: anywhere;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.page-content.organization #org-info .ui.header {
|
||||
|
|
|
@ -2466,7 +2466,7 @@ tbody.commit-list {
|
|||
.sidebar-item-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.diff-file-header {
|
||||
|
|
|
@ -212,6 +212,7 @@
|
|||
--color-button: #171a1e;
|
||||
--color-code-bg: #14171a;
|
||||
--color-shadow: #00001758;
|
||||
--color-shadow-opaque: #000017;
|
||||
--color-secondary-bg: #2a3137;
|
||||
--color-expand-button: #2f363d;
|
||||
--color-placeholder-text: var(--color-text-light-3);
|
||||
|
|
|
@ -212,6 +212,7 @@
|
|||
--color-button: #f8f9fb;
|
||||
--color-code-bg: #fafdff;
|
||||
--color-shadow: #00001726;
|
||||
--color-shadow-opaque: #c7ced5;
|
||||
--color-secondary-bg: #f2f5f8;
|
||||
--color-expand-button: #cfe8fa;
|
||||
--color-placeholder-text: var(--color-text-light-3);
|
||||
|
|
|
@ -96,7 +96,7 @@ export async function createMonaco(textarea, filename, editorOpts) {
|
|||
'input.background': getColor('--color-input-background'),
|
||||
'input.border': getColor('--color-input-border'),
|
||||
'input.foreground': getColor('--color-input-text'),
|
||||
'scrollbar.shadow': getColor('--color-shadow'),
|
||||
'scrollbar.shadow': getColor('--color-shadow-opaque'),
|
||||
'progressBar.background': getColor('--color-primary'),
|
||||
'focusBorder': '#0000', // prevent blue border
|
||||
},
|
||||
|
|
5
web_src/js/features/oauth2-settings.ts
Normal file
5
web_src/js/features/oauth2-settings.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export function initOAuth2SettingsDisableCheckbox() {
|
||||
for (const e of document.querySelectorAll('.disable-setting')) e.addEventListener('change', ({target}) => {
|
||||
document.querySelector(e.getAttribute('data-target')).classList.toggle('disabled', target.checked);
|
||||
});
|
||||
}
|
|
@ -125,7 +125,7 @@ export function initRepoCommentForm() {
|
|||
|
||||
$listMenu.find('.item:not(.no-select)').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
if ($(this).hasClass('ban-change')) {
|
||||
if (this.classList.contains('ban-change')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,7 @@ export function initRepoCommentForm() {
|
|||
if (this.getAttribute('data-scope') !== scope) {
|
||||
return true;
|
||||
}
|
||||
if (this !== clickedItem && !$(this).hasClass('checked')) {
|
||||
if (this !== clickedItem && !this.classList.contains('checked')) {
|
||||
return true;
|
||||
}
|
||||
} else if (this !== clickedItem) {
|
||||
|
@ -148,7 +148,7 @@ export function initRepoCommentForm() {
|
|||
return true;
|
||||
}
|
||||
|
||||
if ($(this).hasClass('checked')) {
|
||||
if (this.classList.contains('checked')) {
|
||||
$(this).removeClass('checked');
|
||||
$(this).find('.octicon-check').addClass('tw-invisible');
|
||||
if (hasUpdateAction) {
|
||||
|
@ -187,7 +187,7 @@ export function initRepoCommentForm() {
|
|||
|
||||
const listIds = [];
|
||||
$(this).parent().find('.item').each(function () {
|
||||
if ($(this).hasClass('checked')) {
|
||||
if (this.classList.contains('checked')) {
|
||||
listIds.push($(this).data('id'));
|
||||
$($(this).data('id-selector')).removeClass('tw-hidden');
|
||||
} else {
|
||||
|
|
|
@ -5,6 +5,10 @@ import {GET, POST} from '../modules/fetch.ts';
|
|||
const {appSubUrl} = window.config;
|
||||
|
||||
export async function initUserAuthWebAuthn() {
|
||||
if (!document.querySelector('.user.signin')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!detectWebAuthnSupport()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ import {initDirAuto} from './modules/dirauto.ts';
|
|||
import {initRepositorySearch} from './features/repo-search.ts';
|
||||
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 {
|
||||
initFootLanguageMenu,
|
||||
|
@ -225,5 +226,7 @@ onDomReady(() => {
|
|||
initPdfViewer,
|
||||
initScopedAccessTokenCategories,
|
||||
initColorPickers,
|
||||
|
||||
initOAuth2SettingsDisableCheckbox,
|
||||
]);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user