Merge remote-tracking branch 'origin/main' into dev
# Conflicts: # docs/release/LICENSE # internal/service/notification/new_question_notification_test.go
This commit is contained in:
@@ -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}" \
|
||||
.
|
||||
|
||||
@@ -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 }}" \
|
||||
.
|
||||
|
||||
@@ -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}" \
|
||||
.
|
||||
|
||||
@@ -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}" \
|
||||
.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user