diff --git a/.github/workflows/build-image-for-latest-release.yml b/.github/workflows/build-image-for-latest-release.yml index 315d7fef..0f81013f 100644 --- a/.github/workflows/build-image-for-latest-release.yml +++ b/.github/workflows/build-image-for-latest-release.yml @@ -35,32 +35,29 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Docker meta - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 - with: - images: apache/answer - tags: | - type=raw,value=latest - - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + run: | + sudo apt-get update + sudo apt-get install -y qemu-user-static - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + run: | + docker buildx create --name answer-builder --driver docker-container --use + docker buildx inspect --bootstrap - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login --username "${{ secrets.DOCKERHUB_USER }}" --password-stdin - name: Build and push - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - file: ./Dockerfile - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + run: | + BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + docker buildx build \ + --file ./Dockerfile \ + --platform linux/amd64,linux/arm64 \ + --push \ + --tag apache/answer:latest \ + --label org.opencontainers.image.created="${BUILD_DATE}" \ + --label org.opencontainers.image.revision="${GITHUB_SHA}" \ + --label org.opencontainers.image.source="https://github.com/${GITHUB_REPOSITORY}" \ + --label org.opencontainers.image.version="${GITHUB_REF_NAME#v}" \ + . diff --git a/.github/workflows/build-image-for-manual.yml b/.github/workflows/build-image-for-manual.yml index 8ccf7b4f..459e0f3a 100644 --- a/.github/workflows/build-image-for-manual.yml +++ b/.github/workflows/build-image-for-manual.yml @@ -33,33 +33,29 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Docker meta - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 - with: - images: apache/answer - tags: | - type=ref,enable=true,priority=600,prefix=,suffix=,event=branch - type=semver,pattern={{version}} - - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + run: | + sudo apt-get update + sudo apt-get install -y qemu-user-static - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + run: | + docker buildx create --name answer-builder --driver docker-container --use + docker buildx inspect --bootstrap - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login --username "${{ secrets.DOCKERHUB_USER }}" --password-stdin - name: Build and push - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - file: ./Dockerfile - tags: apache/answer:${{ inputs.tag_name }} - labels: ${{ steps.meta.outputs.labels }} + run: | + BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + docker buildx build \ + --file ./Dockerfile \ + --platform linux/amd64,linux/arm64 \ + --push \ + --tag "apache/answer:${{ inputs.tag_name }}" \ + --label org.opencontainers.image.created="${BUILD_DATE}" \ + --label org.opencontainers.image.revision="${GITHUB_SHA}" \ + --label org.opencontainers.image.source="https://github.com/${GITHUB_REPOSITORY}" \ + --label org.opencontainers.image.version="${{ inputs.tag_name }}" \ + . diff --git a/.github/workflows/build-image-for-release.yml b/.github/workflows/build-image-for-release.yml index da839047..ad7deffb 100644 --- a/.github/workflows/build-image-for-release.yml +++ b/.github/workflows/build-image-for-release.yml @@ -34,33 +34,31 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Docker meta - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 - with: - images: apache/answer - tags: | - type=ref,enable=true,priority=600,prefix=,suffix=,event=branch - type=semver,pattern={{version}} - - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + run: | + sudo apt-get update + sudo apt-get install -y qemu-user-static - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + run: | + docker buildx create --name answer-builder --driver docker-container --use + docker buildx inspect --bootstrap - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login --username "${{ secrets.DOCKERHUB_USER }}" --password-stdin - name: Build and push - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - file: ./Dockerfile - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + env: + IMAGE_TAG: ${{ github.ref_name }} + run: | + BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + docker buildx build \ + --file ./Dockerfile \ + --platform linux/amd64,linux/arm64 \ + --push \ + --tag "apache/answer:${IMAGE_TAG#v}" \ + --label org.opencontainers.image.created="${BUILD_DATE}" \ + --label org.opencontainers.image.revision="${GITHUB_SHA}" \ + --label org.opencontainers.image.source="https://github.com/${GITHUB_REPOSITORY}" \ + --label org.opencontainers.image.version="${IMAGE_TAG#v}" \ + . diff --git a/.github/workflows/build-image-for-test.yml b/.github/workflows/build-image-for-test.yml index b90a1ac4..a650f06e 100644 --- a/.github/workflows/build-image-for-test.yml +++ b/.github/workflows/build-image-for-test.yml @@ -30,32 +30,23 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Docker meta - id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 - with: - images: apache/answer - tags: | - type=raw,value=test - - - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + - name: Login to DockerHub + run: echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login --username "${{ secrets.DOCKERHUB_USER }}" --password-stdin - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - - name: Login to DockerHub - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - with: - username: ${{ secrets.DOCKERHUB_USER }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + docker buildx create --name answer-builder --driver docker-container --use + docker buildx inspect --bootstrap - name: Build and push - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 - with: - context: . - file: ./Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + run: | + BUILD_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + docker buildx build \ + --file ./Dockerfile \ + --platform linux/amd64 \ + --push \ + --tag apache/answer:test \ + --label org.opencontainers.image.created="${BUILD_DATE}" \ + --label org.opencontainers.image.revision="${GITHUB_SHA}" \ + --label org.opencontainers.image.source="https://github.com/${GITHUB_REPOSITORY}" \ + . diff --git a/Dockerfile b/Dockerfile index c06f6f85..ac461d4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. -FROM golang:1.24-alpine AS golang-builder +FROM golang:1.25-alpine AS golang-builder LABEL maintainer="linkinstar@apache.org" ARG GOPROXY diff --git a/Makefile b/Makefile index a02eae9e..0623e1ef 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build clean ui -VERSION=2.0.1 +VERSION=2.0.2 BIN=answer DIR_SRC=./cmd/answer DOCKER_CMD=docker diff --git a/README.md b/README.md index 3b8574aa..cf257aba 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ To learn more about the project, visit [answer.apache.org](https://answer.apache ### Running with docker ```bash -docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:2.0.1 +docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:2.0.2 ``` For more information, see [Installation](https://answer.apache.org/docs/installation). diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index d020e3e5..446f6cc0 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -244,7 +244,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database, notificationRepo := notification2.NewNotificationRepo(dataData) pluginUserConfigRepo := plugin_config.NewPluginUserConfigRepo(dataData) badgeAwardRepo := badge_award.NewBadgeAwardRepo(dataData, uniqueIDRepo) - userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService, questionRepo, answerRepo, commentCommonRepo, userExternalLoginRepo, notificationRepo, pluginUserConfigRepo, badgeAwardRepo) + userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService, questionRepo, answerRepo, commentCommonRepo, userExternalLoginRepo, notificationRepo, pluginUserConfigRepo, badgeAwardRepo, apiKeyRepo) userAdminController := controller_admin.NewUserAdminController(userAdminService) reasonRepo := reason.NewReasonRepo(configService) reasonService := reason2.NewReasonService(reasonRepo) diff --git a/docs/release/LICENSE b/docs/release/LICENSE index 58aea229..abfadafb 100644 --- a/docs/release/LICENSE +++ b/docs/release/LICENSE @@ -265,6 +265,7 @@ The following components are provided under the MIT License. See project link fo (MIT License) Masterminds-semver (https://github.com/Masterminds/semver) [link](./licenses/LICENSE-Masterminds-semver.txt) (MIT License) mattn-go-sqlite3 (https://github.com/mattn/go-sqlite3) [link](./licenses/LICENSE-mattn-go-sqlite3.txt) (MIT License) mozillazg-go-pinyin (https://github.com/mozillazg/go-pinyin) [link](./licenses/LICENSE-mozillazg-go-pinyin.txt) + (MIT License) mozillazg-go-unidecode (https://github.com/mozillazg/go-unidecode) [link](./licenses/LICENSE-mozillazg-go-unidecode.txt) (MIT License) next-share (https://github.com/Bunlong/next-share) [link](./licenses/LIcENSE-Bunlong-next-share.txt) (MIT License) node-qrcode (https://github.com/soldair/node-qrcode) [link](./licenses/LICENSE-soldair-qrcode.txt) (MIT License) react (https://github.com/facebook/react) [link](./licenses/LICENSE-facebook-react.txt) diff --git a/go.mod b/go.mod index 11c3a816..5787c8b1 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ module github.com/apache/answer -go 1.24.0 +go 1.25.0 require ( github.com/Machiel/slugify v1.0.1 @@ -62,11 +62,10 @@ require ( github.com/tidwall/gjson v1.17.3 github.com/yuin/goldmark v1.7.4 go.uber.org/mock v0.6.0 - golang.org/x/crypto v0.41.0 + golang.org/x/crypto v0.53.0 golang.org/x/image v0.20.0 - golang.org/x/net v0.43.0 - golang.org/x/term v0.34.0 - golang.org/x/text v0.28.0 + golang.org/x/term v0.44.0 + golang.org/x/text v0.39.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.33.0 @@ -171,8 +170,9 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.10.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index be61e11f..1001f1da 100644 --- a/go.sum +++ b/go.sum @@ -705,8 +705,8 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= @@ -723,8 +723,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -751,8 +751,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -763,8 +763,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -799,14 +799,14 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -815,8 +815,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= @@ -842,8 +842,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/base/constant/cache_key.go b/internal/base/constant/cache_key.go index 987798d1..ab278cc0 100644 --- a/internal/base/constant/cache_key.go +++ b/internal/base/constant/cache_key.go @@ -42,6 +42,9 @@ const ( ConfigCacheTime = 1 * time.Hour ConnectorUserExternalInfoCacheKey = "answer:connector:" ConnectorUserExternalInfoCacheTime = 10 * time.Minute + ConnectorOAuthStateCacheKey = "answer:connector:oauth-state:" + ConnectorOAuthStateCacheTime = 10 * time.Minute + ConnectorOAuthBindStateCacheTime = 5 * time.Minute SiteMapQuestionCacheKeyPrefix = "answer:sitemap:question:%d" SiteMapQuestionCacheTime = time.Hour SitemapMaxSize = 50000 diff --git a/internal/base/middleware/accept_language.go b/internal/base/middleware/accept_language.go index 5d1b12b2..a4e1b2f5 100644 --- a/internal/base/middleware/accept_language.go +++ b/internal/base/middleware/accept_language.go @@ -29,10 +29,16 @@ import ( "golang.org/x/text/language" ) +const maxAcceptLanguageLength = 256 + // ExtractAndSetAcceptLanguage extract accept language from header and set to context func ExtractAndSetAcceptLanguage(ctx *gin.Context) { // The language of our front-end configuration, like en_US acceptLanguage := ctx.GetHeader(constant.AcceptLanguageFlag) + if len(acceptLanguage) > maxAcceptLanguageLength { + ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish) + return + } tag, _, err := language.ParseAcceptLanguage(acceptLanguage) if err != nil || len(tag) == 0 { ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish) diff --git a/internal/base/queue/queue.go b/internal/base/queue/queue.go index b3a8757a..4389c292 100644 --- a/internal/base/queue/queue.go +++ b/internal/base/queue/queue.go @@ -102,13 +102,11 @@ func (q *Queue[T]) Close() { // startWorker starts the background goroutine that processes messages. func (q *Queue[T]) startWorker() { - q.wg.Add(1) - go func() { - defer q.wg.Done() + q.wg.Go(func() { for msg := range q.queue { q.processMessage(msg) } - }() + }) } // processMessage handles a single message with proper synchronization. diff --git a/internal/base/queue/queue_test.go b/internal/base/queue/queue_test.go index 23f0fda7..b84cbde0 100644 --- a/internal/base/queue/queue_test.go +++ b/internal/base/queue/queue_test.go @@ -200,13 +200,11 @@ func TestQueue_ConcurrentRegisterHandler(t *testing.T) { // Concurrently register handlers - should not race var wg sync.WaitGroup for range 10 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { q.RegisterHandler(func(ctx context.Context, msg *testMessage) error { return nil }) - }() + }) } wg.Wait() } diff --git a/internal/controller/connector_controller.go b/internal/controller/connector_controller.go index 939a2a09..28aa5fc6 100644 --- a/internal/controller/connector_controller.go +++ b/internal/controller/connector_controller.go @@ -22,6 +22,7 @@ package controller import ( "fmt" "net/http" + "net/url" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/middleware" @@ -106,6 +107,27 @@ func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn fu return } + state := ctx.Query("state") + if len(state) > 0 { + stateInfo, err := cc.userExternalService.GetOAuthState(ctx, state) + if err != nil || stateInfo == nil || stateInfo.Provider != connector.ConnectorSlugName() { + log.Errorf("invalid connector oauth state for provider %s", connector.ConnectorSlugName()) + ctx.Redirect(http.StatusFound, "/50x") + return + } + } else { + state, err = cc.userExternalService.GenerateOAuthState(ctx, connector.ConnectorSlugName(), + schema.ExternalLoginOAuthStateLoginIntent, "") + if err != nil { + log.Errorf("generate connector oauth state failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + q := ctx.Request.URL.Query() + q.Set("state", state) + ctx.Request.URL.RawQuery = q.Encode() + } + receiverURL := fmt.Sprintf("%s%s%s%s", general.SiteUrl, commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName()) redirectURL := connector.ConnectorSender(ctx, receiverURL) @@ -141,6 +163,27 @@ func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn Avatar: userInfo.Avatar, MetaInfo: userInfo.MetaInfo, } + stateInfo, err := cc.userExternalService.ConsumeOAuthState(ctx, ctx.Query("state")) + if err != nil { + log.Errorf("get connector oauth state failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + if stateInfo != nil && stateInfo.Provider != connector.ConnectorSlugName() { + log.Errorf("connector oauth state provider mismatch: %s != %s", + stateInfo.Provider, connector.ConnectorSlugName()) + ctx.Redirect(http.StatusFound, "/50x") + return + } + if stateInfo != nil && stateInfo.Intent == schema.ExternalLoginOAuthStateBindIntent { + if err = cc.userExternalService.BindExternalLoginToUser(ctx, stateInfo.UserID, u); err != nil { + log.Errorf("bind external login failed: %v", err) + ctx.Redirect(http.StatusFound, "/50x") + return + } + ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/settings/account", siteGeneral.SiteUrl)) + return + } resp, err := cc.userExternalService.ExternalLogin(ctx, u) if err != nil { log.Errorf("external login failed: %v", err) @@ -237,19 +280,32 @@ func (cc *ConnectorController) ConnectorsUserInfo(ctx *gin.Context) { } resp := make([]*schema.ConnectorUserInfoResp, 0) - _ = plugin.CallConnector(func(fn plugin.Connector) error { + err = plugin.CallConnector(func(fn plugin.Connector) error { externalID := userExternalLoginMapping[fn.ConnectorSlugName()] connectorName := fn.ConnectorName() + link := fmt.Sprintf("%s%s%s%s", general.SiteUrl, + commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()) + if len(externalID) == 0 { + state, err := cc.userExternalService.GenerateOAuthState(ctx, fn.ConnectorSlugName(), + schema.ExternalLoginOAuthStateBindIntent, userID) + if err != nil { + return err + } + link = fmt.Sprintf("%s?state=%s", link, url.QueryEscape(state)) + } resp = append(resp, &schema.ConnectorUserInfoResp{ - Name: connectorName.Translate(ctx), - Icon: fn.ConnectorLogoSVG(), - Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl, - commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()), + Name: connectorName.Translate(ctx), + Icon: fn.ConnectorLogoSVG(), + Link: link, Binding: len(externalID) > 0, ExternalID: externalID, }) return nil }) + if err != nil { + handler.HandleResponse(ctx, err, nil) + return + } handler.HandleResponse(ctx, nil, resp) } diff --git a/internal/controller/mcp_controller.go b/internal/controller/mcp_controller.go index b40c58cf..e24c1a54 100644 --- a/internal/controller/mcp_controller.go +++ b/internal/controller/mcp_controller.go @@ -176,6 +176,9 @@ func (c *MCPController) MCPAnswersHandler() func(ctx context.Context, request mc } resp := make([]*schema.MCPSearchAnswerInfoResp, 0) for _, answer := range answerList { + if answer.Status != entity.AnswerStatusAvailable { + continue + } t := &schema.MCPSearchAnswerInfoResp{ QuestionID: answer.QuestionID, AnswerID: answer.ID, @@ -195,6 +198,9 @@ func (c *MCPController) MCPAnswersHandler() func(ctx context.Context, request mc } resp := make([]*schema.MCPSearchAnswerInfoResp, 0) for _, answer := range answerList { + if answer.Status != entity.AnswerStatusAvailable { + continue + } t := &schema.MCPSearchAnswerInfoResp{ QuestionID: answer.QuestionID, AnswerID: answer.ID, diff --git a/internal/controller/template_render/tags.go b/internal/controller/template_render/tags.go index 6a6dacc9..d7798c63 100644 --- a/internal/controller/template_render/tags.go +++ b/internal/controller/template_render/tags.go @@ -20,10 +20,11 @@ package templaterender import ( + "context" + "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/schema" "github.com/jinzhu/copier" - "golang.org/x/net/context" ) func (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetTagWithPageReq) (resp *pager.PageModel, err error) { diff --git a/internal/controller/template_render/userinfo.go b/internal/controller/template_render/userinfo.go index 1734f65c..eb4fb91c 100644 --- a/internal/controller/template_render/userinfo.go +++ b/internal/controller/template_render/userinfo.go @@ -20,8 +20,9 @@ package templaterender import ( + "context" + "github.com/apache/answer/internal/schema" - "golang.org/x/net/context" ) func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoByUsernameResp, err error) { diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index 52963c44..42e3494a 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -196,8 +196,12 @@ func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err erro // GetAnswerList get answer list all func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) { answerList = make([]*entity.Answer, 0) - answer.ID = uid.DeShortID(answer.ID) - answer.QuestionID = uid.DeShortID(answer.QuestionID) + if len(answer.ID) > 0 { + answer.ID = uid.DeShortID(answer.ID) + } + if len(answer.QuestionID) > 0 { + answer.QuestionID = uid.DeShortID(answer.QuestionID) + } err = ar.data.DB.Context(ctx).Find(&answerList, answer) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/repo/api_key/api_key_repo.go b/internal/repo/api_key/api_key_repo.go index 2309384a..8fd9ed46 100644 --- a/internal/repo/api_key/api_key_repo.go +++ b/internal/repo/api_key/api_key_repo.go @@ -81,3 +81,11 @@ func (ar *apiKeyRepo) DeleteAPIKey(ctx context.Context, id int) (err error) { } return } + +func (ar *apiKeyRepo) DeleteAPIKeysByUserID(ctx context.Context, userID string) (err error) { + _, err = ar.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(&entity.APIKey{}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/repo/tag/tag_rel_repo.go b/internal/repo/tag/tag_rel_repo.go index a52b1bf5..1f2dd63f 100644 --- a/internal/repo/tag/tag_rel_repo.go +++ b/internal/repo/tag/tag_rel_repo.go @@ -195,6 +195,7 @@ func (tr *tagRelRepo) CountTagRelByTagID(ctx context.Context, tagID string) (cou // GetTagRelDefaultStatusByObjectID get tag rel default status func (tr *tagRelRepo) GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error) { question := entity.Question{} + objectID = uid.DeShortID(objectID) exist, err := tr.data.DB.Context(ctx).ID(objectID).Cols("show", "status").Get(&question) if err != nil { err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() diff --git a/internal/repo/user_external_login/user_external_login_repo.go b/internal/repo/user_external_login/user_external_login_repo.go index b5cf85e8..12d2a339 100644 --- a/internal/repo/user_external_login/user_external_login_repo.go +++ b/internal/repo/user_external_login/user_external_login_repo.go @@ -22,6 +22,7 @@ package user_external_login import ( "context" "encoding/json" + "time" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/data" @@ -136,3 +137,28 @@ func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo( _ = json.Unmarshal([]byte(res), &info) return info, nil } + +func (ur *userExternalLoginRepo) SetCacheOAuthState( + ctx context.Context, state string, info *schema.ExternalLoginOAuthState, duration time.Duration) (err error) { + cacheData, _ := json.Marshal(info) + return ur.data.Cache.SetString(ctx, constant.ConnectorOAuthStateCacheKey+state, + string(cacheData), duration) +} + +func (ur *userExternalLoginRepo) GetCacheOAuthState( + ctx context.Context, state string) (info *schema.ExternalLoginOAuthState, err error) { + res, exist, err := ur.data.Cache.GetString(ctx, constant.ConnectorOAuthStateCacheKey+state) + if err != nil { + return info, err + } + if !exist { + return nil, nil + } + info = &schema.ExternalLoginOAuthState{} + _ = json.Unmarshal([]byte(res), &info) + return info, nil +} + +func (ur *userExternalLoginRepo) DeleteCacheOAuthState(ctx context.Context, state string) (err error) { + return ur.data.Cache.Del(ctx, constant.ConnectorOAuthStateCacheKey+state) +} diff --git a/internal/schema/user_external_login_schema.go b/internal/schema/user_external_login_schema.go index 21389e9c..5ec7de12 100644 --- a/internal/schema/user_external_login_schema.go +++ b/internal/schema/user_external_login_schema.go @@ -19,6 +19,11 @@ package schema +const ( + ExternalLoginOAuthStateLoginIntent = "login" + ExternalLoginOAuthStateBindIntent = "bind" +) + // UserExternalLoginResp user external login resp type UserExternalLoginResp struct { BindingKey string `json:"binding_key"` @@ -75,6 +80,13 @@ type ExternalLoginUserInfoCache struct { Bio string } +// ExternalLoginOAuthState stores the local OAuth request state. +type ExternalLoginOAuthState struct { + Provider string `json:"provider"` + Intent string `json:"intent"` + UserID string `json:"user_id,omitempty"` +} + // ExternalLoginUnbindingReq external login unbinding user type ExternalLoginUnbindingReq struct { ExternalID string `validate:"required,gt=0,lte=128" json:"external_id"` diff --git a/internal/service/apikey/apikey_service.go b/internal/service/apikey/apikey_service.go index 43c1294c..2d154ebd 100644 --- a/internal/service/apikey/apikey_service.go +++ b/internal/service/apikey/apikey_service.go @@ -35,6 +35,7 @@ type APIKeyRepo interface { UpdateAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) AddAPIKey(ctx context.Context, apiKey entity.APIKey) (err error) DeleteAPIKey(ctx context.Context, id int) (err error) + DeleteAPIKeysByUserID(ctx context.Context, userID string) (err error) } type APIKeyService struct { @@ -114,3 +115,7 @@ func (s *APIKeyService) DeleteAPIKey(ctx context.Context, req *schema.DeleteAPIK } return nil } + +func (s *APIKeyService) DeleteUserAPIKeys(ctx context.Context, userID string) error { + return s.apiKeyRepo.DeleteAPIKeysByUserID(ctx, userID) +} diff --git a/internal/service/content/answer_service.go b/internal/service/content/answer_service.go index 25b0050e..bda7b582 100644 --- a/internal/service/content/answer_service.go +++ b/internal/service/content/answer_service.go @@ -554,12 +554,18 @@ func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string, if !exist { return nil, nil, false, errors.NotFound(reason.AnswerNotFound) } + if (question.Status == entity.QuestionStatusDeleted || question.Status == entity.QuestionStatusPending || question.Show == entity.QuestionHide) && !isAdminModerator && question.UserID != loginUserID { return nil, nil, false, errors.NotFound(reason.AnswerNotFound) } + if (answerInfo.Status == entity.AnswerStatusDeleted || + answerInfo.Status == entity.AnswerStatusPending) && + !isAdminModerator && answerInfo.UserID != loginUserID { + return nil, nil, false, errors.NotFound(reason.AnswerNotFound) + } info := as.ShowFormat(ctx, answerInfo) // todo questionFunc questionInfo, err := as.questionCommon.Info(ctx, answerInfo.QuestionID, loginUserID) diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go index 474c1fcd..73f66a4c 100644 --- a/internal/service/content/question_service.go +++ b/internal/service/content/question_service.go @@ -20,6 +20,7 @@ package content import ( + "context" "encoding/json" "fmt" "strings" @@ -64,7 +65,6 @@ import ( "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" - "golang.org/x/net/context" ) // QuestionRepo question repository diff --git a/internal/service/content/revision_service.go b/internal/service/content/revision_service.go index 83568088..b932ba8b 100644 --- a/internal/service/content/revision_service.go +++ b/internal/service/content/revision_service.go @@ -114,14 +114,20 @@ func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.Revisi if revisioninfo.Status != entity.RevisionUnreviewedStatus { return } + objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID) + if objectTypeerr != nil { + return objectTypeerr + } if req.Operation == schema.RevisionAuditReject { + if err = checkRevisionAuditPermission(req, objectType); err != nil { + return err + } err = rs.revisionRepo.UpdateStatus(ctx, req.ID, entity.RevisionReviewRejectStatus, req.UserID) return } if req.Operation == schema.RevisionAuditApprove { - objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(revisioninfo.ObjectID) - if objectTypeerr != nil { - return objectTypeerr + if err = checkRevisionAuditPermission(req, objectType); err != nil { + return err } revisionitem := &schema.GetRevisionResp{} _ = copier.Copy(revisionitem, revisioninfo) @@ -129,23 +135,11 @@ func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.Revisi var saveErr error switch objectType { case constant.QuestionObjectType: - if !req.CanReviewQuestion { - saveErr = errors.BadRequest(reason.RevisionNoPermission) - } else { - saveErr = rs.revisionAuditQuestion(ctx, revisionitem) - } + saveErr = rs.revisionAuditQuestion(ctx, revisionitem) case constant.AnswerObjectType: - if !req.CanReviewAnswer { - saveErr = errors.BadRequest(reason.RevisionNoPermission) - } else { - saveErr = rs.revisionAuditAnswer(ctx, revisionitem) - } + saveErr = rs.revisionAuditAnswer(ctx, revisionitem) case constant.TagObjectType: - if !req.CanReviewTag { - saveErr = errors.BadRequest(reason.RevisionNoPermission) - } else { - saveErr = rs.revisionAuditTag(ctx, revisionitem) - } + saveErr = rs.revisionAuditTag(ctx, revisionitem) } if saveErr != nil { return saveErr @@ -179,6 +173,24 @@ func (rs *RevisionService) RevisionAudit(ctx context.Context, req *schema.Revisi return nil } +func checkRevisionAuditPermission(req *schema.RevisionAuditReq, objectType string) error { + switch objectType { + case constant.QuestionObjectType: + if !req.CanReviewQuestion { + return errors.BadRequest(reason.RevisionNoPermission) + } + case constant.AnswerObjectType: + if !req.CanReviewAnswer { + return errors.BadRequest(reason.RevisionNoPermission) + } + case constant.TagObjectType: + if !req.CanReviewTag { + return errors.BadRequest(reason.RevisionNoPermission) + } + } + return nil +} + func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionitem *schema.GetRevisionResp) (err error) { questioninfo, ok := revisionitem.ContentParsed.(*schema.QuestionInfoResp) if ok { diff --git a/internal/service/content/user_service.go b/internal/service/content/user_service.go index d0ebe875..c1f800ff 100644 --- a/internal/service/content/user_service.go +++ b/internal/service/content/user_service.go @@ -360,7 +360,7 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req) - us.cleanUpRemovedAvatar(ctx, oldUserInfo.Avatar, cond.Avatar) + us.cleanUpRemovedAvatar(ctx, req.UserID, oldUserInfo.Avatar, cond.Avatar) err = us.userRepo.UpdateInfo(ctx, cond) if err != nil { @@ -407,6 +407,7 @@ func (us *UserService) validateAvatarInfo( func (us *UserService) cleanUpRemovedAvatar( ctx context.Context, + updatingUserID string, oldAvatarJSON string, newAvatarJSON string, ) { @@ -434,6 +435,13 @@ func (us *UserService) cleanUpRemovedAvatar( log.Warn("no file record found for old avatar url:", oldAvatar.Custom) return } + if fileRecord.UserID != updatingUserID || fileRecord.Source != string(plugin.UserAvatar) { + log.Warnf( + "refuse to clean avatar url %q: file record owner/source mismatch (owner=%s source=%s updating_user=%s)", + oldAvatar.Custom, fileRecord.UserID, fileRecord.Source, updatingUserID, + ) + return + } if err := us.fileRecordService.DeleteAndMoveFileRecord(ctx, fileRecord); err != nil { log.Error(err) } diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go index bb00b828..5b649354 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -20,6 +20,7 @@ package export import ( + "context" "crypto/tls" "encoding/json" "fmt" @@ -40,7 +41,6 @@ import ( "github.com/apache/answer/internal/service/siteinfo_common" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" - "golang.org/x/net/context" "gopkg.in/gomail.v2" ) diff --git a/internal/service/notification/new_question_notification_test.go b/internal/service/notification/new_question_notification_test.go index 754481f6..3bb6a3dd 100644 --- a/internal/service/notification/new_question_notification_test.go +++ b/internal/service/notification/new_question_notification_test.go @@ -798,3 +798,18 @@ func (newQuestionNotificationTestUserExternalLoginRepo) GetCacheUserExternalLogi context.Context, string) (*schema.ExternalLoginUserInfoCache, error) { return nil, nil } + +func (newQuestionNotificationTestUserExternalLoginRepo) SetCacheOAuthState( + context.Context, string, *schema.ExternalLoginOAuthState, time.Duration) error { + return nil +} + +func (newQuestionNotificationTestUserExternalLoginRepo) GetCacheOAuthState( + context.Context, string) (*schema.ExternalLoginOAuthState, error) { + return nil, nil +} + +func (newQuestionNotificationTestUserExternalLoginRepo) DeleteCacheOAuthState( + context.Context, string) error { + return nil +} diff --git a/internal/service/report/report_service.go b/internal/service/report/report_service.go index 84c15d59..3edbc1b4 100644 --- a/internal/service/report/report_service.go +++ b/internal/service/report/report_service.go @@ -20,6 +20,7 @@ package report import ( + "context" "encoding/json" "github.com/apache/answer/internal/service/eventqueue" @@ -44,7 +45,6 @@ import ( "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" - "golang.org/x/net/context" ) // ReportService user service diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go index fcced1c8..29e33804 100644 --- a/internal/service/user_admin/user_backyard.go +++ b/internal/service/user_admin/user_backyard.go @@ -45,6 +45,7 @@ import ( "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" + "github.com/apache/answer/internal/service/apikey" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/siteinfo_common" @@ -87,6 +88,7 @@ type UserAdminService struct { notificationRepo notificationcommon.NotificationRepo pluginUserConfigRepo plugin_common.PluginUserConfigRepo badgeAwardRepo badge.BadgeAwardRepo + apiKeyRepo apikey.APIKeyRepo } // NewUserAdminService new user admin service @@ -105,6 +107,7 @@ func NewUserAdminService( notificationRepo notificationcommon.NotificationRepo, pluginUserConfigRepo plugin_common.PluginUserConfigRepo, badgeAwardRepo badge.BadgeAwardRepo, + apiKeyRepo apikey.APIKeyRepo, ) *UserAdminService { return &UserAdminService{ userRepo: userRepo, @@ -121,6 +124,7 @@ func NewUserAdminService( notificationRepo: notificationRepo, pluginUserConfigRepo: pluginUserConfigRepo, badgeAwardRepo: badgeAwardRepo, + apiKeyRepo: apiKeyRepo, } } @@ -162,6 +166,11 @@ func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.Up if err != nil { return err } + if req.IsInactive() || req.IsSuspended() || req.IsDeleted() { + if err := us.revokeUserAPIKeys(ctx, userInfo.ID); err != nil { + return err + } + } // remove all content that user created, such as question, answer, comment, etc. if req.RemoveAllContent { @@ -227,11 +236,18 @@ func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.Upda if err != nil { return err } + if err := us.revokeUserAPIKeys(ctx, req.UserID); err != nil { + return err + } us.authService.RemoveUserAllTokens(ctx, req.UserID) return } +func (us *UserAdminService) revokeUserAPIKeys(ctx context.Context, userID string) error { + return us.apiKeyRepo.DeleteAPIKeysByUserID(ctx, userID) +} + // AddUser add user func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) { _, has, err := us.userRepo.GetUserInfoByEmail(ctx, req.Email) diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go index 3f82179b..08f17ad4 100644 --- a/internal/service/user_external_login/user_external_login_service.go +++ b/internal/service/user_external_login/user_external_login_service.go @@ -54,6 +54,10 @@ type UserExternalLoginRepo interface { DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error) SetCacheUserExternalLoginInfo(ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) GetCacheUserExternalLoginInfo(ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) + SetCacheOAuthState(ctx context.Context, state string, info *schema.ExternalLoginOAuthState, + duration time.Duration) (err error) + GetCacheOAuthState(ctx context.Context, state string) (info *schema.ExternalLoginOAuthState, err error) + DeleteCacheOAuthState(ctx context.Context, state string) (err error) } // UserExternalLoginService user external login service @@ -88,6 +92,41 @@ func NewUserExternalLoginService( } } +func (us *UserExternalLoginService) GenerateOAuthState( + ctx context.Context, provider, intent, userID string) (state string, err error) { + state = token.GenerateToken() + duration := constant.ConnectorOAuthStateCacheTime + if intent == schema.ExternalLoginOAuthStateBindIntent { + duration = constant.ConnectorOAuthBindStateCacheTime + } + err = us.userExternalLoginRepo.SetCacheOAuthState(ctx, state, &schema.ExternalLoginOAuthState{ + Provider: provider, + Intent: intent, + UserID: userID, + }, duration) + return state, err +} + +func (us *UserExternalLoginService) GetOAuthState( + ctx context.Context, state string) (info *schema.ExternalLoginOAuthState, err error) { + if len(state) == 0 { + return nil, nil + } + return us.userExternalLoginRepo.GetCacheOAuthState(ctx, state) +} + +func (us *UserExternalLoginService) ConsumeOAuthState( + ctx context.Context, state string) (info *schema.ExternalLoginOAuthState, err error) { + info, err = us.GetOAuthState(ctx, state) + if err != nil || info == nil { + return info, err + } + if err = us.userExternalLoginRepo.DeleteCacheOAuthState(ctx, state); err != nil { + log.Errorf("delete oauth state failed: %v", err) + } + return info, nil +} + // ExternalLogin if user is already a member logged in func (us *UserExternalLoginService) ExternalLogin( ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) ( @@ -147,16 +186,18 @@ func (us *UserExternalLoginService) ExternalLogin( }, nil } - oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email) - if err != nil { + if _, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email); err != nil { return nil, err + } else if exist { + return &schema.UserExternalLoginResp{ + ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), + ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied), + }, nil } // if user is not a member, register a new user - if !exist { - oldUserInfo, err = us.registerNewUser(ctx, externalUserInfo) - if err != nil { - return nil, err - } + oldUserInfo, err := us.registerNewUser(ctx, externalUserInfo) + if err != nil { + return nil, err } // bind external user info to user err = us.bindOldUser(ctx, externalUserInfo, oldUserInfo) @@ -176,10 +217,41 @@ func (us *UserExternalLoginService) ExternalLogin( } accessToken, _, err := us.userCommonService.CacheLoginUserInfo( - ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID) + ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, externalUserInfo.ExternalID) return &schema.UserExternalLoginResp{AccessToken: accessToken}, err } +func (us *UserExternalLoginService) BindExternalLoginToUser(ctx context.Context, + userID string, externalUserInfo *schema.ExternalLoginUserInfoCache) error { + if len(userID) == 0 || len(externalUserInfo.ExternalID) == 0 { + return errors.BadRequest(reason.UserAccessDenied) + } + oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, userID) + if err != nil { + return err + } + if !exist || oldUserInfo.Status == entity.UserStatusDeleted { + return errors.BadRequest(reason.UserNotFound) + } + oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx, + externalUserInfo.Provider, externalUserInfo.ExternalID) + if err != nil { + return err + } + if exist && oldExternalLoginUserInfo.UserID != userID { + return errors.BadRequest(reason.UserAccessDenied) + } + currentExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByUserID(ctx, + externalUserInfo.Provider, userID) + if err != nil { + return err + } + if exist && currentExternalLoginUserInfo.ExternalID != externalUserInfo.ExternalID { + return errors.BadRequest(reason.UserAccessDenied) + } + return us.bindOldUser(ctx, externalUserInfo, oldUserInfo) +} + func (us *UserExternalLoginService) registerNewUser(ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) (userInfo *entity.User, err error) { userInfo = &entity.User{} @@ -289,26 +361,25 @@ func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail( return &schema.ExternalLoginBindingUserSendEmailResp{}, nil } - userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email) - if err != nil { + if _, exist, err := us.userRepo.GetByEmail(ctx, req.Email); err != nil { return nil, err - } - if exist && !req.Must { + } else if exist && !req.Must { + resp.EmailExistAndMustBeConfirmed = true + return resp, nil + } else if exist { resp.EmailExistAndMustBeConfirmed = true return resp, nil } - if !exist { - externalLoginInfo.Email = req.Email - userInfo, err = us.registerNewUser(ctx, externalLoginInfo) - if err != nil { - return nil, err - } - resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo( - ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID) - if err != nil { - log.Error(err) - } + externalLoginInfo.Email = req.Email + userInfo, err := us.registerNewUser(ctx, externalLoginInfo) + if err != nil { + return nil, err + } + resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo( + ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID) + if err != nil { + log.Error(err) } err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, req.BindingKey, externalLoginInfo) if err != nil { @@ -340,6 +411,9 @@ func (us *UserExternalLoginService) ExternalLoginBindingUser( if err != nil || externalLoginInfo == nil { return errors.BadRequest(reason.UserNotFound) } + if len(externalLoginInfo.Email) == 0 || externalLoginInfo.Email != oldUserInfo.EMail { + return errors.BadRequest(reason.UserAccessDenied) + } return us.bindOldUser(ctx, externalLoginInfo, oldUserInfo) }