Merge branch 'beta.2/1.1.0' into feat/1.1.0/sql

This commit is contained in:
aichy126
2023-05-16 16:08:03 +08:00
249 changed files with 12198 additions and 2233 deletions
+8 -3
View File
@@ -4,6 +4,7 @@ LABEL maintainer="mingcheng<mc@sf.com>"
COPY . /answer
WORKDIR /answer
RUN node -v
RUN make install-ui-packages ui && mv ui/build /tmp
# stage2 build the main binary within static resource
@@ -11,12 +12,14 @@ FROM golang:1.19-alpine AS golang-builder
LABEL maintainer="aichy@sf.com"
ARG GOPROXY
# ENV GOPROXY ${GOPROXY:-direct}
ENV GOPROXY=https://goproxy.io,direct
ENV GOPATH /go
ENV GOROOT /usr/local/go
ENV PACKAGE github.com/answerdev/answer
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
ENV ANSWER_MODULE ${BUILD_DIR}
ARG TAGS="sqlite sqlite_unlock_notify"
ENV TAGS "bindata timetzdata $TAGS"
@@ -25,9 +28,11 @@ ARG CGO_EXTRA_CFLAGS
COPY . ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
RUN apk --no-cache add build-base git \
&& make clean build \
&& cp answer /usr/bin/answer
RUN apk --no-cache add build-base git bash \
&& make clean build
RUN chmod 755 answer
RUN ["/bin/bash","-c","script/build_plugin.sh"]
RUN cp answer /usr/bin/answer
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
+1
View File
@@ -26,5 +26,6 @@ tmp
vendor/
/answer-data/
/answer
/new_answer
dist/
+8 -4
View File
@@ -13,12 +13,14 @@ FROM golang:1.19-alpine AS golang-builder
LABEL maintainer="aichy@sf.com"
ARG GOPROXY
ENV GOPROXY ${GOPROXY:-direct}
# ENV GOPROXY ${GOPROXY:-direct}
ENV GOPROXY=https://goproxy.io,direct
ENV GOPATH /go
ENV GOROOT /usr/local/go
ENV PACKAGE github.com/answerdev/answer
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
ENV ANSWER_MODULE ${BUILD_DIR}
ARG TAGS="sqlite sqlite_unlock_notify"
ENV TAGS "bindata timetzdata $TAGS"
@@ -27,9 +29,11 @@ ARG CGO_EXTRA_CFLAGS
COPY . ${BUILD_DIR}
WORKDIR ${BUILD_DIR}
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
RUN apk --no-cache add build-base git \
&& make clean build \
&& cp answer /usr/bin/answer
RUN apk --no-cache add build-base git bash \
&& make clean build
RUN chmod 755 answer
RUN ["/bin/bash","-c","script/build_plugin.sh"]
RUN cp answer /usr/bin/answer
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
+2 -2
View File
@@ -1,13 +1,13 @@
.PHONY: build clean ui
VERSION=1.0.9
VERSION=1.1.0
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
GO_ENV=CGO_ENABLED=0 GO111MODULE=on
Revision=$(shell git rev-parse --short HEAD)
GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X 'main.Revision=$(Revision)' -X 'main.Time=`date`' -extldflags -static"
GO_FLAGS=-ldflags="-X github.com/answerdev/answer/cmd.Version=$(VERSION) -X 'github.com/answerdev/answer/cmd.Revision=$(Revision)' -X 'github.com/answerdev/answer/cmd.Time=`date +%s`' -extldflags -static"
GO=$(GO_ENV) $(shell which go)
build: generate
+2 -62
View File
@@ -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()
}
+47 -3
View File
@@ -1,13 +1,15 @@
package main
package answercmd
import (
"fmt"
"os"
"strings"
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/install"
"github.com/answerdev/answer/internal/migrations"
"github.com/answerdev/answer/plugin"
"github.com/spf13/cobra"
)
@@ -16,6 +18,10 @@ var (
dataDirPath string
// dumpDataPath dump data path
dumpDataPath string
// plugins needed to build in answer application
buildWithPlugins []string
// build output path
buildOutput string
)
func init() {
@@ -25,7 +31,11 @@ func init() {
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd} {
buildCmd.Flags().StringSliceVarP(&buildWithPlugins, "with", "w", []string{}, "plugins needed to build")
buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "build output path")
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd} {
rootCmd.AddCommand(cmd)
}
}
@@ -160,10 +170,44 @@ To run answer, use:
fmt.Println("check environment all done")
},
}
// buildCmd used to build another answer with plugins
buildCmd = &cobra.Command{
Use: "build",
Short: "used to build answer with plugins",
Long: `Build a new Answer with plugins that you need`,
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf("try to build a new answer with plugins:\n%s\n", strings.Join(buildWithPlugins, "\n"))
err := cli.BuildNewAnswer(buildOutput, buildWithPlugins, cli.OriginalAnswerInfo{
Version: Version,
Revision: Revision,
Time: Time,
})
if err != nil {
fmt.Printf("build failed %v", err)
} else {
fmt.Printf("build new answer successfully %s\n", buildOutput)
}
},
}
// pluginCmd prints all plugins packed in the binary
pluginCmd = &cobra.Command{
Use: "plugin",
Short: "prints all plugins packed in the binary",
Long: `prints all plugins packed in the binary`,
Run: func(_ *cobra.Command, _ []string) {
_ = plugin.CallBase(func(base plugin.Base) error {
info := base.Info()
fmt.Printf("%s[%s] made by %s\n", info.SlugName, info.Version, info.Author)
return nil
})
},
}
)
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
// This is called by main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
+75
View File
@@ -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)),
)
}
+1 -1
View File
@@ -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"
+19 -6
View File
@@ -4,7 +4,7 @@
//go:build !wireinject
// +build !wireinject
package main
package answercmd
import (
"github.com/answerdev/answer/internal/base/conf"
@@ -28,6 +28,7 @@ import (
"github.com/answerdev/answer/internal/repo/export"
"github.com/answerdev/answer/internal/repo/meta"
"github.com/answerdev/answer/internal/repo/notification"
"github.com/answerdev/answer/internal/repo/plugin_config"
"github.com/answerdev/answer/internal/repo/question"
"github.com/answerdev/answer/internal/repo/rank"
"github.com/answerdev/answer/internal/repo/reason"
@@ -40,6 +41,7 @@ import (
"github.com/answerdev/answer/internal/repo/tag_common"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/answerdev/answer/internal/repo/user"
"github.com/answerdev/answer/internal/repo/user_external_login"
"github.com/answerdev/answer/internal/router"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/action"
@@ -57,6 +59,7 @@ import (
notification2 "github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/plugin_common"
"github.com/answerdev/answer/internal/service/question_common"
rank2 "github.com/answerdev/answer/internal/service/rank"
reason2 "github.com/answerdev/answer/internal/service/reason"
@@ -74,6 +77,7 @@ import (
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/internal/service/user_admin"
"github.com/answerdev/answer/internal/service/user_common"
user_external_login2 "github.com/answerdev/answer/internal/service/user_external_login"
"github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/log"
)
@@ -117,8 +121,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
roleRepo := role.NewRoleRepo(dataData)
roleService := role2.NewRoleService(roleRepo)
userRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService)
userCommon := usercommon.NewUserCommon(userRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, serviceConf, siteInfoCommonService, userRoleRelService, userCommon)
userCommon := usercommon.NewUserCommon(userRepo, userRoleRelService, authService)
userExternalLoginRepo := user_external_login.NewUserExternalLoginRepo(dataData)
userExternalLoginService := user_external_login2.NewUserExternalLoginService(userRepo, userCommon, userExternalLoginRepo, emailService, siteInfoCommonService, userActiveActivityRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, serviceConf, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService)
captchaRepo := captcha.NewCaptchaRepo(dataData)
captchaService := action.NewCaptchaService(captchaRepo)
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
@@ -187,7 +193,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService)
themeController := controller_admin.NewThemeController()
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService)
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configRepo)
siteInfoController := controller_admin.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
@@ -202,7 +208,10 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService)
activityController := controller.NewActivityController(activityCommon, activityService)
roleController := controller_admin.NewRoleController(roleService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController)
pluginConfigRepo := plugin_config.NewPluginConfigRepo(dataData)
pluginCommonService := plugin_common.NewPluginCommonService(pluginConfigRepo, configRepo)
pluginController := controller_admin.NewPluginController(pluginCommonService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_adminReportController, userAdminController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter(siteinfoController, siteInfoCommonService)
authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)
@@ -210,7 +219,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, dataData, siteInfoCommonService)
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService)
templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter)
connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService)
userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService)
userCenterController := controller.NewUserCenterController(userCenterLoginService, siteInfoCommonService)
pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter, pluginAPIRouter)
scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService)
application := newApplication(serverConf, ginEngine, scheduledTaskManager)
return application, func() {
+3
View File
@@ -1,5 +1,6 @@
# url path reserves the keywords list
users:
- unsubscribe
- settings
- login
- register
@@ -9,5 +10,7 @@ users:
- account-activation
- confirm-new-email
- account-suspended
- confirm-email
- auth-landing
questions:
- ask
+945 -40
View File
File diff suppressed because it is too large Load Diff
+945 -40
View File
File diff suppressed because it is too large Load Diff
+595 -32
View File
@@ -1,4 +1,13 @@
definitions:
constant.Privilege:
properties:
key:
type: string
label:
type: string
value:
type: integer
type: object
handler.RespBody:
properties:
code:
@@ -305,6 +314,104 @@ definitions:
switch:
type: boolean
type: object
schema.ConfigField:
properties:
description:
type: string
name:
type: string
options:
items:
$ref: '#/definitions/schema.ConfigFieldOption'
type: array
required:
type: boolean
title:
type: string
type:
type: string
ui_options:
$ref: '#/definitions/schema.ConfigFieldUIOptions'
value: {}
type: object
schema.ConfigFieldOption:
properties:
label:
type: string
value:
type: string
type: object
schema.ConfigFieldUIOptions:
properties:
action:
$ref: '#/definitions/schema.UIOptionAction'
input_type:
type: string
label:
type: string
placeholder:
type: string
rows:
type: string
text:
type: string
variant:
type: string
type: object
schema.ConnectorInfoResp:
properties:
icon:
type: string
link:
type: string
name:
type: string
type: object
schema.ConnectorUserInfoResp:
properties:
binding:
type: boolean
external_id:
type: string
icon:
type: string
link:
type: string
name:
type: string
type: object
schema.ExternalLoginBindingUserSendEmailReq:
properties:
binding_key:
maxLength: 100
type: string
email:
maxLength: 512
type: string
must:
description: |-
If must is true, whatever email if exists, try to bind user.
If must is false, when email exist, will only be prompted with a warning.
type: boolean
required:
- binding_key
- email
type: object
schema.ExternalLoginBindingUserSendEmailResp:
properties:
access_token:
type: string
email_exist_and_must_be_confirmed:
type: boolean
type: object
schema.ExternalLoginUnbindingReq:
properties:
external_id:
maxLength: 128
type: string
required:
- external_id
type: object
schema.FollowReq:
properties:
is_cancel:
@@ -507,6 +614,47 @@ definitions:
info:
$ref: '#/definitions/schema.GetOtherUserInfoByUsernameResp'
type: object
schema.GetPluginConfigResp:
properties:
config_fields:
items:
$ref: '#/definitions/schema.ConfigField'
type: array
description:
type: string
name:
type: string
slug_name:
type: string
version:
type: string
type: object
schema.GetPluginListResp:
properties:
description:
type: string
enabled:
type: boolean
have_config:
type: boolean
link:
type: string
name:
type: string
slug_name:
type: string
version:
type: string
type: object
schema.GetPrivilegesConfigResp:
properties:
options:
items:
$ref: '#/definitions/schema.PrivilegeOption'
type: array
selected_level:
type: integer
type: object
schema.GetRankPersonalWithPageResp:
properties:
answer_id:
@@ -820,6 +968,9 @@ definitions:
follow_count:
description: follow count
type: integer
have_password:
description: user have password
type: boolean
id:
description: user id
type: string
@@ -894,6 +1045,8 @@ definitions:
follow_count:
description: follow count
type: integer
have_password:
type: boolean
id:
description: user id
type: string
@@ -972,6 +1125,13 @@ definitions:
description: vote type
type: string
type: object
schema.LoadingAction:
properties:
state:
type: string
text:
type: string
type: object
schema.NotificationClearIDRequest:
properties:
id:
@@ -983,6 +1143,13 @@ definitions:
description: inbox achievement
type: string
type: object
schema.OnCompleteAction:
properties:
refresh_form_config:
type: boolean
toast_return_message:
type: boolean
type: object
schema.OperationQuestionReq:
properties:
id:
@@ -1007,6 +1174,17 @@ definitions:
content:
type: string
type: object
schema.PrivilegeOption:
properties:
level:
type: integer
level_desc:
type: string
privileges:
items:
$ref: '#/definitions/constant.Privilege'
type: array
type: object
schema.QuestionAdd:
properties:
content:
@@ -1058,6 +1236,9 @@ definitions:
type: object
schema.QuestionPageReq:
properties:
inDays:
minimum: 1
type: integer
orderCond:
enum:
- newest
@@ -1325,6 +1506,9 @@ definitions:
custom_header:
maxLength: 65536
type: string
custom_sidebar:
maxLength: 65536
type: string
type: object
schema.SiteCustomCssHTMLResp:
properties:
@@ -1340,6 +1524,9 @@ definitions:
custom_header:
maxLength: 65536
type: string
custom_sidebar:
maxLength: 65536
type: string
type: object
schema.SiteGeneralReq:
properties:
@@ -1401,6 +1588,8 @@ definitions:
type: string
site_seo:
$ref: '#/definitions/schema.SiteSeoReq'
site_users:
$ref: '#/definitions/schema.SiteUsersResp'
theme:
$ref: '#/definitions/schema.SiteThemeResp'
version:
@@ -1408,11 +1597,6 @@ definitions:
type: object
schema.SiteInterfaceReq:
properties:
default_avatar:
enum:
- system
- gravatar
type: string
language:
maxLength: 128
type: string
@@ -1420,17 +1604,11 @@ definitions:
maxLength: 128
type: string
required:
- default_avatar
- language
- time_zone
type: object
schema.SiteInterfaceResp:
properties:
default_avatar:
enum:
- system
- gravatar
type: string
language:
maxLength: 128
type: string
@@ -1438,7 +1616,6 @@ definitions:
maxLength: 128
type: string
required:
- default_avatar
- language
- time_zone
type: object
@@ -1466,6 +1643,12 @@ definitions:
type: object
schema.SiteLoginReq:
properties:
allow_email_domains:
items:
type: string
type: array
allow_email_registrations:
type: boolean
allow_new_registrations:
type: boolean
login_required:
@@ -1473,6 +1656,12 @@ definitions:
type: object
schema.SiteLoginResp:
properties:
allow_email_domains:
items:
type: string
type: array
allow_email_registrations:
type: boolean
allow_new_registrations:
type: boolean
login_required:
@@ -1525,6 +1714,50 @@ definitions:
$ref: '#/definitions/schema.ThemeOption'
type: array
type: object
schema.SiteUsersReq:
properties:
allow_update_avatar:
type: boolean
allow_update_bio:
type: boolean
allow_update_display_name:
type: boolean
allow_update_location:
type: boolean
allow_update_username:
type: boolean
allow_update_website:
type: boolean
default_avatar:
enum:
- system
- gravatar
type: string
required:
- default_avatar
type: object
schema.SiteUsersResp:
properties:
allow_update_avatar:
type: boolean
allow_update_bio:
type: boolean
allow_update_display_name:
type: boolean
allow_update_location:
type: boolean
allow_update_username:
type: boolean
allow_update_website:
type: boolean
default_avatar:
enum:
- system
- gravatar
type: string
required:
- default_avatar
type: object
schema.SiteWriteReq:
properties:
recommend_tags:
@@ -1603,6 +1836,17 @@ definitions:
value:
type: string
type: object
schema.UIOptionAction:
properties:
loading:
$ref: '#/definitions/schema.LoadingAction'
method:
type: string
on_complete:
$ref: '#/definitions/schema.OnCompleteAction'
url:
type: string
type: object
schema.UnreviewedRevisionInfoInfo:
properties:
content:
@@ -1665,8 +1909,36 @@ definitions:
description: website
maxLength: 500
type: string
type: object
schema.UpdatePluginConfigReq:
properties:
config_fields:
additionalProperties: {}
type: object
plugin_slug_name:
maxLength: 100
type: string
required:
- display_name
- plugin_slug_name
type: object
schema.UpdatePluginStatusReq:
properties:
enabled:
type: boolean
plugin_slug_name:
maxLength: 100
type: string
required:
- plugin_slug_name
type: object
schema.UpdatePrivilegesConfigReq:
properties:
level:
maximum: 3
minimum: 1
type: integer
required:
- level
type: object
schema.UpdateSMTPConfigReq:
properties:
@@ -1824,6 +2096,10 @@ definitions:
e_mail:
maxLength: 500
type: string
pass:
maxLength: 32
minLength: 8
type: string
required:
- e_mail
type: object
@@ -1856,14 +2132,18 @@ definitions:
- e_mail
- pass
type: object
schema.UserModifyPassWordRequest:
schema.UserModifyPasswordReq:
properties:
old_pass:
description: old password
maxLength: 32
minLength: 8
type: string
pass:
description: password
maxLength: 32
minLength: 8
type: string
required:
- pass
type: object
schema.UserNoticeSetRequest:
properties:
@@ -2104,6 +2384,112 @@ paths:
summary: Get language options
tags:
- Lang
/answer/admin/api/plugin/config:
get:
description: get plugin config
parameters:
- description: plugin_slug_name
in: query
name: plugin_slug_name
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetPluginConfigResp'
type: object
security:
- ApiKeyAuth: []
summary: get plugin config
tags:
- AdminPlugin
put:
consumes:
- application/json
description: update plugin config
parameters:
- description: UpdatePluginConfigReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdatePluginConfigReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update plugin config
tags:
- AdminPlugin
/answer/admin/api/plugin/status:
put:
consumes:
- application/json
description: update plugin status
parameters:
- description: UpdatePluginStatusReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdatePluginStatusReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update plugin status
tags:
- AdminPlugin
/answer/admin/api/plugins:
get:
consumes:
- application/json
description: get plugin list
parameters:
- description: 'status: active/inactive'
in: query
name: status
type: string
- description: have config
in: query
name: have_config
type: boolean
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.GetPluginListResp'
type: array
type: object
security:
- ApiKeyAuth: []
summary: get plugin list
tags:
- AdminPlugin
/answer/admin/api/question/page:
get:
consumes:
@@ -2294,6 +2680,47 @@ paths:
summary: get role list
tags:
- admin
/answer/admin/api/setting/privileges:
get:
description: GetPrivilegesConfig get privileges config
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetPrivilegesConfigResp'
type: object
security:
- ApiKeyAuth: []
summary: GetPrivilegesConfig get privileges config
tags:
- admin
put:
description: update privileges config
parameters:
- description: config
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdatePrivilegesConfigReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update privileges config
tags:
- admin
/answer/admin/api/setting/smtp:
get:
description: GetSMTPConfig get smtp config
@@ -2663,6 +3090,47 @@ paths:
summary: update site custom css html config
tags:
- admin
/answer/admin/api/siteinfo/users:
get:
description: get site user config
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteUsersResp'
type: object
security:
- ApiKeyAuth: []
summary: get site user config
tags:
- admin
put:
description: update site info config about users
parameters:
- description: users info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteUsersReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site info config about users
tags:
- admin
/answer/admin/api/siteinfo/write:
get:
description: get site interface
@@ -3263,6 +3731,101 @@ paths:
summary: get comment page
tags:
- Comment
/answer/api/v1/connector/binding/email:
post:
consumes:
- application/json
description: external login binding user send email
parameters:
- description: external login binding user send email
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.ExternalLoginBindingUserSendEmailResp'
type: object
summary: external login binding user send email
tags:
- PluginConnector
/answer/api/v1/connector/info:
get:
description: get all enabled connectors
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.ConnectorInfoResp'
type: array
type: object
security:
- ApiKeyAuth: []
summary: get all enabled connectors
tags:
- PluginConnector
/answer/api/v1/connector/user/info:
get:
description: get all connectors info about user
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.ConnectorUserInfoResp'
type: array
type: object
security:
- ApiKeyAuth: []
summary: get all connectors info about user
tags:
- PluginConnector
/answer/api/v1/connector/user/unbinding:
delete:
consumes:
- application/json
description: unbind external user login
parameters:
- description: ExternalLoginUnbindingReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.ExternalLoginUnbindingReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: unbind external user login
tags:
- PluginConnector
/answer/api/v1/file:
post:
consumes:
@@ -3509,7 +4072,7 @@ paths:
get:
consumes:
- application/json
description: UserAnswerList
description: list personal answers
parameters:
- default: string
description: username
@@ -3532,9 +4095,9 @@ paths:
required: true
type: string
- default: "20"
description: pagesize
description: page_size
in: query
name: pagesize
name: page_size
required: true
type: string
produces:
@@ -3546,14 +4109,14 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: UserAnswerList
summary: list personal answers
tags:
- api-answer
- Personal
/answer/api/v1/personal/collection/page:
get:
consumes:
- application/json
description: UserCollectionList
description: list personal collections
parameters:
- default: "0"
description: page
@@ -3562,9 +4125,9 @@ paths:
required: true
type: string
- default: "20"
description: pagesize
description: page_size
in: query
name: pagesize
name: page_size
required: true
type: string
produces:
@@ -3576,7 +4139,7 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: UserCollectionList
summary: list personal collections
tags:
- Collection
/answer/api/v1/personal/comment/page:
@@ -4893,12 +5456,12 @@ paths:
- application/json
description: UserModifyPassWord
parameters:
- description: UserModifyPassWordRequest
- description: UserModifyPasswordReq
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UserModifyPassWordRequest'
$ref: '#/definitions/schema.UserModifyPasswordReq'
produces:
- application/json
responses:
@@ -5210,7 +5773,7 @@ paths:
get:
consumes:
- application/json
description: UserList
description: list personal questions
parameters:
- default: string
description: username
@@ -5233,9 +5796,9 @@ paths:
required: true
type: string
- default: "20"
description: pagesize
description: page_size
in: query
name: pagesize
name: page_size
required: true
type: string
produces:
@@ -5247,9 +5810,9 @@ paths:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: UserList
summary: list personal questions
tags:
- Question
- Personal
/robots.txt:
get:
description: get site robots information
+4
View File
@@ -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
+5 -3
View File
@@ -52,6 +52,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU=
github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
@@ -64,6 +65,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/aichy126/uint128 v1.1.1 h1:xH1bCWDzq7Ebm4lpXCeIiWco0VWi7UmiKkvTQSWBmb0=
github.com/aichy126/uint128 v1.1.1/go.mod h1:Hke/MPGXUxOl0OXHoNcVesBL4N+XalHEJ9e1jaIbl8o=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -287,7 +290,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -1202,8 +1206,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g=
+177 -12
View File
@@ -12,6 +12,8 @@ backend:
other: Unauthorized.
database_error:
other: Data server error.
forbidden_error:
other: Forbidden.
action:
report:
other: Flag
@@ -23,6 +25,8 @@ backend:
other: Close
reopen:
other: Reopen
forbidden_error:
other: Forbidden.
pin:
other: Pin
hide:
@@ -46,6 +50,58 @@ backend:
other: Have the full power to access the site.
moderator:
other: Has access to all posts except admin settings.
privilege:
level_1:
description:
other: Level 1 (less reputation required for private team, group)
level_2:
description:
other: Level 2 (low reputation required for startup community)
level_3:
description:
other: Level 3 (high reputation required for mature community)
rank_question_add_label:
other: Ask question
rank_answer_add_label:
other: Write answer
rank_comment_add_label:
other: Write comment
rank_report_add_label:
other: Flag
rank_comment_vote_up_label:
other: Upvote comment
rank_link_url_limit_label:
other: Post more than 2 links at a time
rank_question_vote_up_label:
other: Upvote question
rank_answer_vote_up_label:
other: Upvote answer
rank_question_vote_down_label:
other: Downvote question
rank_answer_vote_down_label:
other: Downvote answer
rank_tag_add_label:
other: Create new tag
rank_tag_edit_label:
other: Edit tag description (need to review)
rank_question_edit_label:
other: Edit other's question (need to review)
rank_answer_edit_label:
other: Edit other's answer (need to review)
rank_question_edit_without_review_label:
other: Edit other's question without review
rank_answer_edit_without_review_label:
other: Edit other's answer without review
rank_question_audit_label:
other: Review question edits
rank_answer_audit_label:
other: Review answer edits
rank_tag_audit_label:
other: Review tag edits
rank_tag_edit_without_review_label:
other: Edit tag description without review
rank_tag_synonym_label:
other: Manage tag synonyms
email:
other: Email
password:
@@ -83,6 +139,8 @@ backend:
other: Email should be verified.
verify_url_expired:
other: Email verified URL has expired, please resend the email.
illegal_email_domain_error:
other: Email is not allowed from that email domain. Please use another one.
lang:
not_found:
other: Language file not found.
@@ -155,6 +213,8 @@ backend:
no_permission:
other: No permission to Revision.
user:
external_login_unbinding_forbidden:
other: Please set a login password for your account before you remove this login.
email_or_password_wrong:
other:
other: Email and password do not match.
@@ -172,6 +232,10 @@ backend:
other: You cannot modify your role.
not_allowed_registration:
other: Currently the site is not open for registration
access_denied:
other: Access denied
page_access_denied:
other: You do not have access to this page.
config:
read_config_failed:
other: Read config failed
@@ -277,6 +341,16 @@ backend:
other: Your answer has been deleted
your_comment_was_deleted:
other: Your comment has been deleted
up_voted_question:
other: upvoted question
down_voted_question:
other: downvoted question
up_voted_answer:
other: upvoted answer
down_voted_answer:
other: downvoted answer
up_voted_comment:
other: upvoted comment
# The following fields are used for interface presentation(Front-end)
ui:
@@ -322,6 +396,7 @@ ui:
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
oauth_callback: Processing
http_404: HTTP Error 404
http_50X: HTTP Error 500
http_403: HTTP Error 403
@@ -331,11 +406,13 @@ ui:
achievement: Achievements
all_read: Mark all as read
show_more: Show more
someone: Someone
suspended:
title: Your Account has been Suspended
until_time: "Your account was suspended until {{ time }}."
forever: This user was suspended forever.
end: You don't meet a community guideline.
contact_us: Contact us
editor:
blockquote:
text: Blockquote
@@ -547,7 +624,7 @@ ui:
tip_answer: >-
Use comments to reply to other users or notify them of changes. If you are
adding new information, edit your post instead of commenting.
tip_vote: It adds something useful to the post
tip_vote: It adds something useful to the post
edit_answer:
title: Edit Answer
default_reason: Edit answer
@@ -658,7 +735,6 @@ ui:
msg:
empty: Cannot be empty.
login:
page_title: Welcome to {{site_name}}
login_to_continue: Log in to continue
info_sign: Don't have an account? <1>Sign up</1>
info_login: Already have an account? <1>Log in</1>
@@ -669,6 +745,7 @@ ui:
msg:
empty: Name cannot be empty.
range: Name up to 30 characters.
character: 'Must use the character set "a-z", "0-9", " - . _"'
email:
label: Email
msg:
@@ -689,7 +766,6 @@ ui:
msg:
empty: Email cannot be empty.
change_email:
page_title: Welcome to {{site_name}}
btn_cancel: Cancel
btn_update: Update email address
send_success: >-
@@ -699,6 +775,17 @@ ui:
label: New Email
msg:
empty: Email cannot be empty.
oauth_bind_email:
subtitle: Add a recovery email to your account.
btn_update: Update email address
email:
label: Email
msg:
empty: Email cannot be empty.
modal_title: Email already existes.
modal_content: This email address already registered. Are you sure you want to connect to the existing account?
modal_cancel: Change email
modal_confirm: Connect to the existing account
password_reset:
page_title: Password Reset
btn_name: Reset my password
@@ -719,6 +806,7 @@ ui:
label: Confirm New Password
settings:
page_title: Settings
goto_modify: Go to Modify
nav:
profile: Profile
notification: Notifications
@@ -768,8 +856,11 @@ ui:
We've sent an email to that address. Please follow the confirmation
instructions.
email:
label: Email
msg: Email cannot be empty.
label: New Email
msg: New Email cannot be empty.
pass:
label: Current Password
msg: Password cannot be empty.
password_title: Password
current_pass:
label: Current Password
@@ -786,6 +877,13 @@ ui:
lang:
label: Interface Language
text: User interface language. It will change when you refresh the page.
my_logins:
title: My Logins
label: Log in or sign up on this site using these accounts.
modal_title: Remove Login
modal_content: Are you sure you want to remove this login from your account?
modal_confirm_btn: Remove
remove_success: Removed successfully
toast:
update: update success
update_password: Password changed successfully.
@@ -809,8 +907,8 @@ ui:
closed_in: Closed in
show_exist: Show existing question.
useful: Useful
question_useful: It is useful and clear
question_un_useful: It is unclear or not useful
question_useful: It is useful and clear
question_un_useful: It is unclear or not useful
answer_useful: It is useful
answer_un_useful: It is not useful
answers:
@@ -873,6 +971,10 @@ ui:
skip: Skip
discard_draft: Discard draft
pinned: Pinned
all: All
question: Question
answer: Answer
comment: Comment
search:
title: Search Results
keywords: Keywords
@@ -907,7 +1009,6 @@ ui:
modal_confirm:
title: Error...
account_result:
page_title: Welcome to {{site_name}}
success: Your new account is confirmed; you will be redirected to the home page.
link: Continue to homepage
invalid: >-
@@ -1034,6 +1135,7 @@ ui:
admin_name:
label: Name
msg: Name cannot be empty.
character: 'Must use the character set "a-z", "0-9", " - . _"'
admin_password:
label: Password
text: >-
@@ -1095,8 +1197,20 @@ ui:
seo: SEO
customize: Customize
themes: Themes
css-html: CSS/HTML
css_html: CSS/HTML
login: Login
privileges: Privileges
plugins: Plugins
installed_plugins: Installed Plugins
website_welcome: Welcome to {{site_name}}
plugins:
login: Login
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
oauth:
connect: Connect with {{ auth_name }}
remove: Remove {{ auth_name }}
admin:
admin_header:
title: Admin
@@ -1139,6 +1253,7 @@ ui:
pending: Pending
completed: Completed
flagged: Flagged
flagged_type: Flagged {{ type }}
created: Created
action: Action
review: Review
@@ -1289,9 +1404,6 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:
@@ -1401,16 +1513,67 @@ ui:
footer:
label: Footer
text: This will insert before </html>.
sidebar:
label: Sidebar
text: This will insert in sidebar.
login:
page_title: Login
membership:
title: Membership
label: Allow new registrations
text: Turn off to prevent anyone from creating a new account.
email_registration:
title: Email registration
label: Allow email registration
text: Turn off to prevent anyone creating new account through email.
allowed_email_domains:
title: Allowed email domains
text: Email domains that users must register accounts with. One domain per line. Ignored when empty.
private:
title: Private
label: Login required
text: Only logged in users can access this community.
installed_plugins:
title: Installed Plugins
filter:
all: All
active: Active
inactive: Inactive
outdated: Outdated
plugins:
label: Plugins
text: Select an existing plugin.
name: Name
version: Version
status: Status
action: Action
deactivate: Deactivate
activate: Activate
settings: Settings
settings_users:
title: Users
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
profile_editable:
title: Profile Editable
allow_update_display_name:
label: Allow users to change their display name
allow_update_username:
label: Allow users to change their username
allow_update_avatar:
label: Allow users to change their profile image
allow_update_bio:
label: Allow users to change their about me
allow_update_website:
label: Allow users to change their website
allow_update_location:
label: Allow users to change their location
privilege:
title: Privileges
level:
label: Reputation required level
text: Choose the reputation required for the privileges
form:
optional: (optional)
@@ -1418,6 +1581,8 @@ ui:
invalid: is invalid
btn_submit: Save
not_found_props: "Required property {{ key }} not found."
select: Select
page_review:
review: Review
proposed: proposed
+126
View File
@@ -45,6 +45,58 @@ backend:
other: 拥有管理网站的全部权限。
moderator:
other: 拥有访问除管理员设置以外的所有权限。
privilege:
level_1:
description:
other: 等级1(创业社区所需的声望最低)
level_2:
description:
other: 等级2(创业社区所需的声望较低)
level_3:
description:
other: 等级3(成熟社区所需的声望较高)
rank_question_add_label:
other: 提问
rank_answer_add_label:
other: 回答问题
rank_comment_add_label:
other: 发表评论
rank_report_add_label:
other: 举报
rank_comment_vote_up_label:
other: 评论点赞
rank_link_url_limit_label:
other: 一次发布超过两个链接
rank_question_vote_up_label:
other: 问题点赞
rank_answer_vote_up_label:
other: 答案点赞
rank_question_vote_down_label:
other: 问题点踩
rank_answer_vote_down_label:
other: 答案点踩
rank_tag_add_label:
other: 创建新标签
rank_tag_edit_label:
other: 编辑标签描述(需要审核)
rank_question_edit_label:
other: 编辑他人提问(需要审核)
rank_answer_edit_label:
other: 编辑他人回答(需要审核)
rank_question_edit_without_review_label:
other: 编辑他人提问(无需审核)
rank_answer_edit_without_review_label:
other: 编辑他人回答(无需审核)
rank_question_audit_label:
other: 审核问题编辑
rank_answer_audit_label:
other: 审核答案编辑
rank_tag_audit_label:
other: 审核标签编辑
rank_tag_edit_without_review_label:
other: 编辑标签描述(无需审核)
rank_tag_synonym_label:
other: 管理标签同义词
email:
other: 邮箱
password:
@@ -82,6 +134,8 @@ backend:
other: 邮箱需要验证。
verify_url_expired:
other: 邮箱验证的网址已过期,请重新发送邮件。
illegal_email_domain_error:
other: 该域名的邮箱无法使用。请尝试更换其他邮箱。
lang:
not_found:
other: 语言未找到
@@ -171,6 +225,10 @@ backend:
other: 您不能修改自己的角色。
not_allowed_registration:
other: 目前该站点未开放注册
access_denied:
other: 访问被拒绝
page_access_denied:
other: 你没有权限进入这个页面。
config:
read_config_failed:
other: 读取配置失败
@@ -271,6 +329,16 @@ backend:
other: 你的答案已被删除
your_comment_was_deleted:
other: 你的评论已被删除
up_voted_question:
other: 赞了问题
down_voted_question:
other: 踩了问题
up_voted_answer:
other: 赞了答案
down_voted_answer:
other: 踩了答案
up_voted_comment:
other: 赞了评论
#The following fields are used for interface presentation(Front-end)
ui:
how_to_format:
@@ -316,6 +384,7 @@ ui:
achievement: 成就
all_read: 全部标记为已读
show_more: 显示更多
someone: 有人
suspended:
title: 账号已封禁
until_time: "你的账号被封禁至{{ time }}。"
@@ -690,6 +759,7 @@ ui:
label: 确认新密码
settings:
page_title: 设置
goto_modify: 前往修改
nav:
profile: 我的资料
notification: 通知
@@ -836,6 +906,10 @@ ui:
skip: 略过
discard_draft: 丢弃草稿
pinned: 已置顶
all: 所有
question: 问题
answer: 回答
comment: 评论
search:
title: 搜索结果
keywords: 关键词
@@ -1053,6 +1127,16 @@ ui:
themes: 主题
css-html: CSS/HTML
login: 登录
plugins: 插件
installed_plugins: 插件列表
website_welcome: 欢迎来到 {{site_name}}
plugins:
login: 登录
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。
oauth:
connect: 连接到 {{ auth_name }}
remove: 解绑 {{ auth_name }}
admin:
admin_header:
title: 后台管理
@@ -1095,6 +1179,7 @@ ui:
pending: 等待处理
completed: 已完成
flagged: 被举报内容
flagged_type: 被举报的{{ type }}
created: 创建于
action: 操作
review: 审查
@@ -1365,6 +1450,47 @@ ui:
title: 非公开的
label: 需要登录
text: 只有登录用户才能访问这个社区。
installed_plugins:
title: 插件列表
filter:
all: 全部
active: 启用
inactive: 未启用
outdated: 已过期
plugins:
label: 插件
text: 选择一个插件
name: 插件名称
version: 插件版本
status: 状态
action: 操作
deactivate: 停用
activate: 启用
settings: 设置
settings_users:
title: 用户
avatar:
label: 默认头像
text: 未设置自定义头像的用户所展示的头像。
profile_editable:
title: 可编辑的个人资料
allow_update_display_name:
label: 允许用户更改显示名称
allow_update_username:
label: 允许用户更改用户名
allow_update_avatar:
label: 允许用户更改头像
allow_update_bio:
label: 允许用户更改自我介绍
allow_update_website:
label: 允许用户更改个人网站
allow_update_location:
label: 允许用户更改所在地
privilege:
title: 声望权限
level:
label: 所需声望等级
text: 选择所需的声望等级以获取权限
form:
optional: (选填)
empty: 不能为空
+5
View File
@@ -0,0 +1,5 @@
package constant
const (
PluginStatus = "plugin.status"
)
+8
View File
@@ -0,0 +1,8 @@
package constant
import "time"
const (
ConnectorUserExternalInfoCacheKey = "answer:connector:"
ConnectorUserExternalInfoCacheTime = 10 * time.Minute
)
+2
View File
@@ -66,6 +66,8 @@ const (
SiteTypeLogin = "login"
SiteTypeCustomCssHTML = "css-html"
SiteTypeTheme = "theme"
SiteTypePrivileges = "privileges"
SiteTypeUsers = "users"
)
func ExistInPathIgnore(name string) bool {
+34 -24
View File
@@ -1,28 +1,38 @@
package constant
const (
// UpdateQuestion update question
UpdateQuestion = "notification.action.update_question"
// AnswerTheQuestion answer the question
AnswerTheQuestion = "notification.action.answer_the_question"
// UpdateAnswer update answer
UpdateAnswer = "notification.action.update_answer"
// AcceptAnswer accept answer
AcceptAnswer = "notification.action.accept_answer"
// CommentQuestion comment question
CommentQuestion = "notification.action.comment_question"
// CommentAnswer comment answer
CommentAnswer = "notification.action.comment_answer"
// ReplyToYou reply to you
ReplyToYou = "notification.action.reply_to_you"
// MentionYou mention you
MentionYou = "notification.action.mention_you"
// YourQuestionIsClosed your question is closed
YourQuestionIsClosed = "notification.action.your_question_is_closed"
// YourQuestionWasDeleted your question was deleted
YourQuestionWasDeleted = "notification.action.your_question_was_deleted"
// YourAnswerWasDeleted your answer was deleted
YourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
// YourCommentWasDeleted your comment was deleted
YourCommentWasDeleted = "notification.action.your_comment_was_deleted"
// NotificationUpdateQuestion update question
NotificationUpdateQuestion = "notification.action.update_question"
// NotificationAnswerTheQuestion answer the question
NotificationAnswerTheQuestion = "notification.action.answer_the_question"
// NotificationUpVotedTheQuestion up voted the question
NotificationUpVotedTheQuestion = "notification.action.up_voted_question"
// NotificationDownVotedTheQuestion down voted the question
NotificationDownVotedTheQuestion = "notification.action.down_voted_question"
// NotificationUpdateAnswer update answer
NotificationUpdateAnswer = "notification.action.update_answer"
// NotificationAcceptAnswer accept answer
NotificationAcceptAnswer = "notification.action.accept_answer"
// NotificationUpVotedTheAnswer up voted the answer
NotificationUpVotedTheAnswer = "notification.action.up_voted_answer"
// NotificationDownVotedTheAnswer down voted the answer
NotificationDownVotedTheAnswer = "notification.action.down_voted_answer"
// NotificationCommentQuestion comment question
NotificationCommentQuestion = "notification.action.comment_question"
// NotificationCommentAnswer comment answer
NotificationCommentAnswer = "notification.action.comment_answer"
// NotificationUpVotedTheComment up voted the comment
NotificationUpVotedTheComment = "notification.action.up_voted_comment"
// NotificationReplyToYou reply to you
NotificationReplyToYou = "notification.action.reply_to_you"
// NotificationMentionYou mention you
NotificationMentionYou = "notification.action.mention_you"
// NotificationYourQuestionIsClosed your question is closed
NotificationYourQuestionIsClosed = "notification.action.your_question_is_closed"
// NotificationYourQuestionWasDeleted your question was deleted
NotificationYourQuestionWasDeleted = "notification.action.your_question_was_deleted"
// NotificationYourAnswerWasDeleted your answer was deleted
NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
// NotificationYourCommentWasDeleted your comment was deleted
NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted"
)
+70
View File
@@ -0,0 +1,70 @@
package constant
import "github.com/answerdev/answer/internal/base/reason"
type Privilege struct {
Key string `json:"key"`
Label string `json:"label"`
Value int `json:"value"`
}
const (
RankQuestionAddKey = "rank.question.add"
RankQuestionEditKey = "rank.question.edit"
RankQuestionDeleteKey = "rank.question.delete"
RankQuestionVoteUpKey = "rank.question.vote_up"
RankQuestionVoteDownKey = "rank.question.vote_down"
RankAnswerAddKey = "rank.answer.add"
RankAnswerEditKey = "rank.answer.edit"
RankAnswerDeleteKey = "rank.answer.delete"
RankAnswerAcceptKey = "rank.answer.accept"
RankAnswerVoteUpKey = "rank.answer.vote_up"
RankAnswerVoteDownKey = "rank.answer.vote_down"
RankCommentAddKey = "rank.comment.add"
RankCommentEditKey = "rank.comment.edit"
RankCommentDeleteKey = "rank.comment.delete"
RankReportAddKey = "rank.report.add"
RankTagAddKey = "rank.tag.add"
RankTagEditKey = "rank.tag.edit"
RankTagDeleteKey = "rank.tag.delete"
RankTagSynonymKey = "rank.tag.synonym"
RankLinkUrlLimitKey = "rank.link.url_limit"
RankVoteDetailKey = "rank.vote.detail"
RankCommentVoteUpKey = "rank.comment.vote_up"
RankCommentVoteDownKey = "rank.comment.vote_down"
RankQuestionEditWithoutReviewKey = "rank.question.edit_without_review"
RankAnswerEditWithoutReviewKey = "rank.answer.edit_without_review"
RankTagEditWithoutReviewKey = "rank.tag.edit_without_review"
RankAnswerAuditKey = "rank.answer.audit"
RankQuestionAuditKey = "rank.question.audit"
RankTagAuditKey = "rank.tag.audit"
RankQuestionCloseKey = "rank.question.close"
RankQuestionReopenKey = "rank.question.reopen"
RankTagUseReservedTagKey = "rank.tag.use_reserved_tag"
)
var (
RankAllPrivileges = []*Privilege{
{Label: reason.RankQuestionAddLabel, Key: RankQuestionAddKey},
{Label: reason.RankAnswerAddLabel, Key: RankAnswerAddKey},
{Label: reason.RankCommentAddLabel, Key: RankCommentAddKey},
{Label: reason.RankReportAddLabel, Key: RankReportAddKey},
{Label: reason.RankCommentVoteUpLabel, Key: RankCommentVoteUpKey},
{Label: reason.RankLinkUrlLimitLabel, Key: RankLinkUrlLimitKey},
{Label: reason.RankQuestionVoteUpLabel, Key: RankQuestionVoteUpKey},
{Label: reason.RankAnswerVoteUpLabel, Key: RankAnswerVoteUpKey},
{Label: reason.RankQuestionVoteDownLabel, Key: RankQuestionVoteDownKey},
{Label: reason.RankAnswerVoteDownLabel, Key: RankAnswerVoteDownKey},
{Label: reason.RankTagAddLabel, Key: RankTagAddKey},
{Label: reason.RankTagEditLabel, Key: RankTagEditKey},
{Label: reason.RankQuestionEditLabel, Key: RankQuestionEditKey},
{Label: reason.RankAnswerEditLabel, Key: RankAnswerEditKey},
{Label: reason.RankQuestionEditWithoutReviewLabel, Key: RankQuestionEditWithoutReviewKey},
{Label: reason.RankAnswerEditWithoutReviewLabel, Key: RankAnswerEditWithoutReviewKey},
{Label: reason.RankQuestionAuditLabel, Key: RankQuestionAuditKey},
{Label: reason.RankAnswerAuditLabel, Key: RankAnswerAuditKey},
{Label: reason.RankTagAuditLabel, Key: RankTagAuditKey},
{Label: reason.RankTagEditWithoutReviewLabel, Key: RankTagEditWithoutReviewKey},
{Label: reason.RankTagSynonymLabel, Key: RankTagSynonymKey},
}
)
+2 -1
View File
@@ -1,5 +1,6 @@
package constant
var (
DefaultAvatar = "system"
DefaultAvatar = "system"
DefaultSiteURL = ""
)
+10
View File
@@ -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()
+15
View File
@@ -0,0 +1,15 @@
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
)
func HeadersByRequestURI() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.RequestURI, "/static/") {
c.Header("cache-control", "public, max-age=31536000")
}
}
}
@@ -0,0 +1,23 @@
package middleware
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
// BanAPIForUserCenter ban api for user center
func BanAPIForUserCenter(ctx *gin.Context) {
uc, ok := plugin.GetUserCenter()
if !ok {
return
}
if !uc.Description().EnabledOriginalUserSystem {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
ctx.Abort()
return
}
ctx.Next()
}
+29
View File
@@ -0,0 +1,29 @@
package reason
const (
PrivilegeLevel1Desc = "privilege.level_1.description"
PrivilegeLevel2Desc = "privilege.level_2.description"
PrivilegeLevel3Desc = "privilege.level_3.description"
RankQuestionAddLabel = "privilege.rank_question_add_label"
RankAnswerAddLabel = "privilege.rank_answer_add_label"
RankCommentAddLabel = "privilege.rank_comment_add_label"
RankReportAddLabel = "privilege.rank_report_add_label"
RankCommentVoteUpLabel = "privilege.rank_comment_vote_up_label"
RankLinkUrlLimitLabel = "privilege.rank_link_url_limit_label"
RankQuestionVoteUpLabel = "privilege.rank_question_vote_up_label"
RankAnswerVoteUpLabel = "privilege.rank_answer_vote_up_label"
RankQuestionVoteDownLabel = "privilege.rank_question_vote_down_label"
RankAnswerVoteDownLabel = "privilege.rank_answer_vote_down_label"
RankTagAddLabel = "privilege.rank_tag_add_label"
RankTagEditLabel = "privilege.rank_tag_edit_label"
RankQuestionEditLabel = "privilege.rank_question_edit_label"
RankAnswerEditLabel = "privilege.rank_answer_edit_label"
RankQuestionEditWithoutReviewLabel = "privilege.rank_question_edit_without_review_label"
RankAnswerEditWithoutReviewLabel = "privilege.rank_answer_edit_without_review_label"
RankQuestionAuditLabel = "privilege.rank_question_audit_label"
RankAnswerAuditLabel = "privilege.rank_answer_audit_label"
RankTagAuditLabel = "privilege.rank_tag_audit_label"
RankTagEditWithoutReviewLabel = "privilege.rank_tag_edit_without_review_label"
RankTagSynonymLabel = "privilege.rank_tag_synonym_label"
)
+62 -56
View File
@@ -11,63 +11,69 @@ const (
UnauthorizedError = "base.unauthorized_error"
// DatabaseError database error
DatabaseError = "base.database_error"
// ForbiddenError forbidden error
ForbiddenError = "base.forbidden_error"
)
const (
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
QuestionNotFound = "error.question.not_found"
QuestionCannotDeleted = "error.question.cannot_deleted"
QuestionCannotClose = "error.question.cannot_close"
QuestionCannotUpdate = "error.question.cannot_update"
QuestionAlreadyDeleted = "error.question.already_deleted"
AnswerNotFound = "error.answer.not_found"
AnswerCannotDeleted = "error.answer.cannot_deleted"
AnswerCannotUpdate = "error.answer.cannot_update"
AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
UserSetAvatar = "error.user.set_avatar"
EmailDuplicate = "error.email.duplicate"
EmailVerifyURLExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
TagCannotUpdate = "error.tag.cannot_update"
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
TagAlreadyExist = "error.tag.already_exist"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration"
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
CommentNotFound = "error.comment.not_found"
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
QuestionNotFound = "error.question.not_found"
QuestionCannotDeleted = "error.question.cannot_deleted"
QuestionCannotClose = "error.question.cannot_close"
QuestionCannotUpdate = "error.question.cannot_update"
QuestionAlreadyDeleted = "error.question.already_deleted"
AnswerNotFound = "error.answer.not_found"
AnswerCannotDeleted = "error.answer.cannot_deleted"
AnswerCannotUpdate = "error.answer.cannot_update"
AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add"
CommentEditWithoutPermission = "error.comment.edit_without_permission"
DisallowVote = "error.object.disallow_vote"
DisallowFollow = "error.object.disallow_follow"
DisallowVoteYourSelf = "error.object.disallow_vote_your_self"
CaptchaVerificationFailed = "error.object.captcha_verification_failed"
OldPasswordVerificationFailed = "error.object.old_password_verification_failed"
NewPasswordSameAsPreviousSetting = "error.object.new_password_same_as_previous_setting"
UserNotFound = "error.user.not_found"
UsernameInvalid = "error.user.username_invalid"
UsernameDuplicate = "error.user.username_duplicate"
UserSetAvatar = "error.user.set_avatar"
EmailDuplicate = "error.email.duplicate"
EmailVerifyURLExpired = "error.email.verify_url_expired"
EmailNeedToBeVerified = "error.email.need_to_be_verified"
EmailIllegalDomainError = "error.email.illegal_email_domain_error"
UserSuspended = "error.user.suspended"
ObjectNotFound = "error.object.not_found"
TagNotFound = "error.tag.not_found"
TagNotContainSynonym = "error.tag.not_contain_synonym_tags"
TagCannotUpdate = "error.tag.cannot_update"
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
TagAlreadyExist = "error.tag.already_exist"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration"
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
UserExternalLoginUnbindingForbidden = "error.user.external_login_unbinding_forbidden"
UserAccessDenied = "error.user.access_denied"
UserPageAccessDenied = "error.user.page_access_denied"
)
+15 -1
View File
@@ -7,6 +7,7 @@ import (
brotli "github.com/anargu/gin-brotli"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/router"
"github.com/answerdev/answer/plugin"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
)
@@ -20,6 +21,7 @@ func NewHTTPServer(debug bool,
authUserMiddleware *middleware.AuthUserMiddleware,
avatarMiddleware *middleware.AvatarMiddleware,
templateRouter *router.TemplateRouter,
pluginAPIRouter *router.PluginAPIRouter,
) *gin.Engine {
if debug {
@@ -34,7 +36,7 @@ func NewHTTPServer(debug bool,
html, _ := fs.Sub(ui.Template, "template")
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
r.SetHTMLTemplate(htmlTemplate)
r.Use(middleware.HeadersByRequestURI())
viewRouter.Register(r)
rootGroup := r.Group("")
@@ -62,5 +64,17 @@ func NewHTTPServer(debug bool,
answerRouter.RegisterAnswerAdminAPIRouter(adminauthV1)
templateRouter.RegisterTemplateRouter(rootGroup)
// plugin routes
pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1)
pluginAPIRouter.RegisterAuthUserConnectorRouter(authV1)
pluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1)
_ = plugin.CallAgent(func(agent plugin.Agent) error {
agent.RegisterUnAuthRouter(mustUnAuthV1)
agent.RegisterAuthUserRouter(authV1)
agent.RegisterAuthAdminRouter(adminauthV1)
return nil
})
return r
}
+5
View File
@@ -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)
+358
View File
@@ -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)
}
+254
View File
@@ -0,0 +1,254 @@
package controller
import (
"fmt"
"net/http"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
const (
commonRouterPrefix = "/answer/api/v1"
ConnectorLoginRouterPrefix = "/connector/login/"
ConnectorRedirectRouterPrefix = "/connector/redirect/"
)
// ConnectorController comment controller
type ConnectorController struct {
siteInfoService *siteinfo_common.SiteInfoCommonService
userExternalService *user_external_login.UserExternalLoginService
emailService *export.EmailService
}
// NewConnectorController new controller
func NewConnectorController(
siteInfoService *siteinfo_common.SiteInfoCommonService,
emailService *export.EmailService,
userExternalService *user_external_login.UserExternalLoginService,
) *ConnectorController {
return &ConnectorController{
siteInfoService: siteInfoService,
userExternalService: userExternalService,
emailService: emailService,
}
}
// ConnectorLoginDispatcher dispatch connector login request to specific connector by slug name
// We can't register specific router for each connector when application start, because the plugin status will be changed by admin.
// If the plugin is disabled, the router should be unavailable.
func (cc *ConnectorController) ConnectorLoginDispatcher(ctx *gin.Context) {
slugName := ctx.Param("name")
var c plugin.Connector
_ = plugin.CallConnector(func(connector plugin.Connector) error {
if connector.ConnectorSlugName() == slugName {
c = connector
}
return nil
})
if c == nil {
log.Errorf("connector %s not found", slugName)
ctx.Redirect(http.StatusFound, "/50x")
return
}
cc.ConnectorLogin(c)(ctx)
}
func (cc *ConnectorController) ConnectorRedirectDispatcher(ctx *gin.Context) {
slugName := ctx.Param("name")
var c plugin.Connector
_ = plugin.CallConnector(func(connector plugin.Connector) error {
if connector.ConnectorSlugName() == slugName {
c = connector
}
return nil
})
if c == nil {
log.Errorf("connector %s not found", slugName)
ctx.Redirect(http.StatusFound, "/50x")
return
}
cc.ConnectorRedirect(c)(ctx)
}
func (cc *ConnectorController) ConnectorLogin(connector plugin.Connector) (fn func(ctx *gin.Context)) {
return func(ctx *gin.Context) {
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Error(err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
receiverURL := fmt.Sprintf("%s%s%s%s", general.SiteUrl,
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
redirectURL := connector.ConnectorSender(ctx, receiverURL)
if len(redirectURL) > 0 {
ctx.Redirect(http.StatusFound, redirectURL)
}
return
}
}
func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn func(ctx *gin.Context)) {
return func(ctx *gin.Context) {
siteGeneral, err := cc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site info failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
receiverURL := fmt.Sprintf("%s%s%s%s", siteGeneral.SiteUrl,
commonRouterPrefix, ConnectorRedirectRouterPrefix, connector.ConnectorSlugName())
userInfo, err := connector.ConnectorReceiver(ctx, receiverURL)
if err != nil {
log.Errorf("connector received failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
log.Debugf("connector received: %+v", userInfo)
u := &schema.ExternalLoginUserInfoCache{
Provider: connector.ConnectorSlugName(),
ExternalID: userInfo.ExternalID,
DisplayName: userInfo.DisplayName,
Username: userInfo.Username,
Email: userInfo.Email,
Avatar: userInfo.Avatar,
MetaInfo: userInfo.MetaInfo,
}
resp, err := cc.userExternalService.ExternalLogin(ctx, u)
if err != nil {
log.Errorf("external login failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
if len(resp.AccessToken) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
} else {
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/confirm-email?binding_key=%s",
siteGeneral.SiteUrl, resp.BindingKey))
}
}
}
// ConnectorsInfo get all enabled connectors
// @Summary get all enabled connectors
// @Description get all enabled connectors
// @Tags PluginConnector
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorInfoResp}
// @Router /answer/api/v1/connector/info [get]
func (cc *ConnectorController) ConnectorsInfo(ctx *gin.Context) {
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
resp := make([]*schema.ConnectorInfoResp, 0)
_ = plugin.CallConnector(func(fn plugin.Connector) error {
connectorName := fn.ConnectorName()
resp = append(resp, &schema.ConnectorInfoResp{
Name: connectorName.Translate(ctx),
Icon: fn.ConnectorLogoSVG(),
Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl,
commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),
})
return nil
})
handler.HandleResponse(ctx, nil, resp)
}
// ExternalLoginBindingUserSendEmail external login binding user send email
// @Summary external login binding user send email
// @Description external login binding user send email
// @Tags PluginConnector
// @Accept json
// @Produce json
// @Param data body schema.ExternalLoginBindingUserSendEmailReq true "external login binding user send email"
// @Success 200 {object} handler.RespBody{data=schema.ExternalLoginBindingUserSendEmailResp}
// @Router /answer/api/v1/connector/binding/email [post]
func (cc *ConnectorController) ExternalLoginBindingUserSendEmail(ctx *gin.Context) {
req := &schema.ExternalLoginBindingUserSendEmailReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := cc.userExternalService.ExternalLoginBindingUserSendEmail(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// ConnectorsUserInfo get all connectors info about user
// @Summary get all connectors info about user
// @Description get all connectors info about user
// @Tags PluginConnector
// @Security ApiKeyAuth
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]schema.ConnectorUserInfoResp}
// @Router /answer/api/v1/connector/user/info [get]
func (cc *ConnectorController) ConnectorsUserInfo(ctx *gin.Context) {
general, err := cc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
userID := middleware.GetLoginUserIDFromContext(ctx)
userInfoList, err := cc.userExternalService.GetExternalLoginUserInfoList(ctx, userID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
userExternalLoginMapping := make(map[string]string)
for _, userInfo := range userInfoList {
userExternalLoginMapping[userInfo.Provider] = userInfo.ExternalID
}
resp := make([]*schema.ConnectorUserInfoResp, 0)
_ = plugin.CallConnector(func(fn plugin.Connector) error {
externalID := userExternalLoginMapping[fn.ConnectorSlugName()]
connectorName := fn.ConnectorName()
resp = append(resp, &schema.ConnectorUserInfoResp{
Name: connectorName.Translate(ctx),
Icon: fn.ConnectorLogoSVG(),
Link: fmt.Sprintf("%s%s%s%s", general.SiteUrl,
commonRouterPrefix, ConnectorLoginRouterPrefix, fn.ConnectorSlugName()),
Binding: len(externalID) > 0,
ExternalID: externalID,
})
return nil
})
handler.HandleResponse(ctx, nil, resp)
}
// ExternalLoginUnbinding unbind external user login
// @Summary unbind external user login
// @Description unbind external user login
// @Tags PluginConnector
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param data body schema.ExternalLoginUnbindingReq true "ExternalLoginUnbindingReq"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/connector/user/unbinding [delete]
func (cc *ConnectorController) ExternalLoginUnbinding(ctx *gin.Context) {
req := &schema.ExternalLoginUnbindingReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := cc.userExternalService.ExternalLoginUnbinding(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
+2
View File
@@ -24,4 +24,6 @@ var ProviderSetController = wire.NewSet(
NewUploadController,
NewActivityController,
NewTemplateController,
NewConnectorController,
NewUserCenterController,
)
@@ -0,0 +1,196 @@
package controller
import (
"fmt"
"net/http"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
const (
UserCenterLoginRouter = "/user-center/login/redirect"
UserCenterSignUpRedirectRouter = "/user-center/sign-up/redirect"
)
// UserCenterController comment controller
type UserCenterController struct {
userCenterLoginService *user_external_login.UserCenterLoginService
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewUserCenterController new controller
func NewUserCenterController(
userCenterLoginService *user_external_login.UserCenterLoginService,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *UserCenterController {
return &UserCenterController{
userCenterLoginService: userCenterLoginService,
siteInfoService: siteInfoService,
}
}
// UserCenterAgent get user center agent info
func (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) {
resp := &schema.UserCenterAgentResp{}
resp.Enabled = plugin.UserCenterEnabled()
if !resp.Enabled {
handler.HandleResponse(ctx, nil, resp)
return
}
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site info failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
resp.AgentInfo = &schema.AgentInfo{}
resp.AgentInfo.LoginRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl,
commonRouterPrefix, UserCenterLoginRouter)
resp.AgentInfo.SignUpRedirectURL = fmt.Sprintf("%s%s%s", siteGeneral.SiteUrl,
commonRouterPrefix, UserCenterSignUpRedirectRouter)
_ = plugin.CallUserCenter(func(uc plugin.UserCenter) error {
info := uc.Description()
resp.AgentInfo.Name = info.Name
resp.AgentInfo.DisplayName = info.DisplayName.Translate(ctx)
resp.AgentInfo.Icon = info.Icon
resp.AgentInfo.Url = info.Url
resp.AgentInfo.ControlCenterItems = make([]*schema.ControlCenter, 0)
resp.AgentInfo.EnabledOriginalUserSystem = info.EnabledOriginalUserSystem
items := uc.ControlCenterItems()
for _, item := range items {
resp.AgentInfo.ControlCenterItems = append(resp.AgentInfo.ControlCenterItems, &schema.ControlCenter{
Name: item.Name,
Label: item.Label,
Url: item.Url,
})
}
return nil
})
handler.HandleResponse(ctx, nil, resp)
}
// UserCenterPersonalBranding get user center personal user info
func (uc *UserCenterController) UserCenterPersonalBranding(ctx *gin.Context) {
req := &schema.GetOtherUserInfoByUsernameReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := uc.userCenterLoginService.UserCenterPersonalBranding(ctx, req.Username)
handler.HandleResponse(ctx, err, resp)
}
func (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) {
var redirectURL string
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
info := userCenter.Description()
redirectURL = info.LoginRedirectURL
return nil
})
ctx.Redirect(http.StatusFound, redirectURL)
}
func (uc *UserCenterController) UserCenterSignUpRedirect(ctx *gin.Context) {
var redirectURL string
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
info := userCenter.Description()
redirectURL = info.LoginRedirectURL
return nil
})
ctx.Redirect(http.StatusFound, redirectURL)
}
func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) {
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site info failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
userCenter, ok := plugin.GetUserCenter()
if !ok {
ctx.Redirect(http.StatusFound, "/404")
return
}
userInfo, err := userCenter.LoginCallback(ctx)
if err != nil {
log.Error(err)
if !ctx.IsAborted() {
ctx.Redirect(http.StatusFound, "/50x")
}
return
}
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
if err != nil {
log.Errorf("external login failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
if len(resp.ErrMsg) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
return
}
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
}
func (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) {
siteGeneral, err := uc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site info failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
userCenter, ok := plugin.GetUserCenter()
if !ok {
ctx.Redirect(http.StatusFound, "/404")
return
}
userInfo, err := userCenter.SignUpCallback(ctx)
if err != nil {
log.Error(err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
if err != nil {
log.Errorf("external login failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
if len(resp.ErrMsg) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("/50x?title=%s&msg=%s", resp.ErrTitle, resp.ErrMsg))
return
}
userCenter.AfterLogin(userInfo.ExternalID, resp.AccessToken)
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/auth-landing?access_token=%s",
siteGeneral.SiteUrl, resp.AccessToken))
}
// UserCenterUserSettings user center user settings
func (uc *UserCenterController) UserCenterUserSettings(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
resp, err := uc.userCenterLoginService.UserCenterUserSettings(ctx, userID)
handler.HandleResponse(ctx, err, resp)
}
// UserCenterAdminFunctionAgent user center admin function agent
func (uc *UserCenterController) UserCenterAdminFunctionAgent(ctx *gin.Context) {
resp, err := uc.userCenterLoginService.UserCenterAdminFunctionAgent(ctx)
handler.HandleResponse(ctx, err, resp)
}
+45 -52
View File
@@ -11,7 +11,6 @@ import (
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/uid"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
@@ -552,84 +551,75 @@ func (qc *QuestionController) UserTop(ctx *gin.Context) {
})
}
// UserList godoc
// @Summary UserList
// @Description UserList
// @Tags Question
// PersonalQuestionPage list personal questions
// @Summary list personal questions
// @Description list personal questions
// @Tags Personal
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "username" default(string)
// @Param order query string true "order" Enums(newest,score)
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Param page_size query string true "page_size" default(20)
// @Success 200 {object} handler.RespBody
// @Router /personal/question/page [get]
func (qc *QuestionController) UserList(ctx *gin.Context) {
userName := ctx.Query("username")
order := ctx.Query("order")
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserList(ctx, userName, order, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) {
req := &schema.PersonalQuestionPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.PersonalQuestionPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UserAnswerList godoc
// @Summary UserAnswerList
// @Description UserAnswerList
// @Tags api-answer
// PersonalAnswerPage list personal answers
// @Summary list personal answers
// @Description list personal answers
// @Tags Personal
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "username" default(string)
// @Param order query string true "order" Enums(newest,score)
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Param page_size query string true "page_size" default(20)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/personal/answer/page [get]
func (qc *QuestionController) UserAnswerList(ctx *gin.Context) {
userName := ctx.Query("username")
order := ctx.Query("order")
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserAnswerList(ctx, userName, order, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) {
req := &schema.PersonalAnswerPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.PersonalAnswerPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// UserCollectionList godoc
// @Summary UserCollectionList
// @Description UserCollectionList
// PersonalCollectionPage list personal collections
// @Summary list personal collections
// @Description list personal collections
// @Tags Collection
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param page query string true "page" default(0)
// @Param pagesize query string true "pagesize" default(20)
// @Param page_size query string true "page_size" default(20)
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/personal/collection/page [get]
func (qc *QuestionController) UserCollectionList(ctx *gin.Context) {
pageStr := ctx.Query("page")
pageSizeStr := ctx.Query("pagesize")
page := converter.StringToInt(pageStr)
pageSize := converter.StringToInt(pageSizeStr)
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.SearchUserCollectionList(ctx, page, pageSize, userID)
handler.HandleResponse(ctx, err, gin.H{
"list": questionList,
"count": count,
})
func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) {
req := &schema.PersonalCollectionPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := qc.questionService.PersonalCollectionPage(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
// AdminSearchList godoc
@@ -678,6 +668,9 @@ func (qc *QuestionController) AdminSearchAnswerList(ctx *gin.Context) {
return
}
req.QuestionID = uid.DeShortID(req.QuestionID)
if req.QuestionID == "0" {
req.QuestionID = ""
}
userID := middleware.GetLoginUserIDFromContext(ctx)
questionList, count, err := qc.questionService.AdminSearchAnswerList(ctx, req, userID)
handler.HandleResponse(ctx, err, gin.H{
@@ -64,6 +64,10 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
if err != nil {
log.Error(err)
}
resp.SiteUsers, err = sc.siteInfoService.GetSiteUsers(ctx)
if err != nil {
log.Error(err)
}
handler.HandleResponse(ctx, nil, resp)
}
+21 -5
View File
@@ -13,6 +13,7 @@ import (
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/pkg/checker"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@@ -183,9 +184,9 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) {
return
}
resp, err := uc.userService.UseRePassword(ctx, req)
err := uc.userService.UpdatePasswordWhenForgot(ctx, req)
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
handler.HandleResponse(ctx, err, resp)
handler.HandleResponse(ctx, err, nil)
}
// UserLogout user logout
@@ -223,7 +224,7 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
return
}
if !siteInfo.AllowNewRegistrations {
if !siteInfo.AllowNewRegistrations || !siteInfo.AllowEmailRegistrations {
handler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil)
return
}
@@ -232,6 +233,10 @@ func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)
return
}
req.IP = ctx.ClientIP()
captchaPass := uc.actionService.UserRegisterVerifyCaptcha(ctx, req.CaptchaID, req.CaptchaCode)
if !captchaPass {
@@ -334,15 +339,16 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UserModifyPassWordRequest true "UserModifyPassWordRequest"
// @Param data body schema.UserModifyPasswordReq true "UserModifyPasswordReq"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/user/password [put]
func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
req := &schema.UserModifyPassWordRequest{}
req := &schema.UserModifyPasswordReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.AccessToken = middleware.ExtractToken(ctx)
oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)
if err != nil {
@@ -488,6 +494,16 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
return
}
// check whether email allow register or not
siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !checker.EmailInAllowEmailDomain(req.Email, siteInfo.AllowEmailDomains) {
handler.HandleResponse(ctx, errors.BadRequest(reason.EmailIllegalDomainError), nil)
return
}
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass {
+1
View File
@@ -9,4 +9,5 @@ var ProviderSetController = wire.NewSet(
NewThemeController,
NewSiteInfoController,
NewRoleController,
NewPluginController,
)
@@ -0,0 +1,184 @@
package controller_admin
import (
"encoding/json"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/plugin_common"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
)
// PluginController role controller
type PluginController struct {
PluginCommonService *plugin_common.PluginCommonService
}
// NewPluginController new controller
func NewPluginController(PluginCommonService *plugin_common.PluginCommonService) *PluginController {
return &PluginController{PluginCommonService: PluginCommonService}
}
// GetPluginList get plugin list
// @Summary get plugin list
// @Description get plugin list
// @Tags AdminPlugin
// @Security ApiKeyAuth
// @Accept json
// @Produce json
// @Param status query string false "status: active/inactive"
// @Param have_config query boolean false "have config"
// @Success 200 {object} handler.RespBody{data=[]schema.GetPluginListResp}
// @Router /answer/admin/api/plugins [get]
func (pc *PluginController) GetPluginList(ctx *gin.Context) {
req := &schema.GetPluginListReq{}
if handler.BindAndCheck(ctx, req) {
return
}
pluginConfigMapping := make(map[string]bool)
_ = plugin.CallConfig(func(fn plugin.Config) error {
if len(fn.ConfigFields()) > 0 {
pluginConfigMapping[fn.Info().SlugName] = true
}
return nil
})
resp := make([]*schema.GetPluginListResp, 0)
_ = plugin.CallBase(func(base plugin.Base) error {
info := base.Info()
resp = append(resp, &schema.GetPluginListResp{
Name: info.Name.Translate(ctx),
SlugName: info.SlugName,
Description: info.Description.Translate(ctx),
Version: info.Version,
Enabled: plugin.StatusManager.IsEnabled(info.SlugName),
HaveConfig: pluginConfigMapping[info.SlugName],
Link: info.Link,
})
return nil
})
if len(req.Status) > 0 {
resp = pc.filterPluginByStatus(resp, req.Status)
}
if req.HaveConfig {
resp = pc.filterNoConfigPlugin(resp)
}
handler.HandleResponse(ctx, nil, resp)
}
func (pc *PluginController) filterNoConfigPlugin(list []*schema.GetPluginListResp) []*schema.GetPluginListResp {
resp := make([]*schema.GetPluginListResp, 0)
for _, t := range list {
if t.HaveConfig {
resp = append(resp, t)
}
}
return resp
}
func (pc *PluginController) filterPluginByStatus(list []*schema.GetPluginListResp, status schema.PluginStatus,
) []*schema.GetPluginListResp {
resp := make([]*schema.GetPluginListResp, 0)
for _, t := range list {
if status == schema.PluginStatusActive && t.Enabled {
resp = append(resp, t)
} else if status == schema.PluginStatusInactive && !t.Enabled {
resp = append(resp, t)
}
}
return resp
}
// UpdatePluginStatus update plugin status
// @Summary update plugin status
// @Description update plugin status
// @Tags AdminPlugin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UpdatePluginStatusReq true "UpdatePluginStatusReq"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/plugin/status [put]
func (pc *PluginController) UpdatePluginStatus(ctx *gin.Context) {
req := &schema.UpdatePluginStatusReq{}
if handler.BindAndCheck(ctx, req) {
return
}
plugin.StatusManager.Enable(req.PluginSlugName, req.Enabled)
err := pc.PluginCommonService.UpdatePluginStatus(ctx)
handler.HandleResponse(ctx, err, nil)
}
// GetPluginConfig get plugin config
// @Summary get plugin config
// @Description get plugin config
// @Tags AdminPlugin
// @Security ApiKeyAuth
// @Produce json
// @Param plugin_slug_name query string true "plugin_slug_name"
// @Success 200 {object} handler.RespBody{data=schema.GetPluginConfigResp}
// @Router /answer/admin/api/plugin/config [get]
func (pc *PluginController) GetPluginConfig(ctx *gin.Context) {
req := &schema.GetPluginConfigReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp := &schema.GetPluginConfigResp{}
_ = plugin.CallBase(func(base plugin.Base) error {
if base.Info().SlugName != req.PluginSlugName {
return nil
}
info := base.Info()
resp.Name = info.Name.Translate(ctx)
resp.SlugName = info.SlugName
resp.Description = info.Description.Translate(ctx)
resp.Version = info.Version
return nil
})
_ = plugin.CallConfig(func(fn plugin.Config) error {
if fn.Info().SlugName != req.PluginSlugName {
return nil
}
resp.SetConfigFields(ctx, fn.ConfigFields())
return nil
})
handler.HandleResponse(ctx, nil, resp)
}
// UpdatePluginConfig update plugin config
// @Summary update plugin config
// @Description update plugin config
// @Tags AdminPlugin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.UpdatePluginConfigReq true "UpdatePluginConfigReq"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/plugin/config [put]
func (pc *PluginController) UpdatePluginConfig(ctx *gin.Context) {
req := &schema.UpdatePluginConfigReq{}
if handler.BindAndCheck(ctx, req) {
return
}
configFields, _ := json.Marshal(req.ConfigFields)
err := plugin.CallConfig(func(fn plugin.Config) error {
if fn.Info().SlugName == req.PluginSlugName {
return fn.ConfigReceiver(configFields)
}
return nil
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
err = pc.PluginCommonService.UpdatePluginConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -139,6 +139,19 @@ func (sc *SiteInfoController) GetSiteTheme(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// GetSiteUsers get site user config
// @Summary get site user config
// @Description get site user config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteUsersResp}
// @Router /answer/admin/api/siteinfo/users [get]
func (sc *SiteInfoController) GetSiteUsers(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteUsers(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetRobots get site robots information
// @Summary get site robots information
// @Description get site robots information
@@ -336,6 +349,24 @@ func (sc *SiteInfoController) SaveSiteTheme(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UpdateSiteUsers update site config about users
// @Summary update site info config about users
// @Description update site info config about users
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteUsersReq true "users info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/users [put]
func (sc *SiteInfoController) UpdateSiteUsers(ctx *gin.Context) {
req := &schema.SiteUsersReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteUsers(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetSMTPConfig get smtp config
// @Summary GetSMTPConfig get smtp config
// @Description GetSMTPConfig get smtp config
@@ -366,3 +397,34 @@ func (sc *SiteInfoController) UpdateSMTPConfig(ctx *gin.Context) {
err := sc.siteInfoService.UpdateSMTPConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetPrivilegesConfig get privileges config
// @Summary GetPrivilegesConfig get privileges config
// @Description GetPrivilegesConfig get privileges config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.GetPrivilegesConfigResp}
// @Router /answer/admin/api/setting/privileges [get]
func (sc *SiteInfoController) GetPrivilegesConfig(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetPrivilegesConfig(ctx)
handler.HandleResponse(ctx, err, resp)
}
// UpdatePrivilegesConfig update privileges config
// @Summary update privileges config
// @Description update privileges config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.UpdatePrivilegesConfigReq true "config"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/setting/privileges [put]
func (sc *SiteInfoController) UpdatePrivilegesConfig(ctx *gin.Context) {
req := &schema.UpdatePrivilegesConfigReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.UpdatePrivilegesConfig(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -3,9 +3,12 @@ package controller_admin
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/user_admin"
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
)
// UserAdminController user controller
@@ -29,6 +32,10 @@ func NewUserAdminController(userService *user_admin.UserAdminService) *UserAdmin
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/status [put]
func (uc *UserAdminController) UpdateUserStatus(ctx *gin.Context) {
if u, ok := plugin.GetUserCenter(); ok && u.Description().UserStatusAgentEnabled {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
return
}
req := &schema.UpdateUserStatusReq{}
if handler.BindAndCheck(ctx, req) {
return
+1
View File
@@ -6,4 +6,5 @@ type UserCacheInfo struct {
UserStatus int `json:"user_status"`
EmailStatus int `json:"email_status"`
RoleID int `json:"role_id"`
ExternalID string `json:"external_id"`
}
+13
View File
@@ -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"
}
+34 -9
View File
@@ -50,6 +50,8 @@ var tables = []interface{}{
&entity.RolePowerRel{},
&entity.Power{},
&entity.UserRoleRel{},
&entity.PluginConfig{},
&entity.UserExternalLogin{},
}
// InitDB init db
@@ -112,9 +114,8 @@ func initAdminUser(engine *xorm.Engine) error {
func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail string) error {
interfaceData := map[string]string{
"logo": "",
"theme": "black",
"language": language,
"language": language,
"time_zone": "UTC",
}
interfaceDataBytes, _ := json.Marshal(interfaceData)
_, err := engine.InsertOne(&entity.SiteInfo{
@@ -142,8 +143,9 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
}
loginConfig := map[string]bool{
"allow_new_registrations": true,
"login_required": false,
"allow_new_registrations": true,
"allow_email_registrations": true,
"login_required": false,
}
loginConfigDataBytes, _ := json.Marshal(loginConfig)
_, err = engine.InsertOne(&entity.SiteInfo{
@@ -177,6 +179,25 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
if err != nil {
return err
}
usersData := map[string]any{
"default_avatar": "gravatar",
"allow_update_display_name": true,
"allow_update_username": true,
"allow_update_avatar": true,
"allow_update_bio": true,
"allow_update_website": true,
"allow_update_location": true,
}
usersDataBytes, _ := json.Marshal(usersData)
_, err = engine.InsertOne(&entity.SiteInfo{
Type: "users",
Content: string(usersDataBytes),
Status: 1,
})
if err != nil {
return err
}
return err
}
@@ -345,10 +366,14 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
{ID: 118, Key: "plugin.status", Value: `{}`},
{ID: 119, Key: "question.pin", Value: `-1`},
{ID: 120, Key: "question.unpin", Value: `-1`},
{ID: 121, Key: "question.show", Value: `-1`},
{ID: 122, Key: "question.hide", Value: `-1`},
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
}
_, err := engine.Insert(defaultConfigTable)
return err
+4
View File
@@ -58,6 +58,10 @@ var migrations = []Migration{
NewMigration("add new answer notification", addNewAnswerNotification, true),
NewMigration("add user pin hide features", addRolePinAndHideFeatures, true),
NewMigration("update accept answer rank", updateAcceptAnswerRank, true),
NewMigration("add plugin", addPlugin, false),
NewMigration("update user pin hide features", updateRolePinAndHideFeatures, true),
NewMigration("update question post time", updateQuestionPostTime, true),
NewMigration("add login limitations", addLoginLimitations, true),
}
// GetCurrentDBVersion returns the current db version
+69
View File
@@ -0,0 +1,69 @@
package migrations
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/tidwall/gjson"
"xorm.io/xorm"
)
func addLoginLimitations(x *xorm.Engine) error {
loginSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeLogin,
}
exist, err := x.Get(loginSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
content := &schema.SiteLoginReq{}
_ = json.Unmarshal([]byte(loginSiteInfo.Content), content)
content.AllowEmailRegistrations = true
content.AllowEmailDomains = make([]string, 0)
_, err = x.ID(loginSiteInfo.ID).Cols("content").Update(loginSiteInfo)
if err != nil {
return fmt.Errorf("update site info failed: %w", err)
}
}
interfaceSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeInterface,
}
exist, err = x.Get(interfaceSiteInfo)
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
siteUsers := &schema.SiteUsersReq{
AllowUpdateDisplayName: true,
AllowUpdateUsername: true,
AllowUpdateAvatar: true,
AllowUpdateBio: true,
AllowUpdateWebsite: true,
AllowUpdateLocation: true,
}
if exist {
siteUsers.DefaultAvatar = gjson.Get(interfaceSiteInfo.Content, "default_avatar").String()
}
data, _ := json.Marshal(siteUsers)
exist, err = x.Get(&entity.SiteInfo{Type: constant.SiteTypeUsers})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
usersSiteInfo := &entity.SiteInfo{
Type: constant.SiteTypeUsers,
Content: string(data),
Status: 1,
}
_, err = x.InsertOne(usersSiteInfo)
if err != nil {
return fmt.Errorf("insert site info failed: %w", err)
}
}
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package migrations
import (
"fmt"
"github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func updateRolePinAndHideFeatures(x *xorm.Engine) error {
defaultConfigTable := []*entity.Config{
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package migrations
import (
"fmt"
"github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func updateQuestionPostTime(x *xorm.Engine) error {
questionList := make([]entity.Question, 0)
err := x.Find(&questionList, &entity.Question{})
if err != nil {
return fmt.Errorf("get questions failed: %w", err)
}
for _, item := range questionList {
if item.PostUpdateTime.IsZero() {
if !item.UpdatedAt.IsZero() {
item.PostUpdateTime = item.UpdatedAt
} else if !item.CreatedAt.IsZero() {
item.PostUpdateTime = item.CreatedAt
}
if _, err = x.Update(item, &entity.Question{ID: item.ID}); err != nil {
log.Errorf("update %+v config failed: %s", item, err)
return fmt.Errorf("update question failed: %w", err)
}
}
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package migrations
import (
"fmt"
"github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addPlugin(x *xorm.Engine) error {
defaultConfigTable := []*entity.Config{
{ID: 118, Key: "plugin.status", Value: `{}`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return x.Sync(new(entity.PluginConfig), new(entity.UserExternalLogin))
}
+10 -6
View File
@@ -63,18 +63,22 @@ func addRolePinAndHideFeatures(x *xorm.Engine) error {
}
defaultConfigTable := []*entity.Config{
{ID: 119, Key: "question.pin", Value: `-1`},
{ID: 120, Key: "question.unpin", Value: `-1`},
{ID: 121, Key: "question.show", Value: `-1`},
{ID: 122, Key: "question.hide", Value: `-1`},
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
{ID: 123, Key: "rank.question.pin", Value: `-1`},
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
{ID: 125, Key: "rank.question.show", Value: `-1`},
{ID: 126, Key: "rank.question.hide", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
exist, err := x.Get(&entity.Config{ID: c.ID})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
+4 -2
View File
@@ -202,7 +202,9 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
msg.TriggerUserID = questionUserID
msg.ObjectType = constant.AnswerObjectType
}
notice_queue.AddNotification(msg)
if msg.TriggerUserID != msg.ReceiverUserID {
notice_queue.AddNotification(msg)
}
}
for _, act := range addActivityList {
@@ -214,7 +216,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
if act.UserID != questionUserID {
msg.TriggerUserID = questionUserID
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.AcceptAnswer
msg.NotificationAction = constant.NotificationAcceptAnswer
notice_queue.AddNotification(msg)
}
}
+52 -5
View File
@@ -5,6 +5,7 @@ import (
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/internal/base/pager"
@@ -70,7 +71,9 @@ var LimitDownActions = map[string][]string{
func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
notificationUserIDs := make([]string, 0)
achievementNotificationUserIDs := make([]string, 0)
sendInboxNotification := false
upVote := false
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
result = nil
for _, action := range actions {
@@ -127,7 +130,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
if isReachStandard {
insertActivity.Rank = 0
}
notificationUserIDs = append(notificationUserIDs, activityUserID)
achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
}
if has {
@@ -142,13 +145,17 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
if err != nil {
return nil, err
}
sendInboxNotification = true
}
// update votes
if action == "vote_down" || action == "vote_up" {
if action == constant.ActVoteDown || action == constant.ActVoteUp {
votes := 1
if action == "vote_down" {
if action == constant.ActVoteDown {
upVote = false
votes = -1
} else {
upVote = true
}
err = vr.updateVotes(ctx, session, objectID, votes)
if err != nil {
@@ -165,9 +172,12 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
for _, activityUserID := range notificationUserIDs {
for _, activityUserID := range achievementNotificationUserIDs {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
}
if sendInboxNotification {
vr.sendVoteInboxNotification(userID, objectUserID, objectID, upVote)
}
return
}
@@ -441,3 +451,40 @@ func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, object
}
notice_queue.AddNotification(msg)
}
func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, objectID string, upvote bool) {
if triggerUserID == receiverUserID {
return
}
objectType, _ := obj.GetObjectTypeStrByObjectID(objectID)
msg := &schema.NotificationMsg{
TriggerUserID: triggerUserID,
ReceiverUserID: receiverUserID,
Type: schema.NotificationTypeInbox,
ObjectID: objectID,
ObjectType: objectType,
}
if objectType == constant.QuestionObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheQuestion
} else {
msg.NotificationAction = constant.NotificationDownVotedTheQuestion
}
}
if objectType == constant.AnswerObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheAnswer
} else {
msg.NotificationAction = constant.NotificationDownVotedTheAnswer
}
}
if objectType == constant.CommentObjectType {
if upvote {
msg.NotificationAction = constant.NotificationUpVotedTheComment
}
}
if len(msg.NotificationAction) > 0 {
notice_queue.AddNotification(msg)
}
}
+4 -1
View File
@@ -148,7 +148,7 @@ func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken
}
// RemoveUserTokens Log out all users under this user id
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) {
key := constant.UserTokenMappingCacheKey + userID
resp, _ := ar.data.Cache.GetString(ctx, key)
mapping := make(map[string]bool, 0)
@@ -158,6 +158,9 @@ func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
}
for token := range mapping {
if token == remainToken {
continue
}
if err := ar.RemoveUserCacheInfo(ctx, token); err != nil {
log.Error(err)
} else {
@@ -0,0 +1,49 @@
package plugin_config
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/plugin_common"
"github.com/segmentfault/pacman/errors"
)
type pluginConfigRepo struct {
data *data.Data
}
// NewPluginConfigRepo new repository
func NewPluginConfigRepo(data *data.Data) plugin_common.PluginConfigRepo {
return &pluginConfigRepo{
data: data,
}
}
func (ur *pluginConfigRepo) SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error) {
old := &entity.PluginConfig{PluginSlugName: pluginSlugName}
exist, err := ur.data.DB.Get(old)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
old.Value = configValue
_, err = ur.data.DB.ID(old.ID).Update(old)
} else {
_, err = ur.data.DB.InsertOne(&entity.PluginConfig{PluginSlugName: pluginSlugName, Value: configValue})
}
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (ur *pluginConfigRepo) GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error) {
pluginConfigs = make([]*entity.PluginConfig, 0)
err = ur.data.DB.Find(&pluginConfigs)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return pluginConfigs, err
}
+4
View File
@@ -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,
)
+4 -1
View File
@@ -273,7 +273,7 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
}
// GetQuestionPage query question page
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
questionList []*entity.Question, total int64, err error) {
questionList = make([]*entity.Question, 0)
@@ -289,6 +289,9 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
} else {
session.And("question.show = ?", entity.QuestionShow)
}
if inDays > 0 {
session.And("question.created_at > ?", time.Now().AddDate(0, 0, -inDays))
}
switch orderCond {
case "newest":
+5
View File
@@ -9,6 +9,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/plugin"
"github.com/jinzhu/now"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@@ -36,6 +37,10 @@ func NewUserRankRepo(data *data.Data, configRepo config.ConfigRepo) rank.UserRan
func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
session *xorm.Session, userID string, deltaRank int, activityType int,
) (isReachStandard bool, err error) {
// IMPORTANT: If user center enabled the rank agent, then we should not change user rank.
if plugin.RankAgentEnabled() {
return false, nil
}
if deltaRank == 0 {
return false, nil
}
+1 -1
View File
@@ -46,7 +46,7 @@ func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tag
// GetTagBySlugName get tag by slug name
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
tagInfo = &entity.Tag{}
session := tr.data.DB.Where("slug_name = LOWER(?)", slugName)
session := tr.data.DB.Where("LOWER(slug_name) = ?", slugName)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tagInfo)
if err != nil {
+17
View File
@@ -86,6 +86,13 @@ func (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *
if err != nil {
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
if err != nil {
return nil, false, err
}
return
}
@@ -96,6 +103,14 @@ func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (
Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
if !exist {
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
if err != nil {
return nil, false, err
}
return
}
@@ -127,6 +142,8 @@ func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, us
total, err = pager.Help(page, pageSize, &users, user, session)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, users)
return
}
+143 -6
View File
@@ -7,9 +7,14 @@ import (
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/config"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
// userRepo user repository
@@ -28,10 +33,21 @@ func NewUserRepo(data *data.Data, configRepo config.ConfigRepo) usercommon.UserR
// AddUser add user
func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
_, err = ur.data.DB.Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) {
userInfo := &entity.User{}
exist, err := session.Where("username = ?", user.Username).Get(userInfo)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
return nil, errors.InternalServer(reason.UsernameDuplicate)
}
_, err = session.Insert(user)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil, nil
})
return
}
@@ -145,6 +161,11 @@ func (ur *userRepo) GetByUserID(ctx context.Context, userID string) (userInfo *e
exist, err = ur.data.DB.Where("id = ?", userID).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)
if err != nil {
return nil, false, err
}
return
}
@@ -155,6 +176,7 @@ func (ur *userRepo) BatchGetByID(ctx context.Context, ids []string) ([]*entity.U
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
tryToDecorateUserListFromUserCenter(ctx, ur.data, list)
return list, nil
}
@@ -164,6 +186,11 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
exist, err = ur.data.DB.Where("username = ?", username).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return
}
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, userInfo)
if err != nil {
return nil, false, err
}
return
}
@@ -179,11 +206,121 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent
return
}
func (vr *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.User, 0)
count, err = vr.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
count, err = ur.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {
if original == nil {
return nil
}
uc, ok := plugin.GetUserCenter()
if !ok {
return nil
}
userInfo := &entity.UserExternalLogin{}
session := data.DB.Where("user_id = ?", original.ID)
session.Where("provider = ?", uc.Info().SlugName)
exist, err := session.Get(userInfo)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if !exist {
return nil
}
userCenterBasicUserInfo, err := uc.UserInfo(userInfo.ExternalID)
if err != nil {
log.Error(err)
return errors.BadRequest(reason.UserNotFound).WithError(err).WithStack()
}
// In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies.
if original.Username != userCenterBasicUserInfo.Username {
log.Warnf("user %s username is inconsistent with user center", original.ID)
}
decorateByUserCenterUser(original, userCenterBasicUserInfo)
return nil
}
func tryToDecorateUserListFromUserCenter(ctx context.Context, data *data.Data, original []*entity.User) {
uc, ok := plugin.GetUserCenter()
if !ok {
return
}
ids := make([]string, 0)
originalUserIDMapping := make(map[string]*entity.User, 0)
for _, user := range original {
originalUserIDMapping[user.ID] = user
ids = append(ids, user.ID)
}
userExternalLoginList := make([]*entity.UserExternalLogin, 0)
session := data.DB.Where("provider = ?", uc.Info().SlugName)
session.In("user_id", ids)
err := session.Find(&userExternalLoginList)
if err != nil {
log.Error(err)
return
}
userExternalIDs := make([]string, 0)
originalExternalIDMapping := make(map[string]*entity.User, 0)
for _, u := range userExternalLoginList {
originalExternalIDMapping[u.ExternalID] = originalUserIDMapping[u.UserID]
userExternalIDs = append(userExternalIDs, u.ExternalID)
}
if len(userExternalIDs) == 0 {
return
}
ucUsers, err := uc.UserList(userExternalIDs)
if err != nil {
log.Errorf("get user list from user center failed: %v, %v", err, userExternalIDs)
return
}
for _, ucUser := range ucUsers {
decorateByUserCenterUser(originalExternalIDMapping[ucUser.ExternalID], ucUser)
}
}
func decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBasicUserInfo) {
if original == nil || ucUser == nil {
return
}
// In general, usernames should be guaranteed unique by the User Center plugin, so there are no inconsistencies.
if original.Username != ucUser.Username {
log.Warnf("user %s username is inconsistent with user center", original.ID)
}
if len(ucUser.DisplayName) > 0 {
original.DisplayName = ucUser.DisplayName
}
if len(ucUser.Email) > 0 {
original.EMail = ucUser.Email
}
if len(ucUser.Avatar) > 0 {
original.Avatar = schema.CustomAvatar(ucUser.Avatar).ToJsonString()
}
if len(ucUser.Mobile) > 0 {
original.Mobile = ucUser.Mobile
}
if len(ucUser.Bio) > 0 {
original.BioHTML = converter.Markdown2HTML(ucUser.Bio) + original.BioHTML
}
// If plugin enable rank agent, use rank from user center.
if plugin.RankAgentEnabled() {
original.Rank = ucUser.Rank
}
if ucUser.Status != plugin.UserStatusAvailable {
original.Status = int(ucUser.Status)
}
}
@@ -0,0 +1,94 @@
package user_external_login
import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/segmentfault/pacman/errors"
)
type userExternalLoginRepo struct {
data *data.Data
}
// NewUserExternalLoginRepo new repository
func NewUserExternalLoginRepo(data *data.Data) user_external_login.UserExternalLoginRepo {
return &userExternalLoginRepo{
data: data,
}
}
// AddUserExternalLogin add external login information
func (ur *userExternalLoginRepo) AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error) {
_, err = ur.data.DB.Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateInfo update user info
func (ur *userExternalLoginRepo) UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error) {
_, err = ur.data.DB.ID(userInfo.ID).Update(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetByExternalID get by external ID
func (ur *userExternalLoginRepo) GetByExternalID(ctx context.Context, provider, externalID string) (
userInfo *entity.UserExternalLogin, exist bool, err error) {
userInfo = &entity.UserExternalLogin{}
exist, err = ur.data.DB.Where("external_id = ?", externalID).Where("provider = ?", provider).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserExternalLoginList get by external ID
func (ur *userExternalLoginRepo) GetUserExternalLoginList(ctx context.Context, userID string) (
resp []*entity.UserExternalLogin, err error) {
resp = make([]*entity.UserExternalLogin, 0)
err = ur.data.DB.Where("user_id = ?", userID).Find(&resp)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// DeleteUserExternalLogin delete external user login info
func (ur *userExternalLoginRepo) DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error) {
cond := &entity.UserExternalLogin{}
_, err = ur.data.DB.Where("user_id = ? AND external_id = ?", userID, externalID).Delete(cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// SetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(
ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) {
cacheData, _ := json.Marshal(info)
return ur.data.Cache.SetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key,
string(cacheData), constant.ConnectorUserExternalInfoCacheTime)
}
// GetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) GetCacheUserExternalLoginInfo(
ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error) {
res, err := ur.data.Cache.GetString(ctx, constant.ConnectorUserExternalInfoCacheKey+key)
if err != nil {
return info, err
}
_ = json.Unmarshal([]byte(res), &info)
return info, nil
}
+41 -26
View File
@@ -1,6 +1,7 @@
package router
import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/controller_admin"
"github.com/gin-gonic/gin"
@@ -31,6 +32,7 @@ type AnswerAPIRouter struct {
uploadController *controller.UploadController
activityController *controller.ActivityController
roleController *controller_admin.RoleController
pluginController *controller_admin.PluginController
}
func NewAnswerAPIRouter(
@@ -58,6 +60,7 @@ func NewAnswerAPIRouter(
uploadController *controller.UploadController,
activityController *controller.ActivityController,
roleController *controller_admin.RoleController,
pluginController *controller_admin.PluginController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@@ -84,6 +87,7 @@ func NewAnswerAPIRouter(
uploadController: uploadController,
activityController: activityController,
roleController: roleController,
pluginController: pluginController,
}
}
@@ -97,37 +101,38 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
// user
r.POST("/user/login/email", a.userController.UserEmailLogin)
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
r.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
r.POST("/user/email/verification", a.userController.UserVerifyEmail)
r.PUT("/user/email", a.userController.UserChangeEmailVerify)
r.GET("/user/action/record", a.userController.ActionRecord)
r.POST("/user/password/reset", a.userController.RetrievePassWord)
r.POST("/user/password/replacement", a.userController.UseRePassWord)
r.GET("/user/info", a.userController.GetUserInfoByUserID)
r.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
routerGroup := r.Group("", middleware.BanAPIForUserCenter)
routerGroup.POST("/user/login/email", a.userController.UserEmailLogin)
routerGroup.POST("/user/register/email", a.userController.UserRegisterByEmail)
routerGroup.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
routerGroup.POST("/user/email/verification", a.userController.UserVerifyEmail)
routerGroup.PUT("/user/email", a.userController.UserChangeEmailVerify)
routerGroup.GET("/user/action/record", a.userController.ActionRecord)
routerGroup.POST("/user/password/reset", a.userController.RetrievePassWord)
routerGroup.POST("/user/password/replacement", a.userController.UseRePassWord)
routerGroup.PUT("/user/email/notification", a.userController.UserUnsubscribeEmailNotification)
}
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// user
r.GET("/user/logout", a.userController.UserLogout)
r.POST("/user/email/change/code", a.userController.UserChangeEmailSendCode)
r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend)
r.POST("/user/email/change/code", middleware.BanAPIForUserCenter, a.userController.UserChangeEmailSendCode)
r.POST("/user/email/verification/send", middleware.BanAPIForUserCenter, a.userController.UserVerifyEmailSend)
r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
r.GET("/user/ranking", a.userController.UserRanking)
//answer
r.GET("/answer/info", a.answerController.Get)
r.GET("/answer/page", a.answerController.AnswerList)
r.GET("/personal/answer/page", a.questionController.UserAnswerList)
r.GET("/personal/answer/page", a.questionController.PersonalAnswerPage)
//question
r.GET("/question/info", a.questionController.GetQuestion)
r.GET("/question/page", a.questionController.QuestionPage)
r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
r.GET("/personal/qa/top", a.questionController.UserTop)
r.GET("/personal/question/page", a.questionController.UserList)
r.GET("/personal/question/page", a.questionController.PersonalQuestionPage)
// comment
r.GET("/comment/page", a.commentController.GetCommentWithPage)
@@ -182,7 +187,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// collection
r.POST("/collection/switch", a.collectionController.CollectionSwitch)
r.GET("/personal/collection/page", a.questionController.UserCollectionList)
r.GET("/personal/collection/page", a.questionController.PersonalCollectionPage)
// question
r.POST("/question", a.questionController.AddQuestion)
@@ -201,7 +206,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.DELETE("/answer", a.answerController.RemoveAnswer)
// user
r.PUT("/user/password", a.userController.UserModifyPassWord)
r.PUT("/user/password", middleware.BanAPIForUserCenter, a.userController.UserModifyPassWord)
r.PUT("/user/info", a.userController.UserUpdateInfo)
r.PUT("/user/interface", a.userController.UserUpdateInterface)
r.POST("/user/notice/set", a.userController.UserNoticeSet)
@@ -257,29 +262,39 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
// siteinfo
r.GET("/siteinfo/general", a.siteInfoController.GetGeneral)
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
r.GET("/siteinfo/seo", a.siteInfoController.GetSeo)
r.GET("/siteinfo/login", a.siteInfoController.GetSiteLogin)
r.GET("/siteinfo/custom-css-html", a.siteInfoController.GetSiteCustomCssHTML)
r.GET("/siteinfo/theme", a.siteInfoController.GetSiteTheme)
r.PUT("/siteinfo/general", a.siteInfoController.UpdateGeneral)
r.GET("/siteinfo/interface", a.siteInfoController.GetInterface)
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
r.GET("/siteinfo/branding", a.siteInfoController.GetSiteBranding)
r.PUT("/siteinfo/branding", a.siteInfoController.UpdateBranding)
r.GET("/siteinfo/write", a.siteInfoController.GetSiteWrite)
r.PUT("/siteinfo/write", a.siteInfoController.UpdateSiteWrite)
r.GET("/siteinfo/legal", a.siteInfoController.GetSiteLegal)
r.PUT("/siteinfo/legal", a.siteInfoController.UpdateSiteLegal)
r.PUT("/siteinfo/login", a.siteInfoController.UpdateSiteLogin)
r.PUT("/siteinfo/custom-css-html", a.siteInfoController.UpdateSiteCustomCssHTML)
r.PUT("/siteinfo/theme", a.siteInfoController.SaveSiteTheme)
r.GET("/siteinfo/seo", a.siteInfoController.GetSeo)
r.PUT("/siteinfo/seo", a.siteInfoController.UpdateSeo)
r.GET("/siteinfo/login", a.siteInfoController.GetSiteLogin)
r.PUT("/siteinfo/login", a.siteInfoController.UpdateSiteLogin)
r.GET("/siteinfo/custom-css-html", a.siteInfoController.GetSiteCustomCssHTML)
r.PUT("/siteinfo/custom-css-html", a.siteInfoController.UpdateSiteCustomCssHTML)
r.GET("/siteinfo/theme", a.siteInfoController.GetSiteTheme)
r.PUT("/siteinfo/theme", a.siteInfoController.SaveSiteTheme)
r.GET("/siteinfo/users", a.siteInfoController.GetSiteUsers)
r.PUT("/siteinfo/users", a.siteInfoController.UpdateSiteUsers)
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
r.GET("/setting/privileges", a.siteInfoController.GetPrivilegesConfig)
r.PUT("/setting/privileges", a.siteInfoController.UpdatePrivilegesConfig)
// dashboard
r.GET("/dashboard", a.dashboardController.DashboardInfo)
// roles
r.GET("/roles", a.roleController.GetRoleList)
// plugin
r.GET("/plugins", a.pluginController.GetPluginList)
r.PUT("/plugin/status", a.pluginController.UpdatePluginStatus)
r.GET("/plugin/config", a.pluginController.GetPluginConfig)
r.PUT("/plugin/config", a.pluginController.UpdatePluginConfig)
}
+50
View File
@@ -0,0 +1,50 @@
package router
import (
"github.com/answerdev/answer/internal/controller"
"github.com/gin-gonic/gin"
)
type PluginAPIRouter struct {
connectorController *controller.ConnectorController
userCenterController *controller.UserCenterController
}
func NewPluginAPIRouter(
connectorController *controller.ConnectorController,
userCenterController *controller.UserCenterController,
) *PluginAPIRouter {
return &PluginAPIRouter{
connectorController: connectorController,
userCenterController: userCenterController,
}
}
func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) {
// connector plugin
connectorController := pr.connectorController
r.GET(controller.ConnectorLoginRouterPrefix+":name", connectorController.ConnectorLoginDispatcher)
r.GET(controller.ConnectorRedirectRouterPrefix+":name", connectorController.ConnectorRedirectDispatcher)
r.GET("/connector/info", connectorController.ConnectorsInfo)
r.POST("/connector/binding/email", connectorController.ExternalLoginBindingUserSendEmail)
// user center plugin
r.GET("/user-center/agent", pr.userCenterController.UserCenterAgent)
r.GET("/user-center/personal/branding", pr.userCenterController.UserCenterPersonalBranding)
r.GET(controller.UserCenterLoginRouter, pr.userCenterController.UserCenterLoginRedirect)
r.GET(controller.UserCenterSignUpRedirectRouter, pr.userCenterController.UserCenterSignUpRedirect)
r.GET("/user-center/login/callback", pr.userCenterController.UserCenterLoginCallback)
r.GET("/user-center/sign-up/callback", pr.userCenterController.UserCenterSignUpCallback)
}
func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) {
connectorController := pr.connectorController
r.GET("/connector/user/info", connectorController.ConnectorsUserInfo)
r.DELETE("/connector/user/unbinding", connectorController.ExternalLoginUnbinding)
r.GET("/user-center/user/settings", pr.userCenterController.UserCenterUserSettings)
}
func (pr *PluginAPIRouter) RegisterAuthAdminConnectorRouter(r *gin.RouterGroup) {
r.GET("/user-center/agent", pr.userCenterController.UserCenterAdminFunctionAgent)
}
+8 -1
View File
@@ -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,
)
+15
View File
@@ -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"`
}
+11 -8
View File
@@ -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 {
+141
View File
@@ -0,0 +1,141 @@
package schema
import (
"github.com/answerdev/answer/plugin"
"github.com/gin-gonic/gin"
)
const (
PluginStatusActive PluginStatus = "active"
PluginStatusInactive PluginStatus = "inactive"
)
type PluginStatus string
type GetPluginListReq struct {
Status PluginStatus `form:"status"`
HaveConfig bool `form:"have_config"`
}
type GetPluginListResp struct {
Name string `json:"name"`
SlugName string `json:"slug_name"`
Description string `json:"description"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
HaveConfig bool `json:"have_config"`
Link string `json:"link"`
}
type UpdatePluginStatusReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
Enabled bool `json:"enabled"`
}
type GetPluginConfigReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" form:"plugin_slug_name"`
}
type GetPluginConfigResp struct {
Name string `json:"name"`
SlugName string `json:"slug_name"`
Description string `json:"description"`
Version string `json:"version"`
ConfigFields []ConfigField `json:"config_fields"`
}
func (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.ConfigField) {
for _, field := range fields {
configField := ConfigField{
Name: field.Name,
Type: string(field.Type),
Title: field.Title.Translate(ctx),
Description: field.Description.Translate(ctx),
Required: field.Required,
Value: field.Value,
UIOptions: ConfigFieldUIOptions{
Rows: field.UIOptions.Rows,
InputType: string(field.UIOptions.InputType),
Variant: field.UIOptions.Variant,
},
}
configField.UIOptions.Placeholder = field.UIOptions.Placeholder.Translate(ctx)
configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)
configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx)
if field.UIOptions.Action != nil {
uiOptionAction := &UIOptionAction{
Url: field.UIOptions.Action.Url,
Method: field.UIOptions.Action.Method,
}
if field.UIOptions.Action.Loading != nil {
uiOptionAction.Loading = &LoadingAction{
Text: field.UIOptions.Action.Loading.Text.Translate(ctx),
State: string(field.UIOptions.Action.Loading.State),
}
}
if field.UIOptions.Action.OnComplete != nil {
uiOptionAction.OnCompleteAction = &OnCompleteAction{
ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage,
RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig,
}
}
configField.UIOptions.Action = uiOptionAction
}
for _, option := range field.Options {
configField.Options = append(configField.Options, ConfigFieldOption{
Label: option.Label.Translate(ctx),
Value: option.Value,
})
}
g.ConfigFields = append(g.ConfigFields, configField)
}
}
type ConfigField struct {
Name string `json:"name"`
Type string `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
Required bool `json:"required"`
Value any `json:"value"`
UIOptions ConfigFieldUIOptions `json:"ui_options"`
Options []ConfigFieldOption `json:"options,omitempty"`
}
type ConfigFieldUIOptions struct {
Placeholder string `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType string `json:"input_type,omitempty"`
Label string `json:"label,omitempty"`
Action *UIOptionAction `json:"action,omitempty"`
Variant string `json:"variant,omitempty"`
Text string `json:"text,omitempty"`
}
type ConfigFieldOption struct {
Label string `json:"label"`
Value string `json:"value"`
}
type UIOptionAction struct {
Url string `json:"url"`
Method string `json:"method,omitempty"`
Loading *LoadingAction `json:"loading,omitempty"`
OnCompleteAction *OnCompleteAction `json:"on_complete,omitempty"`
}
type LoadingAction struct {
Text string `json:"text"`
State string `json:"state"`
}
type OnCompleteAction struct {
ToastReturnMessage bool `json:"toast_return_message"`
RefreshFormConfig bool `json:"refresh_form_config"`
}
type UpdatePluginConfigReq struct {
PluginSlugName string `validate:"required,gt=1,lte=100" json:"plugin_slug_name"`
ConfigFields map[string]any `json:"config_fields"`
}
+35
View File
@@ -0,0 +1,35 @@
package schema
type UserCenterAgentResp struct {
Enabled bool `json:"enabled"`
AgentInfo *AgentInfo `json:"agent_info"`
}
type AgentInfo struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Icon string `json:"icon"`
Url string `json:"url"`
LoginRedirectURL string `json:"login_redirect_url"`
SignUpRedirectURL string `json:"sign_up_redirect_url"`
ControlCenterItems []*ControlCenter `json:"control_center"`
EnabledOriginalUserSystem bool `json:"enabled_original_user_system"`
}
type ControlCenter struct {
Name string `json:"name"`
Label string `json:"label"`
Url string `json:"url"`
}
type UserCenterPersonalBranding struct {
Enabled bool `json:"enabled"`
PersonalBranding []*PersonalBranding `json:"personal_branding"`
}
type PersonalBranding struct {
Icon string `json:"icon"`
Name string `json:"name"`
Label string `json:"label"`
Url string `json:"url"`
}
+23
View File
@@ -297,6 +297,7 @@ type QuestionPageReq struct {
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"`
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
InDays int `validate:"omitempty,min=1" form:"in_days"`
LoginUserID string `json:"-"`
UserIDBeSearched string `json:"-"`
@@ -374,3 +375,25 @@ type SiteMapQuestionInfo struct {
Title string `json:"title"`
UpdateTime string `json:"time"`
}
type PersonalQuestionPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
LoginUserID string `json:"-"`
}
type PersonalAnswerPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
LoginUserID string `json:"-"`
}
type PersonalCollectionPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
UserID string `json:"-"`
}
+109 -9
View File
@@ -6,6 +6,7 @@ import (
"net/mail"
"net/url"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
@@ -42,9 +43,8 @@ func (r *SiteGeneralReq) FormatSiteUrl() {
// SiteInterfaceReq site interface request
type SiteInterfaceReq struct {
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
}
// SiteBrandingReq site branding request
@@ -92,18 +92,32 @@ type GetSiteLegalInfoResp struct {
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"`
}
// SiteUsersReq site users config request
type SiteUsersReq struct {
DefaultAvatar string `validate:"required,oneof=system gravatar" form:"default_avatar" json:"default_avatar"`
AllowUpdateDisplayName bool `form:"allow_update_display_name" json:"allow_update_display_name"`
AllowUpdateUsername bool `form:"allow_update_username" json:"allow_update_username"`
AllowUpdateAvatar bool `form:"allow_update_avatar" json:"allow_update_avatar"`
AllowUpdateBio bool `form:"allow_update_bio" json:"allow_update_bio"`
AllowUpdateWebsite bool `form:"allow_update_website" json:"allow_update_website"`
AllowUpdateLocation bool `form:"allow_update_location" json:"allow_update_location"`
}
// SiteLoginReq site login request
type SiteLoginReq struct {
AllowNewRegistrations bool `json:"allow_new_registrations"`
LoginRequired bool `json:"login_required"`
AllowNewRegistrations bool `json:"allow_new_registrations"`
AllowEmailRegistrations bool `json:"allow_email_registrations"`
LoginRequired bool `json:"login_required"`
AllowEmailDomains []string `json:"allow_email_domains"`
}
// SiteCustomCssHTMLReq site custom css html
type SiteCustomCssHTMLReq struct {
CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"`
CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"`
CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"`
CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"`
CustomHead string `validate:"omitempty,gt=0,lte=65536" json:"custom_head"`
CustomCss string `validate:"omitempty,gt=0,lte=65536" json:"custom_css"`
CustomHeader string `validate:"omitempty,gt=0,lte=65536" json:"custom_header"`
CustomFooter string `validate:"omitempty,gt=0,lte=65536" json:"custom_footer"`
CustomSideBar string `validate:"omitempty,gt=0,lte=65536" json:"custom_sidebar"`
}
// SiteThemeReq site theme config
@@ -127,6 +141,9 @@ type SiteLoginResp SiteLoginReq
// SiteCustomCssHTMLResp site custom css html response
type SiteCustomCssHTMLResp SiteCustomCssHTMLReq
// SiteUsersResp site users response
type SiteUsersResp SiteUsersReq
// SiteThemeResp site theme response
type SiteThemeResp struct {
ThemeOptions []*ThemeOption `json:"theme_options"`
@@ -169,6 +186,7 @@ type SiteInfoResp struct {
Theme *SiteThemeResp `json:"theme"`
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
SiteSeo *SiteSeoReq `json:"site_seo"`
SiteUsers *SiteUsersResp `json:"site_users"`
Version string `json:"version"`
Revision string `json:"revision"`
}
@@ -235,3 +253,85 @@ type GetManifestJsonResp struct {
ThemeColor string `json:"theme_color"`
BackgroundColor string `json:"background_color"`
}
const (
// PrivilegeLevel1 low
PrivilegeLevel1 PrivilegeLevel = 1
// PrivilegeLevel2 medium
PrivilegeLevel2 PrivilegeLevel = 2
// PrivilegeLevel3 high
PrivilegeLevel3 PrivilegeLevel = 3
)
type PrivilegeLevel int
// GetPrivilegesConfigResp get privileges config response
type GetPrivilegesConfigResp struct {
Options []*PrivilegeOption `json:"options"`
SelectedLevel PrivilegeLevel `json:"selected_level"`
}
// PrivilegeOption privilege option
type PrivilegeOption struct {
Level PrivilegeLevel `json:"level"`
LevelDesc string `json:"level_desc"`
Privileges []*constant.Privilege `json:"privileges"`
}
// UpdatePrivilegesConfigReq update privileges config request
type UpdatePrivilegesConfigReq struct {
Level PrivilegeLevel `validate:"required,min=1,max=3" json:"level"`
}
var (
DefaultPrivilegeOptions []*PrivilegeOption
privilegeOptionsLevelMapping = map[string][]int{
constant.RankQuestionAddKey: {1, 1, 1},
constant.RankAnswerAddKey: {1, 1, 1},
constant.RankCommentAddKey: {1, 1, 1},
constant.RankReportAddKey: {1, 1, 1},
constant.RankCommentVoteUpKey: {1, 1, 1},
constant.RankLinkUrlLimitKey: {1, 10, 10},
constant.RankQuestionVoteUpKey: {1, 1, 15},
constant.RankAnswerVoteUpKey: {1, 1, 15},
constant.RankQuestionVoteDownKey: {125, 125, 125},
constant.RankAnswerVoteDownKey: {125, 125, 125},
constant.RankTagAddKey: {1, 750, 1500},
constant.RankTagEditKey: {1, 50, 100},
constant.RankQuestionEditKey: {1, 100, 200},
constant.RankAnswerEditKey: {1, 100, 200},
constant.RankQuestionEditWithoutReviewKey: {1, 1000, 2000},
constant.RankAnswerEditWithoutReviewKey: {1, 1000, 2000},
constant.RankQuestionAuditKey: {1, 1000, 2000},
constant.RankAnswerAuditKey: {1, 1000, 2000},
constant.RankTagAuditKey: {1, 2500, 5000},
constant.RankTagEditWithoutReviewKey: {1, 10000, 20000},
constant.RankTagSynonymKey: {1, 10000, 20000},
}
)
func init() {
DefaultPrivilegeOptions = append(DefaultPrivilegeOptions, &PrivilegeOption{
Level: PrivilegeLevel1,
LevelDesc: reason.PrivilegeLevel1Desc,
}, &PrivilegeOption{
Level: PrivilegeLevel2,
LevelDesc: reason.PrivilegeLevel2Desc,
}, &PrivilegeOption{
Level: PrivilegeLevel3,
LevelDesc: reason.PrivilegeLevel3Desc,
})
for _, option := range DefaultPrivilegeOptions {
for _, privilege := range constant.RankAllPrivileges {
if len(privilegeOptionsLevelMapping[privilege.Key]) == 0 {
continue
}
option.Privileges = append(option.Privileges, &constant.Privilege{
Label: privilege.Label,
Value: privilegeOptionsLevelMapping[privilege.Key][option.Level-1],
Key: privilege.Key,
})
}
}
}
@@ -0,0 +1,81 @@
package schema
// UserExternalLoginResp user external login resp
type UserExternalLoginResp struct {
BindingKey string `json:"binding_key"`
AccessToken string `json:"access_token"`
// ErrMsg error message, if not empty, means login failed and this message should be displayed.
ErrMsg string `json:"-"`
ErrTitle string `json:"-"`
}
// ExternalLoginBindingUserSendEmailReq external login binding user request
type ExternalLoginBindingUserSendEmailReq struct {
BindingKey string `validate:"required,gt=1,lte=100" json:"binding_key"`
Email string `validate:"required,gt=1,lte=512,email" json:"email"`
// If must is true, whatever email if exists, try to bind user.
// If must is false, when email exist, will only be prompted with a warning.
Must bool `json:"must"`
}
// ExternalLoginBindingUserSendEmailResp external login binding user response
type ExternalLoginBindingUserSendEmailResp struct {
EmailExistAndMustBeConfirmed bool `json:"email_exist_and_must_be_confirmed"`
AccessToken string `json:"access_token"`
}
// ExternalLoginBindingUserReq external login binding user request
type ExternalLoginBindingUserReq struct {
Code string `validate:"required,gt=0,lte=500" json:"code"`
Content string `json:"-"`
}
// ExternalLoginBindingUserResp external login binding user response
type ExternalLoginBindingUserResp struct {
AccessToken string `json:"access_token"`
}
// ExternalLoginUserInfoCache external login user info
type ExternalLoginUserInfoCache struct {
// Third party identification
// e.g. facebook, twitter, instagram
Provider string
// required. The unique user ID provided by the third-party login
ExternalID string
// optional. This name is used preferentially during registration
DisplayName string
// optional. This username is used preferentially during registration
Username string
// optional. If email exist will bind the existing user
Email string
// optional. The avatar URL provided by the third-party login platform
Avatar string
// optional. The original user information provided by the third-party login platform
MetaInfo string
// optional. The bio provided by the third-party login platform
Bio string
}
// ExternalLoginUnbindingReq external login unbinding user
type ExternalLoginUnbindingReq struct {
ExternalID string `validate:"required,gt=0,lte=128" json:"external_id"`
UserID string `json:"-"`
}
// UserCenterUserSettingsResp user center user info response
type UserCenterUserSettingsResp struct {
ProfileSettingAgent UserSettingAgent `json:"profile_setting_agent"`
AccountSettingAgent UserSettingAgent `json:"account_setting_agent"`
}
type UserCenterAdminFunctionAgentResp struct {
AllowCreateUser bool `json:"allow_create_user"`
AllowUpdateUserStatus bool `json:"allow_update_user_status"`
AllowUpdateUserPassword bool `json:"allow_update_user_password"`
AllowUpdateUserRole bool `json:"allow_update_user_role"`
}
type UserSettingAgent struct {
Enabled bool `json:"enabled"`
RedirectURL string `json:"redirect_url"`
}
+33 -22
View File
@@ -4,14 +4,12 @@ import (
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/gravatar"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
)
// UserVerifyEmailReq user verify email request
@@ -72,6 +70,8 @@ type GetUserResp struct {
RoleID int `json:"role_id"`
// user status
Status string `json:"status"`
// user have password
HavePassword bool `json:"have_password"`
}
func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
@@ -83,11 +83,13 @@ func (r *GetUserResp) GetFromUserEntity(userInfo *entity.User) {
if ok {
r.Status = statusShow
}
r.HavePassword = len(userInfo.Pass) > 0
}
type GetUserToSetShowResp struct {
*GetUserResp
Avatar *AvatarInfo `json:"avatar"`
Avatar *AvatarInfo `json:"avatar"`
HavePassword bool `json:"have_password"`
}
func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) {
@@ -108,6 +110,12 @@ func (r *GetUserToSetShowResp) GetFromUserEntity(userInfo *entity.User) {
r.Avatar = avatarInfo
}
const (
AvatarTypeDefault = "default"
AvatarTypeGravatar = "gravatar"
AvatarTypeCustom = "custom"
)
func FormatAvatarInfo(avatarJson, email string) (res string) {
defer func() {
if constant.DefaultAvatar == "gravatar" && len(res) == 0 {
@@ -124,15 +132,22 @@ func FormatAvatarInfo(avatarJson, email string) (res string) {
return ""
}
switch avatarInfo.Type {
case "gravatar":
case AvatarTypeGravatar:
return avatarInfo.Gravatar
case "custom":
case AvatarTypeCustom:
return avatarInfo.Custom
default:
return ""
}
}
func CustomAvatar(url string) *AvatarInfo {
return &AvatarInfo{
Type: AvatarTypeCustom,
Custom: url,
}
}
// GetUserStatusResp get user status info
type GetUserStatusResp struct {
// user status
@@ -260,14 +275,14 @@ func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err er
return nil, nil
}
// UserModifyPassWordRequest
type UserModifyPassWordRequest struct {
UserID string `json:"-" ` // user_id
OldPass string `json:"old_pass" ` // old password
Pass string `json:"pass" ` // password
type UserModifyPasswordReq struct {
OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
UserID string `json:"-"`
AccessToken string `json:"-"`
}
func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
// TODO i18n
err = checker.CheckPassword(8, 32, 0, u.Pass)
if err != nil {
@@ -283,7 +298,7 @@ func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorFie
type UpdateInfoRequest struct {
// display_name
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
DisplayName string `validate:"omitempty,gt=0,lte=30" json:"display_name"`
// username
Username string `validate:"omitempty,gt=3,lte=30" json:"username"`
// avatar
@@ -306,17 +321,12 @@ type AvatarInfo struct {
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
}
func (a *AvatarInfo) ToJsonString() string {
data, _ := json.Marshal(a)
return string(data)
}
func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
if len(req.Username) > 0 {
if checker.IsInvalidUsername(req.Username) {
errField := &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
}
errFields = append(errFields, errField)
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
}
req.BioHTML = converter.Markdown2BasicHTML(req.Bio)
return nil, nil
}
@@ -399,6 +409,7 @@ type GetOtherUserInfoResp struct {
type UserChangeEmailSendCodeReq struct {
UserVerifyEmailSendReq
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"`
Pass string `validate:"omitempty,gte=8,lte=32" json:"pass"`
UserID string `json:"-"`
}
+6 -1
View File
@@ -3,6 +3,7 @@ package activity
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/answerdev/answer/internal/base/constant"
@@ -111,7 +112,11 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge
item.Username = "N/A"
item.UserDisplayName = "N/A"
} else {
item.UserID = act.UserID
if act.TriggerUserID > 0 {
item.UserID = fmt.Sprintf("%d", act.TriggerUserID)
} else {
item.UserID = act.UserID
}
}
item.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID)
+3 -3
View File
@@ -476,7 +476,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A
msg.ReceiverUserID = answerInfo.UserID
msg.TriggerUserID = answerInfo.UserID
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.YourAnswerWasDeleted
msg.NotificationAction = constant.NotificationYourAnswerWasDeleted
notice_queue.AddNotification(msg)
return nil
@@ -566,7 +566,7 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU
ObjectID: answerID,
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.UpdateAnswer
msg.NotificationAction = constant.NotificationUpdateAnswer
notice_queue.AddNotification(msg)
}
@@ -583,7 +583,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
ObjectID: answerID,
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.AnswerTheQuestion
msg.NotificationAction = constant.NotificationAnswerTheQuestion
notice_queue.AddNotification(msg)
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
+18 -5
View File
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/token"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/log"
)
@@ -20,7 +21,7 @@ type AuthRepo interface {
SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error
RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error)
AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error)
RemoveUserTokens(ctx context.Context, userID string)
RemoveUserTokens(ctx context.Context, userID string, remainToken string)
}
// AuthService kit service
@@ -42,7 +43,6 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
}
cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID)
if cacheInfo != nil {
log.Debugf("user status updated: %+v", cacheInfo)
userCacheInfo.UserStatus = cacheInfo.UserStatus
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
userCacheInfo.RoleID = cacheInfo.RoleID
@@ -52,6 +52,14 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
return nil, err
}
}
// try to get user status from user center
uc, ok := plugin.GetUserCenter()
if ok && len(userCacheInfo.ExternalID) > 0 {
if userStatus := uc.UserStatus(userCacheInfo.ExternalID); userStatus != plugin.UserStatusAvailable {
userCacheInfo.UserStatus = int(userStatus)
}
}
return userCacheInfo, nil
}
@@ -85,9 +93,14 @@ func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessTo
return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken)
}
// RemoveUserTokens Log out all users under this user id
func (as *AuthService) RemoveUserTokens(ctx context.Context, userID string) {
as.authRepo.RemoveUserTokens(ctx, userID)
// RemoveUserAllTokens Log out all users under this user id
func (as *AuthService) RemoveUserAllTokens(ctx context.Context, userID string) {
as.authRepo.RemoveUserTokens(ctx, userID, "")
}
// RemoveTokensExceptCurrentUser remove all tokens except the current user
func (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID string, accessToken string) {
as.authRepo.RemoveUserTokens(ctx, userID, accessToken)
}
//Admin
+4 -4
View File
@@ -471,7 +471,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.CommentQuestion
msg.NotificationAction = constant.NotificationCommentQuestion
notice_queue.AddNotification(msg)
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
@@ -526,7 +526,7 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.CommentAnswer
msg.NotificationAction = constant.NotificationCommentAnswer
notice_queue.AddNotification(msg)
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
@@ -578,7 +578,7 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.ReplyToYou
msg.NotificationAction = constant.NotificationReplyToYou
notice_queue.AddNotification(msg)
}
@@ -599,7 +599,7 @@ func (cs *CommentService) notificationMention(
ObjectID: commentID,
}
msg.ObjectType = constant.CommentObjectType
msg.NotificationAction = constant.MentionYou
msg.NotificationAction = constant.NotificationMentionYou
notice_queue.AddNotification(msg)
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
}
+14 -23
View File
@@ -162,39 +162,30 @@ func (es *EmailService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGen
}
func (es *EmailService) RegisterTemplate(ctx context.Context, registerUrl string) (title, body string, err error) {
ec, err := es.GetEmailConfig()
if err != nil {
return
}
siteinfo, err := es.GetSiteGeneral(ctx)
emailConfig, err := es.GetEmailConfig()
if err != nil {
return
}
siteInfo, err := es.GetSiteGeneral(ctx)
if err != nil {
return
}
templateData := RegisterTemplateData{
SiteName: siteinfo.Name, RegisterUrl: registerUrl,
}
tmpl, err := template.New("register_title").Parse(ec.RegisterTitle)
if err != nil {
return "", "", err
}
titleBuf := &bytes.Buffer{}
bodyBuf := &bytes.Buffer{}
err = tmpl.Execute(titleBuf, templateData)
if err != nil {
return "", "", err
SiteName: siteInfo.Name,
RegisterUrl: registerUrl,
}
tmpl, err = template.New("register_body").Parse(ec.RegisterBody)
title, err = es.parseTemplateData(emailConfig.RegisterTitle, templateData)
if err != nil {
return "", "", err
}
err = tmpl.Execute(bodyBuf, templateData)
if err != nil {
return "", "", err
return "", "", fmt.Errorf("email template parse error: %s", err)
}
return titleBuf.String(), bodyBuf.String(), nil
body, err = es.parseTemplateData(emailConfig.RegisterBody, templateData)
if err != nil {
return "", "", fmt.Errorf("email template parse error: %s", err)
}
return title, body, nil
}
func (es *EmailService) PassResetTemplate(ctx context.Context, passResetUrl string) (title, body string, err error) {
@@ -7,14 +7,15 @@ import (
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/pkg/uid"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
@@ -127,35 +128,47 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
if err != nil {
return nil, err
}
resp, err = ns.formatNotificationPage(ctx, notifications)
if err != nil {
return nil, err
}
return pager.NewPageModel(total, resp), nil
}
func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) (
resp []*schema.NotificationContent, err error) {
lang := handler.GetLangByCtx(ctx)
for _, notificationInfo := range notifications {
item := &schema.NotificationContent{}
err := json.Unmarshal([]byte(notificationInfo.Content), item)
if err != nil {
if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil {
log.Error("NotificationContent Unmarshal Error", err.Error())
continue
}
lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
item.ID = notificationInfo.ID
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
if notificationInfo.IsRead == schema.NotificationRead {
item.IsRead = true
// If notification is downvote, the user info is not needed.
if item.NotificationAction == constant.NotificationDownVotedTheQuestion ||
item.NotificationAction == constant.NotificationDownVotedTheAnswer {
item.UserInfo = nil
}
answerID, ok := item.ObjectInfo.ObjectMap["answer"]
if ok {
item.ID = notificationInfo.ID
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
item.IsRead = notificationInfo.IsRead == schema.NotificationRead
if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok {
if item.ObjectInfo.ObjectID == answerID {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
}
item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
}
questionID, ok := item.ObjectInfo.ObjectMap["question"]
if ok {
if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok {
if item.ObjectInfo.ObjectID == questionID {
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
}
item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
}
resp = append(resp, item)
}
return pager.NewPageModel(total, resp), nil
return resp, nil
}
@@ -15,6 +15,7 @@ import (
"github.com/answerdev/answer/internal/service/object_info"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/uid"
"github.com/answerdev/answer/plugin"
"github.com/goccy/go-json"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
@@ -82,7 +83,9 @@ func (ns *NotificationCommon) HandleNotification() {
// ObjectInfo.ObjectID
// ObjectInfo.ObjectType
func (ns *NotificationCommon) AddNotification(ctx context.Context, msg *schema.NotificationMsg) error {
if msg.Type == schema.NotificationTypeAchievement && plugin.RankAgentEnabled() {
return nil
}
req := &schema.NotificationContent{
TriggerUserID: msg.TriggerUserID,
ReceiverUserID: msg.ReceiverUserID,
@@ -190,10 +193,10 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context,
if msg.NoNeedPushAllFollow {
return
}
if msg.NotificationAction != constant.UpdateQuestion &&
msg.NotificationAction != constant.AnswerTheQuestion &&
msg.NotificationAction != constant.UpdateAnswer &&
msg.NotificationAction != constant.AcceptAnswer {
if msg.NotificationAction != constant.NotificationUpdateQuestion &&
msg.NotificationAction != constant.NotificationAnswerTheQuestion &&
msg.NotificationAction != constant.NotificationUpdateAnswer &&
msg.NotificationAction != constant.NotificationAcceptAnswer {
return
}
condObjectID := msg.ObjectID
@@ -10,10 +10,10 @@ const (
QuestionReopen = "question.reopen"
QuestionVoteUp = "question.vote_up"
QuestionVoteDown = "question.vote_down"
QuestionPin = "question.pin" //Top the question
QuestionUnPin = "question.unpin" //untop the question
QuestionHide = "question.hide" //hide the question
QuestionShow = "question.show" //show the question
QuestionPin = "question.pin"
QuestionUnPin = "question.unpin"
QuestionHide = "question.hide"
QuestionShow = "question.show"
AnswerAdd = "answer.add"
AnswerEdit = "answer.edit"
AnswerEditWithoutReview = "answer.edit_without_review"
@@ -0,0 +1,80 @@
package plugin_common
import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type PluginConfigRepo interface {
SavePluginConfig(ctx context.Context, pluginSlugName, configValue string) (err error)
GetPluginConfigAll(ctx context.Context) (pluginConfigs []*entity.PluginConfig, err error)
}
// PluginCommonService user service
type PluginCommonService struct {
configRepo config.ConfigRepo
pluginConfigRepo PluginConfigRepo
}
// NewPluginCommonService new report service
func NewPluginCommonService(
pluginConfigRepo PluginConfigRepo,
configRepo config.ConfigRepo) *PluginCommonService {
// init plugin status
pluginStatus, err := configRepo.GetString(constant.PluginStatus)
if err != nil {
log.Error(err)
} else {
if err := plugin.StatusManager.UnmarshalJSON([]byte(pluginStatus)); err != nil {
log.Error(err)
}
}
// init plugin config
pluginConfigs, err := pluginConfigRepo.GetPluginConfigAll(context.Background())
if err != nil {
log.Error(err)
} else {
for _, pluginConfig := range pluginConfigs {
err := plugin.CallConfig(func(fn plugin.Config) error {
if fn.Info().SlugName == pluginConfig.PluginSlugName {
return fn.ConfigReceiver([]byte(pluginConfig.Value))
}
return nil
})
if err != nil {
log.Errorf("parse plugin config failed: %s %v", pluginConfig.PluginSlugName, err)
}
}
}
return &PluginCommonService{
configRepo: configRepo,
pluginConfigRepo: pluginConfigRepo,
}
}
// UpdatePluginStatus update plugin status
func (ps *PluginCommonService) UpdatePluginStatus(ctx context.Context) (err error) {
content, err := plugin.StatusManager.MarshalJSON()
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err)
}
return ps.configRepo.SetConfig(constant.PluginStatus, string(content))
}
// UpdatePluginConfig update plugin config
func (ps *PluginCommonService) UpdatePluginConfig(ctx context.Context, req *schema.UpdatePluginConfigReq) (err error) {
configValue, _ := json.Marshal(req.ConfigFields)
return ps.pluginConfigRepo.SavePluginConfig(ctx, req.PluginSlugName, string(configValue))
}
+5
View File
@@ -16,6 +16,7 @@ import (
"github.com/answerdev/answer/internal/service/notification"
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
"github.com/answerdev/answer/internal/service/object_info"
"github.com/answerdev/answer/internal/service/plugin_common"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/reason"
@@ -32,6 +33,7 @@ import (
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/internal/service/user_admin"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/google/wire"
)
@@ -79,4 +81,7 @@ var ProviderSetService = wire.NewSet(
role.NewRoleService,
role.NewUserRoleRelService,
role.NewRolePowerRelService,
user_external_login.NewUserExternalLoginService,
user_external_login.NewUserCenterLoginService,
plugin_common.NewPluginCommonService,
)
+1 -1
View File
@@ -32,7 +32,7 @@ type QuestionRepo interface {
UpdateQuestion(ctx context.Context, question *entity.Question, Cols []string) (err error)
GetQuestion(ctx context.Context, id string) (question *entity.Question, exist bool, err error)
GetQuestionList(ctx context.Context, question *entity.Question) (questions []*entity.Question, err error)
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
questionList []*entity.Question, total int64, err error)
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)
+50 -49
View File
@@ -10,6 +10,7 @@ import (
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/base/validator"
@@ -270,6 +271,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
question.Status = entity.QuestionStatusAvailable
question.RevisionID = "0"
question.CreatedAt = now
question.PostUpdateTime = now
question.Pin = entity.QuestionUnPin
question.Show = entity.QuestionShow
//question.UpdatedAt = nil
@@ -742,70 +744,74 @@ func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobject
return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)
}
func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) {
userlist := make([]*schema.UserQuestionInfo, 0)
// PersonalQuestionPage get question list by user
func (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema.PersonalQuestionPageReq) (
pageModel *pager.PageModel, err error) {
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
return userlist, 0, err
return nil, err
}
if !Exist {
return userlist, 0, nil
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
search := &schema.QuestionPageReq{}
search.OrderCond = order
search.Page = page
search.PageSize = pageSize
search.OrderCond = req.OrderCond
search.Page = req.Page
search.PageSize = req.PageSize
search.UserIDBeSearched = userinfo.ID
search.LoginUserID = loginUserID
questionlist, count, err := qs.GetQuestionPage(ctx, search)
search.LoginUserID = req.LoginUserID
questionList, total, err := qs.GetQuestionPage(ctx, search)
if err != nil {
return userlist, 0, err
return nil, err
}
for _, item := range questionlist {
userQuestionInfoList := make([]*schema.UserQuestionInfo, 0)
for _, item := range questionList {
info := &schema.UserQuestionInfo{}
_ = copier.Copy(info, item)
status, ok := entity.AdminQuestionSearchStatusIntToString[item.Status]
if ok {
info.Status = status
}
userlist = append(userlist, info)
userQuestionInfoList = append(userQuestionInfoList, info)
}
return userlist, count, nil
return pager.NewPageModel(total, userQuestionInfoList), nil
}
func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserAnswerInfo, int64, error) {
answerlist := make([]*schema.AnswerInfo, 0)
userAnswerlist := make([]*schema.UserAnswerInfo, 0)
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.PersonalAnswerPageReq) (
pageModel *pager.PageModel, err error) {
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
return userAnswerlist, 0, err
return nil, err
}
if !Exist {
return userAnswerlist, 0, nil
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
answersearch := &entity.AnswerSearch{}
answersearch.UserID = userinfo.ID
answersearch.PageSize = pageSize
answersearch.Page = page
if order == "newest" {
answersearch.PageSize = req.PageSize
answersearch.Page = req.Page
if req.OrderCond == "newest" {
answersearch.Order = entity.AnswerSearchOrderByTime
} else {
answersearch.Order = entity.AnswerSearchOrderByDefault
}
questionIDs := make([]string, 0)
answerList, count, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
answerList, total, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
if err != nil {
return userAnswerlist, count, err
return nil, err
}
answerlist := make([]*schema.AnswerInfo, 0)
userAnswerlist := make([]*schema.UserAnswerInfo, 0)
for _, item := range answerList {
answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)
answerlist = append(answerlist, answerinfo)
questionIDs = append(questionIDs, uid.DeShortID(item.QuestionID))
}
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)
if err != nil {
return userAnswerlist, count, err
return nil, err
}
for _, item := range answerlist {
@@ -822,34 +828,29 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
}
}
return userAnswerlist, count, nil
return pager.NewPageModel(total, userAnswerlist), nil
}
func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, pageSize int, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
// PersonalCollectionPage get collection list by user
func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) (
pageModel *pager.PageModel, err error) {
list := make([]*schema.QuestionInfo, 0)
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, loginUserID)
if err != nil {
return list, 0, err
}
if !Exist {
return list, 0, nil
}
collectionSearch := &entity.CollectionSearch{}
collectionSearch.UserID = userinfo.ID
collectionSearch.Page = page
collectionSearch.PageSize = pageSize
collectionlist, count, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
collectionSearch.UserID = req.UserID
collectionSearch.Page = req.Page
collectionSearch.PageSize = req.PageSize
collectionList, total, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
if err != nil {
return list, 0, err
return nil, err
}
questionIDs := make([]string, 0)
for _, item := range collectionlist {
for _, item := range collectionList {
questionIDs = append(questionIDs, item.ObjectID)
}
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.UserID)
if err != nil {
return list, count, err
return nil, err
}
for _, id := range questionIDs {
_, ok := questionMaps[uid.EnShortID(id)]
@@ -862,7 +863,7 @@ func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, p
}
}
return list, count, nil
return pager.NewPageModel(total, list), nil
}
func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) {
@@ -1010,7 +1011,7 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques
}
questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,
req.UserIDBeSearched, req.TagID, req.OrderCond)
req.UserIDBeSearched, req.TagID, req.OrderCond, req.InDays)
if err != nil {
return nil, 0, err
}
@@ -1072,7 +1073,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
msg.ReceiverUserID = questionInfo.UserID
msg.TriggerUserID = questionInfo.UserID
msg.ObjectType = constant.QuestionObjectType
msg.NotificationAction = constant.YourQuestionWasDeleted
msg.NotificationAction = constant.NotificationYourQuestionWasDeleted
notice_queue.AddNotification(msg)
return nil
}
+4
View File
@@ -16,6 +16,7 @@ import (
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/uid"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
@@ -228,6 +229,9 @@ func (rs *RankService) checkUserRank(ctx context.Context, userID string, userRan
// GetRankPersonalWithPage get personal comment list page
func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.GetRankPersonalWithPageReq) (
pageModel *pager.PageModel, err error) {
if plugin.RankAgentEnabled() {
return pager.NewPageModel(0, []string{}), nil
}
if len(req.Username) > 0 {
userInfo, exist, err := rs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
if err != nil {
@@ -66,7 +66,7 @@ func (rh *ReportHandle) HandleObject(ctx context.Context, reported *entity.Repor
switch req.FlaggedType {
case reasonDelete:
err = rh.commentRepo.RemoveComment(ctx, objectID)
rh.sendNotification(ctx, reportedUserID, objectID, constant.YourCommentWasDeleted)
rh.sendNotification(ctx, reportedUserID, objectID, constant.NotificationYourCommentWasDeleted)
}
}
return
+1 -1
View File
@@ -209,7 +209,7 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem
ObjectID: answerinfo.ID,
}
msg.ObjectType = constant.AnswerObjectType
msg.NotificationAction = constant.UpdateAnswer
msg.NotificationAction = constant.NotificationUpdateAnswer
notice_queue.AddNotification(msg)
activity_queue.AddActivity(&schema.ActivityMsg{
+119 -44
View File
@@ -3,12 +3,15 @@ package siteinfo
import (
"context"
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
@@ -23,19 +26,23 @@ type SiteInfoService struct {
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
emailService *export.EmailService
tagCommonService *tagcommon.TagCommonService
configRepo config.ConfigRepo
}
func NewSiteInfoService(
siteInfoRepo siteinfo_common.SiteInfoRepo,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
emailService *export.EmailService,
tagCommonService *tagcommon.TagCommonService) *SiteInfoService {
resp, err := siteInfoCommonService.GetSiteInterface(context.Background())
if err != nil {
log.Error(err)
} else {
constant.DefaultAvatar = resp.DefaultAvatar
tagCommonService *tagcommon.TagCommonService,
configRepo config.ConfigRepo,
) *SiteInfoService {
usersSiteInfo, _ := siteInfoCommonService.GetSiteUsers(context.Background())
if usersSiteInfo != nil {
constant.DefaultAvatar = usersSiteInfo.DefaultAvatar
}
generalSiteInfo, _ := siteInfoCommonService.GetSiteGeneral(context.Background())
if generalSiteInfo != nil {
constant.DefaultSiteURL = generalSiteInfo.SiteUrl
}
return &SiteInfoService{
@@ -43,6 +50,7 @@ func NewSiteInfoService(
siteInfoCommonService: siteInfoCommonService,
emailService: emailService,
tagCommonService: tagCommonService,
configRepo: configRepo,
}
}
@@ -61,6 +69,11 @@ func (s *SiteInfoService) GetSiteBranding(ctx context.Context) (resp *schema.Sit
return s.siteInfoCommonService.GetSiteBranding(ctx)
}
// GetSiteUsers get site info about users
func (s *SiteInfoService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {
return s.siteInfoCommonService.GetSiteUsers(ctx)
}
// GetSiteWrite get site info write
func (s *SiteInfoService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
@@ -106,45 +119,32 @@ func (s *SiteInfoService) GetSiteTheme(ctx context.Context) (resp *schema.SiteTh
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
req.FormatSiteUrl()
var (
siteType = "general"
content []byte
)
content, _ = json.Marshal(req)
data := entity.SiteInfo{
Type: siteType,
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeGeneral,
Content: string(content),
Status: 1,
}
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeGeneral, data)
if err == nil {
constant.DefaultSiteURL = req.SiteUrl
}
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
return
}
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
var (
siteType = "interface"
content []byte
)
// check language
if !translator.CheckLanguageIsValid(req.Language) {
err = errors.BadRequest(reason.LangNotFound)
return
}
content, _ = json.Marshal(req)
content, _ := json.Marshal(req)
data := entity.SiteInfo{
Type: siteType,
Type: constant.SiteTypeInterface,
Content: string(content),
}
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
if err == nil {
constant.DefaultAvatar = req.DefaultAvatar
}
return
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeInterface, &data)
}
// SaveSiteBranding save site branding information
@@ -218,6 +218,21 @@ func (s *SiteInfoService) SaveSiteTheme(ctx context.Context, req *schema.SiteThe
return s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeTheme, data)
}
// SaveSiteUsers save site users
func (s *SiteInfoService) SaveSiteUsers(ctx context.Context, req *schema.SiteUsersReq) (err error) {
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypeUsers,
Content: string(content),
Status: 1,
}
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeUsers, data)
if err == nil {
constant.DefaultAvatar = req.DefaultAvatar
}
return err
}
// GetSMTPConfig get smtp config
func (s *SiteInfoService) GetSMTPConfig(ctx context.Context) (
resp *schema.GetSMTPConfigResp, err error,
@@ -253,8 +268,11 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
return
}
func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) {
resp = &schema.SiteSeoResp{}
func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
resp = &schema.SiteSeoReq{}
if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
return resp, err
}
loginConfig, err := s.GetSiteLogin(ctx)
if err != nil {
log.Error(err)
@@ -265,17 +283,6 @@ func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp,
resp.Robots = "User-agent: *\nDisallow: /"
return resp, nil
}
resp = &schema.SiteSeoResp{}
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeSeo)
if err != nil {
log.Error(err)
return resp, nil
}
if !exist {
return resp, nil
}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
@@ -302,3 +309,71 @@ func (s *SiteInfoService) SaveSeo(ctx context.Context, req schema.SiteSeoReq) (e
}
return
}
func (s *SiteInfoService) GetPrivilegesConfig(ctx context.Context) (resp *schema.GetPrivilegesConfigResp, err error) {
privilege := &schema.UpdatePrivilegesConfigReq{}
if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypePrivileges, privilege); err != nil {
return nil, err
}
resp = &schema.GetPrivilegesConfigResp{
Options: s.translatePrivilegeOptions(ctx),
SelectedLevel: schema.PrivilegeLevel3,
}
if privilege != nil && privilege.Level > 0 {
resp.SelectedLevel = privilege.Level
}
return resp, nil
}
func (s *SiteInfoService) translatePrivilegeOptions(ctx context.Context) (options []*schema.PrivilegeOption) {
la := handler.GetLangByCtx(ctx)
for _, option := range schema.DefaultPrivilegeOptions {
op := &schema.PrivilegeOption{
Level: option.Level,
LevelDesc: translator.Tr(la, option.LevelDesc),
}
for _, privilege := range option.Privileges {
op.Privileges = append(op.Privileges, &constant.Privilege{
Key: privilege.Key,
Label: translator.Tr(la, privilege.Label),
Value: privilege.Value,
})
}
options = append(options, op)
}
return
}
func (s *SiteInfoService) UpdatePrivilegesConfig(ctx context.Context, req *schema.UpdatePrivilegesConfigReq) (err error) {
var chooseOption *schema.PrivilegeOption
for _, option := range schema.DefaultPrivilegeOptions {
if option.Level == req.Level {
chooseOption = option
break
}
}
if chooseOption == nil {
return nil
}
// update site info that user choose which privilege level
content, _ := json.Marshal(req)
data := &entity.SiteInfo{
Type: constant.SiteTypePrivileges,
Content: string(content),
Status: 1,
}
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypePrivileges, data)
if err != nil {
return err
}
// update privilege in config
for _, privilege := range chooseOption.Privileges {
err = s.configRepo.SetConfig(privilege.Key, fmt.Sprintf("%d", privilege.Value))
if err != nil {
return err
}
}
return
}
@@ -43,7 +43,7 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService
// GetSiteGeneral get site info general
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
resp = &schema.SiteGeneralResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil {
return nil, err
}
return resp, nil
@@ -52,7 +52,7 @@ func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schem
// GetSiteInterface get site info interface
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
resp = &schema.SiteInterfaceResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeInterface, resp); err != nil {
return nil, err
}
return resp, nil
@@ -61,7 +61,16 @@ func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *sch
// GetSiteBranding get site info branding
func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *schema.SiteBrandingResp, err error) {
resp = &schema.SiteBrandingResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeBranding, resp); err != nil {
return nil, err
}
return resp, nil
}
// GetSiteUsers get site info about users
func (s *SiteInfoCommonService) GetSiteUsers(ctx context.Context) (resp *schema.SiteUsersResp, err error) {
resp = &schema.SiteUsersResp{}
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeUsers, resp); err != nil {
return nil, err
}
return resp, nil
@@ -70,7 +79,7 @@ func (s *SiteInfoCommonService) GetSiteBranding(ctx context.Context) (resp *sche
// GetSiteWrite get site info write
func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.SiteWriteResp, err error) {
resp = &schema.SiteWriteResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeWrite, resp); err != nil {
return nil, err
}
return resp, nil
@@ -79,7 +88,7 @@ func (s *SiteInfoCommonService) GetSiteWrite(ctx context.Context) (resp *schema.
// GetSiteLegal get site info write
func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.SiteLegalResp, err error) {
resp = &schema.SiteLegalResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLegal, resp); err != nil {
return nil, err
}
return resp, nil
@@ -88,7 +97,7 @@ func (s *SiteInfoCommonService) GetSiteLegal(ctx context.Context) (resp *schema.
// GetSiteLogin get site login config
func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.SiteLoginResp, err error) {
resp = &schema.SiteLoginResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeLogin, resp); err != nil {
return nil, err
}
return resp, nil
@@ -97,7 +106,7 @@ func (s *SiteInfoCommonService) GetSiteLogin(ctx context.Context) (resp *schema.
// GetSiteCustomCssHTML get site custom css html config
func (s *SiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (resp *schema.SiteCustomCssHTMLResp, err error) {
resp = &schema.SiteCustomCssHTMLResp{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeCustomCssHTML, resp); err != nil {
return nil, err
}
return resp, nil
@@ -108,7 +117,7 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.
resp = &schema.SiteThemeResp{
ThemeOptions: schema.GetThemeOptions,
}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeTheme, resp); err != nil {
return nil, err
}
resp.TrTheme(ctx)
@@ -118,13 +127,13 @@ func (s *SiteInfoCommonService) GetSiteTheme(ctx context.Context) (resp *schema.
// GetSiteSeo get site seo
func (s *SiteInfoCommonService) GetSiteSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
resp = &schema.SiteSeoReq{}
if err = s.getSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
if err = s.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
return nil, err
}
return resp, nil
}
func (s *SiteInfoCommonService) getSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
func (s *SiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) (err error) {
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, siteType)
if err != nil {
return err
+41
View File
@@ -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 -2
View File
@@ -116,7 +116,7 @@ func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.Upda
return err
}
us.authService.RemoveUserTokens(ctx, req.UserID)
us.authService.RemoveUserAllTokens(ctx, req.UserID)
return
}
@@ -179,7 +179,7 @@ func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.
return err
}
// logout this user
us.authService.RemoveUserTokens(ctx, req.UserID)
us.authService.RemoveUserAllTokens(ctx, req.UserID)
return
}
+40 -7
View File
@@ -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 {
@@ -36,14 +38,20 @@ type UserRepo interface {
// UserCommon user service
type UserCommon struct {
userRepo UserRepo
userRepo UserRepo
userRoleService *role.UserRoleRelService
authService *auth.AuthService
}
func NewUserCommon(
userRepo UserRepo,
userRoleService *role.UserRoleRelService,
authService *auth.AuthService,
) *UserCommon {
return &UserCommon{
userRepo: userRepo,
userRepo: userRepo,
userRoleService: userRoleService,
authService: authService,
}
}
@@ -139,9 +147,34 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
if !has {
break
}
bytes := make([]byte, 2)
_, _ = rand.Read(bytes)
suffix = hex.EncodeToString(bytes)
suffix = random.UsernameSuffix()
}
return username + suffix, nil
}
func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int, externalID string) (
accessToken string, userCacheInfo *entity.UserCacheInfo, err error) {
roleID, err := us.userRoleService.GetUserRole(ctx, userID)
if err != nil {
log.Error(err)
}
userCacheInfo = &entity.UserCacheInfo{
UserID: userID,
EmailStatus: emailStatus,
UserStatus: userStatus,
RoleID: roleID,
ExternalID: externalID,
}
accessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return "", nil, err
}
if userCacheInfo.RoleID == role.RoleAdminID {
if err = us.authService.SetAdminUserCacheInfo(ctx, accessToken, &entity.UserCacheInfo{UserID: userID}); err != nil {
return "", nil, err
}
}
return accessToken, userCacheInfo, nil
}
@@ -0,0 +1,277 @@
package user_external_login
import (
"context"
"encoding/json"
"time"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/random"
"github.com/answerdev/answer/plugin"
"github.com/segmentfault/pacman/log"
)
// UserCenterLoginService user external login service
type UserCenterLoginService struct {
userRepo usercommon.UserRepo
userExternalLoginRepo UserExternalLoginRepo
userCommonService *usercommon.UserCommon
userActivity activity.UserActiveActivityRepo
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
}
// NewUserCenterLoginService new user external login service
func NewUserCenterLoginService(
userRepo usercommon.UserRepo,
userCommonService *usercommon.UserCommon,
userExternalLoginRepo UserExternalLoginRepo,
userActivity activity.UserActiveActivityRepo,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
) *UserCenterLoginService {
return &UserCenterLoginService{
userRepo: userRepo,
userCommonService: userCommonService,
userExternalLoginRepo: userExternalLoginRepo,
userActivity: userActivity,
siteInfoCommonService: siteInfoCommonService,
}
}
func (us *UserCenterLoginService) ExternalLogin(
ctx context.Context, userCenter plugin.UserCenter, basicUserInfo *plugin.UserCenterBasicUserInfo) (
resp *schema.UserExternalLoginResp, err error) {
if len(basicUserInfo.Email) > 0 {
// check whether site allow register or not
siteInfo, err := us.siteInfoCommonService.GetSiteLogin(ctx)
if err != nil {
return nil, err
}
if !checker.EmailInAllowEmailDomain(basicUserInfo.Email, siteInfo.AllowEmailDomains) {
log.Debugf("email domain not allowed: %s", basicUserInfo.Email)
return &schema.UserExternalLoginResp{
ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.EmailIllegalDomainError),
}, nil
}
}
oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
userCenter.Info().SlugName, basicUserInfo.ExternalID)
if err != nil {
return nil, err
}
if exist {
// if user is already a member, login directly
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID)
if err != nil {
return nil, err
}
if exist {
// if user is deleted, do not allow login
if oldUserInfo.Status == entity.UserStatusDeleted {
return &schema.UserExternalLoginResp{
ErrTitle: translator.Tr(handler.GetLangByCtx(ctx), reason.UserAccessDenied),
ErrMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.UserPageAccessDenied),
}, nil
}
if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {
log.Errorf("update user last login date failed: %v", err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
}
// cache external user info, waiting for user enter email address.
if userCenter.Description().MustAuthEmailEnabled && len(basicUserInfo.Email) == 0 {
return &schema.UserExternalLoginResp{ErrMsg: "Requires authorized email to login"}, nil
}
oldUserInfo, err := us.registerNewUser(ctx, userCenter.Info().SlugName, basicUserInfo)
if err != nil {
return nil, err
}
us.activeUser(ctx, oldUserInfo)
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider string,
basicUserInfo *plugin.UserCenterBasicUserInfo) (userInfo *entity.User, err error) {
userInfo = &entity.User{}
userInfo.EMail = basicUserInfo.Email
userInfo.DisplayName = basicUserInfo.DisplayName
userInfo.Username, err = us.userCommonService.MakeUsername(ctx, basicUserInfo.Username)
if err != nil {
log.Error(err)
userInfo.Username = random.Username()
}
if len(basicUserInfo.Avatar) > 0 {
avatarInfo := &schema.AvatarInfo{
Type: schema.AvatarTypeCustom,
Custom: basicUserInfo.Avatar,
}
avatar, _ := json.Marshal(avatarInfo)
userInfo.Avatar = string(avatar)
}
userInfo.MailStatus = entity.EmailStatusAvailable
userInfo.Status = entity.UserStatusAvailable
userInfo.LastLoginDate = time.Now()
userInfo.Bio = basicUserInfo.Bio
userInfo.BioHTML = converter.Markdown2HTML(basicUserInfo.Bio)
err = us.userRepo.AddUser(ctx, userInfo)
if err != nil {
return nil, err
}
metaInfo, _ := json.Marshal(basicUserInfo)
newExternalUserInfo := &entity.UserExternalLogin{
UserID: userInfo.ID,
Provider: provider,
ExternalID: basicUserInfo.ExternalID,
MetaInfo: string(metaInfo),
}
err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo)
return userInfo, nil
}
func (us *UserCenterLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User) {
if err := us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil {
log.Error(err)
}
}
func (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, userID string) (
resp *schema.UserCenterUserSettingsResp, err error) {
resp = &schema.UserCenterUserSettingsResp{}
userCenter, ok := plugin.GetUserCenter()
if !ok {
return resp, nil
}
// get external login info
externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID)
if err != nil {
return nil, err
}
var externalInfo *entity.UserExternalLogin
for _, t := range externalLoginList {
if t.Provider == userCenter.Info().SlugName {
externalInfo = t
}
}
if externalInfo == nil {
return resp, nil
}
settings, err := userCenter.UserSettings(externalInfo.ExternalID)
if err != nil {
log.Error(err)
return resp, nil
}
if len(settings.AccountSettingRedirectURL) > 0 {
resp.AccountSettingAgent = schema.UserSettingAgent{
Enabled: true,
RedirectURL: settings.AccountSettingRedirectURL,
}
}
if len(settings.ProfileSettingRedirectURL) > 0 {
resp.ProfileSettingAgent = schema.UserSettingAgent{
Enabled: true,
RedirectURL: settings.ProfileSettingRedirectURL,
}
}
return resp, nil
}
// UserCenterAdminFunctionAgent Check in the backend administration interface if the user-related functions
// are turned off due to turning on the User Center plugin.
func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Context) (
resp *schema.UserCenterAdminFunctionAgentResp, err error) {
resp = &schema.UserCenterAdminFunctionAgentResp{
AllowCreateUser: true,
AllowUpdateUserStatus: true,
AllowUpdateUserPassword: true,
AllowUpdateUserRole: true,
}
userCenter, ok := plugin.GetUserCenter()
if !ok {
return
}
desc := userCenter.Description()
// If user status agent is enabled, admin can not update user status in answer.
resp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled
// If original user system is enabled, admin can update user password and role in answer.
resp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem
resp.AllowUpdateUserRole = desc.EnabledOriginalUserSystem
resp.AllowCreateUser = desc.EnabledOriginalUserSystem
return resp, nil
}
func (us *UserCenterLoginService) UserCenterPersonalBranding(ctx context.Context, username string) (
resp *schema.UserCenterPersonalBranding, err error) {
resp = &schema.UserCenterPersonalBranding{
PersonalBranding: make([]*schema.PersonalBranding, 0),
}
userCenter, ok := plugin.GetUserCenter()
if !ok {
return
}
userInfo, exist, err := us.userRepo.GetByUsername(ctx, username)
if err != nil {
return nil, err
}
if !exist {
return resp, nil
}
// get external login info
externalLoginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userInfo.ID)
if err != nil {
return nil, err
}
var externalInfo *entity.UserExternalLogin
for _, t := range externalLoginList {
if t.Provider == userCenter.Info().SlugName {
externalInfo = t
}
}
if externalInfo == nil {
return resp, nil
}
resp.Enabled = true
branding := userCenter.PersonalBranding(externalInfo.ExternalID)
for _, t := range branding {
resp.PersonalBranding = append(resp.PersonalBranding, &schema.PersonalBranding{
Icon: t.Icon,
Name: t.Name,
Label: t.Label,
Url: t.Url,
})
}
return resp, nil
}
@@ -0,0 +1,351 @@
package user_external_login
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/random"
"github.com/answerdev/answer/pkg/token"
"github.com/answerdev/answer/plugin"
"github.com/google/uuid"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type UserExternalLoginRepo interface {
AddUserExternalLogin(ctx context.Context, user *entity.UserExternalLogin) (err error)
UpdateInfo(ctx context.Context, userInfo *entity.UserExternalLogin) (err error)
GetByExternalID(ctx context.Context, provider, externalID string) (userInfo *entity.UserExternalLogin, exist bool, err error)
GetUserExternalLoginList(ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error)
DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error)
SetCacheUserExternalLoginInfo(ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error)
GetCacheUserExternalLoginInfo(ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error)
}
// UserExternalLoginService user external login service
type UserExternalLoginService struct {
userRepo usercommon.UserRepo
userExternalLoginRepo UserExternalLoginRepo
userCommonService *usercommon.UserCommon
emailService *export.EmailService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
userActivity activity.UserActiveActivityRepo
}
// NewUserExternalLoginService new user external login service
func NewUserExternalLoginService(
userRepo usercommon.UserRepo,
userCommonService *usercommon.UserCommon,
userExternalLoginRepo UserExternalLoginRepo,
emailService *export.EmailService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
userActivity activity.UserActiveActivityRepo,
) *UserExternalLoginService {
return &UserExternalLoginService{
userRepo: userRepo,
userCommonService: userCommonService,
userExternalLoginRepo: userExternalLoginRepo,
emailService: emailService,
siteInfoCommonService: siteInfoCommonService,
userActivity: userActivity,
}
}
// ExternalLogin if user is already a member logged in
func (us *UserExternalLoginService) ExternalLogin(
ctx context.Context, externalUserInfo *schema.ExternalLoginUserInfoCache) (
resp *schema.UserExternalLoginResp, err error) {
oldExternalLoginUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
externalUserInfo.Provider, externalUserInfo.ExternalID)
if err != nil {
return nil, err
}
if exist {
// if user is already a member, login directly
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, oldExternalLoginUserInfo.UserID)
if err != nil {
return nil, err
}
if exist && oldUserInfo.Status != entity.UserStatusDeleted {
if err := us.userRepo.UpdateLastLoginDate(ctx, oldUserInfo.ID); err != nil {
log.Errorf("update user last login date failed: %v", err)
}
newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)
if err != nil {
log.Error(err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
}
// cache external user info, waiting for user enter email address.
if len(externalUserInfo.Email) == 0 {
bindingKey := token.GenerateToken()
err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, bindingKey, externalUserInfo)
if err != nil {
return nil, err
}
return &schema.UserExternalLoginResp{BindingKey: bindingKey}, nil
}
oldUserInfo, exist, err := us.userRepo.GetByEmail(ctx, externalUserInfo.Email)
if err != nil {
return nil, err
}
// if user is not a member, register a new user
if !exist {
oldUserInfo, err = us.registerNewUser(ctx, externalUserInfo)
if err != nil {
return nil, err
}
}
// bind external user info to user
err = us.bindOldUser(ctx, externalUserInfo, oldUserInfo)
if err != nil {
return nil, err
}
// If user login with external account and email is exist, active user directly.
newMailStatus, err := us.activeUser(ctx, oldUserInfo, externalUserInfo)
if err != nil {
log.Error(err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
func (us *UserExternalLoginService) registerNewUser(ctx context.Context,
externalUserInfo *schema.ExternalLoginUserInfoCache) (userInfo *entity.User, err error) {
userInfo = &entity.User{}
userInfo.EMail = externalUserInfo.Email
userInfo.DisplayName = externalUserInfo.DisplayName
userInfo.Username, err = us.userCommonService.MakeUsername(ctx, externalUserInfo.Username)
if err != nil {
log.Error(err)
userInfo.Username = random.Username()
}
if len(externalUserInfo.Avatar) > 0 {
avatarInfo := &schema.AvatarInfo{
Type: schema.AvatarTypeCustom,
Custom: externalUserInfo.Avatar,
}
avatar, _ := json.Marshal(avatarInfo)
userInfo.Avatar = string(avatar)
}
userInfo.MailStatus = entity.EmailStatusToBeVerified
userInfo.Status = entity.UserStatusAvailable
userInfo.LastLoginDate = time.Now()
userInfo.Bio = externalUserInfo.Bio
userInfo.BioHTML = externalUserInfo.Bio
err = us.userRepo.AddUser(ctx, userInfo)
if err != nil {
return nil, err
}
return userInfo, nil
}
func (us *UserExternalLoginService) bindOldUser(ctx context.Context,
externalUserInfo *schema.ExternalLoginUserInfoCache, oldUserInfo *entity.User) (err error) {
oldExternalUserInfo, exist, err := us.userExternalLoginRepo.GetByExternalID(ctx,
externalUserInfo.Provider,
externalUserInfo.ExternalID)
if err != nil {
return err
}
if exist {
oldExternalUserInfo.MetaInfo = externalUserInfo.MetaInfo
oldExternalUserInfo.UserID = oldUserInfo.ID
err = us.userExternalLoginRepo.UpdateInfo(ctx, oldExternalUserInfo)
} else {
newExternalUserInfo := &entity.UserExternalLogin{
UserID: oldUserInfo.ID,
Provider: externalUserInfo.Provider,
ExternalID: externalUserInfo.ExternalID,
MetaInfo: externalUserInfo.MetaInfo,
}
err = us.userExternalLoginRepo.AddUserExternalLogin(ctx, newExternalUserInfo)
}
return err
}
func (us *UserExternalLoginService) activeUser(ctx context.Context, oldUserInfo *entity.User,
externalUserInfo *schema.ExternalLoginUserInfoCache) (
mailStatus int, err error) {
log.Infof("user %s login with external account, try to active email, old status is %d",
oldUserInfo.ID, oldUserInfo.MailStatus)
// try to active user email
if oldUserInfo.MailStatus == entity.EmailStatusToBeVerified {
err = us.userRepo.UpdateEmailStatus(ctx, oldUserInfo.ID, entity.EmailStatusAvailable)
if err != nil {
return oldUserInfo.MailStatus, err
}
}
// try to update user avatar
if len(externalUserInfo.Avatar) > 0 && len(schema.FormatAvatarInfo(oldUserInfo.Avatar, oldUserInfo.EMail)) == 0 {
avatarInfo := &schema.AvatarInfo{
Type: schema.AvatarTypeCustom,
Custom: externalUserInfo.Avatar,
}
avatar, _ := json.Marshal(avatarInfo)
oldUserInfo.Avatar = string(avatar)
err = us.userRepo.UpdateInfo(ctx, oldUserInfo)
if err != nil {
log.Error(err)
}
}
if err = us.userActivity.UserActive(ctx, oldUserInfo.ID); err != nil {
return oldUserInfo.MailStatus, err
}
return entity.EmailStatusAvailable, nil
}
// ExternalLoginBindingUserSendEmail Send an email for third-party account login for binding user
func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(
ctx context.Context, req *schema.ExternalLoginBindingUserSendEmailReq) (
resp *schema.ExternalLoginBindingUserSendEmailResp, err error) {
siteGeneral, err := us.siteInfoCommonService.GetSiteGeneral(ctx)
if err != nil {
return nil, err
}
resp = &schema.ExternalLoginBindingUserSendEmailResp{}
externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, req.BindingKey)
if err != nil || len(externalLoginInfo.ExternalID) == 0 {
return nil, errors.BadRequest(reason.UserNotFound)
}
if len(externalLoginInfo.Email) > 0 {
log.Warnf("the binding email has been sent %s", req.BindingKey)
return &schema.ExternalLoginBindingUserSendEmailResp{}, nil
}
userInfo, exist, err := us.userRepo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, err
}
if exist && !req.Must {
resp.EmailExistAndMustBeConfirmed = true
return resp, nil
}
if !exist {
externalLoginInfo.Email = req.Email
userInfo, err = us.registerNewUser(ctx, externalLoginInfo)
if err != nil {
return nil, err
}
resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo(
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID)
if err != nil {
log.Error(err)
}
}
err = us.userExternalLoginRepo.SetCacheUserExternalLoginInfo(ctx, req.BindingKey, externalLoginInfo)
if err != nil {
return nil, err
}
// send bind confirmation email
data := &schema.EmailCodeContent{
SourceType: schema.BindingSourceType,
Email: req.Email,
UserID: userInfo.ID,
BindingKey: req.BindingKey,
}
code := uuid.NewString()
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", siteGeneral.SiteUrl, code)
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
if err != nil {
return nil, err
}
go us.emailService.SendAndSaveCode(ctx, userInfo.EMail, title, body, code, data.ToJSONString())
return resp, nil
}
// ExternalLoginBindingUser
// The user clicks on the email link of the bound account and requests the API to bind the user officially
func (us *UserExternalLoginService) ExternalLoginBindingUser(
ctx context.Context, bindingKey string, oldUserInfo *entity.User) (err error) {
externalLoginInfo, err := us.userExternalLoginRepo.GetCacheUserExternalLoginInfo(ctx, bindingKey)
if err != nil || len(externalLoginInfo.ExternalID) == 0 {
return errors.BadRequest(reason.UserNotFound)
}
return us.bindOldUser(ctx, externalLoginInfo, oldUserInfo)
}
// GetExternalLoginUserInfoList get external login user info list
func (us *UserExternalLoginService) GetExternalLoginUserInfoList(
ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error) {
return us.userExternalLoginRepo.GetUserExternalLoginList(ctx, userID)
}
// ExternalLoginUnbinding external login unbinding
func (us *UserExternalLoginService) ExternalLoginUnbinding(
ctx context.Context, req *schema.ExternalLoginUnbindingReq) (resp any, err error) {
// If user has only one external login and never set password, he can't unbind it.
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
if len(userInfo.Pass) == 0 {
loginList, err := us.userExternalLoginRepo.GetUserExternalLoginList(ctx, req.UserID)
if err != nil {
return nil, err
}
if len(loginList) <= 1 {
return schema.ErrTypeToast, errors.BadRequest(reason.UserExternalLoginUnbindingForbidden)
}
}
return nil, us.userExternalLoginRepo.DeleteUserExternalLogin(ctx, req.UserID, req.ExternalID)
}
// CheckUserStatusInUserCenter check user status in user center
func (us *UserExternalLoginService) CheckUserStatusInUserCenter(ctx context.Context, userID string) (
valid bool, externalID string, err error) {
// If enable user center plugin, user status should be checked by user center
userCenter, ok := plugin.GetUserCenter()
if !ok {
return true, "", nil
}
userInfoList, err := us.GetExternalLoginUserInfoList(ctx, userID)
if err != nil {
return false, "", err
}
var thisUcUserInfo *entity.UserExternalLogin
for _, t := range userInfoList {
if t.Provider == userCenter.Info().SlugName {
thisUcUserInfo = t
break
}
}
// If this user not login by user center, no need to check user status
if thisUcUserInfo == nil {
return true, "", nil
}
userStatus := userCenter.UserStatus(thisUcUserInfo.ExternalID)
if userStatus == plugin.UserStatusDeleted {
return false, thisUcUserInfo.ExternalID, nil
}
return true, thisUcUserInfo.ExternalID, nil
}
+166 -85
View File
@@ -20,7 +20,9 @@ import (
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/internal/service/user_external_login"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/plugin"
"github.com/google/uuid"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@@ -31,15 +33,16 @@ import (
// UserService user service
type UserService struct {
userCommonService *usercommon.UserCommon
userRepo usercommon.UserRepo
userActivity activity.UserActiveActivityRepo
activityRepo activity_common.ActivityRepo
serviceConfig *service_config.ServiceConfig
emailService *export.EmailService
authService *auth.AuthService
siteInfoService *siteinfo_common.SiteInfoCommonService
userRoleService *role.UserRoleRelService
userCommonService *usercommon.UserCommon
userRepo usercommon.UserRepo
userActivity activity.UserActiveActivityRepo
activityRepo activity_common.ActivityRepo
serviceConfig *service_config.ServiceConfig
emailService *export.EmailService
authService *auth.AuthService
siteInfoService *siteinfo_common.SiteInfoCommonService
userRoleService *role.UserRoleRelService
userExternalLoginService *user_external_login.UserExternalLoginService
}
func NewUserService(userRepo usercommon.UserRepo,
@@ -51,22 +54,25 @@ func NewUserService(userRepo usercommon.UserRepo,
siteInfoService *siteinfo_common.SiteInfoCommonService,
userRoleService *role.UserRoleRelService,
userCommonService *usercommon.UserCommon,
userExternalLoginService *user_external_login.UserExternalLoginService,
) *UserService {
return &UserService{
userCommonService: userCommonService,
userRepo: userRepo,
userActivity: userActivity,
activityRepo: activityRepo,
emailService: emailService,
serviceConfig: serviceConfig,
authService: authService,
siteInfoService: siteInfoService,
userRoleService: userRoleService,
userCommonService: userCommonService,
userRepo: userRepo,
userActivity: userActivity,
activityRepo: activityRepo,
emailService: emailService,
serviceConfig: serviceConfig,
authService: authService,
siteInfoService: siteInfoService,
userRoleService: userRoleService,
userExternalLoginService: userExternalLoginService,
}
}
// GetUserInfoByUserID get user info by user id
func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (resp *schema.GetUserToSetShowResp, err error) {
func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID string) (
resp *schema.GetUserToSetShowResp, err error) {
userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
@@ -74,6 +80,9 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
if userInfo.Status == entity.UserStatusDeleted {
return nil, errors.Unauthorized(reason.UnauthorizedError)
}
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
if err != nil {
log.Error(err)
@@ -82,6 +91,7 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
resp.GetFromUserEntity(userInfo)
resp.AccessToken = token
resp.RoleID = roleID
resp.HavePassword = len(userInfo.Pass) > 0
return resp, nil
}
@@ -112,10 +122,17 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
if !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {
return nil, errors.BadRequest(reason.EmailOrPasswordWrong)
}
ok, externalID, err := us.userExternalLoginService.CheckUserStatusInUserCenter(ctx, userInfo.ID)
if err != nil {
return nil, err
}
if !ok {
return nil, errors.BadRequest(reason.EmailOrPasswordWrong)
}
err = us.userRepo.UpdateLastLoginDate(ctx, userInfo.ID)
if err != nil {
log.Error("UpdateLastLoginDate", err.Error())
log.Errorf("update last login data failed, err: %v", err)
}
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
@@ -130,6 +147,7 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
RoleID: roleID,
ExternalID: externalID,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
@@ -171,42 +189,43 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
return nil
}
// UseRePassword
func (us *UserService) UseRePassword(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) {
// UpdatePasswordWhenForgot update user password when user forgot password
func (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) {
data := &schema.EmailCodeContent{}
err = data.FromJSONString(req.Content)
if err != nil {
return nil, errors.BadRequest(reason.EmailVerifyURLExpired)
return errors.BadRequest(reason.EmailVerifyURLExpired)
}
userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email)
if err != nil {
return nil, err
return err
}
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
return errors.BadRequest(reason.UserNotFound)
}
enpass, err := us.encryptPassword(ctx, req.Pass)
if err != nil {
return nil, err
return err
}
err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)
if err != nil {
return nil, err
return err
}
resp = &schema.GetUserResp{}
return resp, nil
// When the user changes the password, all the current user's tokens are invalid.
us.authService.RemoveUserAllTokens(ctx, userInfo.ID)
return nil
}
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, request *schema.UserModifyPassWordRequest) (bool, error) {
userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, req *schema.UserModifyPasswordReq) (bool, error) {
userInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return false, err
}
if !has {
return false, fmt.Errorf("user does not exist")
return false, errors.BadRequest(reason.UserNotFound)
}
isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
if !isPass {
return false, nil
}
@@ -215,33 +234,56 @@ func (us *UserService) UserModifyPassWordVerification(ctx context.Context, reque
}
// UserModifyPassword user modify password
func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.UserModifyPassWordRequest) error {
enpass, err := us.encryptPassword(ctx, request.Pass)
func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error {
enpass, err := us.encryptPassword(ctx, req.Pass)
if err != nil {
return err
}
userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return err
}
if !has {
return fmt.Errorf("user does not exist")
if !exist {
return errors.BadRequest(reason.UserNotFound)
}
isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
if !isPass {
return fmt.Errorf("the old password verification failed")
return errors.BadRequest(reason.OldPasswordVerificationFailed)
}
err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)
if err != nil {
return err
}
us.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken)
return nil
}
// UpdateInfo update user info
func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoRequest) (
errFields []*validator.FormErrorField, err error) {
if len(req.Username) > 0 {
siteUsers, err := us.siteInfoService.GetSiteUsers(ctx)
if err != nil {
return nil, err
}
if siteUsers.AllowUpdateUsername && len(req.Username) > 0 {
if checker.IsInvalidUsername(req.Username) {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
})
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
if checker.IsReservedUsername(req.Username) {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
})
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)
if err != nil {
return nil, err
@@ -253,31 +295,57 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
})
return errFields, errors.BadRequest(reason.UsernameDuplicate)
}
if checker.IsReservedUsername(req.Username) {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "username",
ErrorMsg: reason.UsernameInvalid,
})
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
}
avatar, err := json.Marshal(req.Avatar)
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil {
return nil, errors.BadRequest(reason.UserSetAvatar).WithError(err).WithStack()
return nil, err
}
userInfo := entity.User{}
userInfo.ID = req.UserID
userInfo.Avatar = string(avatar)
userInfo.DisplayName = req.DisplayName
userInfo.Bio = req.Bio
userInfo.BioHTML = req.BioHTML
userInfo.Location = req.Location
userInfo.Website = req.Website
userInfo.Username = req.Username
err = us.userRepo.UpdateInfo(ctx, &userInfo)
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req, siteUsers)
err = us.userRepo.UpdateInfo(ctx, cond)
return nil, err
}
func (us *UserService) formatUserInfoForUpdateInfo(
oldUserInfo *entity.User, req *schema.UpdateInfoRequest, siteUsersConf *schema.SiteUsersResp) *entity.User {
avatar, _ := json.Marshal(req.Avatar)
userInfo := &entity.User{}
userInfo.DisplayName = oldUserInfo.DisplayName
userInfo.Username = oldUserInfo.Username
userInfo.Avatar = oldUserInfo.Avatar
userInfo.Bio = oldUserInfo.Bio
userInfo.BioHTML = oldUserInfo.BioHTML
userInfo.Website = oldUserInfo.Website
userInfo.Location = oldUserInfo.Location
userInfo.ID = req.UserID
if len(req.DisplayName) > 0 && siteUsersConf.AllowUpdateDisplayName {
userInfo.DisplayName = req.DisplayName
}
if len(req.Username) > 0 && siteUsersConf.AllowUpdateUsername {
userInfo.Username = req.Username
}
if len(avatar) > 0 && siteUsersConf.AllowUpdateAvatar {
userInfo.Avatar = string(avatar)
}
if siteUsersConf.AllowUpdateBio {
userInfo.Bio = req.Bio
userInfo.BioHTML = req.BioHTML
}
if siteUsersConf.AllowUpdateWebsite {
userInfo.Website = req.Website
}
if siteUsersConf.AllowUpdateLocation {
userInfo.Location = req.Location
}
return userInfo
}
func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, error) {
_, has, err := us.userRepo.GetByEmail(ctx, email)
if err != nil {
@@ -435,50 +503,48 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
if !has {
return nil, errors.BadRequest(reason.UserNotFound)
}
userInfo.MailStatus = entity.EmailStatusAvailable
err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus)
if err != nil {
return nil, err
if userInfo.MailStatus == entity.EmailStatusToBeVerified {
userInfo.MailStatus = entity.EmailStatusAvailable
err = us.userRepo.UpdateEmailStatus(ctx, userInfo.ID, userInfo.MailStatus)
if err != nil {
return nil, err
}
}
if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil {
log.Error(err)
}
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
// In the case of three-party login, the associated users are bound
if len(data.BindingKey) > 0 {
err = us.userExternalLoginService.ExternalLoginBindingUser(ctx, data.BindingKey, userInfo)
if err != nil {
return nil, err
}
}
accessToken, userCacheInfo, err := us.userCommonService.CacheLoginUserInfo(
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, "")
if err != nil {
log.Error(err)
return nil, err
}
resp = &schema.GetUserResp{}
resp.GetFromUserEntity(userInfo)
userCacheInfo := &entity.UserCacheInfo{
UserID: userInfo.ID,
EmailStatus: userInfo.MailStatus,
UserStatus: userInfo.Status,
RoleID: roleID,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return nil, err
}
resp.AccessToken = accessToken
// User verified email will update user email status. So user status cache should be updated.
if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {
return nil, err
}
resp.RoleID = userCacheInfo.RoleID
if resp.RoleID == role.RoleAdminID {
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
if err != nil {
return nil, err
}
}
return resp, nil
}
// verifyPassword
// Compare whether the password is correct
func (us *UserService) verifyPassword(ctx context.Context, LoginPass, UserPass string) bool {
err := bcrypt.CompareHashAndPassword([]byte(UserPass), []byte(LoginPass))
func (us *UserService) verifyPassword(ctx context.Context, loginPass, userPass string) bool {
if len(loginPass) == 0 && len(userPass) == 0 {
return true
}
err := bcrypt.CompareHashAndPassword([]byte(userPass), []byte(loginPass))
return err == nil
}
@@ -501,6 +567,15 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
return nil, errors.BadRequest(reason.UserNotFound)
}
// If user's email already verified, then must verify password first.
if userInfo.MailStatus == entity.EmailStatusAvailable && !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {
resp = append(resp, &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed),
})
return resp, errors.BadRequest(reason.OldPasswordVerificationFailed)
}
_, exist, err = us.userRepo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, err
@@ -664,6 +739,9 @@ func (us *UserService) UserUnsubscribeEmailNotification(
func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, endTime time.Time, limit int,
userIDExist map[string]bool) (rankStat []*entity.ActivityUserRankStat, userIDs []string, err error) {
if plugin.RankAgentEnabled() {
return make([]*entity.ActivityUserRankStat, 0), make([]string, 0), nil
}
rankStat, err = us.activityRepo.GetUsersWhoHasGainedTheMostReputation(ctx, startTime, endTime, limit)
if err != nil {
return nil, nil, err
@@ -683,6 +761,9 @@ func (us *UserService) getActivityUserRankStat(ctx context.Context, startTime, e
func (us *UserService) getActivityUserVoteStat(ctx context.Context, startTime, endTime time.Time, limit int,
userIDExist map[string]bool) (voteStat []*entity.ActivityUserVoteStat, userIDs []string, err error) {
if plugin.RankAgentEnabled() {
return make([]*entity.ActivityUserVoteStat, 0), make([]string, 0), nil
}
voteStat, err = us.activityRepo.GetUsersWhoHasVoteMost(ctx, startTime, endTime, limit)
if err != nil {
return nil, nil, err
+8 -8
View File
@@ -63,12 +63,12 @@ func NewVoteService(
}
// VoteUp vote up
func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{}
var objectUserID string
objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID)
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
if err != nil {
return
}
@@ -80,19 +80,19 @@ func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteRes
}
if dto.IsCancel {
return as.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
} else {
return as.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
}
}
// VoteDown vote down
func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
voteResp = &schema.VoteResp{}
var objectUserID string
objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID)
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
if err != nil {
return
}
@@ -104,9 +104,9 @@ func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteR
}
if dto.IsCancel {
return as.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
} else {
return as.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
}
}
+17
View File
@@ -0,0 +1,17 @@
package checker
import "strings"
func EmailInAllowEmailDomain(email string, allowEmailDomains []string) bool {
if len(allowEmailDomains) == 0 {
return true
}
for _, domain := range allowEmailDomains {
if strings.HasSuffix(email, domain) {
return true
}
}
return false
}
+7 -2
View File
@@ -38,6 +38,7 @@ func Markdown2HTML(source string) string {
filter.RequireNoFollowOnLinks(false)
filter.RequireParseableURLs(false)
filter.RequireNoFollowOnFullyQualifiedLinks(false)
filter.AllowElements("kbd")
html = filter.Sanitize(html)
return html
}
@@ -46,7 +47,7 @@ func Markdown2HTML(source string) string {
func Markdown2BasicHTML(source string) string {
content := Markdown2HTML(source)
filter := bluemonday.NewPolicy()
filter.AllowElements("p", "b", "br")
filter.AllowElements("p", "b", "br", "strong", "em")
filter.AllowAttrs("src").OnElements("img")
filter.AddSpaceWhenStrippingTag(true)
content = filter.Sanitize(content)
@@ -87,7 +88,11 @@ func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, n
l := n.Segments.Len()
for i := 0; i < l; i++ {
segment := n.Segments.At(i)
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
if string(source[segment.Start:segment.Stop]) == "<kbd>" || string(source[segment.Start:segment.Stop]) == "</kbd>" {
_, _ = w.Write(segment.Value(source))
} else {
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
}
}
return ast.WalkSkipChildren, nil
}
+18
View File
@@ -0,0 +1,18 @@
package random
import (
"encoding/hex"
"math/rand"
)
func UsernameSuffix() string {
bytes := make([]byte, 2)
_, _ = rand.Read(bytes)
return hex.EncodeToString(bytes)
}
func Username() string {
bytes := make([]byte, 6)
_, _ = rand.Read(bytes)
return hex.EncodeToString(bytes)
}

Some files were not shown because too many files have changed in this diff Show More