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:
LinkinStars
2026-07-29 17:19:20 +08:00
34 changed files with 421 additions and 182 deletions
@@ -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}" \
.
+19 -23
View File
@@ -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 }}" \
.
+21 -23
View File
@@ -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}" \
.
+16 -25
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -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)
+7 -7
View File
@@ -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
+16 -16
View File
@@ -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=
+3
View File
@@ -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)
+2 -4
View File
@@ -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.
+2 -4
View File
@@ -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()
}
+61 -5
View File
@@ -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)
}
+6
View File
@@ -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,
+2 -1
View File
@@ -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) {
+6 -2
View File
@@ -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()
+8
View File
@@ -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
}
+1
View File
@@ -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)
+1 -1
View File
@@ -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
+30 -18
View File
@@ -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 {
+9 -1
View File
@@ -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)
}
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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)
}