mirror of https://gitee.com/answerdev/answer.git
Merge branch 'beta/1.1.0' into beta.2/1.1.0
# Conflicts: # internal/migrations/migrations.go # internal/schema/user_schema.go # ui/src/common/interface.ts
This commit is contained in:
commit
dacc27e373
|
@ -25,9 +25,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/
|
||||
|
|
11
Dockerfile
11
Dockerfile
|
@ -13,7 +13,8 @@ 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
|
||||
|
@ -27,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
|
||||
|
|
4
Makefile
4
Makefile
|
@ -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
|
||||
|
|
|
@ -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 {
|
|
@ -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)
|
||||
|
@ -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,9 @@ 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)
|
||||
pluginAPIRouter := router.NewPluginAPIRouter(connectorController)
|
||||
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() {
|
580
docs/docs.go
580
docs/docs.go
|
@ -183,6 +183,185 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/plugin/config": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get plugin config",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AdminPlugin"
|
||||
],
|
||||
"summary": "get plugin config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "plugin_slug_name",
|
||||
"name": "plugin_slug_name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/schema.GetPluginConfigResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "update plugin config",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AdminPlugin"
|
||||
],
|
||||
"summary": "update plugin config",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "UpdatePluginConfigReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.UpdatePluginConfigReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/plugin/status": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "update plugin status",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AdminPlugin"
|
||||
],
|
||||
"summary": "update plugin status",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "UpdatePluginStatusReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.UpdatePluginStatusReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/plugins": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get plugin list",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AdminPlugin"
|
||||
],
|
||||
"summary": "get plugin list",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "status: active/inactive",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "have config",
|
||||
"name": "have_config",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.GetPluginListResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/question/page": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -2117,6 +2296,171 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/connector/binding/email": {
|
||||
"post": {
|
||||
"description": "external login binding user send email",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"PluginConnector"
|
||||
],
|
||||
"summary": "external login binding user send email",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "external login binding user send email",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/connector/info": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get all enabled connectors",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"PluginConnector"
|
||||
],
|
||||
"summary": "get all enabled connectors",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.ConnectorInfoResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/connector/user/info": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get all connectors info about user",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"PluginConnector"
|
||||
],
|
||||
"summary": "get all connectors info about user",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.ConnectorUserInfoResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/connector/user/unbinding": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "unbind external user login",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"PluginConnector"
|
||||
],
|
||||
"summary": "unbind external user login",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ExternalLoginUnbindingReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.ExternalLoginUnbindingReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/file": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -5844,6 +6188,142 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.ConfigField": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.ConfigFieldOption"
|
||||
}
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"ui_options": {
|
||||
"$ref": "#/definitions/schema.ConfigFieldUIOptions"
|
||||
},
|
||||
"value": {}
|
||||
}
|
||||
},
|
||||
"schema.ConfigFieldOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ConfigFieldUIOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"rows": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ConnectorInfoResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ConnectorUserInfoResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binding": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ExternalLoginBindingUserSendEmailReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"binding_key",
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"binding_key": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
},
|
||||
"must": {
|
||||
"description": "If must is true, whatever email if exists, try to bind user.\nIf must is false, when email exist, will only be prompted with a warning.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ExternalLoginBindingUserSendEmailResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"email_exist_and_must_be_confirmed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ExternalLoginUnbindingReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"external_id"
|
||||
],
|
||||
"properties": {
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.FollowReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -6120,6 +6600,55 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.GetPluginConfigResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.ConfigField"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"slug_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.GetPluginListResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"have_config": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"slug_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.GetRankPersonalWithPageResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -6556,6 +7085,10 @@ const docTemplate = `{
|
|||
"description": "follow count",
|
||||
"type": "integer"
|
||||
},
|
||||
"have_password": {
|
||||
"description": "user have password",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"description": "user id",
|
||||
"type": "string"
|
||||
|
@ -6656,6 +7189,9 @@ const docTemplate = `{
|
|||
"description": "follow count",
|
||||
"type": "integer"
|
||||
},
|
||||
"have_password": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"description": "user id",
|
||||
"type": "string"
|
||||
|
@ -7757,6 +8293,37 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdatePluginConfigReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"plugin_slug_name"
|
||||
],
|
||||
"properties": {
|
||||
"config_fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"plugin_slug_name": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdatePluginStatusReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"plugin_slug_name"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"plugin_slug_name": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdateSMTPConfigReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8021,14 +8588,19 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.UserModifyPassWordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pass"
|
||||
],
|
||||
"properties": {
|
||||
"old_pass": {
|
||||
"description": "old password",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
"minLength": 8
|
||||
},
|
||||
"pass": {
|
||||
"description": "password",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
"minLength": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -171,6 +171,185 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/plugin/config": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get plugin config",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AdminPlugin"
|
||||
],
|
||||
"summary": "get plugin config",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "plugin_slug_name",
|
||||
"name": "plugin_slug_name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/schema.GetPluginConfigResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "update plugin config",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AdminPlugin"
|
||||
],
|
||||
"summary": "update plugin config",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "UpdatePluginConfigReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.UpdatePluginConfigReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/plugin/status": {
|
||||
"put": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "update plugin status",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AdminPlugin"
|
||||
],
|
||||
"summary": "update plugin status",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "UpdatePluginStatusReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.UpdatePluginStatusReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/plugins": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get plugin list",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AdminPlugin"
|
||||
],
|
||||
"summary": "get plugin list",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "status: active/inactive",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "have config",
|
||||
"name": "have_config",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.GetPluginListResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/admin/api/question/page": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -2105,6 +2284,171 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/connector/binding/email": {
|
||||
"post": {
|
||||
"description": "external login binding user send email",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"PluginConnector"
|
||||
],
|
||||
"summary": "external login binding user send email",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "external login binding user send email",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/schema.ExternalLoginBindingUserSendEmailResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/connector/info": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get all enabled connectors",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"PluginConnector"
|
||||
],
|
||||
"summary": "get all enabled connectors",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.ConnectorInfoResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/connector/user/info": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "get all connectors info about user",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"PluginConnector"
|
||||
],
|
||||
"summary": "get all connectors info about user",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.ConnectorUserInfoResp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/connector/user/unbinding": {
|
||||
"delete": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "unbind external user login",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"PluginConnector"
|
||||
],
|
||||
"summary": "unbind external user login",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "ExternalLoginUnbindingReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.ExternalLoginUnbindingReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/file": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -5832,6 +6176,142 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.ConfigField": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.ConfigFieldOption"
|
||||
}
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"ui_options": {
|
||||
"$ref": "#/definitions/schema.ConfigFieldUIOptions"
|
||||
},
|
||||
"value": {}
|
||||
}
|
||||
},
|
||||
"schema.ConfigFieldOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ConfigFieldUIOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"rows": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ConnectorInfoResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ConnectorUserInfoResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"binding": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"external_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ExternalLoginBindingUserSendEmailReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"binding_key",
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"binding_key": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"maxLength": 512
|
||||
},
|
||||
"must": {
|
||||
"description": "If must is true, whatever email if exists, try to bind user.\nIf must is false, when email exist, will only be prompted with a warning.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ExternalLoginBindingUserSendEmailResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"email_exist_and_must_be_confirmed": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ExternalLoginUnbindingReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"external_id"
|
||||
],
|
||||
"properties": {
|
||||
"external_id": {
|
||||
"type": "string",
|
||||
"maxLength": 128
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.FollowReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -6108,6 +6588,55 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.GetPluginConfigResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config_fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/schema.ConfigField"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"slug_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.GetPluginListResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"have_config": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"slug_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.GetRankPersonalWithPageResp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -6544,6 +7073,10 @@
|
|||
"description": "follow count",
|
||||
"type": "integer"
|
||||
},
|
||||
"have_password": {
|
||||
"description": "user have password",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"description": "user id",
|
||||
"type": "string"
|
||||
|
@ -6644,6 +7177,9 @@
|
|||
"description": "follow count",
|
||||
"type": "integer"
|
||||
},
|
||||
"have_password": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"description": "user id",
|
||||
"type": "string"
|
||||
|
@ -7745,6 +8281,37 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdatePluginConfigReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"plugin_slug_name"
|
||||
],
|
||||
"properties": {
|
||||
"config_fields": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"plugin_slug_name": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdatePluginStatusReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"plugin_slug_name"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"plugin_slug_name": {
|
||||
"type": "string",
|
||||
"maxLength": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.UpdateSMTPConfigReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8009,14 +8576,19 @@
|
|||
},
|
||||
"schema.UserModifyPassWordRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pass"
|
||||
],
|
||||
"properties": {
|
||||
"old_pass": {
|
||||
"description": "old password",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
"minLength": 8
|
||||
},
|
||||
"pass": {
|
||||
"description": "password",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
"minLength": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -305,6 +305,98 @@ 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:
|
||||
input_type:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
placeholder:
|
||||
type: string
|
||||
rows:
|
||||
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 +599,38 @@ 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.GetRankPersonalWithPageResp:
|
||||
properties:
|
||||
answer_id:
|
||||
|
@ -820,6 +944,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 +1021,8 @@ definitions:
|
|||
follow_count:
|
||||
description: follow count
|
||||
type: integer
|
||||
have_password:
|
||||
type: boolean
|
||||
id:
|
||||
description: user id
|
||||
type: string
|
||||
|
@ -1668,6 +1797,27 @@ definitions:
|
|||
required:
|
||||
- display_name
|
||||
type: object
|
||||
schema.UpdatePluginConfigReq:
|
||||
properties:
|
||||
config_fields:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
plugin_slug_name:
|
||||
maxLength: 100
|
||||
type: string
|
||||
required:
|
||||
- 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.UpdateSMTPConfigReq:
|
||||
properties:
|
||||
encryption:
|
||||
|
@ -1859,11 +2009,15 @@ definitions:
|
|||
schema.UserModifyPassWordRequest:
|
||||
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 +2258,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:
|
||||
|
@ -3263,6 +3523,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:
|
||||
|
|
4
go.mod
4
go.mod
|
@ -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
|
||||
|
|
8
go.sum
8
go.sum
|
@ -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=
|
||||
|
|
|
@ -155,6 +155,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.
|
||||
|
|
|
@ -1054,7 +1054,13 @@ ui:
|
|||
themes: 主题
|
||||
css-html: CSS/HTML
|
||||
login: 登录
|
||||
plugins: 插件
|
||||
installed_plugins: 插件列表
|
||||
website_welcome: 欢迎来到 {{site_name}}
|
||||
plugins:
|
||||
oauth:
|
||||
connect: 连接到 {{ auth_name }}
|
||||
remove: 解绑 {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: 后台管理
|
||||
|
@ -1367,6 +1373,23 @@ ui:
|
|||
title: 非公开的
|
||||
label: 需要登录
|
||||
text: 只有登录用户才能访问这个社区。
|
||||
installed_plugins:
|
||||
title: 插件列表
|
||||
filter:
|
||||
all: 全部
|
||||
active: 启用
|
||||
inactive: 未启用
|
||||
outdated: 已过期
|
||||
plugins:
|
||||
label: 插件
|
||||
text: 选择一个插件
|
||||
name: 插件名称
|
||||
version: 插件版本
|
||||
status: 状态
|
||||
action: 操作
|
||||
deactivate: 停用
|
||||
activate: 启用
|
||||
settings: 设置
|
||||
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
|
||||
)
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -14,38 +14,38 @@ const (
|
|||
)
|
||||
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
|
@ -70,4 +70,5 @@ const (
|
|||
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"
|
||||
)
|
||||
|
|
|
@ -20,6 +20,7 @@ func NewHTTPServer(debug bool,
|
|||
authUserMiddleware *middleware.AuthUserMiddleware,
|
||||
avatarMiddleware *middleware.AvatarMiddleware,
|
||||
templateRouter *router.TemplateRouter,
|
||||
pluginAPIRouter *router.PluginAPIRouter,
|
||||
) *gin.Engine {
|
||||
|
||||
if debug {
|
||||
|
@ -62,5 +63,9 @@ func NewHTTPServer(debug bool,
|
|||
answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1)
|
||||
|
||||
templateRouter.RegisterTemplateRouter(rootGroup)
|
||||
|
||||
// plugin routes
|
||||
pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1)
|
||||
pluginAPIRouter.RegisterAuthConnectorRouter(authV1)
|
||||
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/oauth?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,5 @@ var ProviderSetController = wire.NewSet(
|
|||
NewUploadController,
|
||||
NewActivityController,
|
||||
NewTemplateController,
|
||||
NewConnectorController,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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{
|
||||
|
|
|
@ -58,6 +58,7 @@ 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),
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
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 {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); 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 x.Sync(new(entity.PluginConfig), new(entity.UserExternalLogin))
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/config"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// userRepo user repository
|
||||
|
@ -28,10 +29,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
|
||||
}
|
||||
|
||||
|
|
|
@ -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, externalID string) (
|
||||
userInfo *entity.UserExternalLogin, exist bool, err error) {
|
||||
userInfo = &entity.UserExternalLogin{}
|
||||
exist, err = ur.data.DB.Where("external_id = ?", externalID).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
|
||||
}
|
|
@ -31,6 +31,7 @@ type AnswerAPIRouter struct {
|
|||
uploadController *controller.UploadController
|
||||
activityController *controller.ActivityController
|
||||
roleController *controller_admin.RoleController
|
||||
pluginController *controller_admin.PluginController
|
||||
}
|
||||
|
||||
func NewAnswerAPIRouter(
|
||||
|
@ -58,6 +59,7 @@ func NewAnswerAPIRouter(
|
|||
uploadController *controller.UploadController,
|
||||
activityController *controller.ActivityController,
|
||||
roleController *controller_admin.RoleController,
|
||||
pluginController *controller_admin.PluginController,
|
||||
) *AnswerAPIRouter {
|
||||
return &AnswerAPIRouter{
|
||||
langController: langController,
|
||||
|
@ -84,6 +86,7 @@ func NewAnswerAPIRouter(
|
|||
uploadController: uploadController,
|
||||
activityController: activityController,
|
||||
roleController: roleController,
|
||||
pluginController: pluginController,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,4 +285,10 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
// 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,32 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/controller"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type PluginAPIRouter struct {
|
||||
connectorController *controller.ConnectorController
|
||||
}
|
||||
|
||||
func NewPluginAPIRouter(
|
||||
connectorController *controller.ConnectorController,
|
||||
) *PluginAPIRouter {
|
||||
return &PluginAPIRouter{
|
||||
connectorController: connectorController,
|
||||
}
|
||||
}
|
||||
|
||||
func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) {
|
||||
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)
|
||||
}
|
||||
|
||||
func (pr *PluginAPIRouter) RegisterAuthConnectorRouter(r *gin.RouterGroup) {
|
||||
connectorController := pr.connectorController
|
||||
r.GET("/connector/user/info", connectorController.ConnectorsUserInfo)
|
||||
r.DELETE("/connector/user/unbinding", connectorController.ExternalLoginUnbinding)
|
||||
}
|
|
@ -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,100 @@
|
|||
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),
|
||||
},
|
||||
}
|
||||
configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx)
|
||||
configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type ConfigFieldOption struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
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,58 @@
|
|||
package schema
|
||||
|
||||
// UserExternalLoginResp user external login resp
|
||||
type UserExternalLoginResp struct {
|
||||
BindingKey string `json:"binding_key"`
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ExternalLoginUnbindingReq external login unbinding user
|
||||
type ExternalLoginUnbindingReq struct {
|
||||
ExternalID string `validate:"required,gt=0,lte=128" json:"external_id"`
|
||||
UserID string `json:"-"`
|
||||
}
|
|
@ -112,6 +112,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 {
|
||||
|
@ -128,9 +134,9 @@ 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 ""
|
||||
|
@ -265,8 +271,8 @@ func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err er
|
|||
}
|
||||
|
||||
type UserModifyPasswordReq struct {
|
||||
OldPass string `json:"old_pass"`
|
||||
Pass string `json:"pass"`
|
||||
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:"-"`
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,6 @@ var ProviderSetService = wire.NewSet(
|
|||
role.NewRoleService,
|
||||
role.NewUserRoleRelService,
|
||||
role.NewRolePowerRelService,
|
||||
user_external_login.NewUserExternalLoginService,
|
||||
plugin_common.NewPluginCommonService,
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,33 @@ 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) (
|
||||
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,
|
||||
}
|
||||
|
||||
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,313 @@
|
|||
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/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, 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.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 {
|
||||
newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
|
||||
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status)
|
||||
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)
|
||||
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()
|
||||
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.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)
|
||||
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)
|
||||
}
|
|
@ -20,6 +20,7 @@ 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/google/uuid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
|
@ -31,15 +32,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 +53,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
|
||||
|
@ -440,43 +445,38 @@ 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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package plugin
|
||||
|
||||
// Info presents the plugin information
|
||||
type Info struct {
|
||||
Name Translator
|
||||
SlugName string
|
||||
Description Translator
|
||||
Author string
|
||||
Version string
|
||||
Link string
|
||||
}
|
||||
|
||||
// Base is the base plugin
|
||||
type Base interface {
|
||||
// Info returns the plugin information
|
||||
Info() Info
|
||||
}
|
||||
|
||||
var (
|
||||
// CallBase is a function that calls all registered base plugins
|
||||
CallBase,
|
||||
registerBase = MakePlugin[Base](true)
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cache interface {
|
||||
Base
|
||||
|
||||
GetString(ctx context.Context, key string) (string, error)
|
||||
SetString(ctx context.Context, key, value string, ttl time.Duration) error
|
||||
GetInt64(ctx context.Context, key string) (int64, error)
|
||||
SetInt64(ctx context.Context, key string, value int64, ttl time.Duration) error
|
||||
Del(ctx context.Context, key string) error
|
||||
Flush(ctx context.Context) error
|
||||
}
|
||||
|
||||
var (
|
||||
// CallCache is a function that calls all registered cache
|
||||
CallCache,
|
||||
registerCache = MakePlugin[Cache](false)
|
||||
)
|
|
@ -0,0 +1,74 @@
|
|||
package plugin
|
||||
|
||||
type ConfigType string
|
||||
type InputType string
|
||||
|
||||
const (
|
||||
ConfigTypeInput ConfigType = "input"
|
||||
ConfigTypeTextarea ConfigType = "textarea"
|
||||
ConfigTypeCheckbox ConfigType = "checkbox"
|
||||
ConfigTypeRadio ConfigType = "radio"
|
||||
ConfigTypeSelect ConfigType = "select"
|
||||
ConfigTypeUpload ConfigType = "upload"
|
||||
ConfigTypeTimezone ConfigType = "timezone"
|
||||
ConfigTypeSwitch ConfigType = "switch"
|
||||
)
|
||||
|
||||
const (
|
||||
InputTypeText InputType = "text"
|
||||
InputTypeColor InputType = "color"
|
||||
InputTypeDate InputType = "date"
|
||||
InputTypeDatetime InputType = "datetime-local"
|
||||
InputTypeEmail InputType = "email"
|
||||
InputTypeMonth InputType = "month"
|
||||
InputTypeNumber InputType = "number"
|
||||
InputTypePassword InputType = "password"
|
||||
InputTypeRange InputType = "range"
|
||||
InputTypeSearch InputType = "search"
|
||||
InputTypeTel InputType = "tel"
|
||||
InputTypeTime InputType = "time"
|
||||
InputTypeUrl InputType = "url"
|
||||
InputTypeWeek InputType = "week"
|
||||
)
|
||||
|
||||
type ConfigField struct {
|
||||
Name string `json:"name"`
|
||||
Type ConfigType `json:"type"`
|
||||
Title Translator `json:"title"`
|
||||
Description Translator `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
Value any `json:"value"`
|
||||
UIOptions ConfigFieldUIOptions `json:"ui_options"`
|
||||
Options []ConfigFieldOption `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigFieldUIOptions struct {
|
||||
Placeholder Translator `json:"placeholder,omitempty"`
|
||||
Rows string `json:"rows,omitempty"`
|
||||
InputType InputType `json:"input_type,omitempty"`
|
||||
Label Translator `json:"label,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigFieldOption struct {
|
||||
Label Translator `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Config interface {
|
||||
Base
|
||||
|
||||
// ConfigFields returns the list of config fields
|
||||
ConfigFields() []ConfigField
|
||||
|
||||
// ConfigReceiver receives the config data, it calls when the config is saved or initialized.
|
||||
// We recommend to unmarshal the data to a struct, and then use the struct to do something.
|
||||
// The config is encoded in JSON format.
|
||||
// It depends on the definition of ConfigFields.
|
||||
ConfigReceiver(config []byte) error
|
||||
}
|
||||
|
||||
var (
|
||||
// CallConfig is a function that calls all registered config plugins
|
||||
CallConfig,
|
||||
registerConfig = MakePlugin[Config](true)
|
||||
)
|
|
@ -0,0 +1,49 @@
|
|||
package plugin
|
||||
|
||||
type Connector interface {
|
||||
Base
|
||||
|
||||
// ConnectorLogoSVG presents the logo in svg format
|
||||
ConnectorLogoSVG() string
|
||||
|
||||
// ConnectorName presents the name of the connector
|
||||
// e.g. Facebook, Twitter, Instagram
|
||||
ConnectorName() Translator
|
||||
|
||||
// ConnectorSlugName presents the slug name of the connector
|
||||
// Please use lowercase and hyphen as the separator
|
||||
// e.g. facebook, twitter, instagram
|
||||
ConnectorSlugName() string
|
||||
|
||||
// ConnectorSender presents the sender of the connector
|
||||
// It handles the start endpoint of the connector
|
||||
// receiverURL is the whole URL of the receiver
|
||||
ConnectorSender(ctx *GinContext, receiverURL string) (redirectURL string)
|
||||
|
||||
// ConnectorReceiver presents the receiver of the connector
|
||||
// It handles the callback endpoint of the connector, and returns the
|
||||
ConnectorReceiver(ctx *GinContext, receiverURL string) (userInfo ExternalLoginUserInfo, err error)
|
||||
}
|
||||
|
||||
// ExternalLoginUserInfo external login user info
|
||||
type ExternalLoginUserInfo struct {
|
||||
// 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
|
||||
// IMPORTANT: The email must have been verified. If the plugin can't guarantee the email is verified, please leave it empty.
|
||||
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
|
||||
}
|
||||
|
||||
var (
|
||||
// CallConnector is a function that calls all registered connectors
|
||||
CallConnector,
|
||||
registerConnector = MakePlugin[Connector](false)
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
package plugin
|
||||
|
||||
type Filter interface {
|
||||
Base
|
||||
FilterText(text string) (err error)
|
||||
}
|
||||
|
||||
var (
|
||||
// CallFilter is a function that calls all registered parsers
|
||||
CallFilter,
|
||||
registerFilter = MakePlugin[Filter](false)
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
package plugin
|
||||
|
||||
type Parser interface {
|
||||
Base
|
||||
Parse(text string) (string, error)
|
||||
}
|
||||
|
||||
var (
|
||||
// CallParser is a function that calls all registered parsers
|
||||
CallParser,
|
||||
registerParser = MakePlugin[Parser](false)
|
||||
)
|
|
@ -0,0 +1,148 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GinContext is a wrapper of gin.Context
|
||||
// We export it to make it easy to use in plugins
|
||||
type GinContext = gin.Context
|
||||
|
||||
// StatusManager is a manager that manages the status of plugins
|
||||
// Init Plugins:
|
||||
// json.Unmarshal([]byte(`{"plugin1": true, "plugin2": false}`), &plugin.StatusManager)
|
||||
// Dump Status:
|
||||
// json.Marshal(plugin.StatusManager)
|
||||
var StatusManager = statusManager{
|
||||
status: make(map[string]bool),
|
||||
}
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(p Base) {
|
||||
registerBase(p)
|
||||
|
||||
if _, ok := p.(Config); ok {
|
||||
registerConfig(p.(Config))
|
||||
}
|
||||
|
||||
if _, ok := p.(Connector); ok {
|
||||
registerConnector(p.(Connector))
|
||||
}
|
||||
|
||||
if _, ok := p.(Parser); ok {
|
||||
registerParser(p.(Parser))
|
||||
}
|
||||
|
||||
if _, ok := p.(Filter); ok {
|
||||
registerFilter(p.(Filter))
|
||||
}
|
||||
|
||||
if _, ok := p.(Storage); ok {
|
||||
registerStorage(p.(Storage))
|
||||
}
|
||||
|
||||
if _, ok := p.(Cache); ok {
|
||||
registerCache(p.(Cache))
|
||||
}
|
||||
}
|
||||
|
||||
type Stack[T Base] struct {
|
||||
plugins []T
|
||||
}
|
||||
|
||||
type RegisterFn[T Base] func(p T)
|
||||
type Caller[T Base] func(p T) error
|
||||
type CallFn[T Base] func(fn Caller[T]) error
|
||||
|
||||
// MakePlugin creates a plugin caller and register stack manager
|
||||
// The parameter super presents if the plugin can be disabled.
|
||||
// It returns a register function and a caller function
|
||||
// The register function is used to register a plugin, it will be called in the plugin's init function
|
||||
// The caller function is used to call all registered plugins
|
||||
func MakePlugin[T Base](super bool) (CallFn[T], RegisterFn[T]) {
|
||||
stack := Stack[T]{}
|
||||
|
||||
call := func(fn Caller[T]) error {
|
||||
for _, p := range stack.plugins {
|
||||
// If the plugin is disabled, skip it
|
||||
if !super && !StatusManager.IsEnabled(p.Info().SlugName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := fn(p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
register := func(p T) {
|
||||
for _, plugin := range stack.plugins {
|
||||
if plugin.Info().SlugName == p.Info().SlugName {
|
||||
panic("plugin " + p.Info().SlugName + " is already registered")
|
||||
}
|
||||
}
|
||||
stack.plugins = append(stack.plugins, p)
|
||||
}
|
||||
|
||||
return call, register
|
||||
}
|
||||
|
||||
type statusManager struct {
|
||||
status map[string]bool
|
||||
}
|
||||
|
||||
func (m *statusManager) Enable(name string, enabled bool) {
|
||||
m.status[name] = enabled
|
||||
}
|
||||
|
||||
func (m *statusManager) IsEnabled(name string) bool {
|
||||
if status, ok := m.status[name]; ok {
|
||||
return status
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (m *statusManager) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m.status)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (m *statusManager) UnmarshalJSON(data []byte) error {
|
||||
return json.Unmarshal(data, &m.status)
|
||||
}
|
||||
|
||||
// Translate translates the key to the current language of the context
|
||||
func Translate(ctx *GinContext, key string) string {
|
||||
return translator.Tr(handler.GetLang(ctx), key)
|
||||
}
|
||||
|
||||
// TranslateFn presents a generator of translated string.
|
||||
// We use it to delegate the translation work outside the plugin.
|
||||
type TranslateFn func(ctx *GinContext) string
|
||||
|
||||
// Translator contains a function that translates the key to the current language of the context
|
||||
type Translator struct {
|
||||
fn TranslateFn
|
||||
}
|
||||
|
||||
// MakeTranslator generates a translator from the key
|
||||
func MakeTranslator(key string) Translator {
|
||||
t := func(ctx *GinContext) string {
|
||||
return Translate(ctx, key)
|
||||
}
|
||||
return Translator{fn: t}
|
||||
}
|
||||
|
||||
// Translate translates the key to the current language of the context
|
||||
func (t Translator) Translate(ctx *GinContext) string {
|
||||
if &t == nil || t.fn == nil {
|
||||
return ""
|
||||
}
|
||||
return t.fn(ctx)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package plugin
|
||||
|
||||
type UploadSource string
|
||||
|
||||
const (
|
||||
UserAvatar UploadSource = "user_avatar"
|
||||
UserPost UploadSource = "user_post"
|
||||
AdminBranding UploadSource = "admin_branding"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultFileTypeCheckMapping = map[UploadSource]map[string]bool{
|
||||
UserAvatar: {
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
},
|
||||
UserPost: {
|
||||
".jpg": true,
|
||||
".jpeg": true,
|
||||
".png": true,
|
||||
},
|
||||
AdminBranding: {
|
||||
".ico": true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type UploadFileResponse struct {
|
||||
// FullURL is the URL that can be used to access the file
|
||||
FullURL string
|
||||
// OriginalError is the error returned by the storage plugin. It is used for debugging.
|
||||
OriginalError error
|
||||
// DisplayErrorMsg is the error message that will be displayed to the user.
|
||||
DisplayErrorMsg Translator
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
Base
|
||||
|
||||
// UploadFile uploads a file to storage.
|
||||
// The file is in the Form of the ctx and the key is "file"
|
||||
UploadFile(ctx *GinContext, source UploadSource) UploadFileResponse
|
||||
}
|
||||
|
||||
var (
|
||||
// CallStorage is a function that calls all registered storage
|
||||
CallStorage,
|
||||
registerStorage = MakePlugin[Storage](false)
|
||||
)
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
plugin_file=./script/plugin_list
|
||||
if [ ! -f "$plugin_file" ]; then
|
||||
echo "plugin_list is not exist"
|
||||
exit 0
|
||||
fi
|
||||
num=0
|
||||
for line in `cat $plugin_file`
|
||||
do
|
||||
account=$line
|
||||
accounts[$num]=$account
|
||||
((num++))
|
||||
done
|
||||
if [ $num -eq 0 ]; then
|
||||
echo "plugin_list is null"
|
||||
exit 0
|
||||
fi
|
||||
cmd="./answer build "
|
||||
for repo in ${accounts[@]}
|
||||
do
|
||||
echo ${repo}
|
||||
cmd=$cmd" --with "${repo}
|
||||
done
|
||||
$cmd
|
||||
if [ ! -f "./new_answer" ]; then
|
||||
echo "new_answer is not exist build failed"
|
||||
exit 0
|
||||
fi
|
||||
rm answer
|
||||
mv new_answer answer
|
||||
./answer plugin
|
|
@ -0,0 +1 @@
|
|||
github.com/answerdev/plugins/connector/basic@latest
|
|
@ -535,6 +535,11 @@ export interface User {
|
|||
avatar: string;
|
||||
}
|
||||
|
||||
export interface QuestionOperationReq {
|
||||
id: string;
|
||||
operation: 'pin' | 'unpin' | 'hide' | 'show';
|
||||
}
|
||||
|
||||
export interface OauthBindEmailReq {
|
||||
binding_key: string;
|
||||
email: string;
|
||||
|
|
Loading…
Reference in New Issue