Compare commits
378 Commits
v1.0.9
...
v1.1.0-beta.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0aa98fb2af | |||
| dd14fa7b52 | |||
| adc75083b2 | |||
| 26ad0dd3c3 | |||
| fb3b6a862f | |||
| b0a04bbf11 | |||
| 1de9ef6a89 | |||
| d61e0dab9e | |||
| dacc27e373 | |||
| 3a6439a65d | |||
| 4f468b58d0 | |||
| b06695f2e1 | |||
| 58bec8ddd1 | |||
| ba66ae9d4d | |||
| efb78124a9 | |||
| 016fd7b412 | |||
| 140550795f | |||
| 54c07ba829 | |||
| fcb84b7dfb | |||
| 225d766f9c | |||
| 7547d9f91e | |||
| bc9c4ef1b9 | |||
| 5f113285a1 | |||
| 16f2de0ab4 | |||
| b50835f04e | |||
| 73598e8af6 | |||
| dd47eeced6 | |||
| e45a5f151f | |||
| fafdc5435a | |||
| 276e87af08 | |||
| 2672f43525 | |||
| 85e370d7ff | |||
| c56d54b0fe | |||
| 7d722cde4f | |||
| 9afb26fcba | |||
| 3952439d49 | |||
| c02709a337 | |||
| f46af67b2c | |||
| c843dddf0b | |||
| ba475ddfbb | |||
| cba45d1eaa | |||
| c7c2aad205 | |||
| 9d304f0186 | |||
| 9f2607cf35 | |||
| e67dd59ab2 | |||
| 40ac54eb46 | |||
| e3a49132b2 | |||
| db50e33db8 | |||
| 65e5af6482 | |||
| eeb67dd21e | |||
| b56aa16641 | |||
| 78850be999 | |||
| 3eaa2b6293 | |||
| b509739b35 | |||
| 64f2fc70cd | |||
| b26da16ced | |||
| ef47d3da01 | |||
| 29fddeca35 | |||
| 758a7cde95 | |||
| 05898157c8 | |||
| bdd00e636b | |||
| f28b743519 | |||
| e377daaac9 | |||
| f89687d021 | |||
| df9b08abcf | |||
| 496df590e8 | |||
| 0f8c4c79b4 | |||
| 6b53609caf | |||
| ddda7e6d5a | |||
| 9a07d5092a | |||
| 6b2f191d49 | |||
| 201a776cd6 | |||
| f391d3a8e8 | |||
| 76dfc362c9 | |||
| 031954520c | |||
| aaeca560e5 | |||
| 18d25fddbe | |||
| a09e2fbe8a | |||
| 5a4c22c393 | |||
| f024f7aa86 | |||
| 6f32e51319 | |||
| d672e31815 | |||
| 55aeec174c | |||
| 779bd86e1d | |||
| 3f4ed4527a | |||
| f04d5a03f8 | |||
| ddaf4b7863 | |||
| 587f0aa1e3 | |||
| c3c7eb962d | |||
| c37987683b | |||
| 2115a4a4ba | |||
| 8be0b2bffd | |||
| 096c949ae0 | |||
| 16bb4d89c1 | |||
| f844978662 | |||
| f6b98f65be | |||
| 4fa0949abc | |||
| 0292cbb92b | |||
| 3b5c3fc3ff | |||
| e95e2f3a2f | |||
| 1d95b3b510 | |||
| 6349f42234 | |||
| 971bf4da61 | |||
| 62b5008bc5 | |||
| f0ddd30e5d | |||
| b24fc1ff22 | |||
| d7c6172cb4 | |||
| bed55985dd | |||
| 499cfe9917 | |||
| 2e77289428 | |||
| 54497ed932 | |||
| 7824797648 | |||
| f500da413c | |||
| d5f95c5bde | |||
| 9f9258fccd | |||
| e49b6fbb54 | |||
| c5126b4fe0 | |||
| 95a58e19a7 | |||
| 90cd9de7fe | |||
| 9edfde5759 | |||
| 2498ab139a | |||
| 083c5a8a4c | |||
| af08b3e392 | |||
| 40cdca8adf | |||
| 3c96abfcc5 | |||
| cec02e8000 | |||
| 045cd0b1e8 | |||
| 944e773392 | |||
| 2db93af38b | |||
| 86c4c685ee | |||
| 0f5e9845f9 | |||
| 1bbe005f66 | |||
| 128d5c9f87 | |||
| d3f02a35d9 | |||
| 2253a87aea | |||
| f859aa2fba | |||
| c8cf70c112 | |||
| 61cb84ac06 | |||
| 8adc975699 | |||
| cfafd3b3fb | |||
| 67cbe2bd83 | |||
| b215a5b738 | |||
| 8c45d75968 | |||
| 726fab77ac | |||
| 703e76ba7b | |||
| 24afb5e21b | |||
| 8fb303d024 | |||
| a045060b76 | |||
| f297330249 | |||
| fc9de356d6 | |||
| 1d3f61e8a6 | |||
| b5ef7912d1 | |||
| 901f2cb232 | |||
| fd2c5f9946 | |||
| 1799f9e7db | |||
| 9e0999ba55 | |||
| 86685202ac | |||
| 54b7de30a0 | |||
| f50ccea7e5 | |||
| 095bc79758 | |||
| 1402d9e5dc | |||
| 6350cb861e | |||
| 7445dbc336 | |||
| a38c6a833f | |||
| 673ebfbb78 | |||
| 3def6cabd8 | |||
| 357d474503 | |||
| 42faac87ee | |||
| fa0efd82a8 | |||
| cdf5ffa0a8 | |||
| 4c4381e140 | |||
| 8800b376d5 | |||
| 910484013c | |||
| 7bfb5f0126 | |||
| fbb29a0b74 | |||
| 9da53361a7 | |||
| 10afa0a47b | |||
| 285a44be1b | |||
| 2bd0ef8a24 | |||
| c62436a703 | |||
| 36e183dd79 | |||
| 336f54ff49 | |||
| a5a3664f90 | |||
| a2ba1144ee | |||
| f71ece495c | |||
| 0f7121d6fe | |||
| e280358b7c | |||
| ec4c7ab6a7 | |||
| f02f62112a | |||
| a3cc0ac38a | |||
| 2e8e6be9af | |||
| 8584a0e77b | |||
| f37218bc70 | |||
| cfd017de26 | |||
| b68212e840 | |||
| d641bdf11e | |||
| 4cfa0cd8fe | |||
| 6531cf9e6c | |||
| 6ac96948c3 | |||
| 83c2c24683 | |||
| f50414adcf | |||
| a9381056e9 | |||
| 9868b2d9c0 | |||
| 0c3efb66ab | |||
| 7527b62d28 | |||
| 759f1491c9 | |||
| 1c235dbd9b | |||
| 09e4ee10cb | |||
| 6211b468b3 | |||
| b4870d066c | |||
| 9a6f770062 | |||
| 7721946922 | |||
| fd31a26b7b | |||
| 388d1691b0 | |||
| 056fbd8990 | |||
| 71537ec62c | |||
| e914db6a9b | |||
| e3ff77a8ef | |||
| ad13b97eaf | |||
| b046c8005b | |||
| 391ec35fac | |||
| 2a121c573b | |||
| eb0e54c724 | |||
| 6daaa69a93 | |||
| a7d39a1c81 | |||
| 31af841777 | |||
| 24d9b8a641 | |||
| fd13b3580d | |||
| e92e7070f0 | |||
| ad733dc505 | |||
| 8d4bbfc90b | |||
| be22b49139 | |||
| 5d1bfeac6d | |||
| b7977b1367 | |||
| e5df07ad7c | |||
| aa13657e8d | |||
| 481901efd7 | |||
| 9fc8511bf5 | |||
| 934b82de30 | |||
| b47fb3da1c | |||
| 4b1f985be7 | |||
| 528cba58e1 | |||
| e8e5bdf181 | |||
| 4de996ed07 | |||
| 2abc096176 | |||
| ad20a8ff1f | |||
| c976b79bfd | |||
| 072bf002c2 | |||
| 689297e14d | |||
| 28e9916468 | |||
| 9bd9b475f4 | |||
| f4212e2cd0 | |||
| 9950ed5a40 | |||
| 2aae2f771c | |||
| 41404070a7 | |||
| a7ff4cf3b7 | |||
| 38226bf3a0 | |||
| 1ec3f2b704 | |||
| 5e6fb1096e | |||
| 098873eea6 | |||
| 824b68c152 | |||
| 40fb10b4d9 | |||
| 210d26289a | |||
| 71d439cca0 | |||
| 4b78e3583f | |||
| bfc0bb5ac2 | |||
| 6b07d08838 | |||
| 91f5f3290c | |||
| 89ed70f5a8 | |||
| 19108b2124 | |||
| ecf24b5a96 | |||
| 8a57057d9a | |||
| b44824cf78 | |||
| 9c68284623 | |||
| 675c2868e4 | |||
| adcb47a5e1 | |||
| 1f05338aae | |||
| d5b5d9a51c | |||
| de4d156ac9 | |||
| 3312b352d2 | |||
| 4a8ae2b166 | |||
| 2ebf057d53 | |||
| b876b102e2 | |||
| 896e204ae0 | |||
| 666471ec24 | |||
| a1bdb2bd80 | |||
| 4c38c73017 | |||
| 1065946c43 | |||
| 334a1eceaa | |||
| 66ace7edcd | |||
| 8fe50e93f2 | |||
| 1e955a3495 | |||
| 7ded5056f0 | |||
| 17c621bfbb | |||
| 822a96721a | |||
| 73dc7e744f | |||
| 4809bab5c7 | |||
| 5bd8b1895b | |||
| ffb1e07acb | |||
| 9409db902a | |||
| 29da80a11e | |||
| f7e3e59822 | |||
| 26c9ff6998 | |||
| 57843bc0ce | |||
| 3b9070cf4f | |||
| a166ea8eb3 | |||
| e899f4b362 | |||
| 8df84b7684 | |||
| 6c70bc814e | |||
| 593ffad92d | |||
| 373b454728 | |||
| b229d12bb4 | |||
| a148607763 | |||
| 3c2c13f7c4 | |||
| 12109fdb24 | |||
| daa2a785e9 | |||
| ff64dabd16 | |||
| 3fbecefdf3 | |||
| f9a1ffb7e5 | |||
| 2fe00d4630 | |||
| 9c2d2b1df4 | |||
| dfcd64e2a4 | |||
| e8246433a2 | |||
| ea4c67cf2a | |||
| 28e17d95c3 | |||
| 47695b90cf | |||
| 9de92822f3 | |||
| 7a78428cd5 | |||
| 4f0ec66710 | |||
| d538b531bd | |||
| b4ae373309 | |||
| e3bc029fd2 | |||
| ebaaaff4f1 | |||
| 6a8d5e5984 | |||
| 44480d8986 | |||
| e0fb22a62d | |||
| 7e3adc9795 | |||
| 4db1250193 | |||
| d608145266 | |||
| d0bd2f4a98 | |||
| bdaf254af1 | |||
| 1802e6fa2e | |||
| 87f649faf7 | |||
| 3ccdc8670e | |||
| 406ee34fa8 | |||
| 8d0bd2be20 | |||
| ace2e72e77 | |||
| 62bfb2b3f6 | |||
| 487c6def61 | |||
| 3466da2b0c | |||
| 703ff79ed8 | |||
| 2e5eca08f1 | |||
| b35d2564b5 | |||
| be1da88036 | |||
| be1811294e | |||
| 29aa3f7ef2 | |||
| 152d8d2f68 | |||
| b8cfde0425 | |||
| 0529c10c54 | |||
| 3285d54ee9 | |||
| 479892b156 | |||
| 3199693d44 | |||
| a88b476803 | |||
| 0a1f1b1372 | |||
| 7331b49623 | |||
| c08051ec27 | |||
| 8575bbf8c8 | |||
| fe6a53fb62 | |||
| 6a3e6da237 | |||
| d02d086d45 | |||
| d3a803d8a6 | |||
| ec01c3e047 | |||
| 06db2bd091 | |||
| 1db4f5586b | |||
| 01c02ae2b7 | |||
| 81d786ff66 | |||
| 1664eb7d16 | |||
| 154fa84bbb |
+8
-3
@@ -4,6 +4,7 @@ LABEL maintainer="mingcheng<mc@sf.com>"
|
||||
|
||||
COPY . /answer
|
||||
WORKDIR /answer
|
||||
RUN node -v
|
||||
RUN make install-ui-packages ui && mv ui/build /tmp
|
||||
|
||||
# stage2 build the main binary within static resource
|
||||
@@ -11,12 +12,14 @@ FROM golang:1.19-alpine AS golang-builder
|
||||
LABEL maintainer="aichy@sf.com"
|
||||
|
||||
ARG GOPROXY
|
||||
# ENV GOPROXY ${GOPROXY:-direct}
|
||||
ENV GOPROXY=https://goproxy.io,direct
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV GOROOT /usr/local/go
|
||||
ENV PACKAGE github.com/answerdev/answer
|
||||
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
|
||||
ENV ANSWER_MODULE ${BUILD_DIR}
|
||||
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
ENV TAGS "bindata timetzdata $TAGS"
|
||||
@@ -25,9 +28,11 @@ ARG CGO_EXTRA_CFLAGS
|
||||
COPY . ${BUILD_DIR}
|
||||
WORKDIR ${BUILD_DIR}
|
||||
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
|
||||
RUN apk --no-cache add build-base git \
|
||||
&& make clean build \
|
||||
&& cp answer /usr/bin/answer
|
||||
RUN apk --no-cache add build-base git bash \
|
||||
&& make clean build
|
||||
RUN chmod 755 answer
|
||||
RUN ["/bin/bash","-c","script/build_plugin.sh"]
|
||||
RUN cp answer /usr/bin/answer
|
||||
|
||||
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
|
||||
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
|
||||
|
||||
@@ -26,5 +26,6 @@ tmp
|
||||
vendor/
|
||||
/answer-data/
|
||||
/answer
|
||||
/new_answer
|
||||
|
||||
dist/
|
||||
|
||||
+8
-4
@@ -13,12 +13,14 @@ FROM golang:1.19-alpine AS golang-builder
|
||||
LABEL maintainer="aichy@sf.com"
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY ${GOPROXY:-direct}
|
||||
# ENV GOPROXY ${GOPROXY:-direct}
|
||||
ENV GOPROXY=https://goproxy.io,direct
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV GOROOT /usr/local/go
|
||||
ENV PACKAGE github.com/answerdev/answer
|
||||
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
|
||||
ENV ANSWER_MODULE ${BUILD_DIR}
|
||||
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
ENV TAGS "bindata timetzdata $TAGS"
|
||||
@@ -27,9 +29,11 @@ ARG CGO_EXTRA_CFLAGS
|
||||
COPY . ${BUILD_DIR}
|
||||
WORKDIR ${BUILD_DIR}
|
||||
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
|
||||
RUN apk --no-cache add build-base git \
|
||||
&& make clean build \
|
||||
&& cp answer /usr/bin/answer
|
||||
RUN apk --no-cache add build-base git bash \
|
||||
&& make clean build
|
||||
RUN chmod 755 answer
|
||||
RUN ["/bin/bash","-c","script/build_plugin.sh"]
|
||||
RUN cp answer /usr/bin/answer
|
||||
|
||||
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
|
||||
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
.PHONY: build clean ui
|
||||
|
||||
VERSION=1.0.9
|
||||
VERSION=1.1.0
|
||||
BIN=answer
|
||||
DIR_SRC=./cmd/answer
|
||||
DOCKER_CMD=docker
|
||||
|
||||
GO_ENV=CGO_ENABLED=0 GO111MODULE=on
|
||||
Revision=$(shell git rev-parse --short HEAD)
|
||||
GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X 'main.Revision=$(Revision)' -X 'main.Time=`date`' -extldflags -static"
|
||||
GO_FLAGS=-ldflags="-X github.com/answerdev/answer/cmd.Version=$(VERSION) -X 'github.com/answerdev/answer/cmd.Revision=$(Revision)' -X 'github.com/answerdev/answer/cmd.Time=`date +%s`' -extldflags -static"
|
||||
GO=$(GO_ENV) $(shell which go)
|
||||
|
||||
build: generate
|
||||
|
||||
+2
-62
@@ -1,72 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/cron"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman"
|
||||
"github.com/segmentfault/pacman/contrib/log/zap"
|
||||
"github.com/segmentfault/pacman/contrib/server/http"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// go build -ldflags "-X main.Version=x.y.z"
|
||||
var (
|
||||
// Name is the name of the project
|
||||
Name = "answer"
|
||||
// Version is the version of the project
|
||||
Version = "0.0.0"
|
||||
// Revision is the git short commit revision number
|
||||
Revision = ""
|
||||
// Time is the build time of the project
|
||||
Time = ""
|
||||
// log level
|
||||
logLevel = os.Getenv("LOG_LEVEL")
|
||||
// log path
|
||||
logPath = os.Getenv("LOG_PATH")
|
||||
answercmd "github.com/answerdev/answer/cmd"
|
||||
)
|
||||
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
func main() {
|
||||
log.SetLogger(zap.NewLogger(
|
||||
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
|
||||
Execute()
|
||||
}
|
||||
|
||||
func runApp() {
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conf.GetPathIgnoreList()
|
||||
app, cleanup, err := initApplication(
|
||||
c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
constant.Version = Version
|
||||
constant.Revision = Revision
|
||||
schema.AppStartTime = time.Now()
|
||||
|
||||
defer cleanup()
|
||||
if err := app.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application {
|
||||
manager.Run()
|
||||
return pacman.NewApp(
|
||||
pacman.WithName(Name),
|
||||
pacman.WithVersion(Version),
|
||||
pacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)),
|
||||
)
|
||||
answercmd.Main()
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package main
|
||||
package answercmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/install"
|
||||
"github.com/answerdev/answer/internal/migrations"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -16,6 +18,10 @@ var (
|
||||
dataDirPath string
|
||||
// dumpDataPath dump data path
|
||||
dumpDataPath string
|
||||
// plugins needed to build in answer application
|
||||
buildWithPlugins []string
|
||||
// build output path
|
||||
buildOutput string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -25,7 +31,11 @@ func init() {
|
||||
|
||||
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
|
||||
|
||||
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd} {
|
||||
buildCmd.Flags().StringSliceVarP(&buildWithPlugins, "with", "w", []string{}, "plugins needed to build")
|
||||
|
||||
buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "build output path")
|
||||
|
||||
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd} {
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
}
|
||||
@@ -160,10 +170,44 @@ To run answer, use:
|
||||
fmt.Println("check environment all done")
|
||||
},
|
||||
}
|
||||
|
||||
// buildCmd used to build another answer with plugins
|
||||
buildCmd = &cobra.Command{
|
||||
Use: "build",
|
||||
Short: "used to build answer with plugins",
|
||||
Long: `Build a new Answer with plugins that you need`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Printf("try to build a new answer with plugins:\n%s\n", strings.Join(buildWithPlugins, "\n"))
|
||||
err := cli.BuildNewAnswer(buildOutput, buildWithPlugins, cli.OriginalAnswerInfo{
|
||||
Version: Version,
|
||||
Revision: Revision,
|
||||
Time: Time,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("build failed %v", err)
|
||||
} else {
|
||||
fmt.Printf("build new answer successfully %s\n", buildOutput)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// pluginCmd prints all plugins packed in the binary
|
||||
pluginCmd = &cobra.Command{
|
||||
Use: "plugin",
|
||||
Short: "prints all plugins packed in the binary",
|
||||
Long: `prints all plugins packed in the binary`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
_ = plugin.CallBase(func(base plugin.Base) error {
|
||||
info := base.Info()
|
||||
fmt.Printf("%s[%s] made by %s\n", info.SlugName, info.Version, info.Author)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
// This is called by main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package answercmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/cron"
|
||||
"github.com/answerdev/answer/internal/cli"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman"
|
||||
"github.com/segmentfault/pacman/contrib/log/zap"
|
||||
"github.com/segmentfault/pacman/contrib/server/http"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// go build -ldflags "-X github.com/answerdev/answer/cmd.Version=x.y.z"
|
||||
var (
|
||||
// Name is the name of the project
|
||||
Name = "answer"
|
||||
// Version is the version of the project
|
||||
Version = "0.0.0"
|
||||
// Revision is the git short commit revision number
|
||||
Revision = "-"
|
||||
// Time is the build time of the project
|
||||
Time = "-"
|
||||
// log level
|
||||
logLevel = os.Getenv("LOG_LEVEL")
|
||||
// log path
|
||||
logPath = os.Getenv("LOG_PATH")
|
||||
)
|
||||
|
||||
// Main
|
||||
// @securityDefinitions.apikey ApiKeyAuth
|
||||
// @in header
|
||||
// @name Authorization
|
||||
func Main() {
|
||||
log.SetLogger(zap.NewLogger(
|
||||
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
|
||||
Execute()
|
||||
}
|
||||
|
||||
func runApp() {
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conf.GetPathIgnoreList()
|
||||
app, cleanup, err := initApplication(
|
||||
c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
constant.Version = Version
|
||||
constant.Revision = Revision
|
||||
schema.AppStartTime = time.Now()
|
||||
fmt.Println("answer Version:", constant.Version, " Revision:", constant.Revision)
|
||||
|
||||
defer cleanup()
|
||||
if err := app.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application {
|
||||
manager.Run()
|
||||
return pacman.NewApp(
|
||||
pacman.WithName(Name),
|
||||
pacman.WithVersion(Version),
|
||||
pacman.WithServer(http.NewServer(server, serverConf.HTTP.Addr)),
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
// The build tag makes sure the stub is not built in the final build.
|
||||
|
||||
package main
|
||||
package answercmd
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
@@ -4,7 +4,7 @@
|
||||
//go:build !wireinject
|
||||
// +build !wireinject
|
||||
|
||||
package main
|
||||
package answercmd
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/conf"
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/repo/export"
|
||||
"github.com/answerdev/answer/internal/repo/meta"
|
||||
"github.com/answerdev/answer/internal/repo/notification"
|
||||
"github.com/answerdev/answer/internal/repo/plugin_config"
|
||||
"github.com/answerdev/answer/internal/repo/question"
|
||||
"github.com/answerdev/answer/internal/repo/rank"
|
||||
"github.com/answerdev/answer/internal/repo/reason"
|
||||
@@ -40,6 +41,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/repo/tag_common"
|
||||
"github.com/answerdev/answer/internal/repo/unique"
|
||||
"github.com/answerdev/answer/internal/repo/user"
|
||||
"github.com/answerdev/answer/internal/repo/user_external_login"
|
||||
"github.com/answerdev/answer/internal/router"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/action"
|
||||
@@ -57,6 +59,7 @@ import (
|
||||
notification2 "github.com/answerdev/answer/internal/service/notification"
|
||||
"github.com/answerdev/answer/internal/service/notification_common"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
"github.com/answerdev/answer/internal/service/plugin_common"
|
||||
"github.com/answerdev/answer/internal/service/question_common"
|
||||
rank2 "github.com/answerdev/answer/internal/service/rank"
|
||||
reason2 "github.com/answerdev/answer/internal/service/reason"
|
||||
@@ -74,6 +77,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
"github.com/answerdev/answer/internal/service/user_admin"
|
||||
"github.com/answerdev/answer/internal/service/user_common"
|
||||
user_external_login2 "github.com/answerdev/answer/internal/service/user_external_login"
|
||||
"github.com/segmentfault/pacman"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
@@ -117,8 +121,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||
roleRepo := role.NewRoleRepo(dataData)
|
||||
roleService := role2.NewRoleService(roleRepo)
|
||||
userRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService)
|
||||
userCommon := usercommon.NewUserCommon(userRepo)
|
||||
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, serviceConf, siteInfoCommonService, userRoleRelService, userCommon)
|
||||
userCommon := usercommon.NewUserCommon(userRepo, userRoleRelService, authService)
|
||||
userExternalLoginRepo := user_external_login.NewUserExternalLoginRepo(dataData)
|
||||
userExternalLoginService := user_external_login2.NewUserExternalLoginService(userRepo, userCommon, userExternalLoginRepo, emailService, siteInfoCommonService, userActiveActivityRepo)
|
||||
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, serviceConf, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService)
|
||||
captchaRepo := captcha.NewCaptchaRepo(dataData)
|
||||
captchaService := action.NewCaptchaService(captchaRepo)
|
||||
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
|
||||
@@ -187,7 +193,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||
reasonService := reason2.NewReasonService(reasonRepo)
|
||||
reasonController := controller.NewReasonController(reasonService)
|
||||
themeController := controller_admin.NewThemeController()
|
||||
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService)
|
||||
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configRepo)
|
||||
siteInfoController := controller_admin.NewSiteInfoController(siteInfoService)
|
||||
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
|
||||
notificationRepo := notification.NewNotificationRepo(dataData)
|
||||
@@ -202,7 +208,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService)
|
||||
activityController := controller.NewActivityController(activityCommon, activityService)
|
||||
roleController := controller_admin.NewRoleController(roleService)
|
||||
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController)
|
||||
pluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData)
|
||||
pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, configRepo)
|
||||
pluginController := controller_admin.NewPluginController(pluginCommonService)
|
||||
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController)
|
||||
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
|
||||
uiRouter := router.NewUIRouter(siteinfoController, siteInfoCommonService)
|
||||
authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)
|
||||
@@ -210,7 +219,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||
templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, dataData, siteInfoCommonService)
|
||||
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService)
|
||||
templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController)
|
||||
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter)
|
||||
connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService)
|
||||
userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService)
|
||||
userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService)
|
||||
pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController)
|
||||
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter, pluginAPIRouter)
|
||||
scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService)
|
||||
application := newApplication(serverConf, ginEngine, scheduledTaskManager)
|
||||
return application, func() {
|
||||
@@ -1,5 +1,6 @@
|
||||
# url path reserves the keywords list
|
||||
users:
|
||||
- unsubscribe
|
||||
- settings
|
||||
- login
|
||||
- register
|
||||
@@ -9,5 +10,7 @@ users:
|
||||
- account-activation
|
||||
- confirm-new-email
|
||||
- account-suspended
|
||||
- confirm-email
|
||||
- auth-landing
|
||||
questions:
|
||||
- ask
|
||||
|
||||
+945
-40
File diff suppressed because it is too large
Load Diff
+945
-40
File diff suppressed because it is too large
Load Diff
+595
-32
@@ -1,4 +1,13 @@
|
||||
definitions:
|
||||
constant.Privilege:
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
value:
|
||||
type: integer
|
||||
type: object
|
||||
handler.RespBody:
|
||||
properties:
|
||||
code:
|
||||
@@ -305,6 +314,104 @@ definitions:
|
||||
switch:
|
||||
type: boolean
|
||||
type: object
|
||||
schema.ConfigField:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
options:
|
||||
items:
|
||||
$ref: '#/definitions/schema.ConfigFieldOption'
|
||||
type: array
|
||||
required:
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
ui_options:
|
||||
$ref: '#/definitions/schema.ConfigFieldUIOptions'
|
||||
value: {}
|
||||
type: object
|
||||
schema.ConfigFieldOption:
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
schema.ConfigFieldUIOptions:
|
||||
properties:
|
||||
action:
|
||||
$ref: '#/definitions/schema.UIOptionAction'
|
||||
input_type:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
placeholder:
|
||||
type: string
|
||||
rows:
|
||||
type: string
|
||||
text:
|
||||
type: string
|
||||
variant:
|
||||
type: string
|
||||
type: object
|
||||
schema.ConnectorInfoResp:
|
||||
properties:
|
||||
icon:
|
||||
type: string
|
||||
link:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
schema.ConnectorUserInfoResp:
|
||||
properties:
|
||||
binding:
|
||||
type: boolean
|
||||
external_id:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
link:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
schema.ExternalLoginBindingUserSendEmailReq:
|
||||
properties:
|
||||
binding_key:
|
||||
maxLength: 100
|
||||
type: string
|
||||
email:
|
||||
maxLength: 512
|
||||
type: string
|
||||
must:
|
||||
description: |-
|
||||
If must is true, whatever email if exists, try to bind user.
|
||||
If must is false, when email exist, will only be prompted with a warning.
|
||||
type: boolean
|
||||
required:
|
||||
- binding_key
|
||||
- email
|
||||
type: object
|
||||
schema.ExternalLoginBindingUserSendEmailResp:
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
email_exist_and_must_be_confirmed:
|
||||
type: boolean
|
||||
type: object
|
||||
schema.ExternalLoginUnbindingReq:
|
||||
properties:
|
||||
external_id:
|
||||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- external_id
|
||||
type: object
|
||||
schema.FollowReq:
|
||||
properties:
|
||||
is_cancel:
|
||||
@@ -507,6 +614,47 @@ definitions:
|
||||
info:
|
||||
$ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp'
|
||||
type: object
|
||||
schema.GetPluginConfigResp:
|
||||
properties:
|
||||
config_fields:
|
||||
items:
|
||||
$ref: '#/definitions/schema.ConfigField'
|
||||
type: array
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
slug_name:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
schema.GetPluginListResp:
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
have_config:
|
||||
type: boolean
|
||||
link:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
slug_name:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
schema.GetPrivilegesConfigResp:
|
||||
properties:
|
||||
options:
|
||||
items:
|
||||
$ref: '#/definitions/schema.PrivilegeOption'
|
||||
type: array
|
||||
selected_level:
|
||||
type: integer
|
||||
type: object
|
||||
schema.GetRankPersonalWithPageResp:
|
||||
properties:
|
||||
answer_id:
|
||||
@@ -820,6 +968,9 @@ definitions:
|
||||
follow_count:
|
||||
description: follow count
|
||||
type: integer
|
||||
have_password:
|
||||
description: user have password
|
||||
type: boolean
|
||||
id:
|
||||
description: user id
|
||||
type: string
|
||||
@@ -894,6 +1045,8 @@ definitions:
|
||||
follow_count:
|
||||
description: follow count
|
||||
type: integer
|
||||
have_password:
|
||||
type: boolean
|
||||
id:
|
||||
description: user id
|
||||
type: string
|
||||
@@ -972,6 +1125,13 @@ definitions:
|
||||
description: vote type
|
||||
type: string
|
||||
type: object
|
||||
schema.LoadingAction:
|
||||
properties:
|
||||
state:
|
||||
type: string
|
||||
text:
|
||||
type: string
|
||||
type: object
|
||||
schema.NotificationClearIDRequest:
|
||||
properties:
|
||||
id:
|
||||
@@ -983,6 +1143,13 @@ definitions:
|
||||
description: inbox achievement
|
||||
type: string
|
||||
type: object
|
||||
schema.OnCompleteAction:
|
||||
properties:
|
||||
refresh_form_config:
|
||||
type: boolean
|
||||
toast_return_message:
|
||||
type: boolean
|
||||
type: object
|
||||
schema.OperationQuestionReq:
|
||||
properties:
|
||||
id:
|
||||
@@ -1007,6 +1174,17 @@ definitions:
|
||||
content:
|
||||
type: string
|
||||
type: object
|
||||
schema.PrivilegeOption:
|
||||
properties:
|
||||
level:
|
||||
type: integer
|
||||
level_desc:
|
||||
type: string
|
||||
privileges:
|
||||
items:
|
||||
$ref: '#/definitions/constant.Privilege'
|
||||
type: array
|
||||
type: object
|
||||
schema.QuestionAdd:
|
||||
properties:
|
||||
content:
|
||||
@@ -1058,6 +1236,9 @@ definitions:
|
||||
type: object
|
||||
schema.QuestionPageReq:
|
||||
properties:
|
||||
inDays:
|
||||
minimum: 1
|
||||
type: integer
|
||||
orderCond:
|
||||
enum:
|
||||
- newest
|
||||
@@ -1325,6 +1506,9 @@ definitions:
|
||||
custom_header:
|
||||
maxLength: 65536
|
||||
type: string
|
||||
custom_sidebar:
|
||||
maxLength: 65536
|
||||
type: string
|
||||
type: object
|
||||
schema.SiteCustomCssHTMLResp:
|
||||
properties:
|
||||
@@ -1340,6 +1524,9 @@ definitions:
|
||||
custom_header:
|
||||
maxLength: 65536
|
||||
type: string
|
||||
custom_sidebar:
|
||||
maxLength: 65536
|
||||
type: string
|
||||
type: object
|
||||
schema.SiteGeneralReq:
|
||||
properties:
|
||||
@@ -1401,6 +1588,8 @@ definitions:
|
||||
type: string
|
||||
site_seo:
|
||||
$ref: '#/definitions/schema.SiteSeoReq'
|
||||
site_users:
|
||||
$ref: '#/definitions/schema.SiteUsersResp'
|
||||
theme:
|
||||
$ref: '#/definitions/schema.SiteThemeResp'
|
||||
version:
|
||||
@@ -1408,11 +1597,6 @@ definitions:
|
||||
type: object
|
||||
schema.SiteInterfaceReq:
|
||||
properties:
|
||||
default_avatar:
|
||||
enum:
|
||||
- system
|
||||
- gravatar
|
||||
type: string
|
||||
language:
|
||||
maxLength: 128
|
||||
type: string
|
||||
@@ -1420,17 +1604,11 @@ definitions:
|
||||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- default_avatar
|
||||
- language
|
||||
- time_zone
|
||||
type: object
|
||||
schema.SiteInterfaceResp:
|
||||
properties:
|
||||
default_avatar:
|
||||
enum:
|
||||
- system
|
||||
- gravatar
|
||||
type: string
|
||||
language:
|
||||
maxLength: 128
|
||||
type: string
|
||||
@@ -1438,7 +1616,6 @@ definitions:
|
||||
maxLength: 128
|
||||
type: string
|
||||
required:
|
||||
- default_avatar
|
||||
- language
|
||||
- time_zone
|
||||
type: object
|
||||
@@ -1466,6 +1643,12 @@ definitions:
|
||||
type: object
|
||||
schema.SiteLoginReq:
|
||||
properties:
|
||||
allow_email_domains:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
allow_email_registrations:
|
||||
type: boolean
|
||||
allow_new_registrations:
|
||||
type: boolean
|
||||
login_required:
|
||||
@@ -1473,6 +1656,12 @@ definitions:
|
||||
type: object
|
||||
schema.SiteLoginResp:
|
||||
properties:
|
||||
allow_email_domains:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
allow_email_registrations:
|
||||
type: boolean
|
||||
allow_new_registrations:
|
||||
type: boolean
|
||||
login_required:
|
||||
@@ -1525,6 +1714,50 @@ definitions:
|
||||
$ref: '#/definitions/schema.ThemeOption'
|
||||
type: array
|
||||
type: object
|
||||
schema.SiteUsersReq:
|
||||
properties:
|
||||
allow_update_avatar:
|
||||
type: boolean
|
||||
allow_update_bio:
|
||||
type: boolean
|
||||
allow_update_display_name:
|
||||
type: boolean
|
||||
allow_update_location:
|
||||
type: boolean
|
||||
allow_update_username:
|
||||
type: boolean
|
||||
allow_update_website:
|
||||
type: boolean
|
||||
default_avatar:
|
||||
enum:
|
||||
- system
|
||||
- gravatar
|
||||
type: string
|
||||
required:
|
||||
- default_avatar
|
||||
type: object
|
||||
schema.SiteUsersResp:
|
||||
properties:
|
||||
allow_update_avatar:
|
||||
type: boolean
|
||||
allow_update_bio:
|
||||
type: boolean
|
||||
allow_update_display_name:
|
||||
type: boolean
|
||||
allow_update_location:
|
||||
type: boolean
|
||||
allow_update_username:
|
||||
type: boolean
|
||||
allow_update_website:
|
||||
type: boolean
|
||||
default_avatar:
|
||||
enum:
|
||||
- system
|
||||
- gravatar
|
||||
type: string
|
||||
required:
|
||||
- default_avatar
|
||||
type: object
|
||||
schema.SiteWriteReq:
|
||||
properties:
|
||||
recommend_tags:
|
||||
@@ -1603,6 +1836,17 @@ definitions:
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
schema.UIOptionAction:
|
||||
properties:
|
||||
loading:
|
||||
$ref: '#/definitions/schema.LoadingAction'
|
||||
method:
|
||||
type: string
|
||||
on_complete:
|
||||
$ref: '#/definitions/schema.OnCompleteAction'
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
schema.UnreviewedRevisionInfoInfo:
|
||||
properties:
|
||||
content:
|
||||
@@ -1665,8 +1909,36 @@ definitions:
|
||||
description: website
|
||||
maxLength: 500
|
||||
type: string
|
||||
type: object
|
||||
schema.UpdatePluginConfigReq:
|
||||
properties:
|
||||
config_fields:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
plugin_slug_name:
|
||||
maxLength: 100
|
||||
type: string
|
||||
required:
|
||||
- display_name
|
||||
- plugin_slug_name
|
||||
type: object
|
||||
schema.UpdatePluginStatusReq:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
plugin_slug_name:
|
||||
maxLength: 100
|
||||
type: string
|
||||
required:
|
||||
- plugin_slug_name
|
||||
type: object
|
||||
schema.UpdatePrivilegesConfigReq:
|
||||
properties:
|
||||
level:
|
||||
maximum: 3
|
||||
minimum: 1
|
||||
type: integer
|
||||
required:
|
||||
- level
|
||||
type: object
|
||||
schema.UpdateSMTPConfigReq:
|
||||
properties:
|
||||
@@ -1824,6 +2096,10 @@ definitions:
|
||||
e_mail:
|
||||
maxLength: 500
|
||||
type: string
|
||||
pass:
|
||||
maxLength: 32
|
||||
minLength: 8
|
||||
type: string
|
||||
required:
|
||||
- e_mail
|
||||
type: object
|
||||
@@ -1856,14 +2132,18 @@ definitions:
|
||||
- e_mail
|
||||
- pass
|
||||
type: object
|
||||
schema.UserModifyPassWordRequest:
|
||||
schema.UserModifyPasswordReq:
|
||||
properties:
|
||||
old_pass:
|
||||
description: old password
|
||||
maxLength: 32
|
||||
minLength: 8
|
||||
type: string
|
||||
pass:
|
||||
description: password
|
||||
maxLength: 32
|
||||
minLength: 8
|
||||
type: string
|
||||
required:
|
||||
- pass
|
||||
type: object
|
||||
schema.UserNoticeSetRequest:
|
||||
properties:
|
||||
@@ -2104,6 +2384,112 @@ paths:
|
||||
summary: Get language options
|
||||
tags:
|
||||
- Lang
|
||||
/answer/admin/api/plugin/config:
|
||||
get:
|
||||
description: get plugin config
|
||||
parameters:
|
||||
- description: plugin_slug_name
|
||||
in: query
|
||||
name: plugin_slug_name
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.GetPluginConfigResp'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get plugin config
|
||||
tags:
|
||||
- AdminPlugin
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: update plugin config
|
||||
parameters:
|
||||
- description: UpdatePluginConfigReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.UpdatePluginConfigReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: update plugin config
|
||||
tags:
|
||||
- AdminPlugin
|
||||
/answer/admin/api/plugin/status:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: update plugin status
|
||||
parameters:
|
||||
- description: UpdatePluginStatusReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.UpdatePluginStatusReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: update plugin status
|
||||
tags:
|
||||
- AdminPlugin
|
||||
/answer/admin/api/plugins:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: get plugin list
|
||||
parameters:
|
||||
- description: 'status: active/inactive'
|
||||
in: query
|
||||
name: status
|
||||
type: string
|
||||
- description: have config
|
||||
in: query
|
||||
name: have_config
|
||||
type: boolean
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/schema.GetPluginListResp'
|
||||
type: array
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get plugin list
|
||||
tags:
|
||||
- AdminPlugin
|
||||
/answer/admin/api/question/page:
|
||||
get:
|
||||
consumes:
|
||||
@@ -2294,6 +2680,47 @@ paths:
|
||||
summary: get role list
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/setting/privileges:
|
||||
get:
|
||||
description: GetPrivilegesConfig get privileges config
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.GetPrivilegesConfigResp'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: GetPrivilegesConfig get privileges config
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: update privileges config
|
||||
parameters:
|
||||
- description: config
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.UpdatePrivilegesConfigReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: update privileges config
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/setting/smtp:
|
||||
get:
|
||||
description: GetSMTPConfig get smtp config
|
||||
@@ -2663,6 +3090,47 @@ paths:
|
||||
summary: update site custom css html config
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/siteinfo/users:
|
||||
get:
|
||||
description: get site user config
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.SiteUsersResp'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get site user config
|
||||
tags:
|
||||
- admin
|
||||
put:
|
||||
description: update site info config about users
|
||||
parameters:
|
||||
- description: users info
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.SiteUsersReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: update site info config about users
|
||||
tags:
|
||||
- admin
|
||||
/answer/admin/api/siteinfo/write:
|
||||
get:
|
||||
description: get site interface
|
||||
@@ -3263,6 +3731,101 @@ paths:
|
||||
summary: get comment page
|
||||
tags:
|
||||
- Comment
|
||||
/answer/api/v1/connector/binding/email:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: external login binding user send email
|
||||
parameters:
|
||||
- description: external login binding user send email
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
$ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailResp'
|
||||
type: object
|
||||
summary: external login binding user send email
|
||||
tags:
|
||||
- PluginConnector
|
||||
/answer/api/v1/connector/info:
|
||||
get:
|
||||
description: get all enabled connectors
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/schema.ConnectorInfoResp'
|
||||
type: array
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get all enabled connectors
|
||||
tags:
|
||||
- PluginConnector
|
||||
/answer/api/v1/connector/user/info:
|
||||
get:
|
||||
description: get all connectors info about user
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/definitions/handler.RespBody'
|
||||
- properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/schema.ConnectorUserInfoResp'
|
||||
type: array
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: get all connectors info about user
|
||||
tags:
|
||||
- PluginConnector
|
||||
/answer/api/v1/connector/user/unbinding:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: unbind external user login
|
||||
parameters:
|
||||
- description: ExternalLoginUnbindingReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.ExternalLoginUnbindingReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: unbind external user login
|
||||
tags:
|
||||
- PluginConnector
|
||||
/answer/api/v1/file:
|
||||
post:
|
||||
consumes:
|
||||
@@ -3509,7 +4072,7 @@ paths:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: UserAnswerList
|
||||
description: list personal answers
|
||||
parameters:
|
||||
- default: string
|
||||
description: username
|
||||
@@ -3532,9 +4095,9 @@ paths:
|
||||
required: true
|
||||
type: string
|
||||
- default: "20"
|
||||
description: pagesize
|
||||
description: page_size
|
||||
in: query
|
||||
name: pagesize
|
||||
name: page_size
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
@@ -3546,14 +4109,14 @@ paths:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserAnswerList
|
||||
summary: list personal answers
|
||||
tags:
|
||||
- api-answer
|
||||
- Personal
|
||||
/answer/api/v1/personal/collection/page:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: UserCollectionList
|
||||
description: list personal collections
|
||||
parameters:
|
||||
- default: "0"
|
||||
description: page
|
||||
@@ -3562,9 +4125,9 @@ paths:
|
||||
required: true
|
||||
type: string
|
||||
- default: "20"
|
||||
description: pagesize
|
||||
description: page_size
|
||||
in: query
|
||||
name: pagesize
|
||||
name: page_size
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
@@ -3576,7 +4139,7 @@ paths:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserCollectionList
|
||||
summary: list personal collections
|
||||
tags:
|
||||
- Collection
|
||||
/answer/api/v1/personal/comment/page:
|
||||
@@ -4893,12 +5456,12 @@ paths:
|
||||
- application/json
|
||||
description: UserModifyPassWord
|
||||
parameters:
|
||||
- description: UserModifyPassWordRequest
|
||||
- description: UserModifyPasswordReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.UserModifyPassWordRequest'
|
||||
$ref: '#/definitions/schema.UserModifyPasswordReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -5210,7 +5773,7 @@ paths:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: UserList
|
||||
description: list personal questions
|
||||
parameters:
|
||||
- default: string
|
||||
description: username
|
||||
@@ -5233,9 +5796,9 @@ paths:
|
||||
required: true
|
||||
type: string
|
||||
- default: "20"
|
||||
description: pagesize
|
||||
description: page_size
|
||||
in: query
|
||||
name: pagesize
|
||||
name: page_size
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
@@ -5247,9 +5810,9 @@ paths:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserList
|
||||
summary: list personal questions
|
||||
tags:
|
||||
- Question
|
||||
- Personal
|
||||
/robots.txt:
|
||||
get:
|
||||
description: get site robots information
|
||||
|
||||
@@ -4,6 +4,7 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/Chain-Zhang/pinyin v0.1.3
|
||||
github.com/Masterminds/semver/v3 v3.1.1
|
||||
github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||
github.com/bwmarrin/snowflake v0.3.0
|
||||
@@ -80,6 +81,7 @@ require (
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/geo v0.0.0-20190812012225-f41920e961ce // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
@@ -146,3 +148,5 @@ require (
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
replace lukechampine.com/uint128 v1.1.1 => github.com/aichy126/uint128 v1.1.1
|
||||
|
||||
@@ -52,6 +52,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU=
|
||||
github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
@@ -64,6 +65,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
|
||||
github.com/aichy126/uint128 v1.1.1 h1:xH1bCWDzq7Ebm4lpXCeIiWco0VWi7UmiKkvTQSWBmb0=
|
||||
github.com/aichy126/uint128 v1.1.1/go.mod h1:Hke/MPGXUxOl0OXHoNcVesBL4N+XalHEJ9e1jaIbl8o=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -287,7 +290,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
@@ -1202,8 +1206,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
|
||||
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
|
||||
|
||||
+177
-12
@@ -12,6 +12,8 @@ backend:
|
||||
other: Unauthorized.
|
||||
database_error:
|
||||
other: Data server error.
|
||||
forbidden_error:
|
||||
other: Forbidden.
|
||||
action:
|
||||
report:
|
||||
other: Flag
|
||||
@@ -23,6 +25,8 @@ backend:
|
||||
other: Close
|
||||
reopen:
|
||||
other: Reopen
|
||||
forbidden_error:
|
||||
other: Forbidden.
|
||||
pin:
|
||||
other: Pin
|
||||
hide:
|
||||
@@ -46,6 +50,58 @@ backend:
|
||||
other: Have the full power to access the site.
|
||||
moderator:
|
||||
other: Has access to all posts except admin settings.
|
||||
privilege:
|
||||
level_1:
|
||||
description:
|
||||
other: Level 1 (less reputation required for private team, group)
|
||||
level_2:
|
||||
description:
|
||||
other: Level 2 (low reputation required for startup community)
|
||||
level_3:
|
||||
description:
|
||||
other: Level 3 (high reputation required for mature community)
|
||||
rank_question_add_label:
|
||||
other: Ask question
|
||||
rank_answer_add_label:
|
||||
other: Write answer
|
||||
rank_comment_add_label:
|
||||
other: Write comment
|
||||
rank_report_add_label:
|
||||
other: Flag
|
||||
rank_comment_vote_up_label:
|
||||
other: Upvote comment
|
||||
rank_link_url_limit_label:
|
||||
other: Post more than 2 links at a time
|
||||
rank_question_vote_up_label:
|
||||
other: Upvote question
|
||||
rank_answer_vote_up_label:
|
||||
other: Upvote answer
|
||||
rank_question_vote_down_label:
|
||||
other: Downvote question
|
||||
rank_answer_vote_down_label:
|
||||
other: Downvote answer
|
||||
rank_tag_add_label:
|
||||
other: Create new tag
|
||||
rank_tag_edit_label:
|
||||
other: Edit tag description (need to review)
|
||||
rank_question_edit_label:
|
||||
other: Edit other's question (need to review)
|
||||
rank_answer_edit_label:
|
||||
other: Edit other's answer (need to review)
|
||||
rank_question_edit_without_review_label:
|
||||
other: Edit other's question without review
|
||||
rank_answer_edit_without_review_label:
|
||||
other: Edit other's answer without review
|
||||
rank_question_audit_label:
|
||||
other: Review question edits
|
||||
rank_answer_audit_label:
|
||||
other: Review answer edits
|
||||
rank_tag_audit_label:
|
||||
other: Review tag edits
|
||||
rank_tag_edit_without_review_label:
|
||||
other: Edit tag description without review
|
||||
rank_tag_synonym_label:
|
||||
other: Manage tag synonyms
|
||||
email:
|
||||
other: Email
|
||||
password:
|
||||
@@ -83,6 +139,8 @@ backend:
|
||||
other: Email should be verified.
|
||||
verify_url_expired:
|
||||
other: Email verified URL has expired, please resend the email.
|
||||
illegal_email_domain_error:
|
||||
other: Email is not allowed from that email domain. Please use another one.
|
||||
lang:
|
||||
not_found:
|
||||
other: Language file not found.
|
||||
@@ -155,6 +213,8 @@ backend:
|
||||
no_permission:
|
||||
other: No permission to Revision.
|
||||
user:
|
||||
external_login_unbinding_forbidden:
|
||||
other: Please set a login password for your account before you remove this login.
|
||||
email_or_password_wrong:
|
||||
other:
|
||||
other: Email and password do not match.
|
||||
@@ -172,6 +232,10 @@ backend:
|
||||
other: You cannot modify your role.
|
||||
not_allowed_registration:
|
||||
other: Currently the site is not open for registration
|
||||
access_denied:
|
||||
other: Access denied
|
||||
page_access_denied:
|
||||
other: You do not have access to this page.
|
||||
config:
|
||||
read_config_failed:
|
||||
other: Read config failed
|
||||
@@ -277,6 +341,16 @@ backend:
|
||||
other: Your answer has been deleted
|
||||
your_comment_was_deleted:
|
||||
other: Your comment has been deleted
|
||||
up_voted_question:
|
||||
other: upvoted question
|
||||
down_voted_question:
|
||||
other: downvoted question
|
||||
up_voted_answer:
|
||||
other: upvoted answer
|
||||
down_voted_answer:
|
||||
other: downvoted answer
|
||||
up_voted_comment:
|
||||
other: upvoted comment
|
||||
|
||||
# The following fields are used for interface presentation(Front-end)
|
||||
ui:
|
||||
@@ -322,6 +396,7 @@ ui:
|
||||
upgrade: Answer Upgrade
|
||||
maintenance: Website Maintenance
|
||||
users: Users
|
||||
oauth_callback: Processing
|
||||
http_404: HTTP Error 404
|
||||
http_50X: HTTP Error 500
|
||||
http_403: HTTP Error 403
|
||||
@@ -331,11 +406,13 @@ ui:
|
||||
achievement: Achievements
|
||||
all_read: Mark all as read
|
||||
show_more: Show more
|
||||
someone: Someone
|
||||
suspended:
|
||||
title: Your Account has been Suspended
|
||||
until_time: "Your account was suspended until {{ time }}."
|
||||
forever: This user was suspended forever.
|
||||
end: You don't meet a community guideline.
|
||||
contact_us: Contact us
|
||||
editor:
|
||||
blockquote:
|
||||
text: Blockquote
|
||||
@@ -547,7 +624,7 @@ ui:
|
||||
tip_answer: >-
|
||||
Use comments to reply to other users or notify them of changes. If you are
|
||||
adding new information, edit your post instead of commenting.
|
||||
tip_vote: It adds something useful to the post
|
||||
tip_vote: It adds something useful to the post
|
||||
edit_answer:
|
||||
title: Edit Answer
|
||||
default_reason: Edit answer
|
||||
@@ -658,7 +735,6 @@ ui:
|
||||
msg:
|
||||
empty: Cannot be empty.
|
||||
login:
|
||||
page_title: Welcome to {{site_name}}
|
||||
login_to_continue: Log in to continue
|
||||
info_sign: Don't have an account? <1>Sign up</1>
|
||||
info_login: Already have an account? <1>Log in</1>
|
||||
@@ -669,6 +745,7 @@ ui:
|
||||
msg:
|
||||
empty: Name cannot be empty.
|
||||
range: Name up to 30 characters.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
email:
|
||||
label: Email
|
||||
msg:
|
||||
@@ -689,7 +766,6 @@ ui:
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
change_email:
|
||||
page_title: Welcome to {{site_name}}
|
||||
btn_cancel: Cancel
|
||||
btn_update: Update email address
|
||||
send_success: >-
|
||||
@@ -699,6 +775,17 @@ ui:
|
||||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
email:
|
||||
label: Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
modal_title: Email already existes.
|
||||
modal_content: This email address already registered. Are you sure you want to connect to the existing account?
|
||||
modal_cancel: Change email
|
||||
modal_confirm: Connect to the existing account
|
||||
password_reset:
|
||||
page_title: Password Reset
|
||||
btn_name: Reset my password
|
||||
@@ -719,6 +806,7 @@ ui:
|
||||
label: Confirm New Password
|
||||
settings:
|
||||
page_title: Settings
|
||||
goto_modify: Go to Modify
|
||||
nav:
|
||||
profile: Profile
|
||||
notification: Notifications
|
||||
@@ -768,8 +856,11 @@ ui:
|
||||
We've sent an email to that address. Please follow the confirmation
|
||||
instructions.
|
||||
email:
|
||||
label: Email
|
||||
msg: Email cannot be empty.
|
||||
label: New Email
|
||||
msg: New Email cannot be empty.
|
||||
pass:
|
||||
label: Current Password
|
||||
msg: Password cannot be empty.
|
||||
password_title: Password
|
||||
current_pass:
|
||||
label: Current Password
|
||||
@@ -786,6 +877,13 @@ ui:
|
||||
lang:
|
||||
label: Interface Language
|
||||
text: User interface language. It will change when you refresh the page.
|
||||
my_logins:
|
||||
title: My Logins
|
||||
label: Log in or sign up on this site using these accounts.
|
||||
modal_title: Remove Login
|
||||
modal_content: Are you sure you want to remove this login from your account?
|
||||
modal_confirm_btn: Remove
|
||||
remove_success: Removed successfully
|
||||
toast:
|
||||
update: update success
|
||||
update_password: Password changed successfully.
|
||||
@@ -809,8 +907,8 @@ ui:
|
||||
closed_in: Closed in
|
||||
show_exist: Show existing question.
|
||||
useful: Useful
|
||||
question_useful: It is useful and clear
|
||||
question_un_useful: It is unclear or not useful
|
||||
question_useful: It is useful and clear
|
||||
question_un_useful: It is unclear or not useful
|
||||
answer_useful: It is useful
|
||||
answer_un_useful: It is not useful
|
||||
answers:
|
||||
@@ -873,6 +971,10 @@ ui:
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
pinned: Pinned
|
||||
all: All
|
||||
question: Question
|
||||
answer: Answer
|
||||
comment: Comment
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
@@ -907,7 +1009,6 @@ ui:
|
||||
modal_confirm:
|
||||
title: Error...
|
||||
account_result:
|
||||
page_title: Welcome to {{site_name}}
|
||||
success: Your new account is confirmed; you will be redirected to the home page.
|
||||
link: Continue to homepage
|
||||
invalid: >-
|
||||
@@ -1034,6 +1135,7 @@ ui:
|
||||
admin_name:
|
||||
label: Name
|
||||
msg: Name cannot be empty.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
admin_password:
|
||||
label: Password
|
||||
text: >-
|
||||
@@ -1095,8 +1197,20 @@ ui:
|
||||
seo: SEO
|
||||
customize: Customize
|
||||
themes: Themes
|
||||
css-html: CSS/HTML
|
||||
css_html: CSS/HTML
|
||||
login: Login
|
||||
privileges: Privileges
|
||||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
@@ -1139,6 +1253,7 @@ ui:
|
||||
pending: Pending
|
||||
completed: Completed
|
||||
flagged: Flagged
|
||||
flagged_type: Flagged {{ type }}
|
||||
created: Created
|
||||
action: Action
|
||||
review: Review
|
||||
@@ -1289,9 +1404,6 @@ ui:
|
||||
label: Timezone
|
||||
msg: Timezone cannot be empty.
|
||||
text: Choose a city in the same timezone as you.
|
||||
avatar:
|
||||
label: Default Avatar
|
||||
text: For users without a custom avatar of their own.
|
||||
smtp:
|
||||
page_title: SMTP
|
||||
from_email:
|
||||
@@ -1401,16 +1513,67 @@ ui:
|
||||
footer:
|
||||
label: Footer
|
||||
text: This will insert before </html>.
|
||||
sidebar:
|
||||
label: Sidebar
|
||||
text: This will insert in sidebar.
|
||||
login:
|
||||
page_title: Login
|
||||
membership:
|
||||
title: Membership
|
||||
label: Allow new registrations
|
||||
text: Turn off to prevent anyone from creating a new account.
|
||||
email_registration:
|
||||
title: Email registration
|
||||
label: Allow email registration
|
||||
text: Turn off to prevent anyone creating new account through email.
|
||||
allowed_email_domains:
|
||||
title: Allowed email domains
|
||||
text: Email domains that users must register accounts with. One domain per line. Ignored when empty.
|
||||
private:
|
||||
title: Private
|
||||
label: Login required
|
||||
text: Only logged in users can access this community.
|
||||
installed_plugins:
|
||||
title: Installed Plugins
|
||||
filter:
|
||||
all: All
|
||||
active: Active
|
||||
inactive: Inactive
|
||||
outdated: Outdated
|
||||
plugins:
|
||||
label: Plugins
|
||||
text: Select an existing plugin.
|
||||
name: Name
|
||||
version: Version
|
||||
status: Status
|
||||
action: Action
|
||||
deactivate: Deactivate
|
||||
activate: Activate
|
||||
settings: Settings
|
||||
settings_users:
|
||||
title: Users
|
||||
avatar:
|
||||
label: Default Avatar
|
||||
text: For users without a custom avatar of their own.
|
||||
profile_editable:
|
||||
title: Profile Editable
|
||||
allow_update_display_name:
|
||||
label: Allow users to change their display name
|
||||
allow_update_username:
|
||||
label: Allow users to change their username
|
||||
allow_update_avatar:
|
||||
label: Allow users to change their profile image
|
||||
allow_update_bio:
|
||||
label: Allow users to change their about me
|
||||
allow_update_website:
|
||||
label: Allow users to change their website
|
||||
allow_update_location:
|
||||
label: Allow users to change their location
|
||||
privilege:
|
||||
title: Privileges
|
||||
level:
|
||||
label: Reputation required level
|
||||
text: Choose the reputation required for the privileges
|
||||
|
||||
form:
|
||||
optional: (optional)
|
||||
@@ -1418,6 +1581,8 @@ ui:
|
||||
invalid: is invalid
|
||||
btn_submit: Save
|
||||
not_found_props: "Required property {{ key }} not found."
|
||||
select: Select
|
||||
|
||||
page_review:
|
||||
review: Review
|
||||
proposed: proposed
|
||||
|
||||
+126
@@ -45,6 +45,58 @@ backend:
|
||||
other: 拥有管理网站的全部权限。
|
||||
moderator:
|
||||
other: 拥有访问除管理员设置以外的所有权限。
|
||||
privilege:
|
||||
level_1:
|
||||
description:
|
||||
other: 等级1(创业社区所需的声望最低)
|
||||
level_2:
|
||||
description:
|
||||
other: 等级2(创业社区所需的声望较低)
|
||||
level_3:
|
||||
description:
|
||||
other: 等级3(成熟社区所需的声望较高)
|
||||
rank_question_add_label:
|
||||
other: 提问
|
||||
rank_answer_add_label:
|
||||
other: 回答问题
|
||||
rank_comment_add_label:
|
||||
other: 发表评论
|
||||
rank_report_add_label:
|
||||
other: 举报
|
||||
rank_comment_vote_up_label:
|
||||
other: 评论点赞
|
||||
rank_link_url_limit_label:
|
||||
other: 一次发布超过两个链接
|
||||
rank_question_vote_up_label:
|
||||
other: 问题点赞
|
||||
rank_answer_vote_up_label:
|
||||
other: 答案点赞
|
||||
rank_question_vote_down_label:
|
||||
other: 问题点踩
|
||||
rank_answer_vote_down_label:
|
||||
other: 答案点踩
|
||||
rank_tag_add_label:
|
||||
other: 创建新标签
|
||||
rank_tag_edit_label:
|
||||
other: 编辑标签描述(需要审核)
|
||||
rank_question_edit_label:
|
||||
other: 编辑他人提问(需要审核)
|
||||
rank_answer_edit_label:
|
||||
other: 编辑他人回答(需要审核)
|
||||
rank_question_edit_without_review_label:
|
||||
other: 编辑他人提问(无需审核)
|
||||
rank_answer_edit_without_review_label:
|
||||
other: 编辑他人回答(无需审核)
|
||||
rank_question_audit_label:
|
||||
other: 审核问题编辑
|
||||
rank_answer_audit_label:
|
||||
other: 审核答案编辑
|
||||
rank_tag_audit_label:
|
||||
other: 审核标签编辑
|
||||
rank_tag_edit_without_review_label:
|
||||
other: 编辑标签描述(无需审核)
|
||||
rank_tag_synonym_label:
|
||||
other: 管理标签同义词
|
||||
email:
|
||||
other: 邮箱
|
||||
password:
|
||||
@@ -82,6 +134,8 @@ backend:
|
||||
other: 邮箱需要验证。
|
||||
verify_url_expired:
|
||||
other: 邮箱验证的网址已过期,请重新发送邮件。
|
||||
illegal_email_domain_error:
|
||||
other: 该域名的邮箱无法使用。请尝试更换其他邮箱。
|
||||
lang:
|
||||
not_found:
|
||||
other: 语言未找到
|
||||
@@ -171,6 +225,10 @@ backend:
|
||||
other: 您不能修改自己的角色。
|
||||
not_allowed_registration:
|
||||
other: 目前该站点未开放注册
|
||||
access_denied:
|
||||
other: 访问被拒绝
|
||||
page_access_denied:
|
||||
other: 你没有权限进入这个页面。
|
||||
config:
|
||||
read_config_failed:
|
||||
other: 读取配置失败
|
||||
@@ -271,6 +329,16 @@ backend:
|
||||
other: 你的答案已被删除
|
||||
your_comment_was_deleted:
|
||||
other: 你的评论已被删除
|
||||
up_voted_question:
|
||||
other: 赞了问题
|
||||
down_voted_question:
|
||||
other: 踩了问题
|
||||
up_voted_answer:
|
||||
other: 赞了答案
|
||||
down_voted_answer:
|
||||
other: 踩了答案
|
||||
up_voted_comment:
|
||||
other: 赞了评论
|
||||
#The following fields are used for interface presentation(Front-end)
|
||||
ui:
|
||||
how_to_format:
|
||||
@@ -316,6 +384,7 @@ ui:
|
||||
achievement: 成就
|
||||
all_read: 全部标记为已读
|
||||
show_more: 显示更多
|
||||
someone: 有人
|
||||
suspended:
|
||||
title: 账号已封禁
|
||||
until_time: "你的账号被封禁至{{ time }}。"
|
||||
@@ -690,6 +759,7 @@ ui:
|
||||
label: 确认新密码
|
||||
settings:
|
||||
page_title: 设置
|
||||
goto_modify: 前往修改
|
||||
nav:
|
||||
profile: 我的资料
|
||||
notification: 通知
|
||||
@@ -836,6 +906,10 @@ ui:
|
||||
skip: 略过
|
||||
discard_draft: 丢弃草稿
|
||||
pinned: 已置顶
|
||||
all: 所有
|
||||
question: 问题
|
||||
answer: 回答
|
||||
comment: 评论
|
||||
search:
|
||||
title: 搜索结果
|
||||
keywords: 关键词
|
||||
@@ -1053,6 +1127,16 @@ ui:
|
||||
themes: 主题
|
||||
css-html: CSS/HTML
|
||||
login: 登录
|
||||
plugins: 插件
|
||||
installed_plugins: 插件列表
|
||||
website_welcome: 欢迎来到 {{site_name}}
|
||||
plugins:
|
||||
login: 登录
|
||||
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
|
||||
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。
|
||||
oauth:
|
||||
connect: 连接到 {{ auth_name }}
|
||||
remove: 解绑 {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: 后台管理
|
||||
@@ -1095,6 +1179,7 @@ ui:
|
||||
pending: 等待处理
|
||||
completed: 已完成
|
||||
flagged: 被举报内容
|
||||
flagged_type: 被举报的{{ type }}
|
||||
created: 创建于
|
||||
action: 操作
|
||||
review: 审查
|
||||
@@ -1365,6 +1450,47 @@ ui:
|
||||
title: 非公开的
|
||||
label: 需要登录
|
||||
text: 只有登录用户才能访问这个社区。
|
||||
installed_plugins:
|
||||
title: 插件列表
|
||||
filter:
|
||||
all: 全部
|
||||
active: 启用
|
||||
inactive: 未启用
|
||||
outdated: 已过期
|
||||
plugins:
|
||||
label: 插件
|
||||
text: 选择一个插件
|
||||
name: 插件名称
|
||||
version: 插件版本
|
||||
status: 状态
|
||||
action: 操作
|
||||
deactivate: 停用
|
||||
activate: 启用
|
||||
settings: 设置
|
||||
settings_users:
|
||||
title: 用户
|
||||
avatar:
|
||||
label: 默认头像
|
||||
text: 未设置自定义头像的用户所展示的头像。
|
||||
profile_editable:
|
||||
title: 可编辑的个人资料
|
||||
allow_update_display_name:
|
||||
label: 允许用户更改显示名称
|
||||
allow_update_username:
|
||||
label: 允许用户更改用户名
|
||||
allow_update_avatar:
|
||||
label: 允许用户更改头像
|
||||
allow_update_bio:
|
||||
label: 允许用户更改自我介绍
|
||||
allow_update_website:
|
||||
label: 允许用户更改个人网站
|
||||
allow_update_location:
|
||||
label: 允许用户更改所在地
|
||||
privilege:
|
||||
title: 声望权限
|
||||
level:
|
||||
label: 所需声望等级
|
||||
text: 选择所需的声望等级以获取权限
|
||||
form:
|
||||
optional: (选填)
|
||||
empty: 不能为空
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
PluginStatus = "plugin.status"
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package constant
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
ConnectorUserExternalInfoCacheKey = "answer:connector:"
|
||||
ConnectorUserExternalInfoCacheTime = 10 * time.Minute
|
||||
)
|
||||
@@ -66,6 +66,8 @@ const (
|
||||
SiteTypeLogin = "login"
|
||||
SiteTypeCustomCssHTML = "css-html"
|
||||
SiteTypeTheme = "theme"
|
||||
SiteTypePrivileges = "privileges"
|
||||
SiteTypeUsers = "users"
|
||||
)
|
||||
|
||||
func ExistInPathIgnore(name string) bool {
|
||||
|
||||
@@ -1,28 +1,38 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
// UpdateQuestion update question
|
||||
UpdateQuestion = "notification.action.update_question"
|
||||
// AnswerTheQuestion answer the question
|
||||
AnswerTheQuestion = "notification.action.answer_the_question"
|
||||
// UpdateAnswer update answer
|
||||
UpdateAnswer = "notification.action.update_answer"
|
||||
// AcceptAnswer accept answer
|
||||
AcceptAnswer = "notification.action.accept_answer"
|
||||
// CommentQuestion comment question
|
||||
CommentQuestion = "notification.action.comment_question"
|
||||
// CommentAnswer comment answer
|
||||
CommentAnswer = "notification.action.comment_answer"
|
||||
// ReplyToYou reply to you
|
||||
ReplyToYou = "notification.action.reply_to_you"
|
||||
// MentionYou mention you
|
||||
MentionYou = "notification.action.mention_you"
|
||||
// YourQuestionIsClosed your question is closed
|
||||
YourQuestionIsClosed = "notification.action.your_question_is_closed"
|
||||
// YourQuestionWasDeleted your question was deleted
|
||||
YourQuestionWasDeleted = "notification.action.your_question_was_deleted"
|
||||
// YourAnswerWasDeleted your answer was deleted
|
||||
YourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
|
||||
// YourCommentWasDeleted your comment was deleted
|
||||
YourCommentWasDeleted = "notification.action.your_comment_was_deleted"
|
||||
// NotificationUpdateQuestion update question
|
||||
NotificationUpdateQuestion = "notification.action.update_question"
|
||||
// NotificationAnswerTheQuestion answer the question
|
||||
NotificationAnswerTheQuestion = "notification.action.answer_the_question"
|
||||
// NotificationUpVotedTheQuestion up voted the question
|
||||
NotificationUpVotedTheQuestion = "notification.action.up_voted_question"
|
||||
// NotificationDownVotedTheQuestion down voted the question
|
||||
NotificationDownVotedTheQuestion = "notification.action.down_voted_question"
|
||||
// NotificationUpdateAnswer update answer
|
||||
NotificationUpdateAnswer = "notification.action.update_answer"
|
||||
// NotificationAcceptAnswer accept answer
|
||||
NotificationAcceptAnswer = "notification.action.accept_answer"
|
||||
// NotificationUpVotedTheAnswer up voted the answer
|
||||
NotificationUpVotedTheAnswer = "notification.action.up_voted_answer"
|
||||
// NotificationDownVotedTheAnswer down voted the answer
|
||||
NotificationDownVotedTheAnswer = "notification.action.down_voted_answer"
|
||||
// NotificationCommentQuestion comment question
|
||||
NotificationCommentQuestion = "notification.action.comment_question"
|
||||
// NotificationCommentAnswer comment answer
|
||||
NotificationCommentAnswer = "notification.action.comment_answer"
|
||||
// NotificationUpVotedTheComment up voted the comment
|
||||
NotificationUpVotedTheComment = "notification.action.up_voted_comment"
|
||||
// NotificationReplyToYou reply to you
|
||||
NotificationReplyToYou = "notification.action.reply_to_you"
|
||||
// NotificationMentionYou mention you
|
||||
NotificationMentionYou = "notification.action.mention_you"
|
||||
// NotificationYourQuestionIsClosed your question is closed
|
||||
NotificationYourQuestionIsClosed = "notification.action.your_question_is_closed"
|
||||
// NotificationYourQuestionWasDeleted your question was deleted
|
||||
NotificationYourQuestionWasDeleted = "notification.action.your_question_was_deleted"
|
||||
// NotificationYourAnswerWasDeleted your answer was deleted
|
||||
NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
|
||||
// NotificationYourCommentWasDeleted your comment was deleted
|
||||
NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package constant
|
||||
|
||||
import "github.com/answerdev/answer/internal/base/reason"
|
||||
|
||||
type Privilege struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
const (
|
||||
RankQuestionAddKey = "rank.question.add"
|
||||
RankQuestionEditKey = "rank.question.edit"
|
||||
RankQuestionDeleteKey = "rank.question.delete"
|
||||
RankQuestionVoteUpKey = "rank.question.vote_up"
|
||||
RankQuestionVoteDownKey = "rank.question.vote_down"
|
||||
RankAnswerAddKey = "rank.answer.add"
|
||||
RankAnswerEditKey = "rank.answer.edit"
|
||||
RankAnswerDeleteKey = "rank.answer.delete"
|
||||
RankAnswerAcceptKey = "rank.answer.accept"
|
||||
RankAnswerVoteUpKey = "rank.answer.vote_up"
|
||||
RankAnswerVoteDownKey = "rank.answer.vote_down"
|
||||
RankCommentAddKey = "rank.comment.add"
|
||||
RankCommentEditKey = "rank.comment.edit"
|
||||
RankCommentDeleteKey = "rank.comment.delete"
|
||||
RankReportAddKey = "rank.report.add"
|
||||
RankTagAddKey = "rank.tag.add"
|
||||
RankTagEditKey = "rank.tag.edit"
|
||||
RankTagDeleteKey = "rank.tag.delete"
|
||||
RankTagSynonymKey = "rank.tag.synonym"
|
||||
RankLinkUrlLimitKey = "rank.link.url_limit"
|
||||
RankVoteDetailKey = "rank.vote.detail"
|
||||
RankCommentVoteUpKey = "rank.comment.vote_up"
|
||||
RankCommentVoteDownKey = "rank.comment.vote_down"
|
||||
RankQuestionEditWithoutReviewKey = "rank.question.edit_without_review"
|
||||
RankAnswerEditWithoutReviewKey = "rank.answer.edit_without_review"
|
||||
RankTagEditWithoutReviewKey = "rank.tag.edit_without_review"
|
||||
RankAnswerAuditKey = "rank.answer.audit"
|
||||
RankQuestionAuditKey = "rank.question.audit"
|
||||
RankTagAuditKey = "rank.tag.audit"
|
||||
RankQuestionCloseKey = "rank.question.close"
|
||||
RankQuestionReopenKey = "rank.question.reopen"
|
||||
RankTagUseReservedTagKey = "rank.tag.use_reserved_tag"
|
||||
)
|
||||
|
||||
var (
|
||||
RankAllPrivileges = []*Privilege{
|
||||
{Label: reason.RankQuestionAddLabel, Key: RankQuestionAddKey},
|
||||
{Label: reason.RankAnswerAddLabel, Key: RankAnswerAddKey},
|
||||
{Label: reason.RankCommentAddLabel, Key: RankCommentAddKey},
|
||||
{Label: reason.RankReportAddLabel, Key: RankReportAddKey},
|
||||
{Label: reason.RankCommentVoteUpLabel, Key: RankCommentVoteUpKey},
|
||||
{Label: reason.RankLinkUrlLimitLabel, Key: RankLinkUrlLimitKey},
|
||||
{Label: reason.RankQuestionVoteUpLabel, Key: RankQuestionVoteUpKey},
|
||||
{Label: reason.RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey},
|
||||
{Label: reason.RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey},
|
||||
{Label: reason.RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey},
|
||||
{Label: reason.RankTagAddLabel, Key: RankTagAddKey},
|
||||
{Label: reason.RankTagEditLabel, Key: RankTagEditKey},
|
||||
{Label: reason.RankQuestionEditLabel, Key: RankQuestionEditKey},
|
||||
{Label: reason.RankAnswerEditLabel, Key: RankAnswerEditKey},
|
||||
{Label: reason.RankQuestionEditWithoutReviewLabel, Key: RankQuestionEditWithoutReviewKey},
|
||||
{Label: reason.RankAnswerEditWithoutReviewLabel, Key: RankAnswerEditWithoutReviewKey},
|
||||
{Label: reason.RankQuestionAuditLabel, Key: RankQuestionAuditKey},
|
||||
{Label: reason.RankAnswerAuditLabel, Key: RankAnswerAuditKey},
|
||||
{Label: reason.RankTagAuditLabel, Key: RankTagAuditKey},
|
||||
{Label: reason.RankTagEditWithoutReviewLabel, Key: RankTagEditWithoutReviewKey},
|
||||
{Label: reason.RankTagSynonymLabel, Key: RankTagSynonymKey},
|
||||
}
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
DefaultAvatar = "system"
|
||||
DefaultAvatar = "system"
|
||||
DefaultSiteURL = ""
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
"github.com/segmentfault/pacman/cache"
|
||||
@@ -76,6 +77,15 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {
|
||||
|
||||
// NewCache new cache instance
|
||||
func NewCache(c *CacheConf) (cache.Cache, func(), error) {
|
||||
var pluginCache plugin.Cache
|
||||
_ = plugin.CallCache(func(fn plugin.Cache) error {
|
||||
pluginCache = fn
|
||||
return nil
|
||||
})
|
||||
if pluginCache != nil {
|
||||
return pluginCache, func() {}, nil
|
||||
}
|
||||
|
||||
// TODO What cache type should be initialized according to the configuration file
|
||||
memCache := memory.NewCache()
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func HeadersByRequestURI() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if strings.HasPrefix(c.Request.RequestURI, "/static/") {
|
||||
c.Header("cache-control", "public, max-age=31536000")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// BanAPIForUserCenter ban api for user center
|
||||
func BanAPIForUserCenter(ctx *gin.Context) {
|
||||
uc, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !uc.Description().EnabledOriginalUserSystem {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
|
||||
ctx.Abort()
|
||||
return
|
||||
}
|
||||
ctx.Next()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package reason
|
||||
|
||||
const (
|
||||
PrivilegeLevel1Desc = "privilege.level_1.description"
|
||||
PrivilegeLevel2Desc = "privilege.level_2.description"
|
||||
PrivilegeLevel3Desc = "privilege.level_3.description"
|
||||
|
||||
RankQuestionAddLabel = "privilege.rank_question_add_label"
|
||||
RankAnswerAddLabel = "privilege.rank_answer_add_label"
|
||||
RankCommentAddLabel = "privilege.rank_comment_add_label"
|
||||
RankReportAddLabel = "privilege.rank_report_add_label"
|
||||
RankCommentVoteUpLabel = "privilege.rank_comment_vote_up_label"
|
||||
RankLinkUrlLimitLabel = "privilege.rank_link_url_limit_label"
|
||||
RankQuestionVoteUpLabel = "privilege.rank_question_vote_up_label"
|
||||
RankAnswerVoteUpLabel = "privilege.rank_answer_vote_up_label"
|
||||
RankQuestionVoteDownLabel = "privilege.rank_question_vote_down_label"
|
||||
RankAnswerVoteDownLabel = "privilege.rank_answer_vote_down_label"
|
||||
RankTagAddLabel = "privilege.rank_tag_add_label"
|
||||
RankTagEditLabel = "privilege.rank_tag_edit_label"
|
||||
RankQuestionEditLabel = "privilege.rank_question_edit_label"
|
||||
RankAnswerEditLabel = "privilege.rank_answer_edit_label"
|
||||
RankQuestionEditWithoutReviewLabel = "privilege.rank_question_edit_without_review_label"
|
||||
RankAnswerEditWithoutReviewLabel = "privilege.rank_answer_edit_without_review_label"
|
||||
RankQuestionAuditLabel = "privilege.rank_question_audit_label"
|
||||
RankAnswerAuditLabel = "privilege.rank_answer_audit_label"
|
||||
RankTagAuditLabel = "privilege.rank_tag_audit_label"
|
||||
RankTagEditWithoutReviewLabel = "privilege.rank_tag_edit_without_review_label"
|
||||
RankTagSynonymLabel = "privilege.rank_tag_synonym_label"
|
||||
)
|
||||
@@ -11,63 +11,69 @@ const (
|
||||
UnauthorizedError = "base.unauthorized_error"
|
||||
// DatabaseError database error
|
||||
DatabaseError = "base.database_error"
|
||||
// ForbiddenError forbidden error
|
||||
ForbiddenError = "base.forbidden_error"
|
||||
)
|
||||
|
||||
const (
|
||||
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
|
||||
CommentNotFound = "error.comment.not_found"
|
||||
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
|
||||
QuestionNotFound = "error.question.not_found"
|
||||
QuestionCannotDeleted = "error.question.cannot_deleted"
|
||||
QuestionCannotClose = "error.question.cannot_close"
|
||||
QuestionCannotUpdate = "error.question.cannot_update"
|
||||
QuestionAlreadyDeleted = "error.question.already_deleted"
|
||||
AnswerNotFound = "error.answer.not_found"
|
||||
AnswerCannotDeleted = "error.answer.cannot_deleted"
|
||||
AnswerCannotUpdate = "error.answer.cannot_update"
|
||||
AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add"
|
||||
CommentEditWithoutPermission = "error.comment.edit_without_permission"
|
||||
DisallowVote = "error.object.disallow_vote"
|
||||
DisallowFollow = "error.object.disallow_follow"
|
||||
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
|
||||
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
|
||||
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
|
||||
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
|
||||
UserNotFound = "error.user.not_found"
|
||||
UsernameInvalid = "error.user.username_invalid"
|
||||
UsernameDuplicate = "error.user.username_duplicate"
|
||||
UserSetAvatar = "error.user.set_avatar"
|
||||
EmailDuplicate = "error.email.duplicate"
|
||||
EmailVerifyURLExpired = "error.email.verify_url_expired"
|
||||
EmailNeedToBeVerified = "error.email.need_to_be_verified"
|
||||
UserSuspended = "error.user.suspended"
|
||||
ObjectNotFound = "error.object.not_found"
|
||||
TagNotFound = "error.tag.not_found"
|
||||
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
|
||||
TagCannotUpdate = "error.tag.cannot_update"
|
||||
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
|
||||
TagAlreadyExist = "error.tag.already_exist"
|
||||
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
|
||||
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
|
||||
ThemeNotFound = "error.theme.not_found"
|
||||
LangNotFound = "error.lang.not_found"
|
||||
ReportHandleFailed = "error.report.handle_failed"
|
||||
ReportNotFound = "error.report.not_found"
|
||||
ReadConfigFailed = "error.config.read_config_failed"
|
||||
DatabaseConnectionFailed = "error.database.connection_failed"
|
||||
InstallCreateTableFailed = "error.database.create_table_failed"
|
||||
InstallConfigFailed = "error.install.create_config_failed"
|
||||
SiteInfoNotFound = "error.site_info.not_found"
|
||||
UploadFileSourceUnsupported = "error.upload.source_unsupported"
|
||||
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
|
||||
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
|
||||
RecommendTagEnter = "error.tag.recommend_tag_enter"
|
||||
RevisionReviewUnderway = "error.revision.review_underway"
|
||||
RevisionNoPermission = "error.revision.no_permission"
|
||||
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
|
||||
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
|
||||
NotAllowedRegistration = "error.user.not_allowed_registration"
|
||||
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
|
||||
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
|
||||
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
|
||||
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
|
||||
CommentNotFound = "error.comment.not_found"
|
||||
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
|
||||
QuestionNotFound = "error.question.not_found"
|
||||
QuestionCannotDeleted = "error.question.cannot_deleted"
|
||||
QuestionCannotClose = "error.question.cannot_close"
|
||||
QuestionCannotUpdate = "error.question.cannot_update"
|
||||
QuestionAlreadyDeleted = "error.question.already_deleted"
|
||||
AnswerNotFound = "error.answer.not_found"
|
||||
AnswerCannotDeleted = "error.answer.cannot_deleted"
|
||||
AnswerCannotUpdate = "error.answer.cannot_update"
|
||||
AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add"
|
||||
CommentEditWithoutPermission = "error.comment.edit_without_permission"
|
||||
DisallowVote = "error.object.disallow_vote"
|
||||
DisallowFollow = "error.object.disallow_follow"
|
||||
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
|
||||
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
|
||||
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
|
||||
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
|
||||
UserNotFound = "error.user.not_found"
|
||||
UsernameInvalid = "error.user.username_invalid"
|
||||
UsernameDuplicate = "error.user.username_duplicate"
|
||||
UserSetAvatar = "error.user.set_avatar"
|
||||
EmailDuplicate = "error.email.duplicate"
|
||||
EmailVerifyURLExpired = "error.email.verify_url_expired"
|
||||
EmailNeedToBeVerified = "error.email.need_to_be_verified"
|
||||
EmailIllegalDomainError = "error.email.illegal_email_domain_error"
|
||||
UserSuspended = "error.user.suspended"
|
||||
ObjectNotFound = "error.object.not_found"
|
||||
TagNotFound = "error.tag.not_found"
|
||||
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
|
||||
TagCannotUpdate = "error.tag.cannot_update"
|
||||
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
|
||||
TagAlreadyExist = "error.tag.already_exist"
|
||||
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
|
||||
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
|
||||
ThemeNotFound = "error.theme.not_found"
|
||||
LangNotFound = "error.lang.not_found"
|
||||
ReportHandleFailed = "error.report.handle_failed"
|
||||
ReportNotFound = "error.report.not_found"
|
||||
ReadConfigFailed = "error.config.read_config_failed"
|
||||
DatabaseConnectionFailed = "error.database.connection_failed"
|
||||
InstallCreateTableFailed = "error.database.create_table_failed"
|
||||
InstallConfigFailed = "error.install.create_config_failed"
|
||||
SiteInfoNotFound = "error.site_info.not_found"
|
||||
UploadFileSourceUnsupported = "error.upload.source_unsupported"
|
||||
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
|
||||
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
|
||||
RecommendTagEnter = "error.tag.recommend_tag_enter"
|
||||
RevisionReviewUnderway = "error.revision.review_underway"
|
||||
RevisionNoPermission = "error.revision.no_permission"
|
||||
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
|
||||
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
|
||||
NotAllowedRegistration = "error.user.not_allowed_registration"
|
||||
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
|
||||
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
|
||||
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
|
||||
UserExternalLoginUnbindingForbidden = "error.user.external_login_unbinding_forbidden"
|
||||
UserAccessDenied = "error.user.access_denied"
|
||||
UserPageAccessDenied = "error.user.page_access_denied"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
brotli "github.com/anargu/gin-brotli"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/router"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/answerdev/answer/ui"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -20,6 +21,7 @@ func NewHTTPServer(debug bool,
|
||||
authUserMiddleware *middleware.AuthUserMiddleware,
|
||||
avatarMiddleware *middleware.AvatarMiddleware,
|
||||
templateRouter *router.TemplateRouter,
|
||||
pluginAPIRouter *router.PluginAPIRouter,
|
||||
) *gin.Engine {
|
||||
|
||||
if debug {
|
||||
@@ -34,7 +36,7 @@ func NewHTTPServer(debug bool,
|
||||
html, _ := fs.Sub(ui.Template, "template")
|
||||
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
|
||||
r.SetHTMLTemplate(htmlTemplate)
|
||||
|
||||
r.Use(middleware.HeadersByRequestURI())
|
||||
viewRouter.Register(r)
|
||||
|
||||
rootGroup := r.Group("")
|
||||
@@ -62,5 +64,17 @@ func NewHTTPServer(debug bool,
|
||||
answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1)
|
||||
|
||||
templateRouter.RegisterTemplateRouter(rootGroup)
|
||||
|
||||
// plugin routes
|
||||
pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1)
|
||||
pluginAPIRouter.RegisterAuthUserConnectorRouter(authV1)
|
||||
pluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1)
|
||||
|
||||
_ = plugin.CallAgent(func(agent plugin.Agent) error {
|
||||
agent.RegisterUnAuthRouter(mustUnAuthV1)
|
||||
agent.RegisterAuthUserRouter(authV1)
|
||||
agent.RegisterAuthAdminRouter(adminauthV1)
|
||||
return nil
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
|
||||
originalTr := struct {
|
||||
Backend map[string]map[string]interface{} `yaml:"backend"`
|
||||
UI map[string]interface{} `yaml:"ui"`
|
||||
Plugin map[string]interface{} `yaml:"plugin"`
|
||||
}{}
|
||||
if err = yaml.Unmarshal(buf, &originalTr); err != nil {
|
||||
return nil, err
|
||||
@@ -69,6 +70,7 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
|
||||
}
|
||||
translation["backend"] = originalTr.Backend
|
||||
translation["ui"] = originalTr.UI
|
||||
translation["plugin"] = originalTr.Plugin
|
||||
|
||||
content, err := yaml.Marshal(translation)
|
||||
if err != nil {
|
||||
@@ -120,6 +122,9 @@ func CheckLanguageIsValid(lang string) bool {
|
||||
|
||||
// Tr use language to translate data. If this language translation is not available, return default english translation.
|
||||
func Tr(lang i18n.Language, data string) string {
|
||||
if GlobalTrans == nil {
|
||||
return data
|
||||
}
|
||||
translation := GlobalTrans.Tr(lang, data)
|
||||
if translation == data {
|
||||
return GlobalTrans.Tr(i18n.DefaultLanguage, data)
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
"github.com/answerdev/answer/pkg/writer"
|
||||
"github.com/answerdev/answer/ui"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const (
|
||||
mainGoTpl = `package main
|
||||
|
||||
import (
|
||||
answercmd "github.com/answerdev/answer/cmd"
|
||||
|
||||
// remote plugins
|
||||
{{- range .remote_plugins}}
|
||||
_ "{{.}}"
|
||||
{{- end}}
|
||||
|
||||
// local plugins
|
||||
{{- range .local_plugins}}
|
||||
_ "answer/{{.}}"
|
||||
{{- end}}
|
||||
)
|
||||
|
||||
func main() {
|
||||
answercmd.Main()
|
||||
}
|
||||
`
|
||||
goModTpl = `module answer
|
||||
|
||||
go 1.19
|
||||
`
|
||||
)
|
||||
|
||||
type answerBuilder struct {
|
||||
buildingMaterial *buildingMaterial
|
||||
BuildError error
|
||||
}
|
||||
|
||||
type buildingMaterial struct {
|
||||
answerModuleReplacement string
|
||||
plugins []*pluginInfo
|
||||
outputPath string
|
||||
tmpDir string
|
||||
originalAnswerInfo OriginalAnswerInfo
|
||||
}
|
||||
|
||||
type OriginalAnswerInfo struct {
|
||||
Version string
|
||||
Revision string
|
||||
Time string
|
||||
}
|
||||
|
||||
type pluginInfo struct {
|
||||
// Name of the plugin e.g. github.com/answerdev/github-connector
|
||||
Name string
|
||||
// Path to the plugin. If path exist, read plugin from local filesystem
|
||||
Path string
|
||||
// Version of the plugin
|
||||
Version string
|
||||
}
|
||||
|
||||
func newAnswerBuilder(outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) *answerBuilder {
|
||||
material := &buildingMaterial{originalAnswerInfo: originalAnswerInfo}
|
||||
parentDir, _ := filepath.Abs(".")
|
||||
material.tmpDir, _ = os.MkdirTemp(parentDir, "answer_build")
|
||||
if len(outputPath) == 0 {
|
||||
outputPath = filepath.Join(parentDir, "new_answer")
|
||||
}
|
||||
material.outputPath = outputPath
|
||||
material.plugins = formatPlugins(plugins)
|
||||
material.answerModuleReplacement = os.Getenv("ANSWER_MODULE")
|
||||
return &answerBuilder{
|
||||
buildingMaterial: material,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *answerBuilder) DoTask(task func(b *buildingMaterial) error) {
|
||||
if a.BuildError != nil {
|
||||
return
|
||||
}
|
||||
a.BuildError = task(a.buildingMaterial)
|
||||
}
|
||||
|
||||
// BuildNewAnswer builds a new answer with specified plugins
|
||||
func BuildNewAnswer(outputPath string, plugins []string, originalAnswerInfo OriginalAnswerInfo) (err error) {
|
||||
builder := newAnswerBuilder(outputPath, plugins, originalAnswerInfo)
|
||||
builder.DoTask(createMainGoFile)
|
||||
builder.DoTask(downloadGoModFile)
|
||||
builder.DoTask(mergeI18nFiles)
|
||||
builder.DoTask(replaceNecessaryFile)
|
||||
builder.DoTask(buildBinary)
|
||||
builder.DoTask(cleanByproduct)
|
||||
return builder.BuildError
|
||||
}
|
||||
|
||||
func formatPlugins(plugins []string) (formatted []*pluginInfo) {
|
||||
for _, plugin := range plugins {
|
||||
plugin = strings.TrimSpace(plugin)
|
||||
// plugin description like this 'github.com/answerdev/github-connector@latest=/local/path'
|
||||
info := &pluginInfo{}
|
||||
plugin, info.Path, _ = strings.Cut(plugin, "=")
|
||||
info.Name, info.Version, _ = strings.Cut(plugin, "@")
|
||||
formatted = append(formatted, info)
|
||||
}
|
||||
return formatted
|
||||
}
|
||||
|
||||
func createMainGoFile(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("[build] tmp dir: %s\n", b.tmpDir)
|
||||
err = dir.CreateDirIfNotExist(b.tmpDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
remotePlugins []string
|
||||
)
|
||||
for _, p := range b.plugins {
|
||||
remotePlugins = append(remotePlugins, versionedModulePath(p.Name, p.Version))
|
||||
}
|
||||
|
||||
mainGoFile := &bytes.Buffer{}
|
||||
tmpl, err := template.New("main").Parse(mainGoTpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = tmpl.Execute(mainGoFile, map[string]any{
|
||||
"remote_plugins": remotePlugins,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writer.WriteFile(filepath.Join(b.tmpDir, "main.go"), mainGoFile.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = writer.WriteFile(filepath.Join(b.tmpDir, "go.mod"), goModTpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, p := range b.plugins {
|
||||
if len(p.Path) == 0 {
|
||||
continue
|
||||
}
|
||||
replacement := fmt.Sprintf("%s@v%s=%s", p.Name, p.Version, p.Path)
|
||||
err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func downloadGoModFile(b *buildingMaterial) (err error) {
|
||||
// If user specify a module replacement, use it. Otherwise, use the latest version.
|
||||
if len(b.answerModuleReplacement) > 0 {
|
||||
replacement := fmt.Sprintf("%s=%s", "github.com/answerdev/answer", b.answerModuleReplacement)
|
||||
err = b.newExecCmd("go", "mod", "edit", "-replace", replacement).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = b.newExecCmd("go", "mod", "tidy").Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = b.newExecCmd("go", "mod", "vendor").Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func replaceNecessaryFile(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("try to replace ui build directory\n")
|
||||
uiBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui")
|
||||
err = copyDirEntries(ui.Build, ".", uiBuildDir)
|
||||
return err
|
||||
}
|
||||
|
||||
func mergeI18nFiles(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("try to merge i18n files\n")
|
||||
|
||||
type YamlPluginContent struct {
|
||||
Plugin map[string]any `yaml:"plugin"`
|
||||
}
|
||||
|
||||
pluginAllTranslations := make(map[string]*YamlPluginContent)
|
||||
for _, plugin := range b.plugins {
|
||||
i18nDir := filepath.Join(b.tmpDir, fmt.Sprintf("vendor/%s/i18n", plugin.Name))
|
||||
fmt.Println("i18n dir: ", i18nDir)
|
||||
if !dir.CheckDirExist(i18nDir) {
|
||||
continue
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(i18nDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range entries {
|
||||
// ignore directory
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
// ignore non-YAML file
|
||||
if filepath.Ext(file.Name()) != ".yaml" {
|
||||
continue
|
||||
}
|
||||
buf, err := os.ReadFile(filepath.Join(i18nDir, file.Name()))
|
||||
if err != nil {
|
||||
log.Debugf("read translation file failed: %s %s", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
translation := &YamlPluginContent{}
|
||||
if err = yaml.Unmarshal(buf, translation); err != nil {
|
||||
log.Debugf("unmarshal translation file failed: %s %s", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if pluginAllTranslations[file.Name()] == nil {
|
||||
pluginAllTranslations[file.Name()] = &YamlPluginContent{Plugin: make(map[string]any)}
|
||||
}
|
||||
for k, v := range translation.Plugin {
|
||||
pluginAllTranslations[file.Name()].Plugin[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
originalI18nDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/i18n")
|
||||
entries, err := os.ReadDir(originalI18nDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range entries {
|
||||
// ignore directory
|
||||
if file.IsDir() {
|
||||
continue
|
||||
}
|
||||
// ignore non-YAML file
|
||||
filename := file.Name()
|
||||
if filepath.Ext(filename) != ".yaml" && filename != "i18n.yaml" {
|
||||
continue
|
||||
}
|
||||
|
||||
// if plugin don't have this translation file, ignore it
|
||||
if pluginAllTranslations[filename] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
out, _ := yaml.Marshal(pluginAllTranslations[filename])
|
||||
|
||||
buf, err := os.OpenFile(filepath.Join(originalI18nDir, filename), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
log.Debugf("read translation file failed: %s %s", filename, err)
|
||||
continue
|
||||
}
|
||||
|
||||
_, _ = buf.WriteString("\n")
|
||||
_, _ = buf.Write(out)
|
||||
_ = buf.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func copyDirEntries(sourceFs embed.FS, sourceDir string, targetDir string) (err error) {
|
||||
entries, err := ui.Build.ReadDir(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dir.CreateDirIfNotExist(targetDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
err = copyDirEntries(sourceFs, filepath.Join(sourceDir, entry.Name()), filepath.Join(targetDir, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
file, err := sourceFs.ReadFile(filepath.Join(sourceDir, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := filepath.Join(targetDir, entry.Name())
|
||||
err = os.WriteFile(filename, file, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildBinary(b *buildingMaterial) (err error) {
|
||||
versionInfo := b.originalAnswerInfo
|
||||
cmdPkg := "github.com/answerdev/answer/cmd"
|
||||
ldflags := fmt.Sprintf("-X %s.Version=%s -X %s.Revision=%s -X %s.Time=%s",
|
||||
cmdPkg, versionInfo.Version, cmdPkg, versionInfo.Revision, cmdPkg, versionInfo.Time)
|
||||
err = b.newExecCmd("go", "build",
|
||||
"-ldflags", ldflags, "-o", b.outputPath, ".").Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func cleanByproduct(b *buildingMaterial) (err error) {
|
||||
return os.RemoveAll(b.tmpDir)
|
||||
}
|
||||
|
||||
func (b *buildingMaterial) newExecCmd(command string, args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(command, args...)
|
||||
fmt.Println(cmd.Args)
|
||||
cmd.Dir = b.tmpDir
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
}
|
||||
|
||||
func versionedModulePath(modulePath, moduleVersion string) string {
|
||||
if moduleVersion == "" {
|
||||
return modulePath
|
||||
}
|
||||
ver, err := semver.StrictNewVersion(strings.TrimPrefix(moduleVersion, "v"))
|
||||
if err != nil {
|
||||
return modulePath
|
||||
}
|
||||
major := ver.Major()
|
||||
if major > 1 {
|
||||
modulePath += fmt.Sprintf("/v%d", major)
|
||||
}
|
||||
return path.Clean(modulePath)
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/answerdev/answer/internal/service/user_external_login"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
const (
|
||||
commonRouterPrefix = "/answer/api/v1"
|
||||
ConnectorLoginRouterPrefix = "/connector/login/"
|
||||
ConnectorRedirectRouterPrefix = "/connector/redirect/"
|
||||
)
|
||||
|
||||
// ConnectorController comment controller
|
||||
type ConnectorController struct {
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
userExternalService *user_external_login.UserExternalLoginService
|
||||
emailService *export.EmailService
|
||||
}
|
||||
|
||||
// NewConnectorController new controller
|
||||
func NewConnectorController(
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService,
|
||||
emailService *export.EmailService,
|
||||
userExternalService *user_external_login.UserExternalLoginService,
|
||||
) *ConnectorController {
|
||||
return &ConnectorController{
|
||||
siteInfoService: siteInfoService,
|
||||
userExternalService: userExternalService,
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectorLoginDispatcher dispatch connector login request to specific connector by slug name
|
||||
// We can't register specific router for each connector when application start, because the plugin status will be changed by admin.
|
||||
// If the plugin is disabled, the router should be unavailable.
|
||||
func (cc *ConnectorController) ConnectorLoginDispatcher(ctx *gin.Context) {
|
||||
slugName := ctx.Param("name")
|
||||
var c plugin.Connector
|
||||
_ = plugin.CallConnector(func(connector plugin.Connector) error {
|
||||
if connector.ConnectorSlugName() == slugName {
|
||||
c = connector
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if c == nil {
|
||||
log.Errorf("connector %s not found", slugName)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
cc.ConnectorLogin(c)(ctx)
|
||||
}
|
||||
|
||||
func (cc *ConnectorController) ConnectorRedirectDispatcher(ctx *gin.Context) {
|
||||
slugName := ctx.Param("name")
|
||||
var c plugin.Connector
|
||||
_ = plugin.CallConnector(func(connector plugin.Connector) error {
|
||||
if connector.ConnectorSlugName() == slugName {
|
||||
c = connector
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if c == nil {
|
||||
log.Errorf("connector %s not found", slugName)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
cc.ConnectorRedirect(c)(ctx)
|
||||
}
|
||||
|
||||
func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn func(ctx *gin.Context)) {
|
||||
return func(ctx *gin.Context) {
|
||||
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
receiverURL := fmt.Sprintf("%s%s%s%s", general.SiteUrl,
|
||||
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
|
||||
redirectURL := connector.ConnectorSender(ctx, receiverURL)
|
||||
if len(redirectURL) > 0 {
|
||||
ctx.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn func(ctx *gin.Context)) {
|
||||
return func(ctx *gin.Context) {
|
||||
siteGeneral, err := cc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site info failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
receiverURL := fmt.Sprintf("%s%s%s%s", siteGeneral.SiteUrl,
|
||||
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
|
||||
userInfo, err := connector.ConnectorReceiver(ctx, receiverURL)
|
||||
if err != nil {
|
||||
log.Errorf("connector received failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
log.Debugf("connector received: %+v", userInfo)
|
||||
u := &schema.ExternalLoginUserInfoCache{
|
||||
Provider: connector.ConnectorSlugName(),
|
||||
ExternalID: userInfo.ExternalID,
|
||||
DisplayName: userInfo.DisplayName,
|
||||
Username: userInfo.Username,
|
||||
Email: userInfo.Email,
|
||||
Avatar: userInfo.Avatar,
|
||||
MetaInfo: userInfo.MetaInfo,
|
||||
}
|
||||
resp, err := cc.userExternalService.ExternalLogin(ctx, u)
|
||||
if err != nil {
|
||||
log.Errorf("external login failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
if len(resp.AccessToken) > 0 {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
|
||||
siteGeneral.SiteUrl, resp.AccessToken))
|
||||
} else {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/confirm-email?binding_key=%s",
|
||||
siteGeneral.SiteUrl, resp.BindingKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectorsInfo get all enabled connectors
|
||||
// @Summary get all enabled connectors
|
||||
// @Description get all enabled connectors
|
||||
// @Tags PluginConnector
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorInfoResp}
|
||||
// @Router /answer/api/v1/connector/info [get]
|
||||
func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) {
|
||||
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp := make([]*schema.ConnectorInfoResp, 0)
|
||||
_ = plugin.CallConnector(func(fn plugin.Connector) error {
|
||||
connectorName := fn.ConnectorName()
|
||||
resp = append(resp, &schema.ConnectorInfoResp{
|
||||
Name: connectorName.Translate(ctx),
|
||||
Icon: fn.ConnectorLogoSVG(),
|
||||
Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl,
|
||||
commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// ExternalLoginBindingUserSendEmail external login binding user send email
|
||||
// @Summary external login binding user send email
|
||||
// @Description external login binding user send email
|
||||
// @Tags PluginConnector
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.ExternalLoginBindingUserSendEmailReq true "external login binding user send email"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.ExternalLoginBindingUserSendEmailResp}
|
||||
// @Router /answer/api/v1/connector/binding/email [post]
|
||||
func (cc *ConnectorController) ExternalLoginBindingUserSendEmail(ctx *gin.Context) {
|
||||
req := &schema.ExternalLoginBindingUserSendEmailReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := cc.userExternalService.ExternalLoginBindingUserSendEmail(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// ConnectorsUserInfo get all connectors info about user
|
||||
// @Summary get all connectors info about user
|
||||
// @Description get all connectors info about user
|
||||
// @Tags PluginConnector
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorUserInfoResp}
|
||||
// @Router /answer/api/v1/connector/user/info [get]
|
||||
func (cc *ConnectorController) ConnectorsUserInfo(ctx *gin.Context) {
|
||||
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
userInfoList, err := cc.userExternalService.GetExternalLoginUserInfoList(ctx, userID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
userExternalLoginMapping := make(map[string]string)
|
||||
for _, userInfo := range userInfoList {
|
||||
userExternalLoginMapping[userInfo.Provider] = userInfo.ExternalID
|
||||
}
|
||||
|
||||
resp := make([]*schema.ConnectorUserInfoResp, 0)
|
||||
_ = plugin.CallConnector(func(fn plugin.Connector) error {
|
||||
externalID := userExternalLoginMapping[fn.ConnectorSlugName()]
|
||||
connectorName := fn.ConnectorName()
|
||||
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()),
|
||||
Binding: len(externalID) > 0,
|
||||
ExternalID: externalID,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// ExternalLoginUnbinding unbind external user login
|
||||
// @Summary unbind external user login
|
||||
// @Description unbind external user login
|
||||
// @Tags PluginConnector
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body schema.ExternalLoginUnbindingReq true "ExternalLoginUnbindingReq"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/api/v1/connector/user/unbinding [delete]
|
||||
func (cc *ConnectorController) ExternalLoginUnbinding(ctx *gin.Context) {
|
||||
req := &schema.ExternalLoginUnbindingReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := cc.userExternalService.ExternalLoginUnbinding(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -24,4 +24,6 @@ var ProviderSetController = wire.NewSet(
|
||||
NewUploadController,
|
||||
NewActivityController,
|
||||
NewTemplateController,
|
||||
NewConnectorController,
|
||||
NewUserCenterController,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/answerdev/answer/internal/service/user_external_login"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
const (
|
||||
UserCenterLoginRouter = "/user-center/login/redirect"
|
||||
UserCenterSignUpRedirectRouter = "/user-center/sign-up/redirect"
|
||||
)
|
||||
|
||||
// UserCenterController comment controller
|
||||
type UserCenterController struct {
|
||||
userCenterLoginService *user_external_login.UserCenterLoginService
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewUserCenterController new controller
|
||||
func NewUserCenterController(
|
||||
userCenterLoginService *user_external_login.UserCenterLoginService,
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService,
|
||||
) *UserCenterController {
|
||||
return &UserCenterController{
|
||||
userCenterLoginService: userCenterLoginService,
|
||||
siteInfoService: siteInfoService,
|
||||
}
|
||||
}
|
||||
|
||||
// UserCenterAgent get user center agent info
|
||||
func (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) {
|
||||
resp := &schema.UserCenterAgentResp{}
|
||||
resp.Enabled = plugin.UserCenterEnabled()
|
||||
if !resp.Enabled {
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
return
|
||||
}
|
||||
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site info failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
resp.AgentInfo = &schema.AgentInfo{}
|
||||
resp.AgentInfo.LoginRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl,
|
||||
commonRouterPrefix, UserCenterLoginRouter)
|
||||
resp.AgentInfo.SignUpRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl,
|
||||
commonRouterPrefix, UserCenterSignUpRedirectRouter)
|
||||
|
||||
_ = plugin.CallUserCenter(func(uc plugin.UserCenter) error {
|
||||
info := uc.Description()
|
||||
resp.AgentInfo.Name = info.Name
|
||||
resp.AgentInfo.DisplayName = info.DisplayName.Translate(ctx)
|
||||
resp.AgentInfo.Icon = info.Icon
|
||||
resp.AgentInfo.Url = info.Url
|
||||
resp.AgentInfo.ControlCenterItems = make([]*schema.ControlCenter, 0)
|
||||
resp.AgentInfo.EnabledOriginalUserSystem = info.EnabledOriginalUserSystem
|
||||
items := uc.ControlCenterItems()
|
||||
for _, item := range items {
|
||||
resp.AgentInfo.ControlCenterItems = append(resp.AgentInfo.ControlCenterItems, &schema.ControlCenter{
|
||||
Name: item.Name,
|
||||
Label: item.Label,
|
||||
Url: item.Url,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// UserCenterPersonalBranding get user center personal user info
|
||||
func (uc *UserCenterController) UserCenterPersonalBranding(ctx *gin.Context) {
|
||||
req := &schema.GetOtherUserInfoByUsernameReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userCenterLoginService.UserCenterPersonalBranding(ctx, req.Username)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
func (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) {
|
||||
var redirectURL string
|
||||
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
|
||||
info := userCenter.Description()
|
||||
redirectURL = info.LoginRedirectURL
|
||||
return nil
|
||||
})
|
||||
ctx.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
func (uc *UserCenterController) UserCenterSignUpRedirect(ctx *gin.Context) {
|
||||
var redirectURL string
|
||||
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
|
||||
info := userCenter.Description()
|
||||
redirectURL = info.LoginRedirectURL
|
||||
return nil
|
||||
})
|
||||
ctx.Redirect(http.StatusFound, redirectURL)
|
||||
}
|
||||
|
||||
func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) {
|
||||
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site info failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
ctx.Redirect(http.StatusFound, "/404")
|
||||
return
|
||||
}
|
||||
userInfo, err := userCenter.LoginCallback(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
if !ctx.IsAborted() {
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
|
||||
if err != nil {
|
||||
log.Errorf("external login failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
if len(resp.ErrMsg) > 0 {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
|
||||
return
|
||||
}
|
||||
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
|
||||
siteGeneral.SiteUrl, resp.AccessToken))
|
||||
}
|
||||
|
||||
func (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) {
|
||||
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site info failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
ctx.Redirect(http.StatusFound, "/404")
|
||||
return
|
||||
}
|
||||
userInfo, err := userCenter.SignUpCallback(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
|
||||
if err != nil {
|
||||
log.Errorf("external login failed: %v", err)
|
||||
ctx.Redirect(http.StatusFound, "/50x")
|
||||
return
|
||||
}
|
||||
if len(resp.ErrMsg) > 0 {
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
|
||||
return
|
||||
}
|
||||
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
|
||||
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
|
||||
siteGeneral.SiteUrl, resp.AccessToken))
|
||||
}
|
||||
|
||||
// UserCenterUserSettings user center user settings
|
||||
func (uc *UserCenterController) UserCenterUserSettings(ctx *gin.Context) {
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := uc.userCenterLoginService.UserCenterUserSettings(ctx, userID)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserCenterAdminFunctionAgent user center admin function agent
|
||||
func (uc *UserCenterController) UserCenterAdminFunctionAgent(ctx *gin.Context) {
|
||||
resp, err := uc.userCenterLoginService.UserCenterAdminFunctionAgent(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
@@ -552,84 +551,75 @@ func (qc *QuestionController) UserTop(ctx *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// UserList godoc
|
||||
// @Summary UserList
|
||||
// @Description UserList
|
||||
// @Tags Question
|
||||
// PersonalQuestionPage list personal questions
|
||||
// @Summary list personal questions
|
||||
// @Description list personal questions
|
||||
// @Tags Personal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username" default(string)
|
||||
// @Param order query string true "order" Enums(newest,score)
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param pagesize query string true "pagesize" default(20)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /personal/question/page [get]
|
||||
func (qc *QuestionController) UserList(ctx *gin.Context) {
|
||||
userName := ctx.Query("username")
|
||||
order := ctx.Query("order")
|
||||
pageStr := ctx.Query("page")
|
||||
pageSizeStr := ctx.Query("pagesize")
|
||||
page := converter.StringToInt(pageStr)
|
||||
pageSize := converter.StringToInt(pageSizeStr)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, count, err := qc.questionService.SearchUserList(ctx, userName, order, page, pageSize, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"list": questionList,
|
||||
"count": count,
|
||||
})
|
||||
func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalQuestionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := qc.questionService.PersonalQuestionPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserAnswerList godoc
|
||||
// @Summary UserAnswerList
|
||||
// @Description UserAnswerList
|
||||
// @Tags api-answer
|
||||
// PersonalAnswerPage list personal answers
|
||||
// @Summary list personal answers
|
||||
// @Description list personal answers
|
||||
// @Tags Personal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username" default(string)
|
||||
// @Param order query string true "order" Enums(newest,score)
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param pagesize query string true "pagesize" default(20)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/personal/answer/page [get]
|
||||
func (qc *QuestionController) UserAnswerList(ctx *gin.Context) {
|
||||
userName := ctx.Query("username")
|
||||
order := ctx.Query("order")
|
||||
pageStr := ctx.Query("page")
|
||||
pageSizeStr := ctx.Query("pagesize")
|
||||
page := converter.StringToInt(pageStr)
|
||||
pageSize := converter.StringToInt(pageSizeStr)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, count, err := qc.questionService.SearchUserAnswerList(ctx, userName, order, page, pageSize, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"list": questionList,
|
||||
"count": count,
|
||||
})
|
||||
func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalAnswerPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := qc.questionService.PersonalAnswerPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserCollectionList godoc
|
||||
// @Summary UserCollectionList
|
||||
// @Description UserCollectionList
|
||||
// PersonalCollectionPage list personal collections
|
||||
// @Summary list personal collections
|
||||
// @Description list personal collections
|
||||
// @Tags Collection
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param pagesize query string true "pagesize" default(20)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/personal/collection/page [get]
|
||||
func (qc *QuestionController) UserCollectionList(ctx *gin.Context) {
|
||||
pageStr := ctx.Query("page")
|
||||
pageSizeStr := ctx.Query("pagesize")
|
||||
page := converter.StringToInt(pageStr)
|
||||
pageSize := converter.StringToInt(pageSizeStr)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, count, err := qc.questionService.SearchUserCollectionList(ctx, page, pageSize, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"list": questionList,
|
||||
"count": count,
|
||||
})
|
||||
func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalCollectionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := qc.questionService.PersonalCollectionPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// AdminSearchList godoc
|
||||
@@ -678,6 +668,9 @@ func (qc *QuestionController) AdminSearchAnswerList(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
req.QuestionID = uid.DeShortID(req.QuestionID)
|
||||
if req.QuestionID == "0" {
|
||||
req.QuestionID = ""
|
||||
}
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, count, err := qc.questionService.AdminSearchAnswerList(ctx, req, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
|
||||
@@ -64,6 +64,10 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
resp.SiteUsers, err = sc.siteInfoService.GetSiteUsers(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
@@ -183,9 +184,9 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userService.UseRePassword(ctx, req)
|
||||
err := uc.userService.UpdatePasswordWhenForgot(ctx, req)
|
||||
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserLogout user logout
|
||||
@@ -223,7 +224,7 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !siteInfo.AllowNewRegistrations {
|
||||
if !siteInfo.AllowNewRegistrations || !siteInfo.AllowEmailRegistrations {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil)
|
||||
return
|
||||
}
|
||||
@@ -232,6 +233,10 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)
|
||||
return
|
||||
}
|
||||
req.IP = ctx.ClientIP()
|
||||
captchaPass := uc.actionService.UserRegisterVerifyCaptcha(ctx, req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
@@ -334,15 +339,16 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UserModifyPassWordRequest true "UserModifyPassWordRequest"
|
||||
// @Param data body schema.UserModifyPasswordReq true "UserModifyPasswordReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/password [put]
|
||||
func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
||||
req := &schema.UserModifyPassWordRequest{}
|
||||
req := &schema.UserModifyPasswordReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.AccessToken = middleware.ExtractToken(ctx)
|
||||
|
||||
oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)
|
||||
if err != nil {
|
||||
@@ -488,6 +494,16 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
|
||||
return
|
||||
}
|
||||
// check whether email allow register or not
|
||||
siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {
|
||||
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)
|
||||
return
|
||||
}
|
||||
|
||||
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
|
||||
if !captchaPass {
|
||||
|
||||
@@ -9,4 +9,5 @@ var ProviderSetController = wire.NewSet(
|
||||
NewThemeController,
|
||||
NewSiteInfoController,
|
||||
NewRoleController,
|
||||
NewPluginController,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package controller_admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/plugin_common"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PluginController role controller
|
||||
type PluginController struct {
|
||||
PluginCommonService *plugin_common.PluginCommonService
|
||||
}
|
||||
|
||||
// NewPluginController new controller
|
||||
func NewPluginController(PluginCommonService *plugin_common.PluginCommonService) *PluginController {
|
||||
return &PluginController{PluginCommonService: PluginCommonService}
|
||||
}
|
||||
|
||||
// GetPluginList get plugin list
|
||||
// @Summary get plugin list
|
||||
// @Description get plugin list
|
||||
// @Tags AdminPlugin
|
||||
// @Security ApiKeyAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status query string false "status: active/inactive"
|
||||
// @Param have_config query boolean false "have config"
|
||||
// @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp}
|
||||
// @Router /answer/admin/api/plugins [get]
|
||||
func (pc *PluginController) GetPluginList(ctx *gin.Context) {
|
||||
req := &schema.GetPluginListReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
pluginConfigMapping := make(map[string]bool)
|
||||
_ = plugin.CallConfig(func(fn plugin.Config) error {
|
||||
if len(fn.ConfigFields()) > 0 {
|
||||
pluginConfigMapping[fn.Info().SlugName] = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
resp := make([]*schema.GetPluginListResp, 0)
|
||||
_ = plugin.CallBase(func(base plugin.Base) error {
|
||||
info := base.Info()
|
||||
resp = append(resp, &schema.GetPluginListResp{
|
||||
Name: info.Name.Translate(ctx),
|
||||
SlugName: info.SlugName,
|
||||
Description: info.Description.Translate(ctx),
|
||||
Version: info.Version,
|
||||
Enabled: plugin.StatusManager.IsEnabled(info.SlugName),
|
||||
HaveConfig: pluginConfigMapping[info.SlugName],
|
||||
Link: info.Link,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
if len(req.Status) > 0 {
|
||||
resp = pc.filterPluginByStatus(resp, req.Status)
|
||||
}
|
||||
if req.HaveConfig {
|
||||
resp = pc.filterNoConfigPlugin(resp)
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
func (pc *PluginController) filterNoConfigPlugin(list []*schema.GetPluginListResp) []*schema.GetPluginListResp {
|
||||
resp := make([]*schema.GetPluginListResp, 0)
|
||||
for _, t := range list {
|
||||
if t.HaveConfig {
|
||||
resp = append(resp, t)
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (pc *PluginController) filterPluginByStatus(list []*schema.GetPluginListResp, status schema.PluginStatus,
|
||||
) []*schema.GetPluginListResp {
|
||||
resp := make([]*schema.GetPluginListResp, 0)
|
||||
for _, t := range list {
|
||||
if status == schema.PluginStatusActive && t.Enabled {
|
||||
resp = append(resp, t)
|
||||
} else if status == schema.PluginStatusInactive && !t.Enabled {
|
||||
resp = append(resp, t)
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// UpdatePluginStatus update plugin status
|
||||
// @Summary update plugin status
|
||||
// @Description update plugin status
|
||||
// @Tags AdminPlugin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UpdatePluginStatusReq true "UpdatePluginStatusReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/plugin/status [put]
|
||||
func (pc *PluginController) UpdatePluginStatus(ctx *gin.Context) {
|
||||
req := &schema.UpdatePluginStatusReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
plugin.StatusManager.Enable(req.PluginSlugName, req.Enabled)
|
||||
err := pc.PluginCommonService.UpdatePluginStatus(ctx)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetPluginConfig get plugin config
|
||||
// @Summary get plugin config
|
||||
// @Description get plugin config
|
||||
// @Tags AdminPlugin
|
||||
// @Security ApiKeyAuth
|
||||
// @Produce json
|
||||
// @Param plugin_slug_name query string true "plugin_slug_name"
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp}
|
||||
// @Router /answer/admin/api/plugin/config [get]
|
||||
func (pc *PluginController) GetPluginConfig(ctx *gin.Context) {
|
||||
req := &schema.GetPluginConfigReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp := &schema.GetPluginConfigResp{}
|
||||
_ = plugin.CallBase(func(base plugin.Base) error {
|
||||
if base.Info().SlugName != req.PluginSlugName {
|
||||
return nil
|
||||
}
|
||||
info := base.Info()
|
||||
resp.Name = info.Name.Translate(ctx)
|
||||
resp.SlugName = info.SlugName
|
||||
resp.Description = info.Description.Translate(ctx)
|
||||
resp.Version = info.Version
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = plugin.CallConfig(func(fn plugin.Config) error {
|
||||
if fn.Info().SlugName != req.PluginSlugName {
|
||||
return nil
|
||||
}
|
||||
resp.SetConfigFields(ctx, fn.ConfigFields())
|
||||
return nil
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
|
||||
// UpdatePluginConfig update plugin config
|
||||
// @Summary update plugin config
|
||||
// @Description update plugin config
|
||||
// @Tags AdminPlugin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UpdatePluginConfigReq true "UpdatePluginConfigReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/plugin/config [put]
|
||||
func (pc *PluginController) UpdatePluginConfig(ctx *gin.Context) {
|
||||
req := &schema.UpdatePluginConfigReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
configFields, _ := json.Marshal(req.ConfigFields)
|
||||
err := plugin.CallConfig(func(fn plugin.Config) error {
|
||||
if fn.Info().SlugName == req.PluginSlugName {
|
||||
return fn.ConfigReceiver(configFields)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
err = pc.PluginCommonService.UpdatePluginConfig(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
@@ -139,6 +139,19 @@ func (sc *SiteInfoController) GetSiteTheme(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetSiteUsers get site user config
|
||||
// @Summary get site user config
|
||||
// @Description get site user config
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.SiteUsersResp}
|
||||
// @Router /answer/admin/api/siteinfo/users [get]
|
||||
func (sc *SiteInfoController) GetSiteUsers(ctx *gin.Context) {
|
||||
resp, err := sc.siteInfoService.GetSiteUsers(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// GetRobots get site robots information
|
||||
// @Summary get site robots information
|
||||
// @Description get site robots information
|
||||
@@ -336,6 +349,24 @@ func (sc *SiteInfoController) SaveSiteTheme(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UpdateSiteUsers update site config about users
|
||||
// @Summary update site info config about users
|
||||
// @Description update site info config about users
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Param data body schema.SiteUsersReq true "users info"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/admin/api/siteinfo/users [put]
|
||||
func (sc *SiteInfoController) UpdateSiteUsers(ctx *gin.Context) {
|
||||
req := &schema.SiteUsersReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
err := sc.siteInfoService.SaveSiteUsers(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetSMTPConfig get smtp config
|
||||
// @Summary GetSMTPConfig get smtp config
|
||||
// @Description GetSMTPConfig get smtp config
|
||||
@@ -366,3 +397,34 @@ func (sc *SiteInfoController) UpdateSMTPConfig(ctx *gin.Context) {
|
||||
err := sc.siteInfoService.UpdateSMTPConfig(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// GetPrivilegesConfig get privileges config
|
||||
// @Summary GetPrivilegesConfig get privileges config
|
||||
// @Description GetPrivilegesConfig get privileges config
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Success 200 {object} handler.RespBody{data=schema.GetPrivilegesConfigResp}
|
||||
// @Router /answer/admin/api/setting/privileges [get]
|
||||
func (sc *SiteInfoController) GetPrivilegesConfig(ctx *gin.Context) {
|
||||
resp, err := sc.siteInfoService.GetPrivilegesConfig(ctx)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UpdatePrivilegesConfig update privileges config
|
||||
// @Summary update privileges config
|
||||
// @Description update privileges config
|
||||
// @Security ApiKeyAuth
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Param data body schema.UpdatePrivilegesConfigReq true "config"
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /answer/admin/api/setting/privileges [put]
|
||||
func (sc *SiteInfoController) UpdatePrivilegesConfig(ctx *gin.Context) {
|
||||
req := &schema.UpdatePrivilegesConfigReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
err := sc.siteInfoService.UpdatePrivilegesConfig(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,12 @@ package controller_admin
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/user_admin"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// UserAdminController user controller
|
||||
@@ -29,6 +32,10 @@ func NewUserAdminController(userService *user_admin.UserAdminService) *UserAdmin
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/user/status [put]
|
||||
func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) {
|
||||
if u, ok := plugin.GetUserCenter(); ok && u.Description().UserStatusAgentEnabled {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
|
||||
return
|
||||
}
|
||||
req := &schema.UpdateUserStatusReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
|
||||
@@ -6,4 +6,5 @@ type UserCacheInfo struct {
|
||||
UserStatus int `json:"user_status"`
|
||||
EmailStatus int `json:"email_status"`
|
||||
RoleID int `json:"role_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package entity
|
||||
|
||||
// PluginConfig plugin config
|
||||
type PluginConfig struct {
|
||||
ID int `xorm:"not null pk autoincr INT(11) id"`
|
||||
PluginSlugName string `xorm:"unique VARCHAR(128) plugin_slug_name"`
|
||||
Value string `xorm:"TEXT value"`
|
||||
}
|
||||
|
||||
// TableName config table name
|
||||
func (PluginConfig) TableName() string {
|
||||
return "plugin_config"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
// UserExternalLogin user external login
|
||||
type UserExternalLogin struct {
|
||||
ID int64 `xorm:"not null pk autoincr BIGINT(20) id"`
|
||||
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
|
||||
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
|
||||
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
|
||||
Provider string `xorm:"not null default '' VARCHAR(100) provider"`
|
||||
ExternalID string `xorm:"not null default '' VARCHAR(128) external_id"`
|
||||
MetaInfo string `xorm:"TEXT meta_info"`
|
||||
}
|
||||
|
||||
// TableName table name
|
||||
func (UserExternalLogin) TableName() string {
|
||||
return "user_external_login"
|
||||
}
|
||||
@@ -50,6 +50,8 @@ var tables = []interface{}{
|
||||
&entity.RolePowerRel{},
|
||||
&entity.Power{},
|
||||
&entity.UserRoleRel{},
|
||||
&entity.PluginConfig{},
|
||||
&entity.UserExternalLogin{},
|
||||
}
|
||||
|
||||
// InitDB init db
|
||||
@@ -112,9 +114,8 @@ func initAdminUser(engine *xorm.Engine) error {
|
||||
|
||||
func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail string) error {
|
||||
interfaceData := map[string]string{
|
||||
"logo": "",
|
||||
"theme": "black",
|
||||
"language": language,
|
||||
"language": language,
|
||||
"time_zone": "UTC",
|
||||
}
|
||||
interfaceDataBytes, _ := json.Marshal(interfaceData)
|
||||
_, err := engine.InsertOne(&entity.SiteInfo{
|
||||
@@ -142,8 +143,9 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
|
||||
}
|
||||
|
||||
loginConfig := map[string]bool{
|
||||
"allow_new_registrations": true,
|
||||
"login_required": false,
|
||||
"allow_new_registrations": true,
|
||||
"allow_email_registrations": true,
|
||||
"login_required": false,
|
||||
}
|
||||
loginConfigDataBytes, _ := json.Marshal(loginConfig)
|
||||
_, err = engine.InsertOne(&entity.SiteInfo{
|
||||
@@ -177,6 +179,25 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usersData := map[string]any{
|
||||
"default_avatar": "gravatar",
|
||||
"allow_update_display_name": true,
|
||||
"allow_update_username": true,
|
||||
"allow_update_avatar": true,
|
||||
"allow_update_bio": true,
|
||||
"allow_update_website": true,
|
||||
"allow_update_location": true,
|
||||
}
|
||||
usersDataBytes, _ := json.Marshal(usersData)
|
||||
_, err = engine.InsertOne(&entity.SiteInfo{
|
||||
Type: "users",
|
||||
Content: string(usersDataBytes),
|
||||
Status: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -345,10 +366,14 @@ func initConfigTable(engine *xorm.Engine) error {
|
||||
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
|
||||
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
|
||||
{ID: 118, Key: "plugin.status", Value: `{}`},
|
||||
{ID: 119, Key: "question.pin", Value: `-1`},
|
||||
{ID: 120, Key: "question.unpin", Value: `-1`},
|
||||
{ID: 121, Key: "question.show", Value: `-1`},
|
||||
{ID: 122, Key: "question.hide", Value: `-1`},
|
||||
{ID: 119, Key: "question.pin", Value: `0`},
|
||||
{ID: 120, Key: "question.unpin", Value: `0`},
|
||||
{ID: 121, Key: "question.show", Value: `0`},
|
||||
{ID: 122, Key: "question.hide", Value: `0`},
|
||||
{ID: 123, Key: "rank.question.pin", Value: `-1`},
|
||||
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
|
||||
{ID: 125, Key: "rank.question.show", Value: `-1`},
|
||||
{ID: 126, Key: "rank.question.hide", Value: `-1`},
|
||||
}
|
||||
_, err := engine.Insert(defaultConfigTable)
|
||||
return err
|
||||
|
||||
@@ -58,6 +58,10 @@ var migrations = []Migration{
|
||||
NewMigration("add new answer notification", addNewAnswerNotification, true),
|
||||
NewMigration("add user pin hide features", addRolePinAndHideFeatures, true),
|
||||
NewMigration("update accept answer rank", updateAcceptAnswerRank, true),
|
||||
NewMigration("add plugin", addPlugin, false),
|
||||
NewMigration("update user pin hide features", updateRolePinAndHideFeatures, true),
|
||||
NewMigration("update question post time", updateQuestionPostTime, true),
|
||||
NewMigration("add login limitations", addLoginLimitations, true),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/tidwall/gjson"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addLoginLimitations(x *xorm.Engine) error {
|
||||
loginSiteInfo := &entity.SiteInfo{
|
||||
Type: constant.SiteTypeLogin,
|
||||
}
|
||||
exist, err := x.Get(loginSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
content := &schema.SiteLoginReq{}
|
||||
_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
|
||||
content.AllowEmailRegistrations = true
|
||||
content.AllowEmailDomains = make([]string, 0)
|
||||
_, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update site info failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
interfaceSiteInfo := &entity.SiteInfo{
|
||||
Type: constant.SiteTypeInterface,
|
||||
}
|
||||
exist, err = x.Get(interfaceSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
siteUsers := &schema.SiteUsersReq{
|
||||
AllowUpdateDisplayName: true,
|
||||
AllowUpdateUsername: true,
|
||||
AllowUpdateAvatar: true,
|
||||
AllowUpdateBio: true,
|
||||
AllowUpdateWebsite: true,
|
||||
AllowUpdateLocation: true,
|
||||
}
|
||||
if exist {
|
||||
siteUsers.DefaultAvatar = gjson.Get(interfaceSiteInfo.Content, "default_avatar").String()
|
||||
}
|
||||
data, _ := json.Marshal(siteUsers)
|
||||
|
||||
exist, err = x.Get(&entity.SiteInfo{Type: constant.SiteTypeUsers})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if !exist {
|
||||
usersSiteInfo := &entity.SiteInfo{
|
||||
Type: constant.SiteTypeUsers,
|
||||
Content: string(data),
|
||||
Status: 1,
|
||||
}
|
||||
_, err = x.InsertOne(usersSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert site info failed: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func updateRolePinAndHideFeatures(x *xorm.Engine) error {
|
||||
|
||||
defaultConfigTable := []*entity.Config{
|
||||
{ID: 119, Key: "question.pin", Value: `0`},
|
||||
{ID: 120, Key: "question.unpin", Value: `0`},
|
||||
{ID: 121, Key: "question.show", Value: `0`},
|
||||
{ID: 122, Key: "question.hide", Value: `0`},
|
||||
{ID: 123, Key: "rank.question.pin", Value: `-1`},
|
||||
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
|
||||
{ID: 125, Key: "rank.question.show", Value: `-1`},
|
||||
{ID: 126, Key: "rank.question.hide", Value: `-1`},
|
||||
}
|
||||
for _, c := range defaultConfigTable {
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("update config failed: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
|
||||
log.Errorf("insert %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("add config failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func updateQuestionPostTime(x *xorm.Engine) error {
|
||||
questionList := make([]entity.Question, 0)
|
||||
err := x.Find(&questionList, &entity.Question{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get questions failed: %w", err)
|
||||
}
|
||||
for _, item := range questionList {
|
||||
if item.PostUpdateTime.IsZero() {
|
||||
if !item.UpdatedAt.IsZero() {
|
||||
item.PostUpdateTime = item.UpdatedAt
|
||||
} else if !item.CreatedAt.IsZero() {
|
||||
item.PostUpdateTime = item.CreatedAt
|
||||
}
|
||||
if _, err = x.Update(item, &entity.Question{ID: item.ID}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", item, err)
|
||||
return fmt.Errorf("update question failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addPlugin(x *xorm.Engine) error {
|
||||
defaultConfigTable := []*entity.Config{
|
||||
{ID: 118, Key: "plugin.status", Value: `{}`},
|
||||
}
|
||||
for _, c := range defaultConfigTable {
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
continue
|
||||
}
|
||||
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
|
||||
log.Errorf("insert %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("add config failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return x.Sync(new(entity.PluginConfig), new(entity.UserExternalLogin))
|
||||
}
|
||||
@@ -63,18 +63,22 @@ func addRolePinAndHideFeatures(x *xorm.Engine) error {
|
||||
}
|
||||
|
||||
defaultConfigTable := []*entity.Config{
|
||||
{ID: 119, Key: "question.pin", Value: `-1`},
|
||||
{ID: 120, Key: "question.unpin", Value: `-1`},
|
||||
{ID: 121, Key: "question.show", Value: `-1`},
|
||||
{ID: 122, Key: "question.hide", Value: `-1`},
|
||||
{ID: 119, Key: "question.pin", Value: `0`},
|
||||
{ID: 120, Key: "question.unpin", Value: `0`},
|
||||
{ID: 121, Key: "question.show", Value: `0`},
|
||||
{ID: 122, Key: "question.hide", Value: `0`},
|
||||
{ID: 123, Key: "rank.question.pin", Value: `-1`},
|
||||
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
|
||||
{ID: 125, Key: "rank.question.show", Value: `-1`},
|
||||
{ID: 126, Key: "rank.question.hide", Value: `-1`},
|
||||
}
|
||||
for _, c := range defaultConfigTable {
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("update config failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -202,7 +202,9 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
|
||||
msg.TriggerUserID = questionUserID
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
}
|
||||
notice_queue.AddNotification(msg)
|
||||
if msg.TriggerUserID != msg.ReceiverUserID {
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, act := range addActivityList {
|
||||
@@ -214,7 +216,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
|
||||
if act.UserID != questionUserID {
|
||||
msg.TriggerUserID = questionUserID
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.AcceptAnswer
|
||||
msg.NotificationAction = constant.NotificationAcceptAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
@@ -70,7 +71,9 @@ var LimitDownActions = map[string][]string{
|
||||
|
||||
func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
|
||||
resp = &schema.VoteResp{}
|
||||
notificationUserIDs := make([]string, 0)
|
||||
achievementNotificationUserIDs := make([]string, 0)
|
||||
sendInboxNotification := false
|
||||
upVote := false
|
||||
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
|
||||
result = nil
|
||||
for _, action := range actions {
|
||||
@@ -127,7 +130,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
|
||||
if isReachStandard {
|
||||
insertActivity.Rank = 0
|
||||
}
|
||||
notificationUserIDs = append(notificationUserIDs, activityUserID)
|
||||
achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
|
||||
}
|
||||
|
||||
if has {
|
||||
@@ -142,13 +145,17 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sendInboxNotification = true
|
||||
}
|
||||
|
||||
// update votes
|
||||
if action == "vote_down" || action == "vote_up" {
|
||||
if action == constant.ActVoteDown || action == constant.ActVoteUp {
|
||||
votes := 1
|
||||
if action == "vote_down" {
|
||||
if action == constant.ActVoteDown {
|
||||
upVote = false
|
||||
votes = -1
|
||||
} else {
|
||||
upVote = true
|
||||
}
|
||||
err = vr.updateVotes(ctx, session, objectID, votes)
|
||||
if err != nil {
|
||||
@@ -165,9 +172,12 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
|
||||
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
|
||||
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
|
||||
|
||||
for _, activityUserID := range notificationUserIDs {
|
||||
for _, activityUserID := range achievementNotificationUserIDs {
|
||||
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
|
||||
}
|
||||
if sendInboxNotification {
|
||||
vr.sendVoteInboxNotification(userID, objectUserID, objectID, upVote)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -441,3 +451,40 @@ func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, object
|
||||
}
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, objectID string, upvote bool) {
|
||||
if triggerUserID == receiverUserID {
|
||||
return
|
||||
}
|
||||
objectType, _ := obj.GetObjectTypeStrByObjectID(objectID)
|
||||
|
||||
msg := &schema.NotificationMsg{
|
||||
TriggerUserID: triggerUserID,
|
||||
ReceiverUserID: receiverUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: objectID,
|
||||
ObjectType: objectType,
|
||||
}
|
||||
if objectType == constant.QuestionObjectType {
|
||||
if upvote {
|
||||
msg.NotificationAction = constant.NotificationUpVotedTheQuestion
|
||||
} else {
|
||||
msg.NotificationAction = constant.NotificationDownVotedTheQuestion
|
||||
}
|
||||
}
|
||||
if objectType == constant.AnswerObjectType {
|
||||
if upvote {
|
||||
msg.NotificationAction = constant.NotificationUpVotedTheAnswer
|
||||
} else {
|
||||
msg.NotificationAction = constant.NotificationDownVotedTheAnswer
|
||||
}
|
||||
}
|
||||
if objectType == constant.CommentObjectType {
|
||||
if upvote {
|
||||
msg.NotificationAction = constant.NotificationUpVotedTheComment
|
||||
}
|
||||
}
|
||||
if len(msg.NotificationAction) > 0 {
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken
|
||||
}
|
||||
|
||||
// RemoveUserTokens Log out all users under this user id
|
||||
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
|
||||
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) {
|
||||
key := constant.UserTokenMappingCacheKey + userID
|
||||
resp, _ := ar.data.Cache.GetString(ctx, key)
|
||||
mapping := make(map[string]bool, 0)
|
||||
@@ -158,6 +158,9 @@ func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
|
||||
}
|
||||
|
||||
for token := range mapping {
|
||||
if token == remainToken {
|
||||
continue
|
||||
}
|
||||
if err := ar.RemoveUserCacheInfo(ctx, token); err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package plugin_config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/service/plugin_common"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
type pluginConfigRepo struct {
|
||||
data *data.Data
|
||||
}
|
||||
|
||||
// NewPluginConfigRepo new repository
|
||||
func NewPluginConfigRepo(data *data.Data) plugin_common.PluginConfigRepo {
|
||||
return &pluginConfigRepo{
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (ur *pluginConfigRepo) SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) {
|
||||
old := &entity.PluginConfig{PluginSlugName: pluginSlugName}
|
||||
exist, err := ur.data.DB.Get(old)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
if exist {
|
||||
old.Value = configValue
|
||||
_, err = ur.data.DB.ID(old.ID).Update(old)
|
||||
} else {
|
||||
_, err = ur.data.DB.InsertOne(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue})
|
||||
}
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ur *pluginConfigRepo) GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) {
|
||||
pluginConfigs = make([]*entity.PluginConfig, 0)
|
||||
err = ur.data.DB.Find(&pluginConfigs)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return pluginConfigs, err
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/repo/export"
|
||||
"github.com/answerdev/answer/internal/repo/meta"
|
||||
"github.com/answerdev/answer/internal/repo/notification"
|
||||
"github.com/answerdev/answer/internal/repo/plugin_config"
|
||||
"github.com/answerdev/answer/internal/repo/question"
|
||||
"github.com/answerdev/answer/internal/repo/rank"
|
||||
"github.com/answerdev/answer/internal/repo/reason"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/repo/tag_common"
|
||||
"github.com/answerdev/answer/internal/repo/unique"
|
||||
"github.com/answerdev/answer/internal/repo/user"
|
||||
"github.com/answerdev/answer/internal/repo/user_external_login"
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
@@ -72,4 +74,6 @@ var ProviderSetRepo = wire.NewSet(
|
||||
role.NewUserRoleRelRepo,
|
||||
role.NewRolePowerRelRepo,
|
||||
role.NewPowerRepo,
|
||||
user_external_login.NewUserExternalLoginRepo,
|
||||
plugin_config.NewPluginConfigRepo,
|
||||
)
|
||||
|
||||
@@ -255,7 +255,7 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
|
||||
}
|
||||
|
||||
// GetQuestionPage query question page
|
||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
|
||||
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
|
||||
questionList []*entity.Question, total int64, err error) {
|
||||
questionList = make([]*entity.Question, 0)
|
||||
|
||||
@@ -271,6 +271,9 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
|
||||
} else {
|
||||
session.And("question.show = ?", entity.QuestionShow)
|
||||
}
|
||||
if inDays > 0 {
|
||||
session.And("question.created_at > ?", time.Now().AddDate(0, 0, -inDays))
|
||||
}
|
||||
|
||||
switch orderCond {
|
||||
case "newest":
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/jinzhu/now"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
@@ -36,6 +37,10 @@ func NewUserRankRepo(data *data.Data, configRepo config.ConfigRepo) rank.UserRan
|
||||
func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
|
||||
session *xorm.Session, userID string, deltaRank int, activityType int,
|
||||
) (isReachStandard bool, err error) {
|
||||
// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
|
||||
if plugin.RankAgentEnabled() {
|
||||
return false, nil
|
||||
}
|
||||
if deltaRank == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tag
|
||||
// GetTagBySlugName get tag by slug name
|
||||
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
|
||||
tagInfo = &entity.Tag{}
|
||||
session := tr.data.DB.Where("slug_name = LOWER(?)", slugName)
|
||||
session := tr.data.DB.Where("LOWER(slug_name) = ?", slugName)
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
exist, err = session.Get(tagInfo)
|
||||
if err != nil {
|
||||
|
||||
@@ -86,6 +86,13 @@ func (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *
|
||||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -96,6 +103,14 @@ func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (
|
||||
Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -127,6 +142,8 @@ func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, us
|
||||
total, err = pager.Help(page, pageSize, &users, user, session)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return
|
||||
}
|
||||
tryToDecorateUserListFromUserCenter(ctx, ur.data, users)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,9 +7,14 @@ import (
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// userRepo user repository
|
||||
@@ -28,10 +33,21 @@ func NewUserRepo(data *data.Data, configRepo config.ConfigRepo) usercommon.UserR
|
||||
|
||||
// AddUser add user
|
||||
func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
|
||||
_, err = ur.data.DB.Insert(user)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) {
|
||||
userInfo := &entity.User{}
|
||||
exist, err := session.Where("username = ?", user.Username).Get(userInfo)
|
||||
if err != nil {
|
||||
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
if exist {
|
||||
return nil, errors.InternalServer(reason.UsernameDuplicate)
|
||||
}
|
||||
_, err = session.Insert(user)
|
||||
if err != nil {
|
||||
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -125,6 +141,11 @@ func (ur *userRepo) GetByUserID(ctx context.Context, userID string) (userInfo *e
|
||||
exist, err = ur.data.DB.Where("id = ?", userID).Get(userInfo)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return
|
||||
}
|
||||
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -135,6 +156,7 @@ func (ur *userRepo) BatchGetByID(ctx context.Context, ids []string) ([]*entity.U
|
||||
if err != nil {
|
||||
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
tryToDecorateUserListFromUserCenter(ctx, ur.data, list)
|
||||
return list, nil
|
||||
}
|
||||
|
||||
@@ -144,6 +166,11 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
|
||||
exist, err = ur.data.DB.Where("username = ?", username).Get(userInfo)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return
|
||||
}
|
||||
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -159,11 +186,121 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent
|
||||
return
|
||||
}
|
||||
|
||||
func (vr *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
|
||||
func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
|
||||
list := make([]*entity.User, 0)
|
||||
count, err = vr.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
|
||||
count, err = ur.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
|
||||
if err != nil {
|
||||
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
uc, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
userInfo := &entity.UserExternalLogin{}
|
||||
session := data.DB.Where("user_id = ?", original.ID)
|
||||
session.Where("provider = ?", uc.Info().SlugName)
|
||||
exist, err := session.Get(userInfo)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
if !exist {
|
||||
return nil
|
||||
}
|
||||
|
||||
userCenterBasicUserInfo, err := uc.UserInfo(userInfo.ExternalID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return errors.BadRequest(reason.UserNotFound).WithError(err).WithStack()
|
||||
}
|
||||
|
||||
// In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies.
|
||||
if original.Username != userCenterBasicUserInfo.Username {
|
||||
log.Warnf("user %s username is inconsistent with user center", original.ID)
|
||||
}
|
||||
decorateByUserCenterUser(original, userCenterBasicUserInfo)
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryToDecorateUserListFromUserCenter(ctx context.Context, data *data.Data, original []*entity.User) {
|
||||
uc, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ids := make([]string, 0)
|
||||
originalUserIDMapping := make(map[string]*entity.User, 0)
|
||||
for _, user := range original {
|
||||
originalUserIDMapping[user.ID] = user
|
||||
ids = append(ids, user.ID)
|
||||
}
|
||||
|
||||
userExternalLoginList := make([]*entity.UserExternalLogin, 0)
|
||||
session := data.DB.Where("provider = ?", uc.Info().SlugName)
|
||||
session.In("user_id", ids)
|
||||
err := session.Find(&userExternalLoginList)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
userExternalIDs := make([]string, 0)
|
||||
originalExternalIDMapping := make(map[string]*entity.User, 0)
|
||||
for _, u := range userExternalLoginList {
|
||||
originalExternalIDMapping[u.ExternalID] = originalUserIDMapping[u.UserID]
|
||||
userExternalIDs = append(userExternalIDs, u.ExternalID)
|
||||
}
|
||||
if len(userExternalIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ucUsers, err := uc.UserList(userExternalIDs)
|
||||
if err != nil {
|
||||
log.Errorf("get user list from user center failed: %v, %v", err, userExternalIDs)
|
||||
return
|
||||
}
|
||||
|
||||
for _, ucUser := range ucUsers {
|
||||
decorateByUserCenterUser(originalExternalIDMapping[ucUser.ExternalID], ucUser)
|
||||
}
|
||||
}
|
||||
|
||||
func decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBasicUserInfo) {
|
||||
if original == nil || ucUser == nil {
|
||||
return
|
||||
}
|
||||
// In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies.
|
||||
if original.Username != ucUser.Username {
|
||||
log.Warnf("user %s username is inconsistent with user center", original.ID)
|
||||
}
|
||||
if len(ucUser.DisplayName) > 0 {
|
||||
original.DisplayName = ucUser.DisplayName
|
||||
}
|
||||
if len(ucUser.Email) > 0 {
|
||||
original.EMail = ucUser.Email
|
||||
}
|
||||
if len(ucUser.Avatar) > 0 {
|
||||
original.Avatar = schema.CustomAvatar(ucUser.Avatar).ToJsonString()
|
||||
}
|
||||
if len(ucUser.Mobile) > 0 {
|
||||
original.Mobile = ucUser.Mobile
|
||||
}
|
||||
if len(ucUser.Bio) > 0 {
|
||||
original.BioHTML = converter.Markdown2HTML(ucUser.Bio) + original.BioHTML
|
||||
}
|
||||
|
||||
// If plugin enable rank agent, use rank from user center.
|
||||
if plugin.RankAgentEnabled() {
|
||||
original.Rank = ucUser.Rank
|
||||
}
|
||||
if ucUser.Status != plugin.UserStatusAvailable {
|
||||
original.Status = int(ucUser.Status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package user_external_login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/user_external_login"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
type userExternalLoginRepo struct {
|
||||
data *data.Data
|
||||
}
|
||||
|
||||
// NewUserExternalLoginRepo new repository
|
||||
func NewUserExternalLoginRepo(data *data.Data) user_external_login.UserExternalLoginRepo {
|
||||
return &userExternalLoginRepo{
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// AddUserExternalLogin add external login information
|
||||
func (ur *userExternalLoginRepo) AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) {
|
||||
_, err = ur.data.DB.Insert(user)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateInfo update user info
|
||||
func (ur *userExternalLoginRepo) UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) {
|
||||
_, err = ur.data.DB.ID(userInfo.ID).Update(userInfo)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetByExternalID get by external ID
|
||||
func (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, provider, externalID string) (
|
||||
userInfo *entity.UserExternalLogin, exist bool, err error) {
|
||||
userInfo = &entity.UserExternalLogin{}
|
||||
exist, err = ur.data.DB.Where("external_id = ?", externalID).Where("provider = ?", provider).Get(userInfo)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetUserExternalLoginList get by external ID
|
||||
func (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) (
|
||||
resp []*entity.UserExternalLogin, err error) {
|
||||
resp = make([]*entity.UserExternalLogin, 0)
|
||||
err = ur.data.DB.Where("user_id = ?", userID).Find(&resp)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteUserExternalLogin delete external user login info
|
||||
func (ur *userExternalLoginRepo) DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) {
|
||||
cond := &entity.UserExternalLogin{}
|
||||
_, err = ur.data.DB.Where("user_id = ? AND external_id = ?", userID, externalID).Delete(cond)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetCacheUserExternalLoginInfo cache user info for external login
|
||||
func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(
|
||||
ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) {
|
||||
cacheData, _ := json.Marshal(info)
|
||||
return ur.data.Cache.SetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key,
|
||||
string(cacheData), constant.ConnectorUserExternalInfoCacheTime)
|
||||
}
|
||||
|
||||
// GetCacheUserExternalLoginInfo cache user info for external login
|
||||
func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo(
|
||||
ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) {
|
||||
res, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key)
|
||||
if err != nil {
|
||||
return info, err
|
||||
}
|
||||
_ = json.Unmarshal([]byte(res), &info)
|
||||
return info, nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/controller"
|
||||
"github.com/answerdev/answer/internal/controller_admin"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -31,6 +32,7 @@ type AnswerAPIRouter struct {
|
||||
uploadController *controller.UploadController
|
||||
activityController *controller.ActivityController
|
||||
roleController *controller_admin.RoleController
|
||||
pluginController *controller_admin.PluginController
|
||||
}
|
||||
|
||||
func NewAnswerAPIRouter(
|
||||
@@ -58,6 +60,7 @@ func NewAnswerAPIRouter(
|
||||
uploadController *controller.UploadController,
|
||||
activityController *controller.ActivityController,
|
||||
roleController *controller_admin.RoleController,
|
||||
pluginController *controller_admin.PluginController,
|
||||
) *AnswerAPIRouter {
|
||||
return &AnswerAPIRouter{
|
||||
langController: langController,
|
||||
@@ -84,6 +87,7 @@ func NewAnswerAPIRouter(
|
||||
uploadController: uploadController,
|
||||
activityController: activityController,
|
||||
roleController: roleController,
|
||||
pluginController: pluginController,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,37 +101,38 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
|
||||
r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
|
||||
|
||||
// user
|
||||
r.POST("/user/login/email", a.userController.UserEmailLogin)
|
||||
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
|
||||
r.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
|
||||
r.POST("/user/email/verification", a.userController.UserVerifyEmail)
|
||||
r.PUT("/user/email", a.userController.UserChangeEmailVerify)
|
||||
r.GET("/user/action/record", a.userController.ActionRecord)
|
||||
r.POST("/user/password/reset", a.userController.RetrievePassWord)
|
||||
r.POST("/user/password/replacement", a.userController.UseRePassWord)
|
||||
r.GET("/user/info", a.userController.GetUserInfoByUserID)
|
||||
r.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
|
||||
routerGroup := r.Group("", middleware.BanAPIForUserCenter)
|
||||
routerGroup.POST("/user/login/email", a.userController.UserEmailLogin)
|
||||
routerGroup.POST("/user/register/email", a.userController.UserRegisterByEmail)
|
||||
routerGroup.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
|
||||
routerGroup.POST("/user/email/verification", a.userController.UserVerifyEmail)
|
||||
routerGroup.PUT("/user/email", a.userController.UserChangeEmailVerify)
|
||||
routerGroup.GET("/user/action/record", a.userController.ActionRecord)
|
||||
routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord)
|
||||
routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord)
|
||||
routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
|
||||
}
|
||||
|
||||
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
// user
|
||||
r.GET("/user/logout", a.userController.UserLogout)
|
||||
r.POST("/user/email/change/code", a.userController.UserChangeEmailSendCode)
|
||||
r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend)
|
||||
r.POST("/user/email/change/code", middleware.BanAPIForUserCenter, a.userController.UserChangeEmailSendCode)
|
||||
r.POST("/user/email/verification/send", middleware.BanAPIForUserCenter, a.userController.UserVerifyEmailSend)
|
||||
r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
|
||||
r.GET("/user/ranking", a.userController.UserRanking)
|
||||
|
||||
//answer
|
||||
r.GET("/answer/info", a.answerController.Get)
|
||||
r.GET("/answer/page", a.answerController.AnswerList)
|
||||
r.GET("/personal/answer/page", a.questionController.UserAnswerList)
|
||||
r.GET("/personal/answer/page", a.questionController.PersonalAnswerPage)
|
||||
|
||||
//question
|
||||
r.GET("/question/info", a.questionController.GetQuestion)
|
||||
r.GET("/question/page", a.questionController.QuestionPage)
|
||||
r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
|
||||
r.GET("/personal/qa/top", a.questionController.UserTop)
|
||||
r.GET("/personal/question/page", a.questionController.UserList)
|
||||
r.GET("/personal/question/page", a.questionController.PersonalQuestionPage)
|
||||
|
||||
// comment
|
||||
r.GET("/comment/page", a.commentController.GetCommentWithPage)
|
||||
@@ -182,7 +187,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
|
||||
// collection
|
||||
r.POST("/collection/switch", a.collectionController.CollectionSwitch)
|
||||
r.GET("/personal/collection/page", a.questionController.UserCollectionList)
|
||||
r.GET("/personal/collection/page", a.questionController.PersonalCollectionPage)
|
||||
|
||||
// question
|
||||
r.POST("/question", a.questionController.AddQuestion)
|
||||
@@ -201,7 +206,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
||||
r.DELETE("/answer", a.answerController.RemoveAnswer)
|
||||
|
||||
// user
|
||||
r.PUT("/user/password", a.userController.UserModifyPassWord)
|
||||
r.PUT("/user/password", middleware.BanAPIForUserCenter, a.userController.UserModifyPassWord)
|
||||
r.PUT("/user/info", a.userController.UserUpdateInfo)
|
||||
r.PUT("/user/interface", a.userController.UserUpdateInterface)
|
||||
r.POST("/user/notice/set", a.userController.UserNoticeSet)
|
||||
@@ -257,29 +262,39 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
|
||||
|
||||
// siteinfo
|
||||
r.GET("/siteinfo/general", a.siteInfoController.GetGeneral)
|
||||
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
|
||||
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
|
||||
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
|
||||
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
|
||||
r.GET("/siteinfo/seo", a.siteInfoController.GetSeo)
|
||||
r.GET("/siteinfo/login", a.siteInfoController.GetSiteLogin)
|
||||
r.GET("/siteinfo/custom-css-html", a.siteInfoController.GetSiteCustomCssHTML)
|
||||
r.GET("/siteinfo/theme", a.siteInfoController.GetSiteTheme)
|
||||
r.PUT("/siteinfo/general", a.siteInfoController.UpdateGeneral)
|
||||
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
|
||||
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
|
||||
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
|
||||
r.PUT("/siteinfo/branding", a.siteInfoController.UpdateBranding)
|
||||
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
|
||||
r.PUT("/siteinfo/write", a.siteInfoController.UpdateSiteWrite)
|
||||
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
|
||||
r.PUT("/siteinfo/legal", a.siteInfoController.UpdateSiteLegal)
|
||||
r.PUT("/siteinfo/login", a.siteInfoController.UpdateSiteLogin)
|
||||
r.PUT("/siteinfo/custom-css-html", a.siteInfoController.UpdateSiteCustomCssHTML)
|
||||
r.PUT("/siteinfo/theme", a.siteInfoController.SaveSiteTheme)
|
||||
r.GET("/siteinfo/seo", a.siteInfoController.GetSeo)
|
||||
r.PUT("/siteinfo/seo", a.siteInfoController.UpdateSeo)
|
||||
r.GET("/siteinfo/login", a.siteInfoController.GetSiteLogin)
|
||||
r.PUT("/siteinfo/login", a.siteInfoController.UpdateSiteLogin)
|
||||
r.GET("/siteinfo/custom-css-html", a.siteInfoController.GetSiteCustomCssHTML)
|
||||
r.PUT("/siteinfo/custom-css-html", a.siteInfoController.UpdateSiteCustomCssHTML)
|
||||
r.GET("/siteinfo/theme", a.siteInfoController.GetSiteTheme)
|
||||
r.PUT("/siteinfo/theme", a.siteInfoController.SaveSiteTheme)
|
||||
r.GET("/siteinfo/users", a.siteInfoController.GetSiteUsers)
|
||||
r.PUT("/siteinfo/users", a.siteInfoController.UpdateSiteUsers)
|
||||
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
|
||||
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
|
||||
r.GET("/setting/privileges", a.siteInfoController.GetPrivilegesConfig)
|
||||
r.PUT("/setting/privileges", a.siteInfoController.UpdatePrivilegesConfig)
|
||||
|
||||
// dashboard
|
||||
r.GET("/dashboard", a.dashboardController.DashboardInfo)
|
||||
|
||||
// roles
|
||||
r.GET("/roles", a.roleController.GetRoleList)
|
||||
|
||||
// plugin
|
||||
r.GET("/plugins", a.pluginController.GetPluginList)
|
||||
r.PUT("/plugin/status", a.pluginController.UpdatePluginStatus)
|
||||
r.GET("/plugin/config", a.pluginController.GetPluginConfig)
|
||||
r.PUT("/plugin/config", a.pluginController.UpdatePluginConfig)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/controller"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PluginAPIRouter struct {
|
||||
connectorController *controller.ConnectorController
|
||||
userCenterController *controller.UserCenterController
|
||||
}
|
||||
|
||||
func NewPluginAPIRouter(
|
||||
connectorController *controller.ConnectorController,
|
||||
userCenterController *controller.UserCenterController,
|
||||
) *PluginAPIRouter {
|
||||
return &PluginAPIRouter{
|
||||
connectorController: connectorController,
|
||||
userCenterController: userCenterController,
|
||||
}
|
||||
}
|
||||
|
||||
func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) {
|
||||
// connector plugin
|
||||
connectorController := pr.connectorController
|
||||
r.GET(controller.ConnectorLoginRouterPrefix+":name", connectorController.ConnectorLoginDispatcher)
|
||||
r.GET(controller.ConnectorRedirectRouterPrefix+":name", connectorController.ConnectorRedirectDispatcher)
|
||||
r.GET("/connector/info", connectorController.ConnectorsInfo)
|
||||
r.POST("/connector/binding/email", connectorController.ExternalLoginBindingUserSendEmail)
|
||||
|
||||
// user center plugin
|
||||
r.GET("/user-center/agent", pr.userCenterController.UserCenterAgent)
|
||||
r.GET("/user-center/personal/branding", pr.userCenterController.UserCenterPersonalBranding)
|
||||
r.GET(controller.UserCenterLoginRouter, pr.userCenterController.UserCenterLoginRedirect)
|
||||
r.GET(controller.UserCenterSignUpRedirectRouter, pr.userCenterController.UserCenterSignUpRedirect)
|
||||
r.GET("/user-center/login/callback", pr.userCenterController.UserCenterLoginCallback)
|
||||
r.GET("/user-center/sign-up/callback", pr.userCenterController.UserCenterSignUpCallback)
|
||||
}
|
||||
|
||||
func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) {
|
||||
connectorController := pr.connectorController
|
||||
r.GET("/connector/user/info", connectorController.ConnectorsUserInfo)
|
||||
r.DELETE("/connector/user/unbinding", connectorController.ExternalLoginUnbinding)
|
||||
|
||||
r.GET("/user-center/user/settings", pr.userCenterController.UserCenterUserSettings)
|
||||
}
|
||||
|
||||
func (pr *PluginAPIRouter) RegisterAuthAdminConnectorRouter(r *gin.RouterGroup) {
|
||||
r.GET("/user-center/agent", pr.userCenterController.UserCenterAdminFunctionAgent)
|
||||
}
|
||||
@@ -3,4 +3,11 @@ package router
|
||||
import "github.com/google/wire"
|
||||
|
||||
// ProviderSetRouter is providers.
|
||||
var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter, NewTemplateRouter)
|
||||
var ProviderSetRouter = wire.NewSet(
|
||||
NewAnswerAPIRouter,
|
||||
NewSwaggerRouter,
|
||||
NewStaticRouter,
|
||||
NewUIRouter,
|
||||
NewTemplateRouter,
|
||||
NewPluginAPIRouter,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package schema
|
||||
|
||||
type ConnectorInfoResp struct {
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
||||
type ConnectorUserInfoResp struct {
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Link string `json:"link"`
|
||||
Binding bool `json:"binding"`
|
||||
ExternalID string `json:"external_id"`
|
||||
}
|
||||
@@ -3,18 +3,21 @@ package schema
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
AccountActivationSourceType SourceType = "account-activation"
|
||||
PasswordResetSourceType SourceType = "password-reset"
|
||||
ConfirmNewEmailSourceType SourceType = "password-reset"
|
||||
UnsubscribeSourceType SourceType = "unsubscribe"
|
||||
AccountActivationSourceType EmailSourceType = "account-activation"
|
||||
PasswordResetSourceType EmailSourceType = "password-reset"
|
||||
ConfirmNewEmailSourceType EmailSourceType = "password-reset"
|
||||
UnsubscribeSourceType EmailSourceType = "unsubscribe"
|
||||
BindingSourceType EmailSourceType = "binding"
|
||||
)
|
||||
|
||||
type SourceType string
|
||||
type EmailSourceType string
|
||||
|
||||
type EmailCodeContent struct {
|
||||
SourceType SourceType `json:"source_type"`
|
||||
Email string `json:"e_mail"`
|
||||
UserID string `json:"user_id"`
|
||||
SourceType EmailSourceType `json:"source_type"`
|
||||
Email string `json:"e_mail"`
|
||||
UserID string `json:"user_id"`
|
||||
// Used for third-party login account binding
|
||||
BindingKey string `json:"binding_key"`
|
||||
}
|
||||
|
||||
func (r *EmailCodeContent) ToJSONString() string {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
PluginStatusActive PluginStatus = "active"
|
||||
PluginStatusInactive PluginStatus = "inactive"
|
||||
)
|
||||
|
||||
type PluginStatus string
|
||||
|
||||
type GetPluginListReq struct {
|
||||
Status PluginStatus `form:"status"`
|
||||
HaveConfig bool `form:"have_config"`
|
||||
}
|
||||
|
||||
type GetPluginListResp struct {
|
||||
Name string `json:"name"`
|
||||
SlugName string `json:"slug_name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
Enabled bool `json:"enabled"`
|
||||
HaveConfig bool `json:"have_config"`
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
||||
type UpdatePluginStatusReq struct {
|
||||
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type GetPluginConfigReq struct {
|
||||
PluginSlugName string `validate:"required,gt=1,lte=100" form:"plugin_slug_name"`
|
||||
}
|
||||
|
||||
type GetPluginConfigResp struct {
|
||||
Name string `json:"name"`
|
||||
SlugName string `json:"slug_name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
ConfigFields []ConfigField `json:"config_fields"`
|
||||
}
|
||||
|
||||
func (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) {
|
||||
for _, field := range fields {
|
||||
configField := ConfigField{
|
||||
Name: field.Name,
|
||||
Type: string(field.Type),
|
||||
Title: field.Title.Translate(ctx),
|
||||
Description: field.Description.Translate(ctx),
|
||||
Required: field.Required,
|
||||
Value: field.Value,
|
||||
UIOptions: ConfigFieldUIOptions{
|
||||
Rows: field.UIOptions.Rows,
|
||||
InputType: string(field.UIOptions.InputType),
|
||||
Variant: field.UIOptions.Variant,
|
||||
},
|
||||
}
|
||||
configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx)
|
||||
configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)
|
||||
configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx)
|
||||
if field.UIOptions.Action != nil {
|
||||
uiOptionAction := &UIOptionAction{
|
||||
Url: field.UIOptions.Action.Url,
|
||||
Method: field.UIOptions.Action.Method,
|
||||
}
|
||||
if field.UIOptions.Action.Loading != nil {
|
||||
uiOptionAction.Loading = &LoadingAction{
|
||||
Text: field.UIOptions.Action.Loading.Text.Translate(ctx),
|
||||
State: string(field.UIOptions.Action.Loading.State),
|
||||
}
|
||||
}
|
||||
if field.UIOptions.Action.OnComplete != nil {
|
||||
uiOptionAction.OnCompleteAction = &OnCompleteAction{
|
||||
ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage,
|
||||
RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig,
|
||||
}
|
||||
}
|
||||
configField.UIOptions.Action = uiOptionAction
|
||||
}
|
||||
|
||||
for _, option := range field.Options {
|
||||
configField.Options = append(configField.Options, ConfigFieldOption{
|
||||
Label: option.Label.Translate(ctx),
|
||||
Value: option.Value,
|
||||
})
|
||||
}
|
||||
g.ConfigFields = append(g.ConfigFields, configField)
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigField struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
Value any `json:"value"`
|
||||
UIOptions ConfigFieldUIOptions `json:"ui_options"`
|
||||
Options []ConfigFieldOption `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigFieldUIOptions struct {
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Rows string `json:"rows,omitempty"`
|
||||
InputType string `json:"input_type,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Action *UIOptionAction `json:"action,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigFieldOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type UIOptionAction struct {
|
||||
Url string `json:"url"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Loading *LoadingAction `json:"loading,omitempty"`
|
||||
OnCompleteAction *OnCompleteAction `json:"on_complete,omitempty"`
|
||||
}
|
||||
|
||||
type LoadingAction struct {
|
||||
Text string `json:"text"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type OnCompleteAction struct {
|
||||
ToastReturnMessage bool `json:"toast_return_message"`
|
||||
RefreshFormConfig bool `json:"refresh_form_config"`
|
||||
}
|
||||
|
||||
type UpdatePluginConfigReq struct {
|
||||
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
|
||||
ConfigFields map[string]any `json:"config_fields"`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package schema
|
||||
|
||||
type UserCenterAgentResp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AgentInfo *AgentInfo `json:"agent_info"`
|
||||
}
|
||||
|
||||
type AgentInfo struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Icon string `json:"icon"`
|
||||
Url string `json:"url"`
|
||||
LoginRedirectURL string `json:"login_redirect_url"`
|
||||
SignUpRedirectURL string `json:"sign_up_redirect_url"`
|
||||
ControlCenterItems []*ControlCenter `json:"control_center"`
|
||||
EnabledOriginalUserSystem bool `json:"enabled_original_user_system"`
|
||||
}
|
||||
|
||||
type ControlCenter struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type UserCenterPersonalBranding struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PersonalBranding []*PersonalBranding `json:"personal_branding"`
|
||||
}
|
||||
|
||||
type PersonalBranding struct {
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
@@ -297,6 +297,7 @@ type QuestionPageReq struct {
|
||||
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
|
||||
Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"`
|
||||
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
|
||||
InDays int `validate:"omitempty,min=1" form:"in_days"`
|
||||
|
||||
LoginUserID string `json:"-"`
|
||||
UserIDBeSearched string `json:"-"`
|
||||
@@ -374,3 +375,25 @@ type SiteMapQuestionInfo struct {
|
||||
Title string `json:"title"`
|
||||
UpdateTime string `json:"time"`
|
||||
}
|
||||
|
||||
type PersonalQuestionPageReq struct {
|
||||
Page int `validate:"omitempty,min=1" form:"page"`
|
||||
PageSize int `validate:"omitempty,min=1" form:"page_size"`
|
||||
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
|
||||
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
|
||||
LoginUserID string `json:"-"`
|
||||
}
|
||||
|
||||
type PersonalAnswerPageReq struct {
|
||||
Page int `validate:"omitempty,min=1" form:"page"`
|
||||
PageSize int `validate:"omitempty,min=1" form:"page_size"`
|
||||
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
|
||||
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
|
||||
LoginUserID string `json:"-"`
|
||||
}
|
||||
|
||||
type PersonalCollectionPageReq struct {
|
||||
Page int `validate:"omitempty,min=1" form:"page"`
|
||||
PageSize int `validate:"omitempty,min=1" form:"page_size"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/mail"
|
||||
"net/url"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
@@ -42,9 +43,8 @@ func (r *SiteGeneralReq) FormatSiteUrl() {
|
||||
|
||||
// SiteInterfaceReq site interface request
|
||||
type SiteInterfaceReq struct {
|
||||
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
|
||||
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
|
||||
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
|
||||
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
|
||||
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
|
||||
}
|
||||
|
||||
// SiteBrandingReq site branding request
|
||||
@@ -92,18 +92,32 @@ type GetSiteLegalInfoResp struct {
|
||||
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"`
|
||||
}
|
||||
|
||||
// SiteUsersReq site users config request
|
||||
type SiteUsersReq struct {
|
||||
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
|
||||
AllowUpdateDisplayName bool `form:"allow_update_display_name" json:"allow_update_display_name"`
|
||||
AllowUpdateUsername bool `form:"allow_update_username" json:"allow_update_username"`
|
||||
AllowUpdateAvatar bool `form:"allow_update_avatar" json:"allow_update_avatar"`
|
||||
AllowUpdateBio bool `form:"allow_update_bio" json:"allow_update_bio"`
|
||||
AllowUpdateWebsite bool `form:"allow_update_website" json:"allow_update_website"`
|
||||
AllowUpdateLocation bool `form:"allow_update_location" json:"allow_update_location"`
|
||||
}
|
||||
|
||||
// SiteLoginReq site login request
|
||||
type SiteLoginReq struct {
|
||||
AllowNewRegistrations bool `json:"allow_new_registrations"`
|
||||
LoginRequired bool `json:"login_required"`
|
||||
AllowNewRegistrations bool `json:"allow_new_registrations"`
|
||||
AllowEmailRegistrations bool `json:"allow_email_registrations"`
|
||||
LoginRequired bool `json:"login_required"`
|
||||
AllowEmailDomains []string `json:"allow_email_domains"`
|
||||
}
|
||||
|
||||
// SiteCustomCssHTMLReq site custom css html
|
||||
type SiteCustomCssHTMLReq struct {
|
||||
CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"`
|
||||
CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"`
|
||||
CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"`
|
||||
CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"`
|
||||
CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"`
|
||||
CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"`
|
||||
CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"`
|
||||
CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"`
|
||||
CustomSideBar string `validate:"omitempty,gt=0,lte=65536" json:"custom_sidebar"`
|
||||
}
|
||||
|
||||
// SiteThemeReq site theme config
|
||||
@@ -127,6 +141,9 @@ type SiteLoginResp SiteLoginReq
|
||||
// SiteCustomCssHTMLResp site custom css html response
|
||||
type SiteCustomCssHTMLResp SiteCustomCssHTMLReq
|
||||
|
||||
// SiteUsersResp site users response
|
||||
type SiteUsersResp SiteUsersReq
|
||||
|
||||
// SiteThemeResp site theme response
|
||||
type SiteThemeResp struct {
|
||||
ThemeOptions []*ThemeOption `json:"theme_options"`
|
||||
@@ -169,6 +186,7 @@ type SiteInfoResp struct {
|
||||
Theme *SiteThemeResp `json:"theme"`
|
||||
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
|
||||
SiteSeo *SiteSeoReq `json:"site_seo"`
|
||||
SiteUsers *SiteUsersResp `json:"site_users"`
|
||||
Version string `json:"version"`
|
||||
Revision string `json:"revision"`
|
||||
}
|
||||
@@ -235,3 +253,85 @@ type GetManifestJsonResp struct {
|
||||
ThemeColor string `json:"theme_color"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
}
|
||||
|
||||
const (
|
||||
// PrivilegeLevel1 low
|
||||
PrivilegeLevel1 PrivilegeLevel = 1
|
||||
// PrivilegeLevel2 medium
|
||||
PrivilegeLevel2 PrivilegeLevel = 2
|
||||
// PrivilegeLevel3 high
|
||||
PrivilegeLevel3 PrivilegeLevel = 3
|
||||
)
|
||||
|
||||
type PrivilegeLevel int
|
||||
|
||||
// GetPrivilegesConfigResp get privileges config response
|
||||
type GetPrivilegesConfigResp struct {
|
||||
Options []*PrivilegeOption `json:"options"`
|
||||
SelectedLevel PrivilegeLevel `json:"selected_level"`
|
||||
}
|
||||
|
||||
// PrivilegeOption privilege option
|
||||
type PrivilegeOption struct {
|
||||
Level PrivilegeLevel `json:"level"`
|
||||
LevelDesc string `json:"level_desc"`
|
||||
Privileges []*constant.Privilege `json:"privileges"`
|
||||
}
|
||||
|
||||
// UpdatePrivilegesConfigReq update privileges config request
|
||||
type UpdatePrivilegesConfigReq struct {
|
||||
Level PrivilegeLevel `validate:"required,min=1,max=3" json:"level"`
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultPrivilegeOptions []*PrivilegeOption
|
||||
privilegeOptionsLevelMapping = map[string][]int{
|
||||
constant.RankQuestionAddKey: {1, 1, 1},
|
||||
constant.RankAnswerAddKey: {1, 1, 1},
|
||||
constant.RankCommentAddKey: {1, 1, 1},
|
||||
constant.RankReportAddKey: {1, 1, 1},
|
||||
constant.RankCommentVoteUpKey: {1, 1, 1},
|
||||
constant.RankLinkUrlLimitKey: {1, 10, 10},
|
||||
constant.RankQuestionVoteUpKey: {1, 1, 15},
|
||||
constant.RankAnswerVoteUpKey: {1, 1, 15},
|
||||
constant.RankQuestionVoteDownKey: {125, 125, 125},
|
||||
constant.RankAnswerVoteDownKey: {125, 125, 125},
|
||||
constant.RankTagAddKey: {1, 750, 1500},
|
||||
constant.RankTagEditKey: {1, 50, 100},
|
||||
constant.RankQuestionEditKey: {1, 100, 200},
|
||||
constant.RankAnswerEditKey: {1, 100, 200},
|
||||
constant.RankQuestionEditWithoutReviewKey: {1, 1000, 2000},
|
||||
constant.RankAnswerEditWithoutReviewKey: {1, 1000, 2000},
|
||||
constant.RankQuestionAuditKey: {1, 1000, 2000},
|
||||
constant.RankAnswerAuditKey: {1, 1000, 2000},
|
||||
constant.RankTagAuditKey: {1, 2500, 5000},
|
||||
constant.RankTagEditWithoutReviewKey: {1, 10000, 20000},
|
||||
constant.RankTagSynonymKey: {1, 10000, 20000},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
DefaultPrivilegeOptions = append(DefaultPrivilegeOptions, &PrivilegeOption{
|
||||
Level: PrivilegeLevel1,
|
||||
LevelDesc: reason.PrivilegeLevel1Desc,
|
||||
}, &PrivilegeOption{
|
||||
Level: PrivilegeLevel2,
|
||||
LevelDesc: reason.PrivilegeLevel2Desc,
|
||||
}, &PrivilegeOption{
|
||||
Level: PrivilegeLevel3,
|
||||
LevelDesc: reason.PrivilegeLevel3Desc,
|
||||
})
|
||||
|
||||
for _, option := range DefaultPrivilegeOptions {
|
||||
for _, privilege := range constant.RankAllPrivileges {
|
||||
if len(privilegeOptionsLevelMapping[privilege.Key]) == 0 {
|
||||
continue
|
||||
}
|
||||
option.Privileges = append(option.Privileges, &constant.Privilege{
|
||||
Label: privilege.Label,
|
||||
Value: privilegeOptionsLevelMapping[privilege.Key][option.Level-1],
|
||||
Key: privilege.Key,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package schema
|
||||
|
||||
// UserExternalLoginResp user external login resp
|
||||
type UserExternalLoginResp struct {
|
||||
BindingKey string `json:"binding_key"`
|
||||
AccessToken string `json:"access_token"`
|
||||
// ErrMsg error message, if not empty, means login failed and this message should be displayed.
|
||||
ErrMsg string `json:"-"`
|
||||
ErrTitle string `json:"-"`
|
||||
}
|
||||
|
||||
// ExternalLoginBindingUserSendEmailReq external login binding user request
|
||||
type ExternalLoginBindingUserSendEmailReq struct {
|
||||
BindingKey string `validate:"required,gt=1,lte=100" json:"binding_key"`
|
||||
Email string `validate:"required,gt=1,lte=512,email" json:"email"`
|
||||
// If must is true, whatever email if exists, try to bind user.
|
||||
// If must is false, when email exist, will only be prompted with a warning.
|
||||
Must bool `json:"must"`
|
||||
}
|
||||
|
||||
// ExternalLoginBindingUserSendEmailResp external login binding user response
|
||||
type ExternalLoginBindingUserSendEmailResp struct {
|
||||
EmailExistAndMustBeConfirmed bool `json:"email_exist_and_must_be_confirmed"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// ExternalLoginBindingUserReq external login binding user request
|
||||
type ExternalLoginBindingUserReq struct {
|
||||
Code string `validate:"required,gt=0,lte=500" json:"code"`
|
||||
Content string `json:"-"`
|
||||
}
|
||||
|
||||
// ExternalLoginBindingUserResp external login binding user response
|
||||
type ExternalLoginBindingUserResp struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// ExternalLoginUserInfoCache external login user info
|
||||
type ExternalLoginUserInfoCache struct {
|
||||
// Third party identification
|
||||
// e.g. facebook, twitter, instagram
|
||||
Provider string
|
||||
// required. The unique user ID provided by the third-party login
|
||||
ExternalID string
|
||||
// optional. This name is used preferentially during registration
|
||||
DisplayName string
|
||||
// optional. This username is used preferentially during registration
|
||||
Username string
|
||||
// optional. If email exist will bind the existing user
|
||||
Email string
|
||||
// optional. The avatar URL provided by the third-party login platform
|
||||
Avatar string
|
||||
// optional. The original user information provided by the third-party login platform
|
||||
MetaInfo string
|
||||
// optional. The bio provided by the third-party login platform
|
||||
Bio string
|
||||
}
|
||||
|
||||
// ExternalLoginUnbindingReq external login unbinding user
|
||||
type ExternalLoginUnbindingReq struct {
|
||||
ExternalID string `validate:"required,gt=0,lte=128" json:"external_id"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
// UserCenterUserSettingsResp user center user info response
|
||||
type UserCenterUserSettingsResp struct {
|
||||
ProfileSettingAgent UserSettingAgent `json:"profile_setting_agent"`
|
||||
AccountSettingAgent UserSettingAgent `json:"account_setting_agent"`
|
||||
}
|
||||
|
||||
type UserCenterAdminFunctionAgentResp struct {
|
||||
AllowCreateUser bool `json:"allow_create_user"`
|
||||
AllowUpdateUserStatus bool `json:"allow_update_user_status"`
|
||||
AllowUpdateUserPassword bool `json:"allow_update_user_password"`
|
||||
AllowUpdateUserRole bool `json:"allow_update_user_role"`
|
||||
}
|
||||
|
||||
type UserSettingAgent struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
}
|
||||
@@ -4,14 +4,12 @@ import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/gravatar"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// UserVerifyEmailReq user verify email request
|
||||
@@ -72,6 +70,8 @@ type GetUserResp struct {
|
||||
RoleID int `json:"role_id"`
|
||||
// user status
|
||||
Status string `json:"status"`
|
||||
// user have password
|
||||
HavePassword bool `json:"have_password"`
|
||||
}
|
||||
|
||||
func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
|
||||
@@ -83,11 +83,13 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
|
||||
if ok {
|
||||
r.Status = statusShow
|
||||
}
|
||||
r.HavePassword = len(userInfo.Pass) > 0
|
||||
}
|
||||
|
||||
type GetUserToSetShowResp struct {
|
||||
*GetUserResp
|
||||
Avatar *AvatarInfo `json:"avatar"`
|
||||
Avatar *AvatarInfo `json:"avatar"`
|
||||
HavePassword bool `json:"have_password"`
|
||||
}
|
||||
|
||||
func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) {
|
||||
@@ -108,6 +110,12 @@ func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) {
|
||||
r.Avatar = avatarInfo
|
||||
}
|
||||
|
||||
const (
|
||||
AvatarTypeDefault = "default"
|
||||
AvatarTypeGravatar = "gravatar"
|
||||
AvatarTypeCustom = "custom"
|
||||
)
|
||||
|
||||
func FormatAvatarInfo(avatarJson, email string) (res string) {
|
||||
defer func() {
|
||||
if constant.DefaultAvatar == "gravatar" && len(res) == 0 {
|
||||
@@ -124,15 +132,22 @@ func FormatAvatarInfo(avatarJson, email string) (res string) {
|
||||
return ""
|
||||
}
|
||||
switch avatarInfo.Type {
|
||||
case "gravatar":
|
||||
case AvatarTypeGravatar:
|
||||
return avatarInfo.Gravatar
|
||||
case "custom":
|
||||
case AvatarTypeCustom:
|
||||
return avatarInfo.Custom
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func CustomAvatar(url string) *AvatarInfo {
|
||||
return &AvatarInfo{
|
||||
Type: AvatarTypeCustom,
|
||||
Custom: url,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserStatusResp get user status info
|
||||
type GetUserStatusResp struct {
|
||||
// user status
|
||||
@@ -260,14 +275,14 @@ func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err er
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UserModifyPassWordRequest
|
||||
type UserModifyPassWordRequest struct {
|
||||
UserID string `json:"-" ` // user_id
|
||||
OldPass string `json:"old_pass" ` // old password
|
||||
Pass string `json:"pass" ` // password
|
||||
type UserModifyPasswordReq struct {
|
||||
OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
|
||||
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
|
||||
UserID string `json:"-"`
|
||||
AccessToken string `json:"-"`
|
||||
}
|
||||
|
||||
func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
// TODO i18n
|
||||
err = checker.CheckPassword(8, 32, 0, u.Pass)
|
||||
if err != nil {
|
||||
@@ -283,7 +298,7 @@ func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorFie
|
||||
|
||||
type UpdateInfoRequest struct {
|
||||
// display_name
|
||||
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
|
||||
DisplayName string `validate:"omitempty,gt=0,lte=30" json:"display_name"`
|
||||
// username
|
||||
Username string `validate:"omitempty,gt=3,lte=30" json:"username"`
|
||||
// avatar
|
||||
@@ -306,17 +321,12 @@ type AvatarInfo struct {
|
||||
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
|
||||
}
|
||||
|
||||
func (a *AvatarInfo) ToJsonString() string {
|
||||
data, _ := json.Marshal(a)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(req.Username) > 0 {
|
||||
if checker.IsInvalidUsername(req.Username) {
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
req.BioHTML = converter.Markdown2BasicHTML(req.Bio)
|
||||
return nil, nil
|
||||
}
|
||||
@@ -399,6 +409,7 @@ type GetOtherUserInfoResp struct {
|
||||
type UserChangeEmailSendCodeReq struct {
|
||||
UserVerifyEmailSendReq
|
||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"`
|
||||
Pass string `validate:"omitempty,gte=8,lte=32" json:"pass"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package activity
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
@@ -111,7 +112,11 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge
|
||||
item.Username = "N/A"
|
||||
item.UserDisplayName = "N/A"
|
||||
} else {
|
||||
item.UserID = act.UserID
|
||||
if act.TriggerUserID > 0 {
|
||||
item.UserID = fmt.Sprintf("%d", act.TriggerUserID)
|
||||
} else {
|
||||
item.UserID = act.UserID
|
||||
}
|
||||
}
|
||||
|
||||
item.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID)
|
||||
|
||||
@@ -476,7 +476,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A
|
||||
msg.ReceiverUserID = answerInfo.UserID
|
||||
msg.TriggerUserID = answerInfo.UserID
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.YourAnswerWasDeleted
|
||||
msg.NotificationAction = constant.NotificationYourAnswerWasDeleted
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
return nil
|
||||
@@ -566,7 +566,7 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU
|
||||
ObjectID: answerID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.UpdateAnswer
|
||||
msg.NotificationAction = constant.NotificationUpdateAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
@@ -583,7 +583,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
|
||||
ObjectID: answerID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.AnswerTheQuestion
|
||||
msg.NotificationAction = constant.NotificationAnswerTheQuestion
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/pkg/token"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
@@ -20,7 +21,7 @@ type AuthRepo interface {
|
||||
SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error
|
||||
RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error)
|
||||
AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error)
|
||||
RemoveUserTokens(ctx context.Context, userID string)
|
||||
RemoveUserTokens(ctx context.Context, userID string, remainToken string)
|
||||
}
|
||||
|
||||
// AuthService kit service
|
||||
@@ -42,7 +43,6 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
|
||||
}
|
||||
cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID)
|
||||
if cacheInfo != nil {
|
||||
log.Debugf("user status updated: %+v", cacheInfo)
|
||||
userCacheInfo.UserStatus = cacheInfo.UserStatus
|
||||
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
|
||||
userCacheInfo.RoleID = cacheInfo.RoleID
|
||||
@@ -52,6 +52,14 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// try to get user status from user center
|
||||
uc, ok := plugin.GetUserCenter()
|
||||
if ok && len(userCacheInfo.ExternalID) > 0 {
|
||||
if userStatus := uc.UserStatus(userCacheInfo.ExternalID); userStatus != plugin.UserStatusAvailable {
|
||||
userCacheInfo.UserStatus = int(userStatus)
|
||||
}
|
||||
}
|
||||
return userCacheInfo, nil
|
||||
}
|
||||
|
||||
@@ -85,9 +93,14 @@ func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessTo
|
||||
return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken)
|
||||
}
|
||||
|
||||
// RemoveUserTokens Log out all users under this user id
|
||||
func (as *AuthService) RemoveUserTokens(ctx context.Context, userID string) {
|
||||
as.authRepo.RemoveUserTokens(ctx, userID)
|
||||
// RemoveUserAllTokens Log out all users under this user id
|
||||
func (as *AuthService) RemoveUserAllTokens(ctx context.Context, userID string) {
|
||||
as.authRepo.RemoveUserTokens(ctx, userID, "")
|
||||
}
|
||||
|
||||
// RemoveTokensExceptCurrentUser remove all tokens except the current user
|
||||
func (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID string, accessToken string) {
|
||||
as.authRepo.RemoveUserTokens(ctx, userID, accessToken)
|
||||
}
|
||||
|
||||
//Admin
|
||||
|
||||
@@ -471,7 +471,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
|
||||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.CommentQuestion
|
||||
msg.NotificationAction = constant.NotificationCommentQuestion
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
|
||||
@@ -526,7 +526,7 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
|
||||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.CommentAnswer
|
||||
msg.NotificationAction = constant.NotificationCommentAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
|
||||
@@ -578,7 +578,7 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
|
||||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.ReplyToYou
|
||||
msg.NotificationAction = constant.NotificationReplyToYou
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
@@ -599,7 +599,7 @@ func (cs *CommentService) notificationMention(
|
||||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.MentionYou
|
||||
msg.NotificationAction = constant.NotificationMentionYou
|
||||
notice_queue.AddNotification(msg)
|
||||
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
|
||||
}
|
||||
|
||||
@@ -162,39 +162,30 @@ func (es *EmailService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGen
|
||||
}
|
||||
|
||||
func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) {
|
||||
ec, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
siteinfo, err := es.GetSiteGeneral(ctx)
|
||||
emailConfig, err := es.GetEmailConfig()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
siteInfo, err := es.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
templateData := RegisterTemplateData{
|
||||
SiteName: siteinfo.Name, RegisterUrl: registerUrl,
|
||||
}
|
||||
tmpl, err := template.New("register_title").Parse(ec.RegisterTitle)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
titleBuf := &bytes.Buffer{}
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
err = tmpl.Execute(titleBuf, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
SiteName: siteInfo.Name,
|
||||
RegisterUrl: registerUrl,
|
||||
}
|
||||
|
||||
tmpl, err = template.New("register_body").Parse(ec.RegisterBody)
|
||||
title, err = es.parseTemplateData(emailConfig.RegisterTitle, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
err = tmpl.Execute(bodyBuf, templateData)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
|
||||
return titleBuf.String(), bodyBuf.String(), nil
|
||||
body, err = es.parseTemplateData(emailConfig.RegisterBody, templateData)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("email template parse error: %s", err)
|
||||
}
|
||||
return title, body, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) {
|
||||
|
||||
@@ -7,14 +7,15 @@ import (
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
@@ -127,35 +128,47 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err = ns.formatNotificationPage(ctx, notifications)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pager.NewPageModel(total, resp), nil
|
||||
}
|
||||
|
||||
func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) (
|
||||
resp []*schema.NotificationContent, err error) {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
for _, notificationInfo := range notifications {
|
||||
item := &schema.NotificationContent{}
|
||||
err := json.Unmarshal([]byte(notificationInfo.Content), item)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil {
|
||||
log.Error("NotificationContent Unmarshal Error", err.Error())
|
||||
continue
|
||||
}
|
||||
lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
|
||||
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
|
||||
item.ID = notificationInfo.ID
|
||||
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
|
||||
if notificationInfo.IsRead == schema.NotificationRead {
|
||||
item.IsRead = true
|
||||
// If notification is downvote, the user info is not needed.
|
||||
if item.NotificationAction == constant.NotificationDownVotedTheQuestion ||
|
||||
item.NotificationAction == constant.NotificationDownVotedTheAnswer {
|
||||
item.UserInfo = nil
|
||||
}
|
||||
answerID, ok := item.ObjectInfo.ObjectMap["answer"]
|
||||
if ok {
|
||||
|
||||
item.ID = notificationInfo.ID
|
||||
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
|
||||
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
|
||||
item.IsRead = notificationInfo.IsRead == schema.NotificationRead
|
||||
|
||||
if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok {
|
||||
if item.ObjectInfo.ObjectID == answerID {
|
||||
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
|
||||
}
|
||||
item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
|
||||
}
|
||||
questionID, ok := item.ObjectInfo.ObjectMap["question"]
|
||||
if ok {
|
||||
if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok {
|
||||
if item.ObjectInfo.ObjectID == questionID {
|
||||
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
|
||||
}
|
||||
item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
|
||||
}
|
||||
|
||||
resp = append(resp, item)
|
||||
}
|
||||
return pager.NewPageModel(total, resp), nil
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
@@ -82,7 +83,9 @@ func (ns *NotificationCommon) HandleNotification() {
|
||||
// ObjectInfo.ObjectID
|
||||
// ObjectInfo.ObjectType
|
||||
func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) error {
|
||||
|
||||
if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() {
|
||||
return nil
|
||||
}
|
||||
req := &schema.NotificationContent{
|
||||
TriggerUserID: msg.TriggerUserID,
|
||||
ReceiverUserID: msg.ReceiverUserID,
|
||||
@@ -190,10 +193,10 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context,
|
||||
if msg.NoNeedPushAllFollow {
|
||||
return
|
||||
}
|
||||
if msg.NotificationAction != constant.UpdateQuestion &&
|
||||
msg.NotificationAction != constant.AnswerTheQuestion &&
|
||||
msg.NotificationAction != constant.UpdateAnswer &&
|
||||
msg.NotificationAction != constant.AcceptAnswer {
|
||||
if msg.NotificationAction != constant.NotificationUpdateQuestion &&
|
||||
msg.NotificationAction != constant.NotificationAnswerTheQuestion &&
|
||||
msg.NotificationAction != constant.NotificationUpdateAnswer &&
|
||||
msg.NotificationAction != constant.NotificationAcceptAnswer {
|
||||
return
|
||||
}
|
||||
condObjectID := msg.ObjectID
|
||||
|
||||
@@ -10,10 +10,10 @@ const (
|
||||
QuestionReopen = "question.reopen"
|
||||
QuestionVoteUp = "question.vote_up"
|
||||
QuestionVoteDown = "question.vote_down"
|
||||
QuestionPin = "question.pin" //Top the question
|
||||
QuestionUnPin = "question.unpin" //untop the question
|
||||
QuestionHide = "question.hide" //hide the question
|
||||
QuestionShow = "question.show" //show the question
|
||||
QuestionPin = "question.pin"
|
||||
QuestionUnPin = "question.unpin"
|
||||
QuestionHide = "question.hide"
|
||||
QuestionShow = "question.show"
|
||||
AnswerAdd = "answer.add"
|
||||
AnswerEdit = "answer.edit"
|
||||
AnswerEditWithoutReview = "answer.edit_without_review"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package plugin_common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
type PluginConfigRepo interface {
|
||||
SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error)
|
||||
GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error)
|
||||
}
|
||||
|
||||
// PluginCommonService user service
|
||||
type PluginCommonService struct {
|
||||
configRepo config.ConfigRepo
|
||||
pluginConfigRepo PluginConfigRepo
|
||||
}
|
||||
|
||||
// NewPluginCommonService new report service
|
||||
func NewPluginCommonService(
|
||||
pluginConfigRepo PluginConfigRepo,
|
||||
configRepo config.ConfigRepo) *PluginCommonService {
|
||||
|
||||
// init plugin status
|
||||
pluginStatus, err := configRepo.GetString(constant.PluginStatus)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
if err := plugin.StatusManager.UnmarshalJSON([]byte(pluginStatus)); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// init plugin config
|
||||
pluginConfigs, err := pluginConfigRepo.GetPluginConfigAll(context.Background())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
for _, pluginConfig := range pluginConfigs {
|
||||
err := plugin.CallConfig(func(fn plugin.Config) error {
|
||||
if fn.Info().SlugName == pluginConfig.PluginSlugName {
|
||||
return fn.ConfigReceiver([]byte(pluginConfig.Value))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("parse plugin config failed: %s %v", pluginConfig.PluginSlugName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &PluginCommonService{
|
||||
configRepo: configRepo,
|
||||
pluginConfigRepo: pluginConfigRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdatePluginStatus update plugin status
|
||||
func (ps *PluginCommonService) UpdatePluginStatus(ctx context.Context) (err error) {
|
||||
content, err := plugin.StatusManager.MarshalJSON()
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err)
|
||||
}
|
||||
return ps.configRepo.SetConfig(constant.PluginStatus, string(content))
|
||||
}
|
||||
|
||||
// UpdatePluginConfig update plugin config
|
||||
func (ps *PluginCommonService) UpdatePluginConfig(ctx context.Context, req *schema.UpdatePluginConfigReq) (err error) {
|
||||
configValue, _ := json.Marshal(req.ConfigFields)
|
||||
return ps.pluginConfigRepo.SavePluginConfig(ctx, req.PluginSlugName, string(configValue))
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/service/notification"
|
||||
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
"github.com/answerdev/answer/internal/service/plugin_common"
|
||||
questioncommon "github.com/answerdev/answer/internal/service/question_common"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/internal/service/reason"
|
||||
@@ -32,6 +33,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/service/uploader"
|
||||
"github.com/answerdev/answer/internal/service/user_admin"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/internal/service/user_external_login"
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
@@ -79,4 +81,7 @@ var ProviderSetService = wire.NewSet(
|
||||
role.NewRoleService,
|
||||
role.NewUserRoleRelService,
|
||||
role.NewRolePowerRelService,
|
||||
user_external_login.NewUserExternalLoginService,
|
||||
user_external_login.NewUserCenterLoginService,
|
||||
plugin_common.NewPluginCommonService,
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ type QuestionRepo interface {
|
||||
UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)
|
||||
GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)
|
||||
GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)
|
||||
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
|
||||
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
|
||||
questionList []*entity.Question, total int64, err error)
|
||||
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
|
||||
UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
@@ -270,6 +271,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
||||
question.Status = entity.QuestionStatusAvailable
|
||||
question.RevisionID = "0"
|
||||
question.CreatedAt = now
|
||||
question.PostUpdateTime = now
|
||||
question.Pin = entity.QuestionUnPin
|
||||
question.Show = entity.QuestionShow
|
||||
//question.UpdatedAt = nil
|
||||
@@ -733,70 +735,74 @@ func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobject
|
||||
return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) {
|
||||
userlist := make([]*schema.UserQuestionInfo, 0)
|
||||
// PersonalQuestionPage get question list by user
|
||||
func (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema.PersonalQuestionPageReq) (
|
||||
pageModel *pager.PageModel, err error) {
|
||||
|
||||
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
|
||||
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
|
||||
if err != nil {
|
||||
return userlist, 0, err
|
||||
return nil, err
|
||||
}
|
||||
if !Exist {
|
||||
return userlist, 0, nil
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
search := &schema.QuestionPageReq{}
|
||||
search.OrderCond = order
|
||||
search.Page = page
|
||||
search.PageSize = pageSize
|
||||
search.OrderCond = req.OrderCond
|
||||
search.Page = req.Page
|
||||
search.PageSize = req.PageSize
|
||||
search.UserIDBeSearched = userinfo.ID
|
||||
search.LoginUserID = loginUserID
|
||||
questionlist, count, err := qs.GetQuestionPage(ctx, search)
|
||||
search.LoginUserID = req.LoginUserID
|
||||
questionList, total, err := qs.GetQuestionPage(ctx, search)
|
||||
if err != nil {
|
||||
return userlist, 0, err
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range questionlist {
|
||||
userQuestionInfoList := make([]*schema.UserQuestionInfo, 0)
|
||||
for _, item := range questionList {
|
||||
info := &schema.UserQuestionInfo{}
|
||||
_ = copier.Copy(info, item)
|
||||
status, ok := entity.AdminQuestionSearchStatusIntToString[item.Status]
|
||||
if ok {
|
||||
info.Status = status
|
||||
}
|
||||
userlist = append(userlist, info)
|
||||
userQuestionInfoList = append(userQuestionInfoList, info)
|
||||
}
|
||||
return userlist, count, nil
|
||||
return pager.NewPageModel(total, userQuestionInfoList), nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserAnswerInfo, int64, error) {
|
||||
answerlist := make([]*schema.AnswerInfo, 0)
|
||||
userAnswerlist := make([]*schema.UserAnswerInfo, 0)
|
||||
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
|
||||
func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.PersonalAnswerPageReq) (
|
||||
pageModel *pager.PageModel, err error) {
|
||||
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
|
||||
if err != nil {
|
||||
return userAnswerlist, 0, err
|
||||
return nil, err
|
||||
}
|
||||
if !Exist {
|
||||
return userAnswerlist, 0, nil
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
answersearch := &entity.AnswerSearch{}
|
||||
answersearch.UserID = userinfo.ID
|
||||
answersearch.PageSize = pageSize
|
||||
answersearch.Page = page
|
||||
if order == "newest" {
|
||||
answersearch.PageSize = req.PageSize
|
||||
answersearch.Page = req.Page
|
||||
if req.OrderCond == "newest" {
|
||||
answersearch.Order = entity.AnswerSearchOrderByTime
|
||||
} else {
|
||||
answersearch.Order = entity.AnswerSearchOrderByDefault
|
||||
}
|
||||
questionIDs := make([]string, 0)
|
||||
answerList, count, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
|
||||
answerList, total, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
|
||||
if err != nil {
|
||||
return userAnswerlist, count, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
answerlist := make([]*schema.AnswerInfo, 0)
|
||||
userAnswerlist := make([]*schema.UserAnswerInfo, 0)
|
||||
for _, item := range answerList {
|
||||
answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)
|
||||
answerlist = append(answerlist, answerinfo)
|
||||
questionIDs = append(questionIDs, uid.DeShortID(item.QuestionID))
|
||||
}
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)
|
||||
if err != nil {
|
||||
return userAnswerlist, count, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range answerlist {
|
||||
@@ -813,34 +819,29 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
|
||||
}
|
||||
}
|
||||
|
||||
return userAnswerlist, count, nil
|
||||
return pager.NewPageModel(total, userAnswerlist), nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, pageSize int, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
|
||||
// PersonalCollectionPage get collection list by user
|
||||
func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) (
|
||||
pageModel *pager.PageModel, err error) {
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, loginUserID)
|
||||
if err != nil {
|
||||
return list, 0, err
|
||||
}
|
||||
if !Exist {
|
||||
return list, 0, nil
|
||||
}
|
||||
collectionSearch := &entity.CollectionSearch{}
|
||||
collectionSearch.UserID = userinfo.ID
|
||||
collectionSearch.Page = page
|
||||
collectionSearch.PageSize = pageSize
|
||||
collectionlist, count, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
|
||||
collectionSearch.UserID = req.UserID
|
||||
collectionSearch.Page = req.Page
|
||||
collectionSearch.PageSize = req.PageSize
|
||||
collectionList, total, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
|
||||
if err != nil {
|
||||
return list, 0, err
|
||||
return nil, err
|
||||
}
|
||||
questionIDs := make([]string, 0)
|
||||
for _, item := range collectionlist {
|
||||
for _, item := range collectionList {
|
||||
questionIDs = append(questionIDs, item.ObjectID)
|
||||
}
|
||||
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.UserID)
|
||||
if err != nil {
|
||||
return list, count, err
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range questionIDs {
|
||||
_, ok := questionMaps[uid.EnShortID(id)]
|
||||
@@ -853,7 +854,7 @@ func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, p
|
||||
}
|
||||
}
|
||||
|
||||
return list, count, nil
|
||||
return pager.NewPageModel(total, list), nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) {
|
||||
@@ -1001,7 +1002,7 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques
|
||||
}
|
||||
|
||||
questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,
|
||||
req.UserIDBeSearched, req.TagID, req.OrderCond)
|
||||
req.UserIDBeSearched, req.TagID, req.OrderCond, req.InDays)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -1063,7 +1064,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
|
||||
msg.ReceiverUserID = questionInfo.UserID
|
||||
msg.TriggerUserID = questionInfo.UserID
|
||||
msg.ObjectType = constant.QuestionObjectType
|
||||
msg.NotificationAction = constant.YourQuestionWasDeleted
|
||||
msg.NotificationAction = constant.NotificationYourQuestionWasDeleted
|
||||
notice_queue.AddNotification(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/htmltext"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
@@ -228,6 +229,9 @@ func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRan
|
||||
// GetRankPersonalWithPage get personal comment list page
|
||||
func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.GetRankPersonalWithPageReq) (
|
||||
pageModel *pager.PageModel, err error) {
|
||||
if plugin.RankAgentEnabled() {
|
||||
return pager.NewPageModel(0, []string{}), nil
|
||||
}
|
||||
if len(req.Username) > 0 {
|
||||
userInfo, exist, err := rs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
|
||||
if err != nil {
|
||||
|
||||
@@ -66,7 +66,7 @@ func (rh *ReportHandle) HandleObject(ctx context.Context, reported *entity.Repor
|
||||
switch req.FlaggedType {
|
||||
case reasonDelete:
|
||||
err = rh.commentRepo.RemoveComment(ctx, objectID)
|
||||
rh.sendNotification(ctx, reportedUserID, objectID, constant.YourCommentWasDeleted)
|
||||
rh.sendNotification(ctx, reportedUserID, objectID, constant.NotificationYourCommentWasDeleted)
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
@@ -209,7 +209,7 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem
|
||||
ObjectID: answerinfo.ID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.UpdateAnswer
|
||||
msg.NotificationAction = constant.NotificationUpdateAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
|
||||
@@ -3,12 +3,15 @@ package siteinfo
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
@@ -23,19 +26,23 @@ type SiteInfoService struct {
|
||||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
|
||||
emailService *export.EmailService
|
||||
tagCommonService *tagcommon.TagCommonService
|
||||
configRepo config.ConfigRepo
|
||||
}
|
||||
|
||||
func NewSiteInfoService(
|
||||
siteInfoRepo siteinfo_common.SiteInfoRepo,
|
||||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
|
||||
emailService *export.EmailService,
|
||||
tagCommonService *tagcommon.TagCommonService) *SiteInfoService {
|
||||
|
||||
resp, err := siteInfoCommonService.GetSiteInterface(context.Background())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
constant.DefaultAvatar = resp.DefaultAvatar
|
||||
tagCommonService *tagcommon.TagCommonService,
|
||||
configRepo config.ConfigRepo,
|
||||
) *SiteInfoService {
|
||||
usersSiteInfo, _ := siteInfoCommonService.GetSiteUsers(context.Background())
|
||||
if usersSiteInfo != nil {
|
||||
constant.DefaultAvatar = usersSiteInfo.DefaultAvatar
|
||||
}
|
||||
generalSiteInfo, _ := siteInfoCommonService.GetSiteGeneral(context.Background())
|
||||
if generalSiteInfo != nil {
|
||||
constant.DefaultSiteURL = generalSiteInfo.SiteUrl
|
||||
}
|
||||
|
||||
return &SiteInfoService{
|
||||
@@ -43,6 +50,7 @@ func NewSiteInfoService(
|
||||
siteInfoCommonService: siteInfoCommonService,
|
||||
emailService: emailService,
|
||||
tagCommonService: tagCommonService,
|
||||
configRepo: configRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +69,11 @@ func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.Sit
|
||||
return s.siteInfoCommonService.GetSiteBranding(ctx)
|
||||
}
|
||||
|
||||
// GetSiteUsers get site info about users
|
||||
func (s *SiteInfoService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {
|
||||
return s.siteInfoCommonService.GetSiteUsers(ctx)
|
||||
}
|
||||
|
||||
// GetSiteWrite get site info write
|
||||
func (s *SiteInfoService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
|
||||
resp = &schema.SiteWriteResp{}
|
||||
@@ -106,45 +119,32 @@ func (s *SiteInfoService) GetSiteTheme(ctx context.Context) (resp *schema.SiteTh
|
||||
|
||||
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
|
||||
req.FormatSiteUrl()
|
||||
var (
|
||||
siteType = "general"
|
||||
content []byte
|
||||
)
|
||||
content, _ = json.Marshal(req)
|
||||
|
||||
data := entity.SiteInfo{
|
||||
Type: siteType,
|
||||
content, _ := json.Marshal(req)
|
||||
data := &entity.SiteInfo{
|
||||
Type: constant.SiteTypeGeneral,
|
||||
Content: string(content),
|
||||
Status: 1,
|
||||
}
|
||||
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeGeneral, data)
|
||||
if err == nil {
|
||||
constant.DefaultSiteURL = req.SiteUrl
|
||||
}
|
||||
|
||||
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
|
||||
var (
|
||||
siteType = "interface"
|
||||
content []byte
|
||||
)
|
||||
|
||||
// check language
|
||||
if !translator.CheckLanguageIsValid(req.Language) {
|
||||
err = errors.BadRequest(reason.LangNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
content, _ = json.Marshal(req)
|
||||
|
||||
content, _ := json.Marshal(req)
|
||||
data := entity.SiteInfo{
|
||||
Type: siteType,
|
||||
Type: constant.SiteTypeInterface,
|
||||
Content: string(content),
|
||||
}
|
||||
|
||||
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
|
||||
if err == nil {
|
||||
constant.DefaultAvatar = req.DefaultAvatar
|
||||
}
|
||||
return
|
||||
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeInterface, &data)
|
||||
}
|
||||
|
||||
// SaveSiteBranding save site branding information
|
||||
@@ -218,6 +218,21 @@ func (s *SiteInfoService) SaveSiteTheme(ctx context.Context, req *schema.SiteThe
|
||||
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTheme, data)
|
||||
}
|
||||
|
||||
// SaveSiteUsers save site users
|
||||
func (s *SiteInfoService) SaveSiteUsers(ctx context.Context, req *schema.SiteUsersReq) (err error) {
|
||||
content, _ := json.Marshal(req)
|
||||
data := &entity.SiteInfo{
|
||||
Type: constant.SiteTypeUsers,
|
||||
Content: string(content),
|
||||
Status: 1,
|
||||
}
|
||||
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data)
|
||||
if err == nil {
|
||||
constant.DefaultAvatar = req.DefaultAvatar
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSMTPConfig get smtp config
|
||||
func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (
|
||||
resp *schema.GetSMTPConfigResp, err error,
|
||||
@@ -253,8 +268,11 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) {
|
||||
resp = &schema.SiteSeoResp{}
|
||||
func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
|
||||
resp = &schema.SiteSeoReq{}
|
||||
if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
loginConfig, err := s.GetSiteLogin(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@@ -265,17 +283,6 @@ func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp,
|
||||
resp.Robots = "User-agent: *\nDisallow: /"
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp = &schema.SiteSeoResp{}
|
||||
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeSeo)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return resp, nil
|
||||
}
|
||||
if !exist {
|
||||
return resp, nil
|
||||
}
|
||||
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -302,3 +309,71 @@ func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (e
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) {
|
||||
privilege := &schema.UpdatePrivilegesConfigReq{}
|
||||
if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp = &schema.GetPrivilegesConfigResp{
|
||||
Options: s.translatePrivilegeOptions(ctx),
|
||||
SelectedLevel: schema.PrivilegeLevel3,
|
||||
}
|
||||
if privilege != nil && privilege.Level > 0 {
|
||||
resp.SelectedLevel = privilege.Level
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) translatePrivilegeOptions(ctx context.Context) (options []*schema.PrivilegeOption) {
|
||||
la := handler.GetLangByCtx(ctx)
|
||||
for _, option := range schema.DefaultPrivilegeOptions {
|
||||
op := &schema.PrivilegeOption{
|
||||
Level: option.Level,
|
||||
LevelDesc: translator.Tr(la, option.LevelDesc),
|
||||
}
|
||||
for _, privilege := range option.Privileges {
|
||||
op.Privileges = append(op.Privileges, &constant.Privilege{
|
||||
Key: privilege.Key,
|
||||
Label: translator.Tr(la, privilege.Label),
|
||||
Value: privilege.Value,
|
||||
})
|
||||
}
|
||||
options = append(options, op)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) UpdatePrivilegesConfig(ctx context.Context, req *schema.UpdatePrivilegesConfigReq) (err error) {
|
||||
var chooseOption *schema.PrivilegeOption
|
||||
for _, option := range schema.DefaultPrivilegeOptions {
|
||||
if option.Level == req.Level {
|
||||
chooseOption = option
|
||||
break
|
||||
}
|
||||
}
|
||||
if chooseOption == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// update site info that user choose which privilege level
|
||||
content, _ := json.Marshal(req)
|
||||
data := &entity.SiteInfo{
|
||||
Type: constant.SiteTypePrivileges,
|
||||
Content: string(content),
|
||||
Status: 1,
|
||||
}
|
||||
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypePrivileges, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update privilege in config
|
||||
for _, privilege := range chooseOption.Privileges {
|
||||
err = s.configRepo.SetConfig(privilege.Key, fmt.Sprintf("%d", privilege.Value))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService
|
||||
// GetSiteGeneral get site info general
|
||||
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
|
||||
resp = &schema.SiteGeneralResp{}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
@@ -52,7 +52,7 @@ func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schem
|
||||
// GetSiteInterface get site info interface
|
||||
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
|
||||
resp = &schema.SiteInterfaceResp{}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
@@ -61,7 +61,16 @@ func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *sch
|
||||
// GetSiteBranding get site info branding
|
||||
func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
|
||||
resp = &schema.SiteBrandingResp{}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetSiteUsers get site info about users
|
||||
func (s *SiteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {
|
||||
resp = &schema.SiteUsersResp{}
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
@@ -70,7 +79,7 @@ func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *sche
|
||||
// GetSiteWrite get site info write
|
||||
func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
|
||||
resp = &schema.SiteWriteResp{}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
@@ -79,7 +88,7 @@ func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.
|
||||
// GetSiteLegal get site info write
|
||||
func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
|
||||
resp = &schema.SiteLegalResp{}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
@@ -88,7 +97,7 @@ func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.
|
||||
// GetSiteLogin get site login config
|
||||
func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {
|
||||
resp = &schema.SiteLoginResp{}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
@@ -97,7 +106,7 @@ func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.
|
||||
// GetSiteCustomCssHTML get site custom css html config
|
||||
func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {
|
||||
resp = &schema.SiteCustomCssHTMLResp{}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
@@ -108,7 +117,7 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.
|
||||
resp = &schema.SiteThemeResp{
|
||||
ThemeOptions: schema.GetThemeOptions,
|
||||
}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.TrTheme(ctx)
|
||||
@@ -118,13 +127,13 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.
|
||||
// GetSiteSeo get site seo
|
||||
func (s *SiteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
|
||||
resp = &schema.SiteSeoReq{}
|
||||
if err = s.getSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
|
||||
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *SiteInfoCommonService) getSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
|
||||
func (s *SiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
|
||||
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -18,10 +18,12 @@ import (
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/dir"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gin-gonic/gin"
|
||||
exifremove "github.com/scottleedavis/go-exif-remove"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -72,6 +74,14 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig,
|
||||
|
||||
// UploadAvatarFile upload avatar file
|
||||
func (us *UploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
|
||||
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(url) > 0 {
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// max size
|
||||
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024)
|
||||
_, file, err := ctx.Request.FormFile("file")
|
||||
@@ -143,6 +153,14 @@ func (us *UploaderService) AvatarThumbFile(ctx *gin.Context, uploadPath, fileNam
|
||||
|
||||
func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
|
||||
url string, err error) {
|
||||
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(url) > 0 {
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// max size
|
||||
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
|
||||
_, file, err := ctx.Request.FormFile("file")
|
||||
@@ -161,6 +179,14 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context) (
|
||||
|
||||
func (us *UploaderService) UploadBrandingFile(ctx *gin.Context) (
|
||||
url string, err error) {
|
||||
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(url) > 0 {
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// max size
|
||||
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
|
||||
_, file, err := ctx.Request.FormFile("file")
|
||||
@@ -204,6 +230,21 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (us *UploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (
|
||||
url string, err error) {
|
||||
_ = plugin.CallStorage(func(fn plugin.Storage) error {
|
||||
resp := fn.UploadFile(ctx, source)
|
||||
if resp.OriginalError != nil {
|
||||
log.Errorf("upload file by plugin failed, err: %v", resp.OriginalError)
|
||||
err = errors.BadRequest("").WithMsg(resp.DisplayErrorMsg.Translate(ctx)).WithError(err)
|
||||
} else {
|
||||
url = resp.FullURL
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return url, err
|
||||
}
|
||||
|
||||
func Dexif(filepath string, destpath string) error {
|
||||
img, err := ioutil.ReadFile(filepath)
|
||||
if err != nil {
|
||||
|
||||
@@ -116,7 +116,7 @@ func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.Upda
|
||||
return err
|
||||
}
|
||||
|
||||
us.authService.RemoveUserTokens(ctx, req.UserID)
|
||||
us.authService.RemoveUserAllTokens(ctx, req.UserID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.
|
||||
return err
|
||||
}
|
||||
// logout this user
|
||||
us.authService.RemoveUserTokens(ctx, req.UserID)
|
||||
us.authService.RemoveUserAllTokens(ctx, req.UserID)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,18 @@ package usercommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/Chain-Zhang/pinyin"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/auth"
|
||||
"github.com/answerdev/answer/internal/service/role"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/random"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
type UserRepo interface {
|
||||
@@ -34,12 +36,20 @@ type UserRepo interface {
|
||||
|
||||
// UserCommon user service
|
||||
type UserCommon struct {
|
||||
userRepo UserRepo
|
||||
userRepo UserRepo
|
||||
userRoleService *role.UserRoleRelService
|
||||
authService *auth.AuthService
|
||||
}
|
||||
|
||||
func NewUserCommon(userRepo UserRepo) *UserCommon {
|
||||
func NewUserCommon(
|
||||
userRepo UserRepo,
|
||||
userRoleService *role.UserRoleRelService,
|
||||
authService *auth.AuthService,
|
||||
) *UserCommon {
|
||||
return &UserCommon{
|
||||
userRepo: userRepo,
|
||||
userRepo: userRepo,
|
||||
userRoleService: userRoleService,
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,9 +145,34 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
|
||||
if !has {
|
||||
break
|
||||
}
|
||||
bytes := make([]byte, 2)
|
||||
_, _ = rand.Read(bytes)
|
||||
suffix = hex.EncodeToString(bytes)
|
||||
suffix = random.UsernameSuffix()
|
||||
}
|
||||
return username + suffix, nil
|
||||
}
|
||||
|
||||
func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int, externalID string) (
|
||||
accessToken string, userCacheInfo *entity.UserCacheInfo, err error) {
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
userCacheInfo = &entity.UserCacheInfo{
|
||||
UserID: userID,
|
||||
EmailStatus: emailStatus,
|
||||
UserStatus: userStatus,
|
||||
RoleID: roleID,
|
||||
ExternalID: externalID,
|
||||
}
|
||||
|
||||
accessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if userCacheInfo.RoleID == role.RoleAdminID {
|
||||
if err = us.authService.SetAdminUserCacheInfo(ctx, accessToken, &entity.UserCacheInfo{UserID: userID}); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
return accessToken, userCacheInfo, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
package user_external_login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/random"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
// UserCenterLoginService user external login service
|
||||
type UserCenterLoginService struct {
|
||||
userRepo usercommon.UserRepo
|
||||
userExternalLoginRepo UserExternalLoginRepo
|
||||
userCommonService *usercommon.UserCommon
|
||||
userActivity activity.UserActiveActivityRepo
|
||||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
|
||||
}
|
||||
|
||||
// NewUserCenterLoginService new user external login service
|
||||
func NewUserCenterLoginService(
|
||||
userRepo usercommon.UserRepo,
|
||||
userCommonService *usercommon.UserCommon,
|
||||
userExternalLoginRepo UserExternalLoginRepo,
|
||||
userActivity activity.UserActiveActivityRepo,
|
||||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
|
||||
) *UserCenterLoginService {
|
||||
return &UserCenterLoginService{
|
||||
userRepo: userRepo,
|
||||
userCommonService: userCommonService,
|
||||
userExternalLoginRepo: userExternalLoginRepo,
|
||||
userActivity: userActivity,
|
||||
siteInfoCommonService: siteInfoCommonService,
|
||||
}
|
||||
}
|
||||
|
||||
func (us *UserCenterLoginService) ExternalLogin(
|
||||
ctx context.Context, userCenter plugin.UserCenter, basicUserInfo *plugin.UserCenterBasicUserInfo) (
|
||||
resp *schema.UserExternalLoginResp, err error) {
|
||||
|
||||
if len(basicUserInfo.Email) > 0 {
|
||||
// check whether site allow register or not
|
||||
siteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !checker.EmailInAllowEmailDomain(basicUserInfo.Email, siteInfo.AllowEmailDomains) {
|
||||
log.Debugf("email domain not allowed: %s", basicUserInfo.Email)
|
||||
return &schema.UserExternalLoginResp{
|
||||
ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),
|
||||
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
|
||||
userCenter.Info().SlugName, basicUserInfo.ExternalID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
// if user is already a member, login directly
|
||||
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
// if user is deleted, do not allow login
|
||||
if oldUserInfo.Status == entity.UserStatusDeleted {
|
||||
return &schema.UserExternalLoginResp{
|
||||
ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),
|
||||
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserPageAccessDenied),
|
||||
}, nil
|
||||
}
|
||||
if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {
|
||||
log.Errorf("update user last login date failed: %v", err)
|
||||
}
|
||||
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
|
||||
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
|
||||
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
|
||||
}
|
||||
}
|
||||
|
||||
// cache external user info, waiting for user enter email address.
|
||||
if userCenter.Description().MustAuthEmailEnabled && len(basicUserInfo.Email) == 0 {
|
||||
return &schema.UserExternalLoginResp{ErrMsg: "Requires authorized email to login"}, nil
|
||||
}
|
||||
|
||||
oldUserInfo, err := us.registerNewUser(ctx, userCenter.Info().SlugName, basicUserInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
us.activeUser(ctx, oldUserInfo)
|
||||
|
||||
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
|
||||
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
|
||||
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
|
||||
}
|
||||
|
||||
func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider string,
|
||||
basicUserInfo *plugin.UserCenterBasicUserInfo) (userInfo *entity.User, err error) {
|
||||
userInfo = &entity.User{}
|
||||
userInfo.EMail = basicUserInfo.Email
|
||||
userInfo.DisplayName = basicUserInfo.DisplayName
|
||||
|
||||
userInfo.Username, err = us.userCommonService.MakeUsername(ctx, basicUserInfo.Username)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
userInfo.Username = random.Username()
|
||||
}
|
||||
|
||||
if len(basicUserInfo.Avatar) > 0 {
|
||||
avatarInfo := &schema.AvatarInfo{
|
||||
Type: schema.AvatarTypeCustom,
|
||||
Custom: basicUserInfo.Avatar,
|
||||
}
|
||||
avatar, _ := json.Marshal(avatarInfo)
|
||||
userInfo.Avatar = string(avatar)
|
||||
}
|
||||
|
||||
userInfo.MailStatus = entity.EmailStatusAvailable
|
||||
userInfo.Status = entity.UserStatusAvailable
|
||||
userInfo.LastLoginDate = time.Now()
|
||||
userInfo.Bio = basicUserInfo.Bio
|
||||
userInfo.BioHTML = converter.Markdown2HTML(basicUserInfo.Bio)
|
||||
err = us.userRepo.AddUser(ctx, userInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metaInfo, _ := json.Marshal(basicUserInfo)
|
||||
newExternalUserInfo := &entity.UserExternalLogin{
|
||||
UserID: userInfo.ID,
|
||||
Provider: provider,
|
||||
ExternalID: basicUserInfo.ExternalID,
|
||||
MetaInfo: string(metaInfo),
|
||||
}
|
||||
err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo)
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (us *UserCenterLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User) {
|
||||
if err := us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, userID string) (
|
||||
resp *schema.UserCenterUserSettingsResp, err error) {
|
||||
resp = &schema.UserCenterUserSettingsResp{}
|
||||
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// get external login info
|
||||
externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var externalInfo *entity.UserExternalLogin
|
||||
for _, t := range externalLoginList {
|
||||
if t.Provider == userCenter.Info().SlugName {
|
||||
externalInfo = t
|
||||
}
|
||||
}
|
||||
if externalInfo == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
settings, err := userCenter.UserSettings(externalInfo.ExternalID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
if len(settings.AccountSettingRedirectURL) > 0 {
|
||||
resp.AccountSettingAgent = schema.UserSettingAgent{
|
||||
Enabled: true,
|
||||
RedirectURL: settings.AccountSettingRedirectURL,
|
||||
}
|
||||
}
|
||||
if len(settings.ProfileSettingRedirectURL) > 0 {
|
||||
resp.ProfileSettingAgent = schema.UserSettingAgent{
|
||||
Enabled: true,
|
||||
RedirectURL: settings.ProfileSettingRedirectURL,
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UserCenterAdminFunctionAgent Check in the backend administration interface if the user-related functions
|
||||
// are turned off due to turning on the User Center plugin.
|
||||
func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Context) (
|
||||
resp *schema.UserCenterAdminFunctionAgentResp, err error) {
|
||||
resp = &schema.UserCenterAdminFunctionAgentResp{
|
||||
AllowCreateUser: true,
|
||||
AllowUpdateUserStatus: true,
|
||||
AllowUpdateUserPassword: true,
|
||||
AllowUpdateUserRole: true,
|
||||
}
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
desc := userCenter.Description()
|
||||
// If user status agent is enabled, admin can not update user status in answer.
|
||||
resp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled
|
||||
|
||||
// If original user system is enabled, admin can update user password and role in answer.
|
||||
resp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem
|
||||
resp.AllowUpdateUserRole = desc.EnabledOriginalUserSystem
|
||||
resp.AllowCreateUser = desc.EnabledOriginalUserSystem
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (us *UserCenterLoginService) UserCenterPersonalBranding(ctx context.Context, username string) (
|
||||
resp *schema.UserCenterPersonalBranding, err error) {
|
||||
resp = &schema.UserCenterPersonalBranding{
|
||||
PersonalBranding: make([]*schema.PersonalBranding, 0),
|
||||
}
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
userInfo, exist, err := us.userRepo.GetByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// get external login info
|
||||
externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var externalInfo *entity.UserExternalLogin
|
||||
for _, t := range externalLoginList {
|
||||
if t.Provider == userCenter.Info().SlugName {
|
||||
externalInfo = t
|
||||
}
|
||||
}
|
||||
if externalInfo == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp.Enabled = true
|
||||
branding := userCenter.PersonalBranding(externalInfo.ExternalID)
|
||||
|
||||
for _, t := range branding {
|
||||
resp.PersonalBranding = append(resp.PersonalBranding, &schema.PersonalBranding{
|
||||
Icon: t.Icon,
|
||||
Name: t.Name,
|
||||
Label: t.Label,
|
||||
Url: t.Url,
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
package user_external_login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/random"
|
||||
"github.com/answerdev/answer/pkg/token"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
type UserExternalLoginRepo interface {
|
||||
AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error)
|
||||
UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error)
|
||||
GetByExternalID(ctx context.Context, provider, externalID string) (userInfo *entity.UserExternalLogin, exist bool, err error)
|
||||
GetUserExternalLoginList(ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error)
|
||||
DeleteUserExternalLogin(ctx context.Context, userID, externalID 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)
|
||||
}
|
||||
|
||||
// UserExternalLoginService user external login service
|
||||
type UserExternalLoginService struct {
|
||||
userRepo usercommon.UserRepo
|
||||
userExternalLoginRepo UserExternalLoginRepo
|
||||
userCommonService *usercommon.UserCommon
|
||||
emailService *export.EmailService
|
||||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
|
||||
userActivity activity.UserActiveActivityRepo
|
||||
}
|
||||
|
||||
// NewUserExternalLoginService new user external login service
|
||||
func NewUserExternalLoginService(
|
||||
userRepo usercommon.UserRepo,
|
||||
userCommonService *usercommon.UserCommon,
|
||||
userExternalLoginRepo UserExternalLoginRepo,
|
||||
emailService *export.EmailService,
|
||||
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
|
||||
userActivity activity.UserActiveActivityRepo,
|
||||
) *UserExternalLoginService {
|
||||
return &UserExternalLoginService{
|
||||
userRepo: userRepo,
|
||||
userCommonService: userCommonService,
|
||||
userExternalLoginRepo: userExternalLoginRepo,
|
||||
emailService: emailService,
|
||||
siteInfoCommonService: siteInfoCommonService,
|
||||
userActivity: userActivity,
|
||||
}
|
||||
}
|
||||
|
||||
// ExternalLogin if user is already a member logged in
|
||||
func (us *UserExternalLoginService) ExternalLogin(
|
||||
ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) (
|
||||
resp *schema.UserExternalLoginResp, err error) {
|
||||
oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
|
||||
externalUserInfo.Provider, externalUserInfo.ExternalID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
// if user is already a member, login directly
|
||||
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist && oldUserInfo.Status != entity.UserStatusDeleted {
|
||||
if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {
|
||||
log.Errorf("update user last login date failed: %v", err)
|
||||
}
|
||||
newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
|
||||
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
|
||||
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
|
||||
}
|
||||
}
|
||||
|
||||
// cache external user info, waiting for user enter email address.
|
||||
if len(externalUserInfo.Email) == 0 {
|
||||
bindingKey := token.GenerateToken()
|
||||
err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, bindingKey, externalUserInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &schema.UserExternalLoginResp{BindingKey: bindingKey}, nil
|
||||
}
|
||||
|
||||
oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if user is not a member, register a new user
|
||||
if !exist {
|
||||
oldUserInfo, err = us.registerNewUser(ctx, externalUserInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// bind external user info to user
|
||||
err = us.bindOldUser(ctx, externalUserInfo, oldUserInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If user login with external account and email is exist, active user directly.
|
||||
newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
|
||||
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
|
||||
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
|
||||
}
|
||||
|
||||
func (us *UserExternalLoginService) registerNewUser(ctx context.Context,
|
||||
externalUserInfo *schema.ExternalLoginUserInfoCache) (userInfo *entity.User, err error) {
|
||||
userInfo = &entity.User{}
|
||||
userInfo.EMail = externalUserInfo.Email
|
||||
userInfo.DisplayName = externalUserInfo.DisplayName
|
||||
|
||||
userInfo.Username, err = us.userCommonService.MakeUsername(ctx, externalUserInfo.Username)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
userInfo.Username = random.Username()
|
||||
}
|
||||
|
||||
if len(externalUserInfo.Avatar) > 0 {
|
||||
avatarInfo := &schema.AvatarInfo{
|
||||
Type: schema.AvatarTypeCustom,
|
||||
Custom: externalUserInfo.Avatar,
|
||||
}
|
||||
avatar, _ := json.Marshal(avatarInfo)
|
||||
userInfo.Avatar = string(avatar)
|
||||
}
|
||||
|
||||
userInfo.MailStatus = entity.EmailStatusToBeVerified
|
||||
userInfo.Status = entity.UserStatusAvailable
|
||||
userInfo.LastLoginDate = time.Now()
|
||||
userInfo.Bio = externalUserInfo.Bio
|
||||
userInfo.BioHTML = externalUserInfo.Bio
|
||||
err = us.userRepo.AddUser(ctx, userInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (us *UserExternalLoginService) bindOldUser(ctx context.Context,
|
||||
externalUserInfo *schema.ExternalLoginUserInfoCache, oldUserInfo *entity.User) (err error) {
|
||||
oldExternalUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
|
||||
externalUserInfo.Provider,
|
||||
externalUserInfo.ExternalID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
oldExternalUserInfo.MetaInfo = externalUserInfo.MetaInfo
|
||||
oldExternalUserInfo.UserID = oldUserInfo.ID
|
||||
err = us.userExternalLoginRepo.UpdateInfo(ctx, oldExternalUserInfo)
|
||||
} else {
|
||||
newExternalUserInfo := &entity.UserExternalLogin{
|
||||
UserID: oldUserInfo.ID,
|
||||
Provider: externalUserInfo.Provider,
|
||||
ExternalID: externalUserInfo.ExternalID,
|
||||
MetaInfo: externalUserInfo.MetaInfo,
|
||||
}
|
||||
err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (us *UserExternalLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User,
|
||||
externalUserInfo *schema.ExternalLoginUserInfoCache) (
|
||||
mailStatus int, err error) {
|
||||
log.Infof("user %s login with external account, try to active email, old status is %d",
|
||||
oldUserInfo.ID, oldUserInfo.MailStatus)
|
||||
|
||||
// try to active user email
|
||||
if oldUserInfo.MailStatus == entity.EmailStatusToBeVerified {
|
||||
err = us.userRepo.UpdateEmailStatus(ctx, oldUserInfo.ID, entity.EmailStatusAvailable)
|
||||
if err != nil {
|
||||
return oldUserInfo.MailStatus, err
|
||||
}
|
||||
}
|
||||
|
||||
// try to update user avatar
|
||||
if len(externalUserInfo.Avatar) > 0 && len(schema.FormatAvatarInfo(oldUserInfo.Avatar, oldUserInfo.EMail)) == 0 {
|
||||
avatarInfo := &schema.AvatarInfo{
|
||||
Type: schema.AvatarTypeCustom,
|
||||
Custom: externalUserInfo.Avatar,
|
||||
}
|
||||
avatar, _ := json.Marshal(avatarInfo)
|
||||
oldUserInfo.Avatar = string(avatar)
|
||||
err = us.userRepo.UpdateInfo(ctx, oldUserInfo)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err = us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil {
|
||||
return oldUserInfo.MailStatus, err
|
||||
}
|
||||
return entity.EmailStatusAvailable, nil
|
||||
}
|
||||
|
||||
// ExternalLoginBindingUserSendEmail Send an email for third-party account login for binding user
|
||||
func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(
|
||||
ctx context.Context, req *schema.ExternalLoginBindingUserSendEmailReq) (
|
||||
resp *schema.ExternalLoginBindingUserSendEmailResp, err error) {
|
||||
siteGeneral, err := us.siteInfoCommonService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp = &schema.ExternalLoginBindingUserSendEmailResp{}
|
||||
externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, req.BindingKey)
|
||||
if err != nil || len(externalLoginInfo.ExternalID) == 0 {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
if len(externalLoginInfo.Email) > 0 {
|
||||
log.Warnf("the binding email has been sent %s", req.BindingKey)
|
||||
return &schema.ExternalLoginBindingUserSendEmailResp{}, nil
|
||||
}
|
||||
|
||||
userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist && !req.Must {
|
||||
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)
|
||||
}
|
||||
}
|
||||
err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, req.BindingKey, externalLoginInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// send bind confirmation email
|
||||
data := &schema.EmailCodeContent{
|
||||
SourceType: schema.BindingSourceType,
|
||||
Email: req.Email,
|
||||
UserID: userInfo.ID,
|
||||
BindingKey: req.BindingKey,
|
||||
}
|
||||
code := uuid.NewString()
|
||||
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", siteGeneral.SiteUrl, code)
|
||||
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ExternalLoginBindingUser
|
||||
// The user clicks on the email link of the bound account and requests the API to bind the user officially
|
||||
func (us *UserExternalLoginService) ExternalLoginBindingUser(
|
||||
ctx context.Context, bindingKey string, oldUserInfo *entity.User) (err error) {
|
||||
externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, bindingKey)
|
||||
if err != nil || len(externalLoginInfo.ExternalID) == 0 {
|
||||
return errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
return us.bindOldUser(ctx, externalLoginInfo, oldUserInfo)
|
||||
}
|
||||
|
||||
// GetExternalLoginUserInfoList get external login user info list
|
||||
func (us *UserExternalLoginService) GetExternalLoginUserInfoList(
|
||||
ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error) {
|
||||
return us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID)
|
||||
}
|
||||
|
||||
// ExternalLoginUnbinding external login unbinding
|
||||
func (us *UserExternalLoginService) ExternalLoginUnbinding(
|
||||
ctx context.Context, req *schema.ExternalLoginUnbindingReq) (resp any, err error) {
|
||||
// If user has only one external login and never set password, he can't unbind it.
|
||||
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
if len(userInfo.Pass) == 0 {
|
||||
loginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(loginList) <= 1 {
|
||||
return schema.ErrTypeToast, errors.BadRequest(reason.UserExternalLoginUnbindingForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, us.userExternalLoginRepo.DeleteUserExternalLogin(ctx, req.UserID, req.ExternalID)
|
||||
}
|
||||
|
||||
// CheckUserStatusInUserCenter check user status in user center
|
||||
func (us *UserExternalLoginService) CheckUserStatusInUserCenter(ctx context.Context, userID string) (
|
||||
valid bool, externalID string, err error) {
|
||||
// If enable user center plugin, user status should be checked by user center
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return true, "", nil
|
||||
}
|
||||
userInfoList, err := us.GetExternalLoginUserInfoList(ctx, userID)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
var thisUcUserInfo *entity.UserExternalLogin
|
||||
for _, t := range userInfoList {
|
||||
if t.Provider == userCenter.Info().SlugName {
|
||||
thisUcUserInfo = t
|
||||
break
|
||||
}
|
||||
}
|
||||
// If this user not login by user center, no need to check user status
|
||||
if thisUcUserInfo == nil {
|
||||
return true, "", nil
|
||||
}
|
||||
userStatus := userCenter.UserStatus(thisUcUserInfo.ExternalID)
|
||||
if userStatus == plugin.UserStatusDeleted {
|
||||
return false, thisUcUserInfo.ExternalID, nil
|
||||
}
|
||||
return true, thisUcUserInfo.ExternalID, nil
|
||||
}
|
||||
@@ -20,7 +20,9 @@ import (
|
||||
"github.com/answerdev/answer/internal/service/service_config"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/internal/service/user_external_login"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
@@ -31,15 +33,16 @@ import (
|
||||
|
||||
// UserService user service
|
||||
type UserService struct {
|
||||
userCommonService *usercommon.UserCommon
|
||||
userRepo usercommon.UserRepo
|
||||
userActivity activity.UserActiveActivityRepo
|
||||
activityRepo activity_common.ActivityRepo
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
emailService *export.EmailService
|
||||
authService *auth.AuthService
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
userRoleService *role.UserRoleRelService
|
||||
userCommonService *usercommon.UserCommon
|
||||
userRepo usercommon.UserRepo
|
||||
userActivity activity.UserActiveActivityRepo
|
||||
activityRepo activity_common.ActivityRepo
|
||||
serviceConfig *service_config.ServiceConfig
|
||||
emailService *export.EmailService
|
||||
authService *auth.AuthService
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService
|
||||
userRoleService *role.UserRoleRelService
|
||||
userExternalLoginService *user_external_login.UserExternalLoginService
|
||||
}
|
||||
|
||||
func NewUserService(userRepo usercommon.UserRepo,
|
||||
@@ -51,22 +54,25 @@ func NewUserService(userRepo usercommon.UserRepo,
|
||||
siteInfoService *siteinfo_common.SiteInfoCommonService,
|
||||
userRoleService *role.UserRoleRelService,
|
||||
userCommonService *usercommon.UserCommon,
|
||||
userExternalLoginService *user_external_login.UserExternalLoginService,
|
||||
) *UserService {
|
||||
return &UserService{
|
||||
userCommonService: userCommonService,
|
||||
userRepo: userRepo,
|
||||
userActivity: userActivity,
|
||||
activityRepo: activityRepo,
|
||||
emailService: emailService,
|
||||
serviceConfig: serviceConfig,
|
||||
authService: authService,
|
||||
siteInfoService: siteInfoService,
|
||||
userRoleService: userRoleService,
|
||||
userCommonService: userCommonService,
|
||||
userRepo: userRepo,
|
||||
userActivity: userActivity,
|
||||
activityRepo: activityRepo,
|
||||
emailService: emailService,
|
||||
serviceConfig: serviceConfig,
|
||||
authService: authService,
|
||||
siteInfoService: siteInfoService,
|
||||
userRoleService: userRoleService,
|
||||
userExternalLoginService: userExternalLoginService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserInfoByUserID get user info by user id
|
||||
func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (resp *schema.GetUserToSetShowResp, err error) {
|
||||
func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (
|
||||
resp *schema.GetUserToSetShowResp, err error) {
|
||||
userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -74,6 +80,9 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
if userInfo.Status == entity.UserStatusDeleted {
|
||||
return nil, errors.Unauthorized(reason.UnauthorizedError)
|
||||
}
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@@ -82,6 +91,7 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
|
||||
resp.GetFromUserEntity(userInfo)
|
||||
resp.AccessToken = token
|
||||
resp.RoleID = roleID
|
||||
resp.HavePassword = len(userInfo.Pass) > 0
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -112,10 +122,17 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
|
||||
if !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {
|
||||
return nil, errors.BadRequest(reason.EmailOrPasswordWrong)
|
||||
}
|
||||
ok, externalID, err := us.userExternalLoginService.CheckUserStatusInUserCenter(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.BadRequest(reason.EmailOrPasswordWrong)
|
||||
}
|
||||
|
||||
err = us.userRepo.UpdateLastLoginDate(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
log.Error("UpdateLastLoginDate", err.Error())
|
||||
log.Errorf("update last login data failed, err: %v", err)
|
||||
}
|
||||
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
|
||||
@@ -130,6 +147,7 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
|
||||
EmailStatus: userInfo.MailStatus,
|
||||
UserStatus: userInfo.Status,
|
||||
RoleID: roleID,
|
||||
ExternalID: externalID,
|
||||
}
|
||||
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
|
||||
if err != nil {
|
||||
@@ -171,42 +189,43 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
|
||||
return nil
|
||||
}
|
||||
|
||||
// UseRePassword
|
||||
func (us *UserService) UseRePassword(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) {
|
||||
// UpdatePasswordWhenForgot update user password when user forgot password
|
||||
func (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) {
|
||||
data := &schema.EmailCodeContent{}
|
||||
err = data.FromJSONString(req.Content)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequest(reason.EmailVerifyURLExpired)
|
||||
return errors.BadRequest(reason.EmailVerifyURLExpired)
|
||||
}
|
||||
|
||||
userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
return errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
enpass, err := us.encryptPassword(ctx, req.Pass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
resp = &schema.GetUserResp{}
|
||||
return resp, nil
|
||||
// When the user changes the password, all the current user's tokens are invalid.
|
||||
us.authService.RemoveUserAllTokens(ctx, userInfo.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, request *schema.UserModifyPassWordRequest) (bool, error) {
|
||||
userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
|
||||
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, req *schema.UserModifyPasswordReq) (bool, error) {
|
||||
userInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !has {
|
||||
return false, fmt.Errorf("user does not exist")
|
||||
return false, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
|
||||
isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
|
||||
if !isPass {
|
||||
return false, nil
|
||||
}
|
||||
@@ -215,33 +234,56 @@ func (us *UserService) UserModifyPassWordVerification(ctx context.Context, reque
|
||||
}
|
||||
|
||||
// UserModifyPassword user modify password
|
||||
func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.UserModifyPassWordRequest) error {
|
||||
enpass, err := us.encryptPassword(ctx, request.Pass)
|
||||
func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error {
|
||||
enpass, err := us.encryptPassword(ctx, req.Pass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
|
||||
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return fmt.Errorf("user does not exist")
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
|
||||
|
||||
isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
|
||||
if !isPass {
|
||||
return fmt.Errorf("the old password verification failed")
|
||||
return errors.BadRequest(reason.OldPasswordVerificationFailed)
|
||||
}
|
||||
err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
us.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateInfo update user info
|
||||
func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (
|
||||
errFields []*validator.FormErrorField, err error) {
|
||||
if len(req.Username) > 0 {
|
||||
siteUsers, err := us.siteInfoService.GetSiteUsers(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if siteUsers.AllowUpdateUsername && len(req.Username) > 0 {
|
||||
if checker.IsInvalidUsername(req.Username) {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
if checker.IsReservedUsername(req.Username) {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
|
||||
userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -253,31 +295,57 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameDuplicate)
|
||||
}
|
||||
if checker.IsReservedUsername(req.Username) {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
avatar, err := json.Marshal(req.Avatar)
|
||||
|
||||
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequest(reason.UserSetAvatar).WithError(err).WithStack()
|
||||
return nil, err
|
||||
}
|
||||
userInfo := entity.User{}
|
||||
userInfo.ID = req.UserID
|
||||
userInfo.Avatar = string(avatar)
|
||||
userInfo.DisplayName = req.DisplayName
|
||||
userInfo.Bio = req.Bio
|
||||
userInfo.BioHTML = req.BioHTML
|
||||
userInfo.Location = req.Location
|
||||
userInfo.Website = req.Website
|
||||
userInfo.Username = req.Username
|
||||
err = us.userRepo.UpdateInfo(ctx, &userInfo)
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
|
||||
cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req, siteUsers)
|
||||
err = us.userRepo.UpdateInfo(ctx, cond)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (us *UserService) formatUserInfoForUpdateInfo(
|
||||
oldUserInfo *entity.User, req *schema.UpdateInfoRequest, siteUsersConf *schema.SiteUsersResp) *entity.User {
|
||||
avatar, _ := json.Marshal(req.Avatar)
|
||||
|
||||
userInfo := &entity.User{}
|
||||
userInfo.DisplayName = oldUserInfo.DisplayName
|
||||
userInfo.Username = oldUserInfo.Username
|
||||
userInfo.Avatar = oldUserInfo.Avatar
|
||||
userInfo.Bio = oldUserInfo.Bio
|
||||
userInfo.BioHTML = oldUserInfo.BioHTML
|
||||
userInfo.Website = oldUserInfo.Website
|
||||
userInfo.Location = oldUserInfo.Location
|
||||
userInfo.ID = req.UserID
|
||||
|
||||
if len(req.DisplayName) > 0 && siteUsersConf.AllowUpdateDisplayName {
|
||||
userInfo.DisplayName = req.DisplayName
|
||||
}
|
||||
if len(req.Username) > 0 && siteUsersConf.AllowUpdateUsername {
|
||||
userInfo.Username = req.Username
|
||||
}
|
||||
if len(avatar) > 0 && siteUsersConf.AllowUpdateAvatar {
|
||||
userInfo.Avatar = string(avatar)
|
||||
}
|
||||
if siteUsersConf.AllowUpdateBio {
|
||||
userInfo.Bio = req.Bio
|
||||
userInfo.BioHTML = req.BioHTML
|
||||
}
|
||||
if siteUsersConf.AllowUpdateWebsite {
|
||||
userInfo.Website = req.Website
|
||||
}
|
||||
if siteUsersConf.AllowUpdateLocation {
|
||||
userInfo.Location = req.Location
|
||||
}
|
||||
return userInfo
|
||||
}
|
||||
|
||||
func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, error) {
|
||||
_, has, err := us.userRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
@@ -435,50 +503,48 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
|
||||
if !has {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
userInfo.MailStatus = entity.EmailStatusAvailable
|
||||
err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if userInfo.MailStatus == entity.EmailStatusToBeVerified {
|
||||
userInfo.MailStatus = entity.EmailStatusAvailable
|
||||
err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
|
||||
// In the case of three-party login, the associated users are bound
|
||||
if len(data.BindingKey) > 0 {
|
||||
err = us.userExternalLoginService.ExternalLoginBindingUser(ctx, data.BindingKey, userInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
accessToken, userCacheInfo, err := us.userCommonService.CacheLoginUserInfo(
|
||||
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, "")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = &schema.GetUserResp{}
|
||||
resp.GetFromUserEntity(userInfo)
|
||||
userCacheInfo := &entity.UserCacheInfo{
|
||||
UserID: userInfo.ID,
|
||||
EmailStatus: userInfo.MailStatus,
|
||||
UserStatus: userInfo.Status,
|
||||
RoleID: roleID,
|
||||
}
|
||||
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.AccessToken = accessToken
|
||||
// User verified email will update user email status. So user status cache should be updated.
|
||||
if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.RoleID = userCacheInfo.RoleID
|
||||
if resp.RoleID == role.RoleAdminID {
|
||||
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// verifyPassword
|
||||
// Compare whether the password is correct
|
||||
func (us *UserService) verifyPassword(ctx context.Context, LoginPass, UserPass string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(UserPass), []byte(LoginPass))
|
||||
func (us *UserService) verifyPassword(ctx context.Context, loginPass, userPass string) bool {
|
||||
if len(loginPass) == 0 && len(userPass) == 0 {
|
||||
return true
|
||||
}
|
||||
err := bcrypt.CompareHashAndPassword([]byte(userPass), []byte(loginPass))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -501,6 +567,15 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
|
||||
// If user's email already verified, then must verify password first.
|
||||
if userInfo.MailStatus == entity.EmailStatusAvailable && !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {
|
||||
resp = append(resp, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed),
|
||||
})
|
||||
return resp, errors.BadRequest(reason.OldPasswordVerificationFailed)
|
||||
}
|
||||
|
||||
_, exist, err = us.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -664,6 +739,9 @@ func (us *UserService) UserUnsubscribeEmailNotification(
|
||||
|
||||
func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int,
|
||||
userIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) {
|
||||
if plugin.RankAgentEnabled() {
|
||||
return make([]*entity.ActivityUserRankStat, 0), make([]string, 0), nil
|
||||
}
|
||||
rankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -683,6 +761,9 @@ func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, e
|
||||
|
||||
func (us *UserService) getActivityUserVoteStat(ctx context.Context, startTime, endTime time.Time, limit int,
|
||||
userIDExist map[string]bool) (voteStat []*entity.ActivityUserVoteStat, userIDs []string, err error) {
|
||||
if plugin.RankAgentEnabled() {
|
||||
return make([]*entity.ActivityUserVoteStat, 0), make([]string, 0), nil
|
||||
}
|
||||
voteStat, err = us.activityRepo.GetUsersWhoHasVoteMost(ctx, startTime, endTime, limit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -63,12 +63,12 @@ func NewVoteService(
|
||||
}
|
||||
|
||||
// VoteUp vote up
|
||||
func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
|
||||
func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
|
||||
voteResp = &schema.VoteResp{}
|
||||
|
||||
var objectUserID string
|
||||
|
||||
objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID)
|
||||
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -80,19 +80,19 @@ func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteRes
|
||||
}
|
||||
|
||||
if dto.IsCancel {
|
||||
return as.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
} else {
|
||||
return as.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
}
|
||||
}
|
||||
|
||||
// VoteDown vote down
|
||||
func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
|
||||
func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
|
||||
voteResp = &schema.VoteResp{}
|
||||
|
||||
var objectUserID string
|
||||
|
||||
objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID)
|
||||
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -104,9 +104,9 @@ func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteR
|
||||
}
|
||||
|
||||
if dto.IsCancel {
|
||||
return as.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
} else {
|
||||
return as.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package checker
|
||||
|
||||
import "strings"
|
||||
|
||||
func EmailInAllowEmailDomain(email string, allowEmailDomains []string) bool {
|
||||
if len(allowEmailDomains) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, domain := range allowEmailDomains {
|
||||
if strings.HasSuffix(email, domain) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -38,6 +38,7 @@ func Markdown2HTML(source string) string {
|
||||
filter.RequireNoFollowOnLinks(false)
|
||||
filter.RequireParseableURLs(false)
|
||||
filter.RequireNoFollowOnFullyQualifiedLinks(false)
|
||||
filter.AllowElements("kbd")
|
||||
html = filter.Sanitize(html)
|
||||
return html
|
||||
}
|
||||
@@ -46,7 +47,7 @@ func Markdown2HTML(source string) string {
|
||||
func Markdown2BasicHTML(source string) string {
|
||||
content := Markdown2HTML(source)
|
||||
filter := bluemonday.NewPolicy()
|
||||
filter.AllowElements("p", "b", "br")
|
||||
filter.AllowElements("p", "b", "br", "strong", "em")
|
||||
filter.AllowAttrs("src").OnElements("img")
|
||||
filter.AddSpaceWhenStrippingTag(true)
|
||||
content = filter.Sanitize(content)
|
||||
@@ -87,7 +88,11 @@ func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, n
|
||||
l := n.Segments.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
segment := n.Segments.At(i)
|
||||
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
|
||||
if string(source[segment.Start:segment.Stop]) == "<kbd>" || string(source[segment.Start:segment.Stop]) == "</kbd>" {
|
||||
_, _ = w.Write(segment.Value(source))
|
||||
} else {
|
||||
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
|
||||
}
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package random
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func UsernameSuffix() string {
|
||||
bytes := make([]byte, 2)
|
||||
_, _ = rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
func Username() string {
|
||||
bytes := make([]byte, 6)
|
||||
_, _ = rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user