From f92e0a4018ca65936c95ac119c57d4b9ab62bc2d Mon Sep 17 00:00:00 2001 From: John Olheiser Date: Fri, 10 Mar 2023 00:13:17 -0600 Subject: [PATCH 01/15] Split CI pipelines (#23385) - This PR attempts to split our various DB tests into separate pipelines. - It splits up some of the extra feature-related tests rather than having most of them in the MySQL test. - It disables the race detector for some of the pipelines as well, as it can cause slower runs and is mostly redundant when the pipelines just swap DBs. - It builds without SQLite support for any of the non-SQLite pipelines. - It moves the e2e test to using SQLite rather than PG (partially because I moved the minio tests to PG and that mucked up the test config, and partially because it avoids another running service) - It splits up the `go mod download` task in the Makefile from the tool installation, as the tools are only needed in the compliance pipeline. (Arguably even some of the tools aren't needed there, but that could be a follow-up PR) - SQLite is now the only arm64 pipeline, moving PG back to amd64 which can leverage autoscaler Should resolve #22010 - one thing that wasn't changed here but is mentioned in that issue, unit tests are needed in the same pipeline as an integration test in order to form a complete coverage report (at least as far as I could tell), so for now it remains in a pipeline with a DB integration test. Please let me know if I've inadvertently changed something that was how it was on purpose. --- I will say sometimes it's hard to pin down the average time, as a pipeline could be waiting for a runner for X minutes and that brings the total up by X minutes as well, but overall this does seem to be faster on average. --------- Signed-off-by: jolheiser Co-authored-by: techknowlogick --- .drone.yml | 462 +++++++++++++++++++++++++++++++------------ Makefile | 6 +- tests/mysql.ini.tmpl | 25 +-- tests/pgsql.ini.tmpl | 21 +- 4 files changed, 364 insertions(+), 150 deletions(-) diff --git a/.drone.yml b/.drone.yml index d1ad625e329f9..0e1d799834ec5 100644 --- a/.drone.yml +++ b/.drone.yml @@ -32,6 +32,7 @@ steps: pull: always commands: - make deps-backend + - make deps-tools volumes: - name: deps path: /go @@ -168,7 +169,7 @@ steps: --- kind: pipeline type: docker -name: testing-amd64 +name: testing-pgsql platform: os: linux @@ -191,51 +192,26 @@ volumes: temp: {} services: - - name: mysql - image: mysql:5.7 - pull: always - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: test - - - name: mysql8 - image: mysql:8 - pull: always - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: testgitea - - - name: mssql - image: mcr.microsoft.com/mssql/server:latest - pull: always + - name: pgsql + pull: default + image: postgres:15 environment: - ACCEPT_EULA: Y - MSSQL_PID: Standard - SA_PASSWORD: MwantsaSecurePassword1 + POSTGRES_DB: test + POSTGRES_PASSWORD: postgres - name: ldap image: gitea/test-openldap:latest pull: always - - name: elasticsearch - image: elasticsearch:7.5.0 - pull: always - environment: - discovery.type: single-node - - name: minio image: minio/minio:RELEASE.2021-03-12T00-00-47Z pull: always commands: - - minio server /data + - minio server /data environment: MINIO_ACCESS_KEY: 123456 MINIO_SECRET_KEY: 12345678 - - name: smtpimap - image: tabascoterrier/docker-imap-devel:latest - pull: always - steps: - name: fetch-tags image: docker:git @@ -257,12 +233,6 @@ steps: - name: deps path: /go - - name: tag-pre-condition - image: drone/git - pull: always - commands: - - git update-ref refs/heads/tag_test ${DRONE_COMMIT_SHA} - - name: prepare-test-env image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env pull: always @@ -278,88 +248,157 @@ steps: environment: GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not GOSUMDB: sum.golang.org - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: unit-test + - name: test-pgsql image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - make unit-test-coverage test-check + - timeout -s ABRT 50m make test-pgsql-migration test-pgsql environment: GOPROXY: https://goproxy.io - TAGS: bindata sqlite sqlite_unlock_notify + TAGS: bindata gogit RACE_ENABLED: true - GITHUB_READ_TOKEN: - from_secret: github_read_token - depends_on: [deps-backend, prepare-test-env] + TEST_TAGS: gogit + TEST_LDAP: 1 + USE_REPO_TEST_DIR: 1 + depends_on: [build] volumes: - name: deps path: /go - - name: unit-test-gogit +--- +kind: pipeline +type: docker +name: testing-mysql + +platform: + os: linux + arch: amd64 + +depends_on: + - compliance + +trigger: + event: + - push + - tag + - pull_request + paths: + exclude: + - docs/** + +volumes: + - name: deps + temp: {} + +services: + - name: mysql + image: mysql:5.7 + pull: always + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: test + + - name: elasticsearch + image: elasticsearch:7.5.0 + pull: always + environment: + discovery.type: single-node + + - name: smtpimap + image: tabascoterrier/docker-imap-devel:latest + pull: always + +steps: + - name: fetch-tags + image: docker:git + pull: always + commands: + - git config --global --add safe.directory /drone/src + - git fetch --tags --force + when: + event: + exclude: + - pull_request + + - name: deps-backend + image: golang:1.20 + pull: always + commands: + - make deps-backend + volumes: + - name: deps + path: /go + + - name: prepare-test-env + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env + pull: always + commands: + - ./build/test-env-prepare.sh + + - name: build image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - make unit-test-coverage test-check + - ./build/test-env-check.sh + - make backend environment: - GOPROXY: https://goproxy.io - TAGS: bindata gogit sqlite sqlite_unlock_notify - RACE_ENABLED: true - GITHUB_READ_TOKEN: - from_secret: github_read_token + GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not + GOSUMDB: sum.golang.org + TAGS: bindata depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: test-mysql + - name: unit-test image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - make test-mysql-migration integration-test-coverage + - make unit-test-coverage test-check environment: GOPROXY: https://goproxy.io TAGS: bindata RACE_ENABLED: true - TEST_LDAP: 1 - USE_REPO_TEST_DIR: 1 - TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" - depends_on: [build] + GITHUB_READ_TOKEN: + from_secret: github_read_token + depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: test-mysql8 + - name: unit-test-gogit image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - timeout -s ABRT 50m make test-mysql8-migration test-mysql8 + - make unit-test-coverage test-check environment: GOPROXY: https://goproxy.io - TAGS: bindata + TAGS: bindata gogit RACE_ENABLED: true - TEST_LDAP: 1 - USE_REPO_TEST_DIR: 1 - depends_on: [build] + GITHUB_READ_TOKEN: + from_secret: github_read_token + depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: test-mssql + - name: test-mysql image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - make test-mssql-migration test-mssql + - make test-mysql-migration integration-test-coverage environment: GOPROXY: https://goproxy.io TAGS: bindata RACE_ENABLED: true - TEST_LDAP: 1 USE_REPO_TEST_DIR: 1 + TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" depends_on: [build] volumes: - name: deps @@ -398,11 +437,12 @@ steps: --- kind: pipeline -name: testing-arm64 +type: docker +name: testing-mysql8 platform: os: linux - arch: arm64 + arch: amd64 depends_on: - compliance @@ -421,16 +461,102 @@ volumes: temp: {} services: - - name: pgsql - pull: default - image: postgres:10 + - name: mysql8 + image: mysql:8 + pull: always environment: - POSTGRES_DB: test - POSTGRES_PASSWORD: postgres + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: testgitea - - name: ldap - pull: default - image: gitea/test-openldap:latest +steps: + - name: fetch-tags + image: docker:git + pull: always + commands: + - git config --global --add safe.directory /drone/src + - git fetch --tags --force + when: + event: + exclude: + - pull_request + + - name: deps-backend + image: golang:1.20 + pull: always + commands: + - make deps-backend + volumes: + - name: deps + path: /go + + - name: prepare-test-env + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env + pull: always + commands: + - ./build/test-env-prepare.sh + + - name: build + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env + user: gitea + commands: + - ./build/test-env-check.sh + - make backend + environment: + GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not + GOSUMDB: sum.golang.org + TAGS: bindata + depends_on: [deps-backend, prepare-test-env] + volumes: + - name: deps + path: /go + + - name: test-mysql8 + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env + user: gitea + commands: + - timeout -s ABRT 50m make test-mysql8-migration test-mysql8 + environment: + GOPROXY: https://goproxy.io + TAGS: bindata + USE_REPO_TEST_DIR: 1 + depends_on: [build] + volumes: + - name: deps + path: /go + +--- +kind: pipeline +type: docker +name: testing-mssql + +platform: + os: linux + arch: amd64 + +depends_on: + - compliance + +trigger: + event: + - push + - tag + - pull_request + paths: + exclude: + - docs/** + +volumes: + - name: deps + temp: {} + +services: + - name: mssql + image: mcr.microsoft.com/mssql/server:latest + pull: always + environment: + ACCEPT_EULA: Y + MSSQL_PID: Standard + SA_PASSWORD: MwantsaSecurePassword1 steps: - name: fetch-tags @@ -454,13 +580,13 @@ steps: path: /go - name: prepare-test-env - image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env pull: always commands: - ./build/test-env-prepare.sh - name: build - image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - ./build/test-env-check.sh @@ -468,39 +594,102 @@ steps: environment: GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not GOSUMDB: sum.golang.org - TAGS: bindata gogit sqlite sqlite_unlock_notify + TAGS: bindata depends_on: [deps-backend, prepare-test-env] volumes: - name: deps path: /go - - name: test-sqlite - image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + - name: test-mssql + image: gitea/test_env:linux-amd64 # https://gitea.com/gitea/test-env user: gitea commands: - - timeout -s ABRT 50m make test-sqlite-migration test-sqlite + - make test-mssql-migration test-mssql environment: GOPROXY: https://goproxy.io - TAGS: bindata gogit sqlite sqlite_unlock_notify - RACE_ENABLED: true - TEST_TAGS: gogit sqlite sqlite_unlock_notify + TAGS: bindata USE_REPO_TEST_DIR: 1 depends_on: [build] volumes: - name: deps path: /go - - name: test-pgsql +--- +kind: pipeline +name: testing-sqlite + +platform: + os: linux + arch: arm64 + +depends_on: + - compliance + +trigger: + event: + - push + - tag + - pull_request + paths: + exclude: + - docs/** + +volumes: + - name: deps + temp: {} + +steps: + - name: fetch-tags + image: docker:git + pull: always + commands: + - git config --global --add safe.directory /drone/src + - git fetch --tags --force + when: + event: + exclude: + - pull_request + + - name: deps-backend + image: golang:1.20 + pull: always + commands: + - make deps-backend + volumes: + - name: deps + path: /go + + - name: prepare-test-env + image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + pull: always + commands: + - ./build/test-env-prepare.sh + + - name: build image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env user: gitea commands: - - timeout -s ABRT 50m make test-pgsql-migration test-pgsql + - ./build/test-env-check.sh + - make backend + environment: + GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not + GOSUMDB: sum.golang.org + TAGS: bindata gogit sqlite sqlite_unlock_notify + depends_on: [deps-backend, prepare-test-env] + volumes: + - name: deps + path: /go + + - name: test-sqlite + image: gitea/test_env:linux-arm64 # https://gitea.com/gitea/test-env + user: gitea + commands: + - timeout -s ABRT 50m make test-sqlite-migration test-sqlite environment: GOPROXY: https://goproxy.io - TAGS: bindata gogit + TAGS: bindata gogit sqlite sqlite_unlock_notify RACE_ENABLED: true - TEST_TAGS: gogit - TEST_LDAP: 1 + TEST_TAGS: gogit sqlite sqlite_unlock_notify USE_REPO_TEST_DIR: 1 depends_on: [build] volumes: @@ -530,15 +719,6 @@ volumes: - name: deps temp: {} -services: - - name: pgsql - pull: default - image: postgres:10 - environment: - POSTGRES_DB: testgitea-e2e - POSTGRES_PASSWORD: postgres - POSTGRES_INITDB_ARGS: --encoding=UTF8 --lc-collate='en_US.UTF-8' --lc-ctype='en_US.UTF-8' - steps: - name: deps-frontend image: node:18 @@ -568,14 +748,12 @@ steps: - curl -sLO https://go.dev/dl/go1.20.linux-amd64.tar.gz && tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz - groupadd --gid 1001 gitea && useradd -m --gid 1001 --uid 1001 gitea - apt-get -qq update && apt-get -qqy install build-essential - - export TEST_PGSQL_SCHEMA='' - ./build/test-env-prepare.sh - - su gitea bash -c "export PATH=$PATH:/usr/local/go/bin && timeout -s ABRT 40m make test-e2e-pgsql" + - su gitea bash -c "export PATH=$PATH:/usr/local/go/bin && timeout -s ABRT 40m make test-e2e-sqlite" environment: GOPROXY: https://goproxy.io GOSUMDB: sum.golang.org USE_REPO_TEST_DIR: 1 - TEST_PGSQL_DBNAME: 'testgitea-e2e' DEBIAN_FRONTEND: noninteractive depends_on: [build-frontend, deps-backend] volumes: @@ -709,8 +887,11 @@ trigger: - docs/** depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite volumes: - name: deps @@ -842,8 +1023,11 @@ trigger: - tag depends_on: - - testing-arm64 - - testing-amd64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite volumes: - name: deps @@ -994,8 +1178,11 @@ platform: arch: amd64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1064,8 +1251,11 @@ platform: arch: amd64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1129,8 +1319,11 @@ platform: arch: amd64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1192,8 +1385,11 @@ platform: arch: amd64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1292,8 +1488,11 @@ platform: arch: arm64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1362,8 +1561,11 @@ platform: arch: arm64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1427,8 +1629,11 @@ platform: arch: arm64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1493,8 +1698,11 @@ platform: arch: arm64 depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite trigger: ref: @@ -1607,7 +1815,6 @@ platform: steps: - name: manifest-rootless - pull: always image: plugins/manifest pull: always settings: @@ -1671,8 +1878,11 @@ trigger: - failure depends_on: - - testing-amd64 - - testing-arm64 + - testing-mysql + - testing-mysql8 + - testing-mssql + - testing-pgsql + - testing-sqlite - release-version - release-latest - docker-linux-amd64-release diff --git a/Makefile b/Makefile index 19cd455aba30c..d770ed453f4fa 100644 --- a/Makefile +++ b/Makefile @@ -190,6 +190,7 @@ help: @echo " - deps install dependencies" @echo " - deps-frontend install frontend dependencies" @echo " - deps-backend install backend dependencies" + @echo " - deps-tools install tool dependencies" @echo " - lint lint everything" @echo " - lint-frontend lint frontend files" @echo " - lint-backend lint backend files" @@ -821,7 +822,7 @@ docs: cd docs; make trans-copy clean build-offline; .PHONY: deps -deps: deps-frontend deps-backend +deps: deps-frontend deps-backend deps-tools .PHONY: deps-frontend deps-frontend: node_modules @@ -829,6 +830,9 @@ deps-frontend: node_modules .PHONY: deps-backend deps-backend: $(GO) mod download + +.PHONY: deps-tools +deps-tools: $(GO) install $(AIR_PACKAGE) $(GO) install $(EDITORCONFIG_CHECKER_PACKAGE) $(GO) install $(ERRCHECK_PACKAGE) diff --git a/tests/mysql.ini.tmpl b/tests/mysql.ini.tmpl index 1dd7bfab2a49c..b286f37bf83e2 100644 --- a/tests/mysql.ini.tmpl +++ b/tests/mysql.ini.tmpl @@ -55,28 +55,6 @@ LFS_START_SERVER = true LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= -[lfs] -MINIO_BASE_PATH = lfs/ - -[attachment] -MINIO_BASE_PATH = attachments/ - -[avatars] -MINIO_BASE_PATH = avatars/ - -[repo-avatars] -MINIO_BASE_PATH = repo-avatars/ - -[storage] -STORAGE_TYPE = minio -SERVE_DIRECT = false -MINIO_ENDPOINT = minio:9000 -MINIO_ACCESS_KEY_ID = 123456 -MINIO_SECRET_ACCESS_KEY = 12345678 -MINIO_BUCKET = gitea -MINIO_LOCATION = us-east-1 -MINIO_USE_SSL = false - [mailer] ENABLED = true MAILER_TYPE = dummy @@ -122,6 +100,9 @@ INSTALL_LOCK = true SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ +[lfs] +PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/lfs + [packages] ENABLED = true diff --git a/tests/pgsql.ini.tmpl b/tests/pgsql.ini.tmpl index c39b6a79c3a8b..fbfbae7c68130 100644 --- a/tests/pgsql.ini.tmpl +++ b/tests/pgsql.ini.tmpl @@ -105,7 +105,26 @@ SECRET_KEY = 9pCviYTWSb INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ [lfs] -PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/lfs +MINIO_BASE_PATH = lfs/ + +[attachment] +MINIO_BASE_PATH = attachments/ + +[avatars] +MINIO_BASE_PATH = avatars/ + +[repo-avatars] +MINIO_BASE_PATH = repo-avatars/ + +[storage] +STORAGE_TYPE = minio +SERVE_DIRECT = false +MINIO_ENDPOINT = minio:9000 +MINIO_ACCESS_KEY_ID = 123456 +MINIO_SECRET_ACCESS_KEY = 12345678 +MINIO_BUCKET = gitea +MINIO_LOCATION = us-east-1 +MINIO_USE_SSL = false [packages] ENABLED = true From dad057b6393548ad389ead07c2cce5b3ac2811e0 Mon Sep 17 00:00:00 2001 From: zeripath Date: Fri, 10 Mar 2023 06:14:43 +0000 Subject: [PATCH 02/15] Handle OpenID discovery URL errors a little nicer when creating/editing sources (#23397) When there is an error creating a new openIDConnect authentication source try to handle the error a little better. Close #23283 Signed-off-by: Andrew Thornton Co-authored-by: techknowlogick --- cmd/admin.go | 11 ++++++++- options/locale/locale_en-US.ini | 2 ++ routers/web/admin/auths.go | 23 +++++++++++++++++++ .../auth/source/oauth2/source_register.go | 4 ++++ templates/admin/auth/source/oauth.tmpl | 2 +- 5 files changed, 40 insertions(+), 2 deletions(-) diff --git a/cmd/admin.go b/cmd/admin.go index b913b817bdc5d..f9fb1b6c68f07 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -7,6 +7,7 @@ package cmd import ( "errors" "fmt" + "net/url" "os" "strings" "text/tabwriter" @@ -469,11 +470,19 @@ func runAddOauth(c *cli.Context) error { return err } + config := parseOAuth2Config(c) + if config.Provider == "openidConnect" { + discoveryURL, err := url.Parse(config.OpenIDConnectAutoDiscoveryURL) + if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { + return fmt.Errorf("invalid Auto Discovery URL: %s (this must be a valid URL starting with http:// or https://)", config.OpenIDConnectAutoDiscoveryURL) + } + } + return auth_model.CreateSource(&auth_model.Source{ Type: auth_model.OAuth2, Name: c.String("name"), IsActive: true, - Cfg: parseOAuth2Config(c), + Cfg: config, }) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3695bd0384aa9..6f0d06a6e8c26 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2808,6 +2808,8 @@ auths.still_in_used = The authentication source is still in use. Convert or dele auths.deletion_success = The authentication source has been deleted. auths.login_source_exist = The authentication source '%s' already exists. auths.login_source_of_type_exist = An authentication source of this type already exists. +auths.unable_to_initialize_openid = Unable to initialize OpenID Connect Provider: %s +auths.invalid_openIdConnectAutoDiscoveryURL = Invalid Auto Discovery URL (this must be a valid URL starting with http:// or https://) config.server_config = Server Configuration config.app_name = Site Title diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 8ce45720fec37..d2953f753d7a4 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -271,6 +271,15 @@ func NewAuthSourcePost(ctx *context.Context) { } case auth.OAuth2: config = parseOAuth2Config(form) + oauth2Config := config.(*oauth2.Source) + if oauth2Config.Provider == "openidConnect" { + discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL) + if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { + ctx.Data["Err_DiscoveryURL"] = true + ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthNew, form) + return + } + } case auth.SSPI: var err error config, err = parseSSPIConfig(ctx, form) @@ -305,6 +314,10 @@ func NewAuthSourcePost(ctx *context.Context) { if auth.IsErrSourceAlreadyExist(err) { ctx.Data["Err_Name"] = true ctx.RenderWithErr(ctx.Tr("admin.auths.login_source_exist", err.(auth.ErrSourceAlreadyExist).Name), tplAuthNew, form) + } else if oauth2.IsErrOpenIDConnectInitialize(err) { + ctx.Data["Err_DiscoveryURL"] = true + unwrapped := err.(oauth2.ErrOpenIDConnectInitialize).Unwrap() + ctx.RenderWithErr(ctx.Tr("admin.auths.unable_to_initialize_openid", unwrapped), tplAuthNew, form) } else { ctx.ServerError("auth.CreateSource", err) } @@ -389,6 +402,15 @@ func EditAuthSourcePost(ctx *context.Context) { } case auth.OAuth2: config = parseOAuth2Config(form) + oauth2Config := config.(*oauth2.Source) + if oauth2Config.Provider == "openidConnect" { + discoveryURL, err := url.Parse(oauth2Config.OpenIDConnectAutoDiscoveryURL) + if err != nil || (discoveryURL.Scheme != "http" && discoveryURL.Scheme != "https") { + ctx.Data["Err_DiscoveryURL"] = true + ctx.RenderWithErr(ctx.Tr("admin.auths.invalid_openIdConnectAutoDiscoveryURL"), tplAuthEdit, form) + return + } + } case auth.SSPI: config, err = parseSSPIConfig(ctx, form) if err != nil { @@ -408,6 +430,7 @@ func EditAuthSourcePost(ctx *context.Context) { if err := auth.UpdateSource(source); err != nil { if oauth2.IsErrOpenIDConnectInitialize(err) { ctx.Flash.Error(err.Error(), true) + ctx.Data["Err_DiscoveryURL"] = true ctx.HTML(http.StatusOK, tplAuthEdit) } else { ctx.ServerError("UpdateSource", err) diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go index 3527d54b65c59..82a36acaa67d4 100644 --- a/services/auth/source/oauth2/source_register.go +++ b/services/auth/source/oauth2/source_register.go @@ -36,6 +36,10 @@ func (err ErrOpenIDConnectInitialize) Error() string { return fmt.Sprintf("Failed to initialize OpenID Connect Provider with name '%s' with url '%s': %v", err.ProviderName, err.OpenIDConnectAutoDiscoveryURL, err.Cause) } +func (err ErrOpenIDConnectInitialize) Unwrap() error { + return err.Cause +} + // wrapOpenIDConnectInitializeError is used to wrap the error but this cannot be done in modules/auth/oauth2 // inside oauth2: import cycle not allowed models -> modules/auth/oauth2 -> models func wrapOpenIDConnectInitializeError(err error, providerName string, source *Source) error { diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl index b7ee00822f522..1080937f9134c 100644 --- a/templates/admin/auth/source/oauth.tmpl +++ b/templates/admin/auth/source/oauth.tmpl @@ -24,7 +24,7 @@ -
+
From 2173f14708ff3b35d7821fc9b6dcb5fcd06b8494 Mon Sep 17 00:00:00 2001 From: KN4CK3R Date: Fri, 10 Mar 2023 15:28:32 +0100 Subject: [PATCH 03/15] Add user webhooks (#21563) Currently we can add webhooks for organizations but not for users. This PR adds the latter. You can access it from the current users settings. ![grafik](https://user-images.githubusercontent.com/1666336/197391408-15dfdc23-b476-4d0c-82f7-9bc9b065988f.png) --- .../doc/developers/oauth2-provider.en-us.md | 1 + models/auth/token_scope.go | 10 +- models/auth/token_scope_test.go | 4 +- models/fixtures/webhook.yml | 2 +- models/migrations/migrations.go | 2 + models/migrations/v1_20/v245.go | 74 +++++++++ models/webhook/webhook.go | 24 +-- models/webhook/webhook_system.go | 10 +- models/webhook/webhook_test.go | 24 +-- options/locale/locale_en-US.ini | 2 + routers/api/v1/admin/hooks.go | 5 +- routers/api/v1/api.go | 7 + routers/api/v1/org/hook.go | 79 +++------ routers/api/v1/repo/hook.go | 6 +- routers/api/v1/user/hook.go | 154 ++++++++++++++++++ routers/api/v1/utils/hook.go | 89 +++++++--- routers/web/org/setting.go | 8 +- routers/web/repo/webhook.go | 67 ++++---- routers/web/user/setting/webhooks.go | 48 ++++++ routers/web/web.go | 123 ++++++-------- services/repository/hooks.go | 2 +- services/webhook/webhook.go | 10 +- templates/repo/settings/webhook/history.tmpl | 2 +- templates/swagger/v1_json.tmpl | 146 +++++++++++++++++ templates/user/settings/applications.tmpl | 6 + templates/user/settings/hook_new.tmpl | 53 ++++++ templates/user/settings/hooks.tmpl | 8 + templates/user/settings/navbar.tmpl | 5 + 28 files changed, 737 insertions(+), 234 deletions(-) create mode 100644 models/migrations/v1_20/v245.go create mode 100644 routers/api/v1/user/hook.go create mode 100644 routers/web/user/setting/webhooks.go create mode 100644 templates/user/settings/hook_new.tmpl create mode 100644 templates/user/settings/hooks.tmpl diff --git a/docs/content/doc/developers/oauth2-provider.en-us.md b/docs/content/doc/developers/oauth2-provider.en-us.md index 17c12d22f2423..1ef30a7f0e4a2 100644 --- a/docs/content/doc/developers/oauth2-provider.en-us.md +++ b/docs/content/doc/developers/oauth2-provider.en-us.md @@ -60,6 +60,7 @@ Gitea supports the following scopes for tokens: |     **write:public_key** | Grant read/write access to public keys | |     **read:public_key** | Grant read-only access to public keys | | **admin:org_hook** | Grants full access to organizational-level hooks | +| **admin:user_hook** | Grants full access to user-level hooks | | **notification** | Grants full access to notifications | | **user** | Grants full access to user profile info | |     **read:user** | Grants read access to user's profile | diff --git a/models/auth/token_scope.go b/models/auth/token_scope.go index 38733a1c8f305..06c89fecc2e4d 100644 --- a/models/auth/token_scope.go +++ b/models/auth/token_scope.go @@ -32,6 +32,8 @@ const ( AccessTokenScopeAdminOrgHook AccessTokenScope = "admin:org_hook" + AccessTokenScopeAdminUserHook AccessTokenScope = "admin:user_hook" + AccessTokenScopeNotification AccessTokenScope = "notification" AccessTokenScopeUser AccessTokenScope = "user" @@ -64,7 +66,7 @@ type AccessTokenScopeBitmap uint64 const ( // AccessTokenScopeAllBits is the bitmap of all access token scopes, except `sudo`. AccessTokenScopeAllBits AccessTokenScopeBitmap = AccessTokenScopeRepoBits | - AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | + AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | AccessTokenScopeAdminUserHookBits | AccessTokenScopeNotificationBits | AccessTokenScopeUserBits | AccessTokenScopeDeleteRepoBits | AccessTokenScopePackageBits | AccessTokenScopeAdminGPGKeyBits | AccessTokenScopeAdminApplicationBits @@ -86,6 +88,8 @@ const ( AccessTokenScopeAdminOrgHookBits AccessTokenScopeBitmap = 1 << iota + AccessTokenScopeAdminUserHookBits AccessTokenScopeBitmap = 1 << iota + AccessTokenScopeNotificationBits AccessTokenScopeBitmap = 1 << iota AccessTokenScopeUserBits AccessTokenScopeBitmap = 1< v245 NewMigration("Add NeedApproval to actions tables", v1_20.AddNeedApprovalToActionRun), + // v245 -> v246 + NewMigration("Rename Webhook org_id to owner_id", v1_20.RenameWebhookOrgToOwner), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_20/v245.go b/models/migrations/v1_20/v245.go new file mode 100644 index 0000000000000..466f21c239d09 --- /dev/null +++ b/models/migrations/v1_20/v245.go @@ -0,0 +1,74 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_20 //nolint + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm" +) + +func RenameWebhookOrgToOwner(x *xorm.Engine) error { + type Webhook struct { + OrgID int64 `xorm:"INDEX"` + } + + // This migration maybe rerun so that we should check if it has been run + ownerExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webhook", "owner_id") + if err != nil { + return err + } + + if ownerExist { + orgExist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "webhook", "org_id") + if err != nil { + return err + } + if !orgExist { + return nil + } + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + if err := sess.Sync2(new(Webhook)); err != nil { + return err + } + + if ownerExist { + if err := base.DropTableColumns(sess, "webhook", "owner_id"); err != nil { + return err + } + } + + switch { + case setting.Database.Type.IsMySQL(): + inferredTable, err := x.TableInfo(new(Webhook)) + if err != nil { + return err + } + sqlType := x.Dialect().SQLType(inferredTable.GetColumn("org_id")) + if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `webhook` CHANGE org_id owner_id %s", sqlType)); err != nil { + return err + } + case setting.Database.Type.IsMSSQL(): + if _, err := sess.Exec("sp_rename 'webhook.org_id', 'owner_id', 'COLUMN'"); err != nil { + return err + } + default: + if _, err := sess.Exec("ALTER TABLE `webhook` RENAME COLUMN org_id TO owner_id"); err != nil { + return err + } + } + + return sess.Commit() +} diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 64119f1494958..e3f6b593d977a 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -122,7 +122,7 @@ func IsValidHookContentType(name string) bool { type Webhook struct { ID int64 `xorm:"pk autoincr"` RepoID int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook - OrgID int64 `xorm:"INDEX"` + OwnerID int64 `xorm:"INDEX"` IsSystemWebhook bool URL string `xorm:"url TEXT"` HTTPMethod string `xorm:"http_method"` @@ -412,11 +412,11 @@ func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) { }) } -// GetWebhookByOrgID returns webhook of organization by given ID. -func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { +// GetWebhookByOwnerID returns webhook of a user or organization by given ID. +func GetWebhookByOwnerID(ownerID, id int64) (*Webhook, error) { return getWebhook(&Webhook{ - ID: id, - OrgID: orgID, + ID: id, + OwnerID: ownerID, }) } @@ -424,7 +424,7 @@ func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { type ListWebhookOptions struct { db.ListOptions RepoID int64 - OrgID int64 + OwnerID int64 IsActive util.OptionalBool } @@ -433,8 +433,8 @@ func (opts *ListWebhookOptions) toCond() builder.Cond { if opts.RepoID != 0 { cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID}) } - if opts.OrgID != 0 { - cond = cond.And(builder.Eq{"webhook.org_id": opts.OrgID}) + if opts.OwnerID != 0 { + cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) } if !opts.IsActive.IsNone() { cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.IsTrue()}) @@ -503,10 +503,10 @@ func DeleteWebhookByRepoID(repoID, id int64) error { }) } -// DeleteWebhookByOrgID deletes webhook of organization by given ID. -func DeleteWebhookByOrgID(orgID, id int64) error { +// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID. +func DeleteWebhookByOwnerID(ownerID, id int64) error { return deleteWebhook(&Webhook{ - ID: id, - OrgID: orgID, + ID: id, + OwnerID: ownerID, }) } diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go index 21dc0406a0d19..2e89f9547bba2 100644 --- a/models/webhook/webhook_system.go +++ b/models/webhook/webhook_system.go @@ -15,7 +15,7 @@ import ( func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) { webhooks := make([]*Webhook, 0, 5) return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, false). + Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, false). Find(&webhooks) } @@ -23,7 +23,7 @@ func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) { func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) { webhook := &Webhook{ID: id} has, err := db.GetEngine(ctx). - Where("repo_id=? AND org_id=?", 0, 0). + Where("repo_id=? AND owner_id=?", 0, 0). Get(webhook) if err != nil { return nil, err @@ -38,11 +38,11 @@ func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webh webhooks := make([]*Webhook, 0, 5) if isActive.IsNone() { return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND org_id=? AND is_system_webhook=?", 0, 0, true). + Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, true). Find(&webhooks) } return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND org_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()). + Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, true, isActive.IsTrue()). Find(&webhooks) } @@ -50,7 +50,7 @@ func GetSystemWebhooks(ctx context.Context, isActive util.OptionalBool) ([]*Webh func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error { return db.WithTx(ctx, func(ctx context.Context) error { count, err := db.GetEngine(ctx). - Where("repo_id=? AND org_id=?", 0, 0). + Where("repo_id=? AND owner_id=?", 0, 0). Delete(&Webhook{ID: id}) if err != nil { return err diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index c368fc620e2f2..74f7aeaa03028 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -109,13 +109,13 @@ func TestGetWebhookByRepoID(t *testing.T) { assert.True(t, IsErrWebhookNotExist(err)) } -func TestGetWebhookByOrgID(t *testing.T) { +func TestGetWebhookByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hook, err := GetWebhookByOrgID(3, 3) + hook, err := GetWebhookByOwnerID(3, 3) assert.NoError(t, err) assert.Equal(t, int64(3), hook.ID) - _, err = GetWebhookByOrgID(unittest.NonexistentID, unittest.NonexistentID) + _, err = GetWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID) assert.Error(t, err) assert.True(t, IsErrWebhookNotExist(err)) } @@ -140,9 +140,9 @@ func TestGetWebhooksByRepoID(t *testing.T) { } } -func TestGetActiveWebhooksByOrgID(t *testing.T) { +func TestGetActiveWebhooksByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OrgID: 3, IsActive: util.OptionalBoolTrue}) + hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OwnerID: 3, IsActive: util.OptionalBoolTrue}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(3), hooks[0].ID) @@ -150,9 +150,9 @@ func TestGetActiveWebhooksByOrgID(t *testing.T) { } } -func TestGetWebhooksByOrgID(t *testing.T) { +func TestGetWebhooksByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OrgID: 3}) + hooks, err := ListWebhooksByOpts(db.DefaultContext, &ListWebhookOptions{OwnerID: 3}) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(3), hooks[0].ID) @@ -181,13 +181,13 @@ func TestDeleteWebhookByRepoID(t *testing.T) { assert.True(t, IsErrWebhookNotExist(err)) } -func TestDeleteWebhookByOrgID(t *testing.T) { +func TestDeleteWebhookByOwnerID(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OrgID: 3}) - assert.NoError(t, DeleteWebhookByOrgID(3, 3)) - unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OrgID: 3}) + unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3}) + assert.NoError(t, DeleteWebhookByOwnerID(3, 3)) + unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3}) - err := DeleteWebhookByOrgID(unittest.NonexistentID, unittest.NonexistentID) + err := DeleteWebhookByOwnerID(unittest.NonexistentID, unittest.NonexistentID) assert.Error(t, err) assert.True(t, IsErrWebhookNotExist(err)) } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6f0d06a6e8c26..095257b365fb8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -821,6 +821,8 @@ remove_account_link = Remove Linked Account remove_account_link_desc = Removing a linked account will revoke its access to your Gitea account. Continue? remove_account_link_success = The linked account has been removed. +hooks.desc = Add webhooks which will be triggered for all repositories owned by this user. + orgs_none = You are not a member of any organizations. repos_none = You do not own any repositories diff --git a/routers/api/v1/admin/hooks.go b/routers/api/v1/admin/hooks.go index 2aed4139f3cb6..8264503c9d2f9 100644 --- a/routers/api/v1/admin/hooks.go +++ b/routers/api/v1/admin/hooks.go @@ -105,10 +105,7 @@ func CreateHook(ctx *context.APIContext) { // "$ref": "#/responses/Hook" form := web.GetForm(ctx).(*api.CreateHookOption) - // TODO in body params - if !utils.CheckCreateHookOption(ctx, form) { - return - } + utils.AddSystemHook(ctx, form) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1d2f8b18e0190..735939a5517c9 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -835,6 +835,13 @@ func Routes(ctx gocontext.Context) *web.Route { m.Get("/stopwatches", reqToken(auth_model.AccessTokenScopeRepo), repo.GetStopwatches) m.Get("/subscriptions", reqToken(auth_model.AccessTokenScopeRepo), user.GetMyWatchedRepos) m.Get("/teams", reqToken(auth_model.AccessTokenScopeRepo), org.ListUserTeams) + m.Group("/hooks", func() { + m.Combo("").Get(user.ListHooks). + Post(bind(api.CreateHookOption{}), user.CreateHook) + m.Combo("/{id}").Get(user.GetHook). + Patch(bind(api.EditHookOption{}), user.EditHook). + Delete(user.DeleteHook) + }, reqToken(auth_model.AccessTokenScopeAdminUserHook), reqWebhooksEnabled()) }, reqToken("")) // Repositories diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go index 4e435c9599a16..a6ea618a7d896 100644 --- a/routers/api/v1/org/hook.go +++ b/routers/api/v1/org/hook.go @@ -6,7 +6,6 @@ package org import ( "net/http" - webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/context" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" @@ -39,34 +38,10 @@ func ListHooks(ctx *context.APIContext) { // "200": // "$ref": "#/responses/HookList" - opts := &webhook_model.ListWebhookOptions{ - ListOptions: utils.GetListOptions(ctx), - OrgID: ctx.Org.Organization.ID, - } - - count, err := webhook_model.CountWebhooksByOpts(opts) - if err != nil { - ctx.InternalServerError(err) - return - } - - orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, opts) - if err != nil { - ctx.InternalServerError(err) - return - } - - hooks := make([]*api.Hook, len(orgHooks)) - for i, hook := range orgHooks { - hooks[i], err = webhook_service.ToHook(ctx.Org.Organization.AsUser().HomeLink(), hook) - if err != nil { - ctx.InternalServerError(err) - return - } - } - - ctx.SetTotalCountHeader(count) - ctx.JSON(http.StatusOK, hooks) + utils.ListOwnerHooks( + ctx, + ctx.ContextUser, + ) } // GetHook get an organization's hook by id @@ -92,14 +67,12 @@ func GetHook(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Hook" - org := ctx.Org.Organization - hookID := ctx.ParamsInt64(":id") - hook, err := utils.GetOrgHook(ctx, org.ID, hookID) + hook, err := utils.GetOwnerHook(ctx, ctx.ContextUser.ID, ctx.ParamsInt64("id")) if err != nil { return } - apiHook, err := webhook_service.ToHook(org.AsUser().HomeLink(), hook) + apiHook, err := webhook_service.ToHook(ctx.ContextUser.HomeLink(), hook) if err != nil { ctx.InternalServerError(err) return @@ -131,15 +104,14 @@ func CreateHook(ctx *context.APIContext) { // "201": // "$ref": "#/responses/Hook" - form := web.GetForm(ctx).(*api.CreateHookOption) - // TODO in body params - if !utils.CheckCreateHookOption(ctx, form) { - return - } - utils.AddOrgHook(ctx, form) + utils.AddOwnerHook( + ctx, + ctx.ContextUser, + web.GetForm(ctx).(*api.CreateHookOption), + ) } -// EditHook modify a hook of a repository +// EditHook modify a hook of an organization func EditHook(ctx *context.APIContext) { // swagger:operation PATCH /orgs/{org}/hooks/{id} organization orgEditHook // --- @@ -168,11 +140,12 @@ func EditHook(ctx *context.APIContext) { // "200": // "$ref": "#/responses/Hook" - form := web.GetForm(ctx).(*api.EditHookOption) - - // TODO in body params - hookID := ctx.ParamsInt64(":id") - utils.EditOrgHook(ctx, form, hookID) + utils.EditOwnerHook( + ctx, + ctx.ContextUser, + web.GetForm(ctx).(*api.EditHookOption), + ctx.ParamsInt64("id"), + ) } // DeleteHook delete a hook of an organization @@ -198,15 +171,9 @@ func DeleteHook(ctx *context.APIContext) { // "204": // "$ref": "#/responses/empty" - org := ctx.Org.Organization - hookID := ctx.ParamsInt64(":id") - if err := webhook_model.DeleteWebhookByOrgID(org.ID, hookID); err != nil { - if webhook_model.IsErrWebhookNotExist(err) { - ctx.NotFound() - } else { - ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOrgID", err) - } - return - } - ctx.Status(http.StatusNoContent) + utils.DeleteOwnerHook( + ctx, + ctx.ContextUser, + ctx.ParamsInt64("id"), + ) } diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index fd54d1f740ef7..39d83912b016e 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -223,12 +223,8 @@ func CreateHook(ctx *context.APIContext) { // responses: // "201": // "$ref": "#/responses/Hook" - form := web.GetForm(ctx).(*api.CreateHookOption) - if !utils.CheckCreateHookOption(ctx, form) { - return - } - utils.AddRepoHook(ctx, form) + utils.AddRepoHook(ctx, web.GetForm(ctx).(*api.CreateHookOption)) } // EditHook modify a hook of a repository diff --git a/routers/api/v1/user/hook.go b/routers/api/v1/user/hook.go new file mode 100644 index 0000000000000..50be519c815fa --- /dev/null +++ b/routers/api/v1/user/hook.go @@ -0,0 +1,154 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + "net/http" + + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" + webhook_service "code.gitea.io/gitea/services/webhook" +) + +// ListHooks list the authenticated user's webhooks +func ListHooks(ctx *context.APIContext) { + // swagger:operation GET /user/hooks user userListHooks + // --- + // summary: List the authenticated user's webhooks + // produces: + // - application/json + // parameters: + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/HookList" + + utils.ListOwnerHooks( + ctx, + ctx.Doer, + ) +} + +// GetHook get the authenticated user's hook by id +func GetHook(ctx *context.APIContext) { + // swagger:operation GET /user/hooks/{id} user userGetHook + // --- + // summary: Get a hook + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to get + // type: integer + // format: int64 + // required: true + // responses: + // "200": + // "$ref": "#/responses/Hook" + + hook, err := utils.GetOwnerHook(ctx, ctx.Doer.ID, ctx.ParamsInt64("id")) + if err != nil { + return + } + + apiHook, err := webhook_service.ToHook(ctx.Doer.HomeLink(), hook) + if err != nil { + ctx.InternalServerError(err) + return + } + ctx.JSON(http.StatusOK, apiHook) +} + +// CreateHook create a hook for the authenticated user +func CreateHook(ctx *context.APIContext) { + // swagger:operation POST /user/hooks user userCreateHook + // --- + // summary: Create a hook + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: body + // in: body + // required: true + // schema: + // "$ref": "#/definitions/CreateHookOption" + // responses: + // "201": + // "$ref": "#/responses/Hook" + + utils.AddOwnerHook( + ctx, + ctx.Doer, + web.GetForm(ctx).(*api.CreateHookOption), + ) +} + +// EditHook modify a hook of the authenticated user +func EditHook(ctx *context.APIContext) { + // swagger:operation PATCH /user/hooks/{id} user userEditHook + // --- + // summary: Update a hook + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to update + // type: integer + // format: int64 + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/EditHookOption" + // responses: + // "200": + // "$ref": "#/responses/Hook" + + utils.EditOwnerHook( + ctx, + ctx.Doer, + web.GetForm(ctx).(*api.EditHookOption), + ctx.ParamsInt64("id"), + ) +} + +// DeleteHook delete a hook of the authenticated user +func DeleteHook(ctx *context.APIContext) { + // swagger:operation DELETE /user/hooks/{id} user userDeleteHook + // --- + // summary: Delete a hook + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the hook to delete + // type: integer + // format: int64 + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + + utils.DeleteOwnerHook( + ctx, + ctx.Doer, + ctx.ParamsInt64("id"), + ) +} diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index f6aaf74aff123..44625cc9b81b8 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" @@ -18,15 +19,46 @@ import ( webhook_service "code.gitea.io/gitea/services/webhook" ) -// GetOrgHook get an organization's webhook. If there is an error, write to -// `ctx` accordingly and return the error -func GetOrgHook(ctx *context.APIContext, orgID, hookID int64) (*webhook.Webhook, error) { - w, err := webhook.GetWebhookByOrgID(orgID, hookID) +// ListOwnerHooks lists the webhooks of the provided owner +func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) { + opts := &webhook.ListWebhookOptions{ + ListOptions: GetListOptions(ctx), + OwnerID: owner.ID, + } + + count, err := webhook.CountWebhooksByOpts(opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + hooks, err := webhook.ListWebhooksByOpts(ctx, opts) + if err != nil { + ctx.InternalServerError(err) + return + } + + apiHooks := make([]*api.Hook, len(hooks)) + for i, hook := range hooks { + apiHooks[i], err = webhook_service.ToHook(owner.HomeLink(), hook) + if err != nil { + ctx.InternalServerError(err) + return + } + } + + ctx.SetTotalCountHeader(count) + ctx.JSON(http.StatusOK, apiHooks) +} + +// GetOwnerHook gets an user or organization webhook. Errors are written to ctx. +func GetOwnerHook(ctx *context.APIContext, ownerID, hookID int64) (*webhook.Webhook, error) { + w, err := webhook.GetWebhookByOwnerID(ownerID, hookID) if err != nil { if webhook.IsErrWebhookNotExist(err) { ctx.NotFound() } else { - ctx.Error(http.StatusInternalServerError, "GetWebhookByOrgID", err) + ctx.Error(http.StatusInternalServerError, "GetWebhookByOwnerID", err) } return nil, err } @@ -48,9 +80,9 @@ func GetRepoHook(ctx *context.APIContext, repoID, hookID int64) (*webhook.Webhoo return w, nil } -// CheckCreateHookOption check if a CreateHookOption form is valid. If invalid, +// checkCreateHookOption check if a CreateHookOption form is valid. If invalid, // write the appropriate error to `ctx`. Return whether the form is valid -func CheckCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool { +func checkCreateHookOption(ctx *context.APIContext, form *api.CreateHookOption) bool { if !webhook_service.IsValidHookTaskType(form.Type) { ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Invalid hook type: %s", form.Type)) return false @@ -81,14 +113,13 @@ func AddSystemHook(ctx *context.APIContext, form *api.CreateHookOption) { } } -// AddOrgHook add a hook to an organization. Writes to `ctx` accordingly -func AddOrgHook(ctx *context.APIContext, form *api.CreateHookOption) { - org := ctx.Org.Organization - hook, ok := addHook(ctx, form, org.ID, 0) +// AddOwnerHook adds a hook to an user or organization +func AddOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.CreateHookOption) { + hook, ok := addHook(ctx, form, owner.ID, 0) if !ok { return } - apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), hook) + apiHook, ok := toAPIHook(ctx, owner.HomeLink(), hook) if !ok { return } @@ -128,14 +159,18 @@ func pullHook(events []string, event string) bool { return util.SliceContainsString(events, event, true) || util.SliceContainsString(events, string(webhook_module.HookEventPullRequest), true) } -// addHook add the hook specified by `form`, `orgID` and `repoID`. If there is +// addHook add the hook specified by `form`, `ownerID` and `repoID`. If there is // an error, write to `ctx` accordingly. Return (webhook, ok) -func addHook(ctx *context.APIContext, form *api.CreateHookOption, orgID, repoID int64) (*webhook.Webhook, bool) { +func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoID int64) (*webhook.Webhook, bool) { + if !checkCreateHookOption(ctx, form) { + return nil, false + } + if len(form.Events) == 0 { form.Events = []string{"push"} } w := &webhook.Webhook{ - OrgID: orgID, + OwnerID: ownerID, RepoID: repoID, URL: form.Config["url"], ContentType: webhook.ToHookContentType(form.Config["content_type"]), @@ -234,21 +269,20 @@ func EditSystemHook(ctx *context.APIContext, form *api.EditHookOption, hookID in ctx.JSON(http.StatusOK, h) } -// EditOrgHook edit webhook `w` according to `form`. Writes to `ctx` accordingly -func EditOrgHook(ctx *context.APIContext, form *api.EditHookOption, hookID int64) { - org := ctx.Org.Organization - hook, err := GetOrgHook(ctx, org.ID, hookID) +// EditOwnerHook updates a webhook of an user or organization +func EditOwnerHook(ctx *context.APIContext, owner *user_model.User, form *api.EditHookOption, hookID int64) { + hook, err := GetOwnerHook(ctx, owner.ID, hookID) if err != nil { return } if !editHook(ctx, form, hook) { return } - updated, err := GetOrgHook(ctx, org.ID, hookID) + updated, err := GetOwnerHook(ctx, owner.ID, hookID) if err != nil { return } - apiHook, ok := toAPIHook(ctx, org.AsUser().HomeLink(), updated) + apiHook, ok := toAPIHook(ctx, owner.HomeLink(), updated) if !ok { return } @@ -362,3 +396,16 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh } return true } + +// DeleteOwnerHook deletes the hook owned by the owner. +func DeleteOwnerHook(ctx *context.APIContext, owner *user_model.User, hookID int64) { + if err := webhook.DeleteWebhookByOwnerID(owner.ID, hookID); err != nil { + if webhook.IsErrWebhookNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "DeleteWebhookByOwnerID", err) + } + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index f713d096639bc..b57ebfbcda23a 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -218,9 +218,9 @@ func Webhooks(ctx *context.Context) { ctx.Data["BaseLinkNew"] = ctx.Org.OrgLink + "/settings/hooks" ctx.Data["Description"] = ctx.Tr("org.settings.hooks_desc") - ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OrgID: ctx.Org.Organization.ID}) + ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OwnerID: ctx.Org.Organization.ID}) if err != nil { - ctx.ServerError("GetWebhooksByOrgId", err) + ctx.ServerError("ListWebhooksByOpts", err) return } @@ -230,8 +230,8 @@ func Webhooks(ctx *context.Context) { // DeleteWebhook response for delete webhook func DeleteWebhook(ctx *context.Context) { - if err := webhook.DeleteWebhookByOrgID(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { - ctx.Flash.Error("DeleteWebhookByOrgID: " + err.Error()) + if err := webhook.DeleteWebhookByOwnerID(ctx.Org.Organization.ID, ctx.FormInt64("id")); err != nil { + ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) } diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index d27d0f1bf01d8..f30588967e836 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -33,6 +33,7 @@ const ( tplHooks base.TplName = "repo/settings/webhook/base" tplHookNew base.TplName = "repo/settings/webhook/new" tplOrgHookNew base.TplName = "org/settings/hook_new" + tplUserHookNew base.TplName = "user/settings/hook_new" tplAdminHookNew base.TplName = "admin/hook_new" ) @@ -54,8 +55,8 @@ func Webhooks(ctx *context.Context) { ctx.HTML(http.StatusOK, tplHooks) } -type orgRepoCtx struct { - OrgID int64 +type ownerRepoCtx struct { + OwnerID int64 RepoID int64 IsAdmin bool IsSystemWebhook bool @@ -64,10 +65,10 @@ type orgRepoCtx struct { NewTemplate base.TplName } -// getOrgRepoCtx determines whether this is a repo, organization, or admin (both default and system) context. -func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { - if len(ctx.Repo.RepoLink) > 0 { - return &orgRepoCtx{ +// getOwnerRepoCtx determines whether this is a repo, owner, or admin (both default and system) context. +func getOwnerRepoCtx(ctx *context.Context) (*ownerRepoCtx, error) { + if is, ok := ctx.Data["IsRepositoryWebhook"]; ok && is.(bool) { + return &ownerRepoCtx{ RepoID: ctx.Repo.Repository.ID, Link: path.Join(ctx.Repo.RepoLink, "settings/hooks"), LinkNew: path.Join(ctx.Repo.RepoLink, "settings/hooks"), @@ -75,37 +76,35 @@ func getOrgRepoCtx(ctx *context.Context) (*orgRepoCtx, error) { }, nil } - if len(ctx.Org.OrgLink) > 0 { - return &orgRepoCtx{ - OrgID: ctx.Org.Organization.ID, + if is, ok := ctx.Data["IsOrganizationWebhook"]; ok && is.(bool) { + return &ownerRepoCtx{ + OwnerID: ctx.ContextUser.ID, Link: path.Join(ctx.Org.OrgLink, "settings/hooks"), LinkNew: path.Join(ctx.Org.OrgLink, "settings/hooks"), NewTemplate: tplOrgHookNew, }, nil } - if ctx.Doer.IsAdmin { - // Are we looking at default webhooks? - if ctx.Params(":configType") == "default-hooks" { - return &orgRepoCtx{ - IsAdmin: true, - Link: path.Join(setting.AppSubURL, "/admin/hooks"), - LinkNew: path.Join(setting.AppSubURL, "/admin/default-hooks"), - NewTemplate: tplAdminHookNew, - }, nil - } + if is, ok := ctx.Data["IsUserWebhook"]; ok && is.(bool) { + return &ownerRepoCtx{ + OwnerID: ctx.Doer.ID, + Link: path.Join(setting.AppSubURL, "/user/settings/hooks"), + LinkNew: path.Join(setting.AppSubURL, "/user/settings/hooks"), + NewTemplate: tplUserHookNew, + }, nil + } - // Must be system webhooks instead - return &orgRepoCtx{ + if ctx.Doer.IsAdmin { + return &ownerRepoCtx{ IsAdmin: true, - IsSystemWebhook: true, + IsSystemWebhook: ctx.Params(":configType") == "system-hooks", Link: path.Join(setting.AppSubURL, "/admin/hooks"), LinkNew: path.Join(setting.AppSubURL, "/admin/system-hooks"), NewTemplate: tplAdminHookNew, }, nil } - return nil, errors.New("unable to set OrgRepo context") + return nil, errors.New("unable to set OwnerRepo context") } func checkHookType(ctx *context.Context) string { @@ -122,9 +121,9 @@ func WebhooksNew(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}} - orCtx, err := getOrgRepoCtx(ctx) + orCtx, err := getOwnerRepoCtx(ctx) if err != nil { - ctx.ServerError("getOrgRepoCtx", err) + ctx.ServerError("getOwnerRepoCtx", err) return } @@ -205,9 +204,9 @@ func createWebhook(ctx *context.Context, params webhookParams) { ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}} ctx.Data["HookType"] = params.Type - orCtx, err := getOrgRepoCtx(ctx) + orCtx, err := getOwnerRepoCtx(ctx) if err != nil { - ctx.ServerError("getOrgRepoCtx", err) + ctx.ServerError("getOwnerRepoCtx", err) return } ctx.Data["BaseLink"] = orCtx.LinkNew @@ -236,7 +235,7 @@ func createWebhook(ctx *context.Context, params webhookParams) { IsActive: params.WebhookForm.Active, Type: params.Type, Meta: string(meta), - OrgID: orCtx.OrgID, + OwnerID: orCtx.OwnerID, IsSystemWebhook: orCtx.IsSystemWebhook, } err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader) @@ -577,19 +576,19 @@ func packagistHookParams(ctx *context.Context) webhookParams { } } -func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) { - orCtx, err := getOrgRepoCtx(ctx) +func checkWebhook(ctx *context.Context) (*ownerRepoCtx, *webhook.Webhook) { + orCtx, err := getOwnerRepoCtx(ctx) if err != nil { - ctx.ServerError("getOrgRepoCtx", err) + ctx.ServerError("getOwnerRepoCtx", err) return nil, nil } ctx.Data["BaseLink"] = orCtx.Link var w *webhook.Webhook if orCtx.RepoID > 0 { - w, err = webhook.GetWebhookByRepoID(ctx.Repo.Repository.ID, ctx.ParamsInt64(":id")) - } else if orCtx.OrgID > 0 { - w, err = webhook.GetWebhookByOrgID(ctx.Org.Organization.ID, ctx.ParamsInt64(":id")) + w, err = webhook.GetWebhookByRepoID(orCtx.RepoID, ctx.ParamsInt64(":id")) + } else if orCtx.OwnerID > 0 { + w, err = webhook.GetWebhookByOwnerID(orCtx.OwnerID, ctx.ParamsInt64(":id")) } else if orCtx.IsAdmin { w, err = webhook.GetSystemOrDefaultWebhook(ctx, ctx.ParamsInt64(":id")) } diff --git a/routers/web/user/setting/webhooks.go b/routers/web/user/setting/webhooks.go new file mode 100644 index 0000000000000..9b0b0c9611c1a --- /dev/null +++ b/routers/web/user/setting/webhooks.go @@ -0,0 +1,48 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "net/http" + + "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsHooks base.TplName = "user/settings/hooks" +) + +// Webhooks render webhook list page +func Webhooks(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsHooks"] = true + ctx.Data["BaseLink"] = setting.AppSubURL + "/user/settings/hooks" + ctx.Data["BaseLinkNew"] = setting.AppSubURL + "/user/settings/hooks" + ctx.Data["Description"] = ctx.Tr("settings.hooks.desc") + + ws, err := webhook.ListWebhooksByOpts(ctx, &webhook.ListWebhookOptions{OwnerID: ctx.Doer.ID}) + if err != nil { + ctx.ServerError("ListWebhooksByOpts", err) + return + } + + ctx.Data["Webhooks"] = ws + ctx.HTML(http.StatusOK, tplSettingsHooks) +} + +// DeleteWebhook response for delete webhook +func DeleteWebhook(ctx *context.Context) { + if err := webhook.DeleteWebhookByOwnerID(ctx.Doer.ID, ctx.FormInt64("id")); err != nil { + ctx.Flash.Error("DeleteWebhookByOwnerID: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.settings.webhook_deletion_success")) + } + + ctx.JSON(http.StatusOK, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/hooks", + }) +} diff --git a/routers/web/web.go b/routers/web/web.go index ff312992dda0f..e4179d5802955 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -315,6 +315,35 @@ func RegisterRoutes(m *web.Route) { } } + addWebhookAddRoutes := func() { + m.Get("/{type}/new", repo.WebhooksNew) + m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) + m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) + m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) + m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) + m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) + m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) + m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) + m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) + m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) + m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) + m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + } + + addWebhookEditRoutes := func() { + m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) + m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) + m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) + m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) + m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) + m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) + m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) + m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) + m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) + m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) + m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + } + // FIXME: not all routes need go through same middleware. // Especially some AJAX requests, we can reduce middleware number to improve performance. // Routers. @@ -482,6 +511,19 @@ func RegisterRoutes(m *web.Route) { m.Get("/organization", user_setting.Organization) m.Get("/repos", user_setting.Repos) m.Post("/repos/unadopted", user_setting.AdoptOrDeleteRepository) + + m.Group("/hooks", func() { + m.Get("", user_setting.Webhooks) + m.Post("/delete", user_setting.DeleteWebhook) + addWebhookAddRoutes() + m.Group("/{id}", func() { + m.Get("", repo.WebHooksEdit) + m.Post("/replay/{uuid}", repo.ReplayWebhook) + }) + addWebhookEditRoutes() + }, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsUserWebhook"] = true + }) }, reqSignIn, func(ctx *context.Context) { ctx.Data["PageIsUserSettings"] = true ctx.Data["AllThemes"] = setting.UI.Themes @@ -575,32 +617,11 @@ func RegisterRoutes(m *web.Route) { m.Get("", repo.WebHooksEdit) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) - m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) - m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) - m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) - m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) - m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) - m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) - m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) - m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) - m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) - m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) + addWebhookEditRoutes() }, webhooksEnabled) m.Group("/{configType:default-hooks|system-hooks}", func() { - m.Get("/{type}/new", repo.WebhooksNew) - m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) - m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) - m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) - m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) - m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) - m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) - m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) - m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) - m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + addWebhookAddRoutes() }) m.Group("/auths", func() { @@ -759,32 +780,15 @@ func RegisterRoutes(m *web.Route) { m.Group("/hooks", func() { m.Get("", org.Webhooks) m.Post("/delete", org.DeleteWebhook) - m.Get("/{type}/new", repo.WebhooksNew) - m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) - m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) - m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) - m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) - m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) - m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) - m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) - m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) - m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) + addWebhookAddRoutes() m.Group("/{id}", func() { m.Get("", repo.WebHooksEdit) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) - m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) - m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) - m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) - m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) - m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) - m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) - m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) - m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) - m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) - }, webhooksEnabled) + addWebhookEditRoutes() + }, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsOrganizationWebhook"] = true + }) m.Group("/labels", func() { m.Get("", org.RetrieveLabels, org.Labels) @@ -962,35 +966,16 @@ func RegisterRoutes(m *web.Route) { m.Group("/hooks", func() { m.Get("", repo.Webhooks) m.Post("/delete", repo.DeleteWebhook) - m.Get("/{type}/new", repo.WebhooksNew) - m.Post("/gitea/new", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksNewPost) - m.Post("/gogs/new", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksNewPost) - m.Post("/slack/new", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksNewPost) - m.Post("/discord/new", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksNewPost) - m.Post("/dingtalk/new", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksNewPost) - m.Post("/telegram/new", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) - m.Post("/matrix/new", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) - m.Post("/msteams/new", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) - m.Post("/feishu/new", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) - m.Post("/wechatwork/new", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) - m.Post("/packagist/new", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) + addWebhookAddRoutes() m.Group("/{id}", func() { m.Get("", repo.WebHooksEdit) m.Post("/test", repo.TestWebhook) m.Post("/replay/{uuid}", repo.ReplayWebhook) }) - m.Post("/gitea/{id}", web.Bind(forms.NewWebhookForm{}), repo.GiteaHooksEditPost) - m.Post("/gogs/{id}", web.Bind(forms.NewGogshookForm{}), repo.GogsHooksEditPost) - m.Post("/slack/{id}", web.Bind(forms.NewSlackHookForm{}), repo.SlackHooksEditPost) - m.Post("/discord/{id}", web.Bind(forms.NewDiscordHookForm{}), repo.DiscordHooksEditPost) - m.Post("/dingtalk/{id}", web.Bind(forms.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) - m.Post("/telegram/{id}", web.Bind(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) - m.Post("/matrix/{id}", web.Bind(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) - m.Post("/msteams/{id}", web.Bind(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) - m.Post("/feishu/{id}", web.Bind(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) - m.Post("/wechatwork/{id}", web.Bind(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) - m.Post("/packagist/{id}", web.Bind(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) - }, webhooksEnabled) + addWebhookEditRoutes() + }, webhooksEnabled, func(ctx *context.Context) { + ctx.Data["IsRepositoryWebhook"] = true + }) m.Group("/keys", func() { m.Combo("").Get(repo.DeployKeys). diff --git a/services/repository/hooks.go b/services/repository/hooks.go index a8b6f7a622280..8506fa341369f 100644 --- a/services/repository/hooks.go +++ b/services/repository/hooks.go @@ -101,7 +101,7 @@ func GenerateWebhooks(ctx context.Context, templateRepo, generateRepo *repo_mode HookEvent: templateWebhook.HookEvent, IsActive: templateWebhook.IsActive, Type: templateWebhook.Type, - OrgID: templateWebhook.OrgID, + OwnerID: templateWebhook.OwnerID, Events: templateWebhook.Events, Meta: templateWebhook.Meta, }) diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index afd8e3c105cb1..b862d5bff10e4 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -229,16 +229,16 @@ func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_modu owner = source.Repository.MustOwner(ctx) } - // check if owner is an org and append additional webhooks - if owner != nil && owner.IsOrganization() { - orgHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ - OrgID: owner.ID, + // append additional webhooks of a user or organization + if owner != nil { + ownerHooks, err := webhook_model.ListWebhooksByOpts(ctx, &webhook_model.ListWebhookOptions{ + OwnerID: owner.ID, IsActive: util.OptionalBoolTrue, }) if err != nil { return fmt.Errorf("ListWebhooksByOpts: %w", err) } - ws = append(ws, orgHooks...) + ws = append(ws, ownerHooks...) } // Add any admin-defined system webhooks diff --git a/templates/repo/settings/webhook/history.tmpl b/templates/repo/settings/webhook/history.tmpl index bf7fe05de2242..f76cdb147d7cc 100644 --- a/templates/repo/settings/webhook/history.tmpl +++ b/templates/repo/settings/webhook/history.tmpl @@ -40,7 +40,7 @@ N/A {{end}} - {{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin}} + {{if or $.Permission.IsAdmin $.IsOrganizationOwner $.PageIsAdmin $.PageIsUserSettings}}
+
+
+ + +
+
diff --git a/templates/user/settings/hook_new.tmpl b/templates/user/settings/hook_new.tmpl new file mode 100644 index 0000000000000..20aaf65f62d78 --- /dev/null +++ b/templates/user/settings/hook_new.tmpl @@ -0,0 +1,53 @@ +{{template "base/head" .}} +
+ {{template "user/settings/navbar" .}} +
+
+ {{template "base/alert" .}} +

+ {{if .PageIsSettingsHooksNew}}{{.locale.Tr "repo.settings.add_webhook"}}{{else}}{{.locale.Tr "repo.settings.update_webhook"}}{{end}} +
+ {{if eq .HookType "gitea"}} + + {{else if eq .HookType "gogs"}} + + {{else if eq .HookType "slack"}} + + {{else if eq .HookType "discord"}} + + {{else if eq .HookType "dingtalk"}} + + {{else if eq .HookType "telegram"}} + + {{else if eq .HookType "msteams"}} + + {{else if eq .HookType "feishu"}} + + {{else if eq .HookType "matrix"}} + + {{else if eq .HookType "wechatwork"}} + + {{else if eq .HookType "packagist"}} + + {{end}} +
+

+
+ {{template "repo/settings/webhook/gitea" .}} + {{template "repo/settings/webhook/gogs" .}} + {{template "repo/settings/webhook/slack" .}} + {{template "repo/settings/webhook/discord" .}} + {{template "repo/settings/webhook/dingtalk" .}} + {{template "repo/settings/webhook/telegram" .}} + {{template "repo/settings/webhook/msteams" .}} + {{template "repo/settings/webhook/feishu" .}} + {{template "repo/settings/webhook/matrix" .}} + {{template "repo/settings/webhook/wechatwork" .}} + {{template "repo/settings/webhook/packagist" .}} +
+ + {{template "repo/settings/webhook/history" .}} +
+
+
+{{template "base/footer" .}} diff --git a/templates/user/settings/hooks.tmpl b/templates/user/settings/hooks.tmpl new file mode 100644 index 0000000000000..02bfa8a4e6704 --- /dev/null +++ b/templates/user/settings/hooks.tmpl @@ -0,0 +1,8 @@ +{{template "base/head" .}} +
+ {{template "user/settings/navbar" .}} +
+ {{template "repo/settings/webhook/list" .}} +
+
+{{template "base/footer" .}} diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl index 8deffde0b2242..4afe2173c2bda 100644 --- a/templates/user/settings/navbar.tmpl +++ b/templates/user/settings/navbar.tmpl @@ -26,6 +26,11 @@ {{.locale.Tr "packages.title"}} {{end}} + {{if not DisableWebhooks}} + + {{.locale.Tr "repo.settings.hooks"}} + + {{end}} {{.locale.Tr "settings.organization"}} From cf29ee6dd290525635a0e1b823506e81f845b978 Mon Sep 17 00:00:00 2001 From: yp05327 <576951401@qq.com> Date: Sat, 11 Mar 2023 00:18:20 +0900 Subject: [PATCH 04/15] Add missing tabs to org projects page (#22705) Fixes https://github.com/go-gitea/gitea/issues/22676 Context Data `IsOrganizationMember` and `IsOrganizationOwner` is used to control the visibility of `people` and `team` tab. https://github.com/go-gitea/gitea/blob/2871ea08096cba15546f357d0ec473734ee9d8be/templates/org/menu.tmpl#L19-L40 And because of the reuse of user projects page, User Context is changed to Organization Context. But the value of `IsOrganizationMember` and `IsOrganizationOwner` are not being given. I reused func `HandleOrgAssignment` to add them to the ctx, but may have some unnecessary variables, idk whether it is ok. I found there is a missing `PageIsViewProjects` at create project page. --- models/organization/org.go | 26 ++++++++++ models/user/user.go | 5 ++ modules/context/org.go | 78 ++++++++++++++++------------- routers/web/org/home.go | 1 + routers/web/org/projects.go | 1 + routers/web/shared/user/header.go | 2 + routers/web/user/code.go | 1 + routers/web/user/profile.go | 1 + routers/web/web.go | 40 +++++++++------ services/context/user.go | 9 ---- templates/org/menu.tmpl | 8 +-- templates/user/overview/header.tmpl | 6 ++- 12 files changed, 115 insertions(+), 63 deletions(-) diff --git a/models/organization/org.go b/models/organization/org.go index f05027be729d4..269b3e83288dc 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -239,6 +239,32 @@ func (org *Organization) CustomAvatarRelativePath() string { return org.Avatar } +// UnitPermission returns unit permission +func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.User, unitType unit.Type) perm.AccessMode { + if doer != nil { + teams, err := GetUserOrgTeams(ctx, org.ID, doer.ID) + if err != nil { + log.Error("GetUserOrgTeams: %v", err) + return perm.AccessModeNone + } + + if err := teams.LoadUnits(ctx); err != nil { + log.Error("LoadUnits: %v", err) + return perm.AccessModeNone + } + + if len(teams) > 0 { + return teams.UnitMaxAccess(unitType) + } + } + + if org.Visibility.IsPublic() { + return perm.AccessModeRead + } + + return perm.AccessModeNone +} + // CreateOrganization creates record of a new organization. func CreateOrganization(org *Organization, owner *user_model.User) (err error) { if !owner.CanCreateOrganization() { diff --git a/models/user/user.go b/models/user/user.go index f6fafe64f3915..454779b9ea361 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -393,6 +393,11 @@ func (u *User) IsOrganization() bool { return u.Type == UserTypeOrganization } +// IsIndividual returns true if user is actually a individual user. +func (u *User) IsIndividual() bool { + return u.Type == UserTypeIndividual +} + // DisplayName returns full name if it's not empty, // returns username otherwise. func (u *User) DisplayName() string { diff --git a/modules/context/org.go b/modules/context/org.go index 0add7f2c0c3de..39a3038f910cb 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" ) @@ -31,29 +30,34 @@ type Organization struct { } func (org *Organization) CanWriteUnit(ctx *Context, unitType unit.Type) bool { - if ctx.Doer == nil { - return false - } - return org.UnitPermission(ctx, ctx.Doer.ID, unitType) >= perm.AccessModeWrite + return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeWrite } -func (org *Organization) UnitPermission(ctx *Context, doerID int64, unitType unit.Type) perm.AccessMode { - if doerID > 0 { - teams, err := organization.GetUserOrgTeams(ctx, org.Organization.ID, doerID) - if err != nil { - log.Error("GetUserOrgTeams: %v", err) - return perm.AccessModeNone - } - if len(teams) > 0 { - return teams.UnitMaxAccess(unitType) - } - } +func (org *Organization) CanReadUnit(ctx *Context, unitType unit.Type) bool { + return org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead +} - if org.Organization.Visibility == structs.VisibleTypePublic { - return perm.AccessModeRead - } +func GetOrganizationByParams(ctx *Context) { + orgName := ctx.Params(":org") + + var err error - return perm.AccessModeNone + ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName) + if err != nil { + if organization.IsErrOrgNotExist(err) { + redirectUserID, err := user_model.LookupUserRedirect(orgName) + if err == nil { + RedirectToUser(ctx, orgName, redirectUserID) + } else if user_model.IsErrUserRedirectNotExist(err) { + ctx.NotFound("GetUserByName", err) + } else { + ctx.ServerError("LookupUserRedirect", err) + } + } else { + ctx.ServerError("GetUserByName", err) + } + return + } } // HandleOrgAssignment handles organization assignment @@ -77,25 +81,26 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { requireTeamAdmin = args[3] } - orgName := ctx.Params(":org") - var err error - ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName) - if err != nil { - if organization.IsErrOrgNotExist(err) { - redirectUserID, err := user_model.LookupUserRedirect(orgName) - if err == nil { - RedirectToUser(ctx, orgName, redirectUserID) - } else if user_model.IsErrUserRedirectNotExist(err) { - ctx.NotFound("GetUserByName", err) - } else { - ctx.ServerError("LookupUserRedirect", err) + + if ctx.ContextUser == nil { + // if Organization is not defined, get it from params + if ctx.Org.Organization == nil { + GetOrganizationByParams(ctx) + if ctx.Written() { + return } - } else { - ctx.ServerError("GetUserByName", err) } + } else if ctx.ContextUser.IsOrganization() { + if ctx.Org == nil { + ctx.Org = &Organization{} + } + ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) + } else { + // ContextUser is an individual User return } + org := ctx.Org.Organization // Handle Visibility @@ -156,6 +161,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { } ctx.Data["IsOrganizationOwner"] = ctx.Org.IsOwner ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember + ctx.Data["IsProjectEnabled"] = true ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["IsPublicMember"] = func(uid int64) bool { @@ -231,6 +237,10 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { return } } + + ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) + ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) + ctx.Data["CanReadCode"] = ctx.Org.CanReadUnit(ctx, unit.TypeCode) } // OrgAssignment returns a middleware to handle organization assignment diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 4cc364acd3a01..8c9cc8a9d86be 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -156,6 +156,7 @@ func Home(ctx *context.Context) { pager.SetDefaultParams(ctx) pager.AddParam(ctx, "language", "Language") ctx.Data["Page"] = pager + ctx.Data["ContextUser"] = ctx.ContextUser ctx.HTML(http.StatusOK, tplOrgHome) } diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 64ae4aa70952d..c9d63fec5df0c 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -123,6 +123,7 @@ func NewProject(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.projects.new") ctx.Data["BoardTypes"] = project_model.GetBoardConfig() ctx.Data["CanWriteProjects"] = canWriteProjects(ctx) + ctx.Data["PageIsViewProjects"] = true ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink() shared_user.RenderUserHeader(ctx) ctx.HTML(http.StatusOK, tplProjectsNew) diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 94e59e2a490fd..05e45f999eed9 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -9,6 +9,8 @@ import ( ) func RenderUserHeader(ctx *context.Context) { + ctx.Data["IsProjectEnabled"] = true + ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["ContextUser"] = ctx.ContextUser } diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 81e3e65b4b67a..b3adbcb8d3a8f 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -24,6 +24,7 @@ func CodeSearch(ctx *context.Context) { return } + ctx.Data["IsProjectEnabled"] = true ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index b4452604599f7..f4d458c040d72 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -304,6 +304,7 @@ func Profile(ctx *context.Context) { pager.AddParam(ctx, "date", "Date") } ctx.Data["Page"] = pager + ctx.Data["IsProjectEnabled"] = true ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/web.go b/routers/web/web.go index e4179d5802955..292268dc8055a 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -711,6 +711,21 @@ func RegisterRoutes(m *web.Route) { } } + reqUnitAccess := func(unitType unit.Type, accessMode perm.AccessMode) func(ctx *context.Context) { + return func(ctx *context.Context) { + if ctx.ContextUser == nil { + ctx.NotFound(unitType.String(), nil) + return + } + if ctx.ContextUser.IsOrganization() { + if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) < accessMode { + ctx.NotFound(unitType.String(), nil) + return + } + } + } + } + // ***** START: Organization ***** m.Group("/org", func() { m.Group("/{org}", func() { @@ -873,8 +888,10 @@ func RegisterRoutes(m *web.Route) { } m.Group("/projects", func() { - m.Get("", org.Projects) - m.Get("/{id}", org.ViewProject) + m.Group("", func() { + m.Get("", org.Projects) + m.Get("/{id}", org.ViewProject) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead)) m.Group("", func() { //nolint:dupl m.Get("/new", org.NewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) @@ -894,25 +911,18 @@ func RegisterRoutes(m *web.Route) { m.Post("/move", org.MoveIssues) }) }) - }, reqSignIn, func(ctx *context.Context) { - if ctx.ContextUser == nil { - ctx.NotFound("NewProject", nil) - return - } - if ctx.ContextUser.IsOrganization() { - if !ctx.Org.CanWriteUnit(ctx, unit.TypeProjects) { - ctx.NotFound("NewProject", nil) - return - } - } else if ctx.ContextUser.ID != ctx.Doer.ID { + }, reqSignIn, reqUnitAccess(unit.TypeProjects, perm.AccessModeWrite), func(ctx *context.Context) { + if ctx.ContextUser.IsIndividual() && ctx.ContextUser.ID != ctx.Doer.ID { ctx.NotFound("NewProject", nil) return } }) }, repo.MustEnableProjects) - m.Get("/code", user.CodeSearch) - }, context_service.UserAssignmentWeb()) + m.Group("", func() { + m.Get("/code", user.CodeSearch) + }, reqUnitAccess(unit.TypeCode, perm.AccessModeRead)) + }, context_service.UserAssignmentWeb(), context.OrgAssignment()) // ***** Release Attachment Download without Signin m.Get("/{username}/{reponame}/releases/download/{vTag}/{fileName}", ignSignIn, context.RepoAssignment, repo.MustBeNotEmpty, repo.RedirectDownload) diff --git a/services/context/user.go b/services/context/user.go index 7642cba4e1f03..9dc84c3ac15e1 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -8,7 +8,6 @@ import ( "net/http" "strings" - org_model "code.gitea.io/gitea/models/organization" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" ) @@ -57,14 +56,6 @@ func userAssignment(ctx *context.Context, errCb func(int, string, interface{})) } else { errCb(http.StatusInternalServerError, "GetUserByName", err) } - } else { - if ctx.ContextUser.IsOrganization() { - if ctx.Org == nil { - ctx.Org = &context.Organization{} - } - ctx.Org.Organization = (*org_model.Organization)(ctx.ContextUser) - ctx.Data["Org"] = ctx.Org.Organization - } } } } diff --git a/templates/org/menu.tmpl b/templates/org/menu.tmpl index 25f459c09c56a..2a359d811134c 100644 --- a/templates/org/menu.tmpl +++ b/templates/org/menu.tmpl @@ -3,16 +3,18 @@ {{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}} + {{if and .IsProjectEnabled .CanReadProjects}} {{svg "octicon-project-symlink"}} {{.locale.Tr "user.projects"}} - {{if .IsPackageEnabled}} + {{end}} + {{if and .IsPackageEnabled .CanReadPackages}} {{svg "octicon-package"}} {{.locale.Tr "packages.title"}} {{end}} - {{if .IsRepoIndexerEnabled}} - + {{if and .IsRepoIndexerEnabled .CanReadCode}} + {{svg "octicon-code"}} {{$.locale.Tr "org.code"}} {{end}} diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl index ce9ecb46adbc8..b4f7d6f900911 100644 --- a/templates/user/overview/header.tmpl +++ b/templates/user/overview/header.tmpl @@ -22,15 +22,17 @@ {{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}} + {{if and .IsProjectEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadProjects))}} {{svg "octicon-project-symlink"}} {{.locale.Tr "user.projects"}} - {{if (not .UnitPackagesGlobalDisabled)}} + {{end}} + {{if and .IsPackageEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadPackages))}} {{svg "octicon-package"}} {{.locale.Tr "packages.title"}} {{end}} - {{if .IsRepoIndexerEnabled}} + {{if and .IsRepoIndexerEnabled (or .ContextUser.IsIndividual (and .ContextUser.IsOrganization .CanReadCode))}} {{svg "octicon-code"}} {{.locale.Tr "user.code"}} From 5155ec35c571de8df62318df78c78cebc20e1aa0 Mon Sep 17 00:00:00 2001 From: sillyguodong <33891828+sillyguodong@users.noreply.github.com> Date: Fri, 10 Mar 2023 23:54:32 +0800 Subject: [PATCH 05/15] Parse external request id from request headers, and print it in access log (#22906) Close: #22890. --- ### Configure in .ini file: ```ini [log] REQUEST_ID_HEADERS = X-Request-ID, X-Trace-Id ``` ### Params in Request Header ``` X-Trace-ID: trace-id-1q2w3e4r ``` ![image](https://user-images.githubusercontent.com/33891828/218665296-8fd19a0f-ada6-4236-8bdb-f99201c703e8.png) ### Log output: ![image](https://user-images.githubusercontent.com/33891828/218665225-cc242a57-4ffc-449a-a1f6-f45ded0ead60.png) --- custom/conf/app.example.ini | 16 +++++++++ .../doc/advanced/config-cheat-sheet.en-us.md | 6 ++++ .../doc/advanced/config-cheat-sheet.zh-cn.md | 17 +++++++++- modules/context/access_log.go | 34 +++++++++++++++++++ modules/setting/log.go | 2 ++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b7875c12dd8e1..c3c20a216c2df 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -576,6 +576,22 @@ ROUTER = console ;; The routing level will default to that of the system but individual router level can be set in ;; [log..router] LEVEL ;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; Print request id which parsed from request headers in access log, when access log is enabled. +;; * E.g: +;; * In request Header: X-Request-ID: test-id-123 +;; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID +;; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "test-id-123" +;; +;; If you configure more than one in the .ini file, it will match in the order of configuration, +;; and the first match will be finally printed in the log. +;; * E.g: +;; * In reuqest Header: X-Trace-ID: trace-id-1q2w3e4r +;; * Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID, X-Trace-ID, X-Req-ID +;; * Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "trace-id-1q2w3e4r" +;; +;; REQUEST_ID_HEADERS = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index c4ff8bafb9209..a5ef977f15bed 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -881,7 +881,13 @@ Default templates for project boards: - `Identity`: the SignedUserName or `"-"` if not logged in. - `Start`: the start time of the request. - `ResponseWriter`: the responseWriter from the request. + - `RequestID`: the value matching REQUEST_ID_HEADERS(default: `-`, if not matched). - You must be very careful to ensure that this template does not throw errors or panics as this template runs outside of the panic/recovery script. +- `REQUEST_ID_HEADERS`: **\**: You can configure multiple values that are splited by comma here. It will match in the order of configuration, and the first match will be finally printed in the access log. + - e.g. + - In the Request Header: X-Request-ID: **test-id-123** + - Configuration in app.ini: REQUEST_ID_HEADERS = X-Request-ID + - Print in log: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "**test-id-123**" ... ### Log subsections (`log.name`, `log.name.*`) diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index aae64d97bac1e..84186d0e9af1b 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -262,7 +262,22 @@ test01.xls: application/vnd.ms-excel; charset=binary - `ROOT_PATH`: 日志文件根目录。 - `MODE`: 日志记录模式,默认是为 `console`。如果要写到多个通道,用逗号分隔 -- `LEVEL`: 日志级别,默认为`Trace`。 +- `LEVEL`: 日志级别,默认为 `Trace`。 +- `DISABLE_ROUTER_LOG`: 关闭日志中的路由日志。 +- `ENABLE_ACCESS_LOG`: 是否开启 Access Log, 默认为 false。 +- `ACCESS_LOG_TEMPLATE`: `access.log` 输出内容的模板,默认模板:**`{{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"`** + 模板支持以下参数: + - `Ctx`: 请求上下文。 + - `Identity`: 登录用户名,默认: “`-`”。 + - `Start`: 请求开始时间。 + - `ResponseWriter`: + - `RequestID`: 从请求头中解析得到的与 `REQUEST_ID_HEADERS` 匹配的值,默认: “`-`”。 + - 一定要谨慎配置该模板,否则可能会引起panic. +- `REQUEST_ID_HEADERS`: 从 Request Header 中匹配指定 Key,并将匹配到的值输出到 `access.log` 中(需要在 `ACCESS_LOG_TEMPLATE` 中指定输出位置)。如果在该参数中配置多个 Key, 请用逗号分割,程序将按照配置的顺序进行匹配。 + - 示例: + - 请求头: X-Request-ID: **test-id-123** + - 配置文件: REQUEST_ID_HEADERS = X-Request-ID + - 日志输出: 127.0.0.1:58384 - - [14/Feb/2023:16:33:51 +0800] "**test-id-123**" ... ## Cron (`cron`) diff --git a/modules/context/access_log.go b/modules/context/access_log.go index 1aaba9dc2d5c9..515682b64b0e6 100644 --- a/modules/context/access_log.go +++ b/modules/context/access_log.go @@ -6,7 +6,9 @@ package context import ( "bytes" "context" + "fmt" "net/http" + "strings" "text/template" "time" @@ -20,13 +22,39 @@ type routerLoggerOptions struct { Start *time.Time ResponseWriter http.ResponseWriter Ctx map[string]interface{} + RequestID *string } var signedUserNameStringPointerKey interface{} = "signedUserNameStringPointerKey" +const keyOfRequestIDInTemplate = ".RequestID" + +// According to: +// TraceId: A valid trace identifier is a 16-byte array with at least one non-zero byte +// MD5 output is 16 or 32 bytes: md5-bytes is 16, md5-hex is 32 +// SHA1: similar, SHA1-bytes is 20, SHA1-hex is 40. +// UUID is 128-bit, 32 hex chars, 36 ASCII chars with 4 dashes +// So, we accept a Request ID with a maximum character length of 40 +const maxRequestIDByteLength = 40 + +func parseRequestIDFromRequestHeader(req *http.Request) string { + requestID := "-" + for _, key := range setting.Log.RequestIDHeaders { + if req.Header.Get(key) != "" { + requestID = req.Header.Get(key) + break + } + } + if len(requestID) > maxRequestIDByteLength { + requestID = fmt.Sprintf("%s...", requestID[:maxRequestIDByteLength]) + } + return requestID +} + // AccessLogger returns a middleware to log access logger func AccessLogger() func(http.Handler) http.Handler { logger := log.GetLogger("access") + needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate) logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate) return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { @@ -34,6 +62,11 @@ func AccessLogger() func(http.Handler) http.Handler { identity := "-" r := req.WithContext(context.WithValue(req.Context(), signedUserNameStringPointerKey, &identity)) + var requestID string + if needRequestID { + requestID = parseRequestIDFromRequestHeader(req) + } + next.ServeHTTP(w, r) rw := w.(ResponseWriter) @@ -47,6 +80,7 @@ func AccessLogger() func(http.Handler) http.Handler { "RemoteAddr": req.RemoteAddr, "Req": req, }, + RequestID: &requestID, }) if err != nil { log.Error("Could not set up chi access logger: %v", err.Error()) diff --git a/modules/setting/log.go b/modules/setting/log.go index 5448650aadb9c..dabdb543abdf5 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -38,6 +38,7 @@ var Log struct { EnableAccessLog bool AccessLogTemplate string BufferLength int64 + RequestIDHeaders []string } // GetLogDescriptions returns a race safe set of descriptions @@ -153,6 +154,7 @@ func loadLogFrom(rootCfg ConfigProvider) { Log.AccessLogTemplate = sec.Key("ACCESS_LOG_TEMPLATE").MustString( `{{.Ctx.RemoteAddr}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}\" \"{{.Ctx.Req.UserAgent}}"`, ) + Log.RequestIDHeaders = sec.Key("REQUEST_ID_HEADERS").Strings(",") // the `MustString` updates the default value, and `log.ACCESS` is used by `generateNamedLogger("access")` later _ = rootCfg.Section("log").Key("ACCESS").MustString("file") From 3de9e63fd04d61e08fcbdec035c9f138347d9f37 Mon Sep 17 00:00:00 2001 From: Zettat123 Date: Sat, 11 Mar 2023 00:42:38 +0800 Subject: [PATCH 06/15] Hide target selector if tag exists when creating new release (#23171) Close #22649. |status|screenshot| |-|-| |empty tag name|| |new tag|| |existing tag|| --- options/locale/locale_en-US.ini | 2 ++ templates/repo/release/new.tmpl | 33 +++++++++++++----------- web_src/js/features/repo-release.js | 39 +++++++++++++++++++++++++++-- web_src/js/index.js | 4 +-- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 095257b365fb8..dccf184335b28 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2290,6 +2290,8 @@ release.edit_subheader = Releases organize project versions. release.tag_name = Tag name release.target = Target release.tag_helper = Choose an existing tag or create a new tag. +release.tag_helper_new = New tag. This tag will be created from the target. +release.tag_helper_existing = Existing tag. release.title = Title release.content = Content release.prerelease_desc = Mark as Pre-Release diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index 37d7ca032196e..d7c580fed9693 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -20,22 +20,27 @@ {{.tag_name}}@{{.tag_target}} {{else}} - @ - diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js index a061c6b23f832..a230d7765e223 100644 --- a/web_src/js/features/repo-release.js +++ b/web_src/js/features/repo-release.js @@ -3,7 +3,7 @@ import {attachTribute} from './tribute.js'; import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js'; import {initEasyMDEImagePaste} from './comp/ImagePaste.js'; import {createCommentEasyMDE} from './comp/EasyMDE.js'; -import {hideElem} from '../utils/dom.js'; +import {hideElem, showElem} from '../utils/dom.js'; export function initRepoRelease() { $(document).on('click', '.remove-rel-attach', function() { @@ -14,8 +14,43 @@ export function initRepoRelease() { }); } +export function initRepoReleaseNew() { + const $repoReleaseNew = $('.repository.new.release'); + if (!$repoReleaseNew.length) return; -export function initRepoReleaseEditor() { + initTagNameEditor(); + initRepoReleaseEditor(); +} + +function initTagNameEditor() { + const el = document.getElementById('tag-name-editor'); + if (!el) return; + + const existingTags = JSON.parse(el.getAttribute('data-existing-tags')); + if (!Array.isArray(existingTags)) return; + + const defaultTagHelperText = el.getAttribute('data-tag-helper'); + const newTagHelperText = el.getAttribute('data-tag-helper-new'); + const existingTagHelperText = el.getAttribute('data-tag-helper-existing'); + + document.getElementById('tag-name').addEventListener('keyup', (e) => { + const value = e.target.value; + if (existingTags.includes(value)) { + // If the tag already exists, hide the target branch selector. + hideElem('#tag-target-selector'); + document.getElementById('tag-helper').innerText = existingTagHelperText; + } else { + showElem('#tag-target-selector'); + if (value) { + document.getElementById('tag-helper').innerText = newTagHelperText; + } else { + document.getElementById('tag-helper').innerText = defaultTagHelperText; + } + } + }); +} + +function initRepoReleaseEditor() { const $editor = $('.repository.new.release .content-editor'); if ($editor.length === 0) { return; diff --git a/web_src/js/index.js b/web_src/js/index.js index 611c09d2b8c16..6b4f4ef3ebe12 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -76,7 +76,7 @@ import { import {initViewedCheckboxListenerFor} from './features/pull-view-file.js'; import {initOrgTeamSearchRepoBox, initOrgTeamSettings} from './features/org-team.js'; import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.js'; -import {initRepoRelease, initRepoReleaseEditor} from './features/repo-release.js'; +import {initRepoRelease, initRepoReleaseNew} from './features/repo-release.js'; import {initRepoEditor} from './features/repo-editor.js'; import {initCompSearchUserBox} from './features/comp/SearchUserBox.js'; import {initInstall} from './features/install.js'; @@ -179,7 +179,7 @@ $(document).ready(() => { initRepoPullRequestAllowMaintainerEdit(); initRepoPullRequestReview(); initRepoRelease(); - initRepoReleaseEditor(); + initRepoReleaseNew(); initRepoSettingGitHook(); initRepoSettingSearchTeamBox(); initRepoSettingsCollaboration(); From f20bf2fe3b17d998940d5277db75c16b41d2a12c Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 11 Mar 2023 00:15:59 +0000 Subject: [PATCH 07/15] [skip ci] Updated translations via Crowdin --- options/locale/locale_tr-TR.ini | 14 ++++----- options/locale/locale_zh-TW.ini | 52 +++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index c0e5bb220847b..877d767ae068f 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -334,7 +334,7 @@ non_local_account=Yerel olmayan kullanıcılar parolalarını Gitea web arayüz verify=Doğrula scratch_code=Çizgi kodu use_scratch_code=Bir çizgi kodu kullanınız -twofa_scratch_used=Çizgi kodunuzu kullandınız. İki aşamalı ayarlar sayfasına yönlendirildiniz, burada cihaz kaydınızı kaldırabilir veya yeni bir çizgi kodu oluşturabilirsiniz. +twofa_scratch_used=Geçici kodunuzu kullandınız. İki aşamalı ayarlar sayfasına yönlendirildiniz, burada aygıt kaydınızı kaldırabilir veya yeni bir geçici kod oluşturabilirsiniz. twofa_passcode_incorrect=Şifreniz yanlış. Aygıtınızı yanlış yerleştirdiyseniz, oturum açmak için çizgi kodunuzu kullanın. twofa_scratch_token_incorrect=Çizgi kodunuz doğru değildir. login_userpass=Oturum Aç @@ -736,12 +736,12 @@ unbind=Bağlantıyı Kaldır unbind_success=Sosyal hesabın bağlantısı Gitea hesabınızdan kaldırılmıştır. manage_access_token=Erişim Jetonlarını Yönet -generate_new_token=Yeni Jeton Üret +generate_new_token=Yeni Erişim Anahtarı Üret tokens_desc=Bu jetonlar Gitea API'sini kullanarak hesabınıza erişim sağlar. new_token_desc=Jeton kullanan uygulamalar hesabınıza tam erişime sahiptir. token_name=Jeton İsmi -generate_token=Jeton Üret -generate_token_success=Yeni bir jeton oluşturuldu. Tekrar gösterilmeyeceği için şimdi kopyalayın. +generate_token=Erişim Anahtarı Üret +generate_token_success=Yeni bir erişim anahtarı oluşturuldu. Tekrar gösterilmeyeceği için şimdi kopyalayın. generate_token_name_duplicate=%s zaten bir uygulama adı olarak kullanılmış. Lütfen yeni bir tane kullanın. delete_token=Sil access_token_deletion=Erişim Jetonunu Sil @@ -784,12 +784,12 @@ twofa_desc=İki faktörlü kimlik doğrulama, hesabınızın güvenliğini artı twofa_is_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmiş. twofa_not_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmemiş. twofa_disable=İki Aşamalı Doğrulamayı Devre Dışı Bırak -twofa_scratch_token_regenerate=Kazıma Belirtecini Yenile -twofa_scratch_token_regenerated=Kazıma belirteciniz şimdi %s. Güvenli bir yerde saklayın. +twofa_scratch_token_regenerate=Geçici Kodu Yeniden Üret +twofa_scratch_token_regenerated=Geçici kodunuz artık %s. Güvenilir bir yerde saklayın. twofa_enroll=İki Faktörlü Kimlik Doğrulamaya Kaydolun twofa_disable_note=Gerekirse iki faktörlü kimlik doğrulamayı devre dışı bırakabilirsiniz. twofa_disable_desc=İki faktörlü kimlik doğrulamayı devre dışı bırakmak hesabınızı daha az güvenli hale getirir. Devam edilsin mi? -regenerate_scratch_token_desc=Karalama belirtecinizi yanlış yerleştirdiyseniz veya oturum açmak için kullandıysanız, buradan sıfırlayabilirsiniz. +regenerate_scratch_token_desc=Geçici kodunuzu kaybettiyseniz veya oturum açmak için kullandıysanız, buradan sıfırlayabilirsiniz. twofa_disabled=İki faktörlü kimlik doğrulama devre dışı bırakıldı. scan_this_image=Kim doğrulama uygulamanızla bu görüntüyü tarayın: or_enter_secret=Veya gizli şeyi girin: %s diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 51610f4e82d74..fa39225c1b894 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -54,7 +54,7 @@ mirror=鏡像 new_repo=新增儲存庫 new_migrate=遷移外部儲存庫 new_mirror=新鏡像 -new_fork=新增儲存庫 fork +new_fork=新增儲存庫 Fork new_org=新增組織 new_project=新增專案 new_project_column=新增欄位 @@ -476,6 +476,8 @@ url_error=`'%s' 是無效的 URL。` include_error=` 必須包含子字串「%s」。 glob_pattern_error=` glob 比對模式無效:%s.` regex_pattern_error=` 正規表示式模式無效:%s.` +username_error=`只能包含英文字母數字 ('0-9'、'a-z'、'A-Z')、破折號 ('-')、底線 ('_')、句點 ('.'),不能以非英文字母數字開頭或結尾,也不允許連續的非英文字母數字。` +invalid_group_team_map_error=` 對應無效: %s` unknown_error=未知錯誤: captcha_incorrect=驗證碼不正確。 password_not_match=密碼錯誤。 @@ -521,11 +523,11 @@ must_use_public_key=您提供的金鑰是私有金鑰,請勿上傳您的私有 unable_verify_ssh_key=無法驗證 SSH 密鑰 auth_failed=授權認證失敗:%v -still_own_repo=此帳戶仍然擁有一個或多個儲存庫,您必須先刪除或轉移它們。 -still_has_org=此帳戶仍是一個或多個組織的成員,您必須先離開它們。 -still_own_packages=您的帳戶擁有一個或多個套件,請先刪除他們。 -org_still_own_repo=該組織仍然是某些儲存庫的擁有者,您必須先轉移或刪除它們才能執行刪除組織! -org_still_own_packages=此組織擁有一個或多個套件,請先刪除他們。 +still_own_repo=您的帳戶擁有一個以上的儲存庫,請先刪除或轉移它們。 +still_has_org=您的帳戶是一個或多個組織的成員,請先離開它們。 +still_own_packages=您的帳戶擁有一個以上的套件,請先刪除它們。 +org_still_own_repo=此組織仍然擁有一個以上的儲存庫,請先刪除或轉移它們。 +org_still_own_packages=此組織仍然擁有一個以上的套件,請先刪除它們。 target_branch_not_exist=目標分支不存在 @@ -1132,6 +1134,7 @@ editor.commit_directly_to_this_branch=直接提交到 命 pulls.merge_instruction_step1_desc=在您的儲存庫中切換到新分支並測試變更。 pulls.merge_instruction_step2_desc=合併變更並更新到 Gitea。 pulls.clear_merge_message=清除合併訊息 +pulls.clear_merge_message_hint=清除合併訊息將僅移除提交訊息內容,留下產生的 git 結尾,如「Co-Authored-By …」。 pulls.auto_merge_button_when_succeed=(當通過檢查後) pulls.auto_merge_when_succeed=通過所有檢查後自動合併 @@ -2619,7 +2623,7 @@ users.still_own_repo=這個使用者還擁有一個或更多的儲存庫。請 users.still_has_org=此使用者是組織的成員。請先將他從組織中移除。 users.purge=清除使用者 users.purge_help=強制刪除使用者和他擁有的所有儲存庫、組織、套件,所有留言也會被刪除。 -users.still_own_packages=此使用者擁有一個或多個套件,請先刪除這些套件。 +users.still_own_packages=此使用者仍然擁有一個以上的套件,請先刪除這些套件。 users.deletion_success=使用者帳戶已被刪除。 users.reset_2fa=重設兩步驟驗證 users.list_status_filter.menu_text=篩選 @@ -2649,7 +2653,7 @@ emails.change_email_header=更新電子信箱屬性 emails.change_email_text=您確定要更新這個電子信箱? orgs.org_manage_panel=組織管理 -orgs.name=組織名稱 +orgs.name=名稱 orgs.teams=團隊數 orgs.members=成員數 orgs.new_orga=新增組織 @@ -2658,7 +2662,7 @@ repos.repo_manage_panel=儲存庫管理 repos.unadopted=未接管的儲存庫 repos.unadopted.no_more=找不到其他未接管的儲存庫 repos.owner=擁有者 -repos.name=儲存庫名稱 +repos.name=名稱 repos.private=私有 repos.watches=關注數 repos.stars=星號數 @@ -2690,8 +2694,8 @@ systemhooks.update_webhook=更新系統 Webhook auths.auth_manage_panel=認證來源管理 auths.new=新增認證來源 -auths.name=認證名稱 -auths.type=認證類型 +auths.name=名稱 +auths.type=類型 auths.enabled=已啟用 auths.syncenabled=啟用使用者同步 auths.updated=最後更新時間 @@ -2762,6 +2766,7 @@ auths.oauth2_required_claim_value_helper=填寫此名稱以限制 Claim 中有 auths.oauth2_group_claim_name=Claim 名稱提供群組名稱給此來源。(選用) auths.oauth2_admin_group=管理員使用者的群組 Claim 值。(選用 - 需要上面的 Claim 名稱) auths.oauth2_restricted_group=受限制使用者的群組 Claim 值。(選用 - 需要上面的 Claim 名稱) +auths.oauth2_map_group_to_team=將已 Claim 的群組對應到組織團隊。(選用 - 需要上述 Claim 名稱) auths.oauth2_map_group_to_team_removal=如果使用者不屬於相對應的群組,將使用者從已同步的團隊移除。 auths.enable_auto_register=允許授權用戶自動註冊 auths.sspi_auto_create_users=自動建立使用者 @@ -2843,7 +2848,7 @@ config.lfs_http_auth_expiry=LFS HTTP 驗證有效時間 config.db_config=資料庫組態 config.db_type=資料庫類型 config.db_host=主機地址 -config.db_name=資料庫名稱 +config.db_name=名稱 config.db_user=使用者名稱 config.db_schema=結構描述 config.db_ssl_mode=SSL @@ -2946,7 +2951,7 @@ config.get_setting_failed=讀取設定值 %s 失敗 config.set_setting_failed=寫入設定值 %s 失敗 monitor.cron=Cron 任務 -monitor.name=任務名稱 +monitor.name=名稱 monitor.schedule=任務安排 monitor.next=下次執行時間 monitor.previous=上次執行時間 @@ -3031,7 +3036,7 @@ notices.deselect_all=取消所有選取 notices.inverse_selection=反向選取 notices.delete_selected=刪除選取項 notices.delete_all=刪除所有提示 -notices.type=提示類型 +notices.type=類型 notices.type_1=儲存庫 notices.type_2=任務 notices.desc=描述 @@ -3196,7 +3201,7 @@ generic.documentation=關於通用 registry 的詳情請參閱說明文件。 -maven.registry=在您的 pom.xml 檔設定此註冊中心: +maven.registry=在您專案的 pom.xml 檔設定此註冊中心: maven.install=若要使用此套件,請在您 pom.xml 檔的 dependencies 段落加入下列內容: maven.install2=透過下列命令執行: maven.download=透過下列命令下載相依性: @@ -3205,7 +3210,7 @@ nuget.registry=透過下列命令設定此註冊中心: nuget.install=執行下列命令以使用 NuGet 安裝此套件: nuget.documentation=關於 NuGet registry 的詳情請參閱說明文件。 nuget.dependency.framework=目標框架 -npm.registry=在您的 .npmrc 檔設定此註冊中心: +npm.registry=在您專案的 .npmrc 檔設定此註冊中心: npm.install=執行下列命令以使用 npm 安裝此套件: npm.install2=或將它加到 package.json 檔: npm.documentation=關於 npm registry 的詳情請參閱說明文件。 @@ -3241,6 +3246,7 @@ settings.delete.success=已刪除該套件。 settings.delete.error=刪除套件失敗。 owner.settings.cargo.title=Cargo Registry 索引 owner.settings.cargo.initialize=初始化索引 +owner.settings.cargo.initialize.description=使用 Cargo Registry 時需要一個特別的 Git 儲存庫作為索引。您可以在此使用必要的設定建立和重建它。 owner.settings.cargo.initialize.error=初始化 Cargo 索引失敗: %v owner.settings.cargo.initialize.success=成功建立了 Cargo 索引。 owner.settings.cargo.rebuild=重建索引 @@ -3252,6 +3258,7 @@ owner.settings.cleanuprules.add=加入清理規則 owner.settings.cleanuprules.edit=編輯清理規則 owner.settings.cleanuprules.none=沒有可用的清理規則。閱讀文件以了解更多。 owner.settings.cleanuprules.preview=清理規則預覽 +owner.settings.cleanuprules.preview.overview=已排定要移除 %d 個套件。 owner.settings.cleanuprules.preview.none=清理規則不符合任何套件。 owner.settings.cleanuprules.enabled=已啟用 owner.settings.cleanuprules.pattern_full_match=將比對規則套用到完整的套件名稱 @@ -3275,8 +3282,9 @@ secrets=Secret description=Secret 會被傳給特定的 Action,其他情況無法讀取。 none=還沒有 Secret。 value=值 -name=組織名稱 +name=名稱 creation=加入 Secret +creation.name_placeholder=不區分大小寫,只能包含英文字母、數字、底線 ('_'),不能以 GITEA_ 或 GITHUB_ 開頭。 creation.value_placeholder=輸入任何內容,頭尾的空白都會被忽略。 creation.success=已加入 Secret「%s」。 creation.failed=加入 Secret 失敗。 @@ -3305,8 +3313,8 @@ runners.new=建立 Runner runners.new_notice=如何啟動 Runner runners.status=狀態 runners.id=ID -runners.name=組織名稱 -runners.owner_type=認證類型 +runners.name=名稱 +runners.owner_type=類型 runners.description=組織描述 runners.labels=標籤 runners.last_online=最後上線時間 From 75022f8b1a513ca2fd7ca66a2f05ecc49e2f1460 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 11 Mar 2023 18:47:09 +0800 Subject: [PATCH 08/15] Refactor branch/tag selector dropdown (first step) (#23394) Follow: * #23345 The branch/tag selector dropdown mixes jQuery/Fomantic UI/Vue together, it's very diffcult to maintain and causes unfixable a11y problems. It also causes problems like #19851 #21314 #21952 This PR is the first step for the refactoring, move `data-` attributes to JS object and use Vue data as much as possible. The old selector `'.choose.reference .dropdown'` was also wrong, it hits `
` and would cause undefined behaviors. I have done some quick tests and it works. After this PR gets merged, I will move the code into a Vue SFC in next PR. ![image](https://user-images.githubusercontent.com/2114189/224099638-378a8a86-0865-47d1-bcba-f972506374c7.png) ![image](https://user-images.githubusercontent.com/2114189/224099690-70276cf5-b1e4-404a-b0c6-582448abf40e.png) --------- Co-authored-by: techknowlogick --- templates/repo/branch_dropdown.tmpl | 128 ++++++++++-------- templates/repo/commit_page.tmpl | 8 +- .../js/components/RepoBranchTagDropdown.js | 67 ++++----- web_src/js/features/repo-legacy.js | 2 +- 4 files changed, 101 insertions(+), 104 deletions(-) diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index 89d65146a9d62..8e81373aec041 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -2,101 +2,111 @@ {{$defaultBranch := $.root.BranchName}}{{if and .root.IsViewTag (not .noTag)}}{{$defaultBranch = .root.TagName}}{{end}}{{if eq $defaultBranch ""}}{{$defaultBranch = $.root.Repository.DefaultBranch}}{{end}} {{$type := ""}}{{if and .root.IsViewTag (not .noTag)}}{{$type = "tag"}}{{else if .root.IsViewBranch}}{{$type = "branch"}}{{else}}{{$type = "tree"}}{{end}} {{$showBranchesInDropdown := not .root.HideBranchesInDropdown}} + + +
- -{{if or .CanWriteIssues .CanWritePulls}} +{{if $.CanWriteProjects}}