Merge remote-tracking branch 'github/feat/1.1.2/user-center' into beta.2/1.1.0

# Conflicts:
#	docs/docs.go
#	docs/swagger.json
#	docs/swagger.yaml
#	go.mod
#	i18n/en_US.yaml
#	internal/base/reason/reason.go
#	internal/migrations/init.go
#	internal/migrations/migrations.go
This commit is contained in:
LinkinStars 2023-05-09 10:49:00 +08:00
commit b0a04bbf11
60 changed files with 1656 additions and 360 deletions

View File

@ -193,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)
@ -220,7 +220,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService)
templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController)
connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService)
userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo)
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)

View File

@ -662,6 +662,77 @@ const docTemplate = `{
}
}
},
"/answer/admin/api/setting/privileges": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "GetPrivilegesConfig get privileges config",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "GetPrivilegesConfig get privileges config",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.GetPrivilegesConfigResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update privileges config",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update privileges config",
"parameters": [
{
"description": "config",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UpdatePrivilegesConfigReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/setting/smtp": {
"get": {
"security": [
@ -1301,6 +1372,77 @@ const docTemplate = `{
}
}
},
"/answer/admin/api/siteinfo/users": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site user config",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site user config",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteUsersResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site info config about users",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site info config about users",
"parameters": [
{
"description": "users info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteUsersReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/write": {
"get": {
"security": [
@ -2861,7 +3003,7 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "list personal answers",
"description": "UserAnswerList",
"consumes": [
"application/json"
],
@ -2869,9 +3011,9 @@ const docTemplate = `{
"application/json"
],
"tags": [
"Personal"
"api-answer"
],
"summary": "list personal answers",
"summary": "UserAnswerList",
"parameters": [
{
"type": "string",
@ -2903,8 +3045,8 @@ const docTemplate = `{
{
"type": "string",
"default": "20",
"description": "page_size",
"name": "page_size",
"description": "pagesize",
"name": "pagesize",
"in": "query",
"required": true
}
@ -2926,7 +3068,7 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "list personal collections",
"description": "UserCollectionList",
"consumes": [
"application/json"
],
@ -2936,7 +3078,7 @@ const docTemplate = `{
"tags": [
"Collection"
],
"summary": "list personal collections",
"summary": "UserCollectionList",
"parameters": [
{
"type": "string",
@ -2949,8 +3091,8 @@ const docTemplate = `{
{
"type": "string",
"default": "20",
"description": "page_size",
"name": "page_size",
"description": "pagesize",
"name": "pagesize",
"in": "query",
"required": true
}
@ -5668,7 +5810,7 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "list personal questions",
"description": "UserList",
"consumes": [
"application/json"
],
@ -5676,9 +5818,9 @@ const docTemplate = `{
"application/json"
],
"tags": [
"Personal"
"Question"
],
"summary": "list personal questions",
"summary": "UserList",
"parameters": [
{
"type": "string",
@ -5710,8 +5852,8 @@ const docTemplate = `{
{
"type": "string",
"default": "20",
"description": "page_size",
"name": "page_size",
"description": "pagesize",
"name": "pagesize",
"in": "query",
"required": true
}
@ -5748,6 +5890,20 @@ const docTemplate = `{
}
},
"definitions": {
"constant.Privilege": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
},
"value": {
"type": "integer"
}
}
},
"handler.RespBody": {
"type": "object",
"properties": {
@ -6229,9 +6385,20 @@ const docTemplate = `{
}
}
},
"schema.ConfigFieldUIOptionAction": {
"type": "object",
"properties": {
"url": {
"type": "string"
}
}
},
"schema.ConfigFieldUIOptions": {
"type": "object",
"properties": {
"action": {
"$ref": "#/definitions/schema.ConfigFieldUIOptionAction"
},
"input_type": {
"type": "string"
},
@ -6243,6 +6410,12 @@ const docTemplate = `{
},
"rows": {
"type": "string"
},
"text": {
"type": "string"
},
"variant": {
"type": "string"
}
}
},
@ -6649,6 +6822,20 @@ const docTemplate = `{
}
}
},
"schema.GetPrivilegesConfigResp": {
"type": "object",
"properties": {
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/schema.PrivilegeOption"
}
},
"selected_level": {
"type": "integer"
}
}
},
"schema.GetRankPersonalWithPageResp": {
"type": "object",
"properties": {
@ -7351,6 +7538,23 @@ const docTemplate = `{
}
}
},
"schema.PrivilegeOption": {
"type": "object",
"properties": {
"level": {
"type": "integer"
},
"level_desc": {
"type": "string"
},
"privileges": {
"type": "array",
"items": {
"$ref": "#/definitions/constant.Privilege"
}
}
}
},
"schema.QuestionAdd": {
"type": "object",
"required": [
@ -7418,6 +7622,10 @@ const docTemplate = `{
"schema.QuestionPageReq": {
"type": "object",
"properties": {
"inDays": {
"type": "integer",
"minimum": 1
},
"orderCond": {
"type": "string",
"enum": [
@ -7800,6 +8008,10 @@ const docTemplate = `{
"custom_header": {
"type": "string",
"maxLength": 65536
},
"custom_sidebar": {
"type": "string",
"maxLength": 65536
}
}
},
@ -7821,6 +8033,10 @@ const docTemplate = `{
"custom_header": {
"type": "string",
"maxLength": 65536
},
"custom_sidebar": {
"type": "string",
"maxLength": 65536
}
}
},
@ -7908,6 +8124,9 @@ const docTemplate = `{
"site_seo": {
"$ref": "#/definitions/schema.SiteSeoReq"
},
"site_users": {
"$ref": "#/definitions/schema.SiteUsersResp"
},
"theme": {
"$ref": "#/definitions/schema.SiteThemeResp"
},
@ -7919,18 +8138,10 @@ const docTemplate = `{
"schema.SiteInterfaceReq": {
"type": "object",
"required": [
"default_avatar",
"language",
"time_zone"
],
"properties": {
"default_avatar": {
"type": "string",
"enum": [
"system",
"gravatar"
]
},
"language": {
"type": "string",
"maxLength": 128
@ -7944,18 +8155,10 @@ const docTemplate = `{
"schema.SiteInterfaceResp": {
"type": "object",
"required": [
"default_avatar",
"language",
"time_zone"
],
"properties": {
"default_avatar": {
"type": "string",
"enum": [
"system",
"gravatar"
]
},
"language": {
"type": "string",
"maxLength": 128
@ -8003,6 +8206,15 @@ const docTemplate = `{
"schema.SiteLoginReq": {
"type": "object",
"properties": {
"allow_email_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"allow_email_registrations": {
"type": "boolean"
},
"allow_new_registrations": {
"type": "boolean"
},
@ -8014,6 +8226,15 @@ const docTemplate = `{
"schema.SiteLoginResp": {
"type": "object",
"properties": {
"allow_email_domains": {
"type": "array",
"items": {
"type": "string"
}
},
"allow_email_registrations": {
"type": "boolean"
},
"allow_new_registrations": {
"type": "boolean"
},
@ -8090,6 +8311,72 @@ const docTemplate = `{
}
}
},
"schema.SiteUsersReq": {
"type": "object",
"required": [
"default_avatar"
],
"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": {
"type": "string",
"enum": [
"system",
"gravatar"
]
}
}
},
"schema.SiteUsersResp": {
"type": "object",
"required": [
"default_avatar"
],
"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": {
"type": "string",
"enum": [
"system",
"gravatar"
]
}
}
},
"schema.SiteWriteReq": {
"type": "object",
"properties": {
@ -8324,6 +8611,19 @@ const docTemplate = `{
}
}
},
"schema.UpdatePrivilegesConfigReq": {
"type": "object",
"required": [
"level"
],
"properties": {
"level": {
"type": "integer",
"maximum": 3,
"minimum": 1
}
}
},
"schema.UpdateSMTPConfigReq": {
"type": "object",
"properties": {

47
go.sum
View File

@ -76,7 +76,6 @@ github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267/go.mod h1:Yj3yPP
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@ -113,10 +112,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
@ -172,7 +167,6 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
@ -231,8 +225,6 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
@ -281,8 +273,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
@ -300,7 +290,6 @@ 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.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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=
@ -317,8 +306,6 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
@ -348,18 +335,13 @@ github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@ -382,8 +364,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imroc/req/v3 v3.33.1 h1:BZnyl+K0hXcJlZBHY2CqbPgmVc1pPJDzjn6aJfB6shI=
github.com/imroc/req/v3 v3.33.1/go.mod h1:cZ+7C3L/AYOr4tLGG16hZF90F1WzAdAdzt1xFSlizXY=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@ -558,10 +538,9 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
@ -625,23 +604,12 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
@ -774,7 +742,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
@ -827,8 +794,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -1014,7 +979,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
@ -1148,7 +1112,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
@ -1162,8 +1125,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
@ -1182,14 +1143,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1201,7 +1157,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=

View File

@ -25,6 +25,8 @@ backend:
other: Close
reopen:
other: Reopen
forbidden_error:
other: Forbidden.
pin:
other: Pin
hide:
@ -48,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:
@ -85,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.
@ -176,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
@ -281,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:

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:
@ -1063,6 +1131,9 @@ ui:
installed_plugins: 插件列表
website_welcome: 欢迎来到 {{site_name}}
plugins:
login: 登录
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。
oauth:
connect: 连接到 {{ auth_name }}
remove: 解绑 {{ auth_name }}
@ -1396,6 +1467,30 @@ ui:
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: 不能为空

View File

@ -66,6 +66,8 @@ const (
SiteTypeLogin = "login"
SiteTypeCustomCssHTML = "css-html"
SiteTypeTheme = "theme"
SiteTypePrivileges = "privileges"
SiteTypeUsers = "users"
)
func ExistInPathIgnore(name string) bool {

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"
)

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},
}
)

View File

@ -1,5 +1,6 @@
package constant
var (
DefaultAvatar = "system"
DefaultAvatar = "system"
DefaultSiteURL = ""
)

View File

@ -8,9 +8,13 @@ import (
"github.com/segmentfault/pacman/errors"
)
// BanAPIWhenUserCenterEnabled ban api when user center enabled
func BanAPIWhenUserCenterEnabled(ctx *gin.Context) {
if plugin.UserCenterEnabled() {
// 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

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"
)

View File

@ -42,6 +42,7 @@ const (
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"
@ -50,7 +51,7 @@ const (
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"
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
@ -73,4 +74,6 @@ const (
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"
)

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"
)
@ -66,6 +67,14 @@ func NewHTTPServer(debug bool,
// plugin routes
pluginAPIRouter.RegisterUnAuthConnectorRouter(mustUnAuthV1)
pluginAPIRouter.RegisterAuthConnectorRouter(authV1)
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
}

View File

@ -130,7 +130,7 @@ func (cc *ConnectorController) ConnectorRedirect(connector plugin.Connector) (fn
return
}
if len(resp.AccessToken) > 0 {
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/oauth?access_token=%s",
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",

View File

@ -60,9 +60,11 @@ func (uc *UserCenterController) UserCenterAgent(ctx *gin.Context) {
_ = 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{
@ -90,8 +92,8 @@ func (uc *UserCenterController) UserCenterPersonalBranding(ctx *gin.Context) {
func (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) {
var redirectURL string
_ = plugin.CallUserCenter(func(uc plugin.UserCenter) error {
info := uc.Description()
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
info := userCenter.Description()
redirectURL = info.LoginRedirectURL
return nil
})
@ -100,9 +102,9 @@ func (uc *UserCenterController) UserCenterLoginRedirect(ctx *gin.Context) {
func (uc *UserCenterController) UserCenterSignUpRedirect(ctx *gin.Context) {
var redirectURL string
_ = plugin.CallUserCenter(func(uc plugin.UserCenter) error {
info := uc.Description()
redirectURL = info.SignUpRedirectURL
_ = plugin.CallUserCenter(func(userCenter plugin.UserCenter) error {
info := userCenter.Description()
redirectURL = info.LoginRedirectURL
return nil
})
ctx.Redirect(http.StatusFound, redirectURL)
@ -124,17 +126,24 @@ func (uc *UserCenterController) UserCenterLoginCallback(ctx *gin.Context) {
userInfo, err := userCenter.LoginCallback(ctx)
if err != nil {
log.Error(err)
ctx.Redirect(http.StatusFound, "/50x")
if !ctx.IsAborted() {
ctx.Redirect(http.StatusFound, "/50x")
}
return
}
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter.Info().SlugName, userInfo)
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
if err != nil {
log.Errorf("external login failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/oauth?access_token=%s",
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))
}
@ -158,13 +167,18 @@ func (uc *UserCenterController) UserCenterSignUpCallback(ctx *gin.Context) {
return
}
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter.Info().SlugName, userInfo)
resp, err := uc.userCenterLoginService.ExternalLogin(ctx, userCenter, userInfo)
if err != nil {
log.Errorf("external login failed: %v", err)
ctx.Redirect(http.StatusFound, "/50x")
return
}
ctx.Redirect(http.StatusFound, fmt.Sprintf("%s/users/oauth?access_token=%s",
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))
}
@ -174,3 +188,9 @@ func (uc *UserCenterController) UserCenterUserSettings(ctx *gin.Context) {
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)
}

View File

@ -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)
}

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"
@ -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 {
@ -489,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 {

View File

@ -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)
}

View File

@ -32,7 +32,7 @@ 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 plugin.UserCenterEnabled() {
if u, ok := plugin.GetUserCenter(); ok && u.Description().UserStatusAgentEnabled {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
return
}
@ -80,10 +80,6 @@ func (uc *UserAdminController) UpdateUserRole(ctx *gin.Context) {
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user [post]
func (uc *UserAdminController) AddUser(ctx *gin.Context) {
if plugin.UserCenterEnabled() {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
return
}
req := &schema.AddUserReq{}
if handler.BindAndCheck(ctx, req) {
return
@ -106,10 +102,6 @@ func (uc *UserAdminController) AddUser(ctx *gin.Context) {
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/password [put]
func (uc *UserAdminController) UpdateUserPassword(ctx *gin.Context) {
if plugin.UserCenterEnabled() {
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
return
}
req := &schema.UpdateUserPasswordReq{}
if handler.BindAndCheck(ctx, req) {
return

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"`
}

View File

@ -143,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{
@ -178,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
}
@ -346,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

View File

@ -61,6 +61,7 @@ var migrations = []Migration{
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

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
}

View File

@ -18,10 +18,6 @@ func addPlugin(x *xorm.Engine) error {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {

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)
}
}

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)
}
}

View File

@ -255,7 +255,7 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
}
// GetQuestionPage query question page
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string) (
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, userID, tagID, orderCond string, inDays int) (
questionList []*entity.Question, total int64, err error) {
questionList = make([]*entity.Question, 0)
@ -271,6 +271,9 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
} else {
session.And("question.show = ?", entity.QuestionShow)
}
if inDays > 0 {
session.And("question.created_at > ?", time.Now().AddDate(0, 0, -inDays))
}
switch orderCond {
case "newest":

View File

@ -86,6 +86,9 @@ 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
@ -102,6 +105,9 @@ func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (
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

View File

@ -10,6 +10,7 @@ import (
"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"
@ -195,6 +196,9 @@ func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
}
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
@ -276,14 +280,27 @@ func decorateByUserCenterUser(original *entity.User, ucUser *plugin.UserCenterBa
if original.Username != ucUser.Username {
log.Warnf("user %s username is inconsistent with user center", original.ID)
}
original.DisplayName = ucUser.DisplayName
original.EMail = ucUser.Email
original.Avatar = schema.CustomAvatar(ucUser.Avatar).ToJsonString()
original.Mobile = ucUser.Mobile
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
}
original.Status = int(ucUser.Status)
if ucUser.Status != plugin.UserStatusAvailable {
original.Status = int(ucUser.Status)
}
}

View File

@ -102,7 +102,7 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
// user
r.GET("/user/info", a.userController.GetUserInfoByUserID)
routerGroup := r.Group("", middleware.BanAPIWhenUserCenterEnabled)
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)
@ -117,8 +117,8 @@ func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup)
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// user
r.GET("/user/logout", a.userController.UserLogout)
r.POST("/user/email/change/code", middleware.BanAPIWhenUserCenterEnabled, a.userController.UserChangeEmailSendCode)
r.POST("/user/email/verification/send", middleware.BanAPIWhenUserCenterEnabled, 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)
@ -206,8 +206,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.DELETE("/answer", a.answerController.RemoveAnswer)
// user
r.PUT("/user/password", middleware.BanAPIWhenUserCenterEnabled, a.userController.UserModifyPassWord)
r.PUT("/user/info", middleware.BanAPIWhenUserCenterEnabled, a.userController.UserUpdateInfo)
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)
@ -246,10 +246,10 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
// user
r.GET("/users/page", a.adminUserController.GetUserPage)
r.PUT("/user/status", middleware.BanAPIWhenUserCenterEnabled, a.adminUserController.UpdateUserStatus)
r.PUT("/user/status", a.adminUserController.UpdateUserStatus)
r.PUT("/user/role", a.adminUserController.UpdateUserRole)
r.POST("/user", middleware.BanAPIWhenUserCenterEnabled, a.adminUserController.AddUser)
r.PUT("/user/password", middleware.BanAPIWhenUserCenterEnabled, a.adminUserController.UpdateUserPassword)
r.POST("/user", a.adminUserController.AddUser)
r.PUT("/user/password", a.adminUserController.UpdateUserPassword)
// reason
r.GET("/reasons", a.reasonController.Reasons)
@ -262,25 +262,29 @@ 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)

View File

@ -37,10 +37,14 @@ func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) {
r.GET("/user-center/sign-up/callback", pr.userCenterController.UserCenterSignUpCallback)
}
func (pr *PluginAPIRouter) RegisterAuthConnectorRouter(r *gin.RouterGroup) {
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)
}

View File

@ -56,10 +56,31 @@ func (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.
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{
@ -83,10 +104,13 @@ type ConfigField struct {
}
type ConfigFieldUIOptions struct {
Placeholder string `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType string `json:"input_type,omitempty"`
Label string `json:"label,omitempty"`
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 {
@ -94,6 +118,23 @@ type ConfigFieldOption struct {
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"`

View File

@ -6,12 +6,14 @@ type UserCenterAgentResp struct {
}
type AgentInfo struct {
Name string `json:"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"`
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 {

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:"-"`

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,
})
}
}
}

View File

@ -4,6 +4,9 @@ package schema
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
@ -49,6 +52,8 @@ type ExternalLoginUserInfoCache struct {
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
@ -63,6 +68,13 @@ type UserCenterUserSettingsResp struct {
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"`

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
@ -300,7 +298,7 @@ func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField,
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
@ -329,16 +327,6 @@ func (a *AvatarInfo) ToJsonString() string {
}
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
}

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)

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"
)
@ -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
}

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)
}

View File

@ -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
}

View File

@ -193,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

View File

@ -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"

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)

View File

@ -1002,7 +1002,7 @@ func (qs *QuestionService) GetQuestionPage(ctx context.Context, req *schema.Ques
}
questionList, total, err := qs.questionRepo.GetQuestionPage(ctx, req.Page, req.PageSize,
req.UserIDBeSearched, req.TagID, req.OrderCond)
req.UserIDBeSearched, req.TagID, req.OrderCond, req.InDays)
if err != nil {
return nil, 0, err
}
@ -1064,7 +1064,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
msg.ReceiverUserID = questionInfo.UserID
msg.TriggerUserID = questionInfo.UserID
msg.ObjectType = constant.QuestionObjectType
msg.NotificationAction = constant.YourQuestionWasDeleted
msg.NotificationAction = constant.NotificationYourQuestionWasDeleted
notice_queue.AddNotification(msg)
return nil
}

View File

@ -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

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{

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
}

View File

@ -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

View File

@ -150,7 +150,7 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
return username + suffix, nil
}
func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, userStatus, emailStatus int) (
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 {
@ -162,6 +162,7 @@ func (us *UserCommon) CacheLoginUserInfo(ctx context.Context, userID string, use
EmailStatus: emailStatus,
UserStatus: userStatus,
RoleID: roleID,
ExternalID: externalID,
}
accessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)

View File

@ -5,10 +5,16 @@ import (
"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"
@ -20,6 +26,7 @@ type UserCenterLoginService struct {
userExternalLoginRepo UserExternalLoginRepo
userCommonService *usercommon.UserCommon
userActivity activity.UserActiveActivityRepo
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
}
// NewUserCenterLoginService new user external login service
@ -28,21 +35,38 @@ func NewUserCenterLoginService(
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, provider string, basicUserInfo *plugin.UserCenterBasicUserInfo) (
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,
provider, basicUserInfo.ExternalID)
userCenter.Info().SlugName, basicUserInfo.ExternalID)
if err != nil {
return nil, err
}
@ -53,16 +77,28 @@ func (us *UserCenterLoginService) ExternalLogin(
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)
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
}
oldUserInfo, err := us.registerNewUser(ctx, provider, basicUserInfo)
// 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
}
@ -70,7 +106,7 @@ func (us *UserCenterLoginService) ExternalLogin(
us.activeUser(ctx, oldUserInfo)
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status)
ctx, oldUserInfo.ID, oldUserInfo.MailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
@ -98,6 +134,8 @@ func (us *UserCenterLoginService) registerNewUser(ctx context.Context, provider
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
@ -166,6 +204,31 @@ func (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, us
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{

View File

@ -15,6 +15,7 @@ import (
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"
@ -83,7 +84,7 @@ func (us *UserExternalLoginService) ExternalLogin(
log.Error(err)
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status)
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
}
@ -122,7 +123,7 @@ func (us *UserExternalLoginService) ExternalLogin(
}
accessToken, _, err := us.userCommonService.CacheLoginUserInfo(
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status)
ctx, oldUserInfo.ID, newMailStatus, oldUserInfo.Status, oldExternalLoginUserInfo.ExternalID)
return &schema.UserExternalLoginResp{AccessToken: accessToken}, err
}
@ -150,6 +151,8 @@ func (us *UserExternalLoginService) registerNewUser(ctx context.Context,
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
@ -249,7 +252,7 @@ func (us *UserExternalLoginService) ExternalLoginBindingUserSendEmail(
return nil, err
}
resp.AccessToken, _, err = us.userCommonService.CacheLoginUserInfo(
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status)
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, externalLoginInfo.ExternalID)
if err != nil {
log.Error(err)
}
@ -316,3 +319,33 @@ func (us *UserExternalLoginService) ExternalLoginUnbinding(
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
}

View File

@ -80,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)
@ -119,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)
@ -137,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 {
@ -252,7 +263,27 @@ func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserM
// 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
@ -264,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 {
@ -466,7 +523,7 @@ func (us *UserService) UserVerifyEmail(ctx context.Context, req *schema.UserVeri
}
accessToken, userCacheInfo, err := us.userCommonService.CacheLoginUserInfo(
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status)
ctx, userInfo.ID, userInfo.MailStatus, userInfo.Status, "")
if err != nil {
return nil, err
}

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
pkg/checker/email.go Normal file
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
}

24
plugin/agent.go Normal file
View File

@ -0,0 +1,24 @@
package plugin
import (
"github.com/answerdev/answer/internal/base/constant"
"github.com/gin-gonic/gin"
)
type Agent interface {
Base
RegisterUnAuthRouter(r *gin.RouterGroup)
RegisterAuthUserRouter(r *gin.RouterGroup)
RegisterAuthAdminRouter(r *gin.RouterGroup)
}
var (
CallAgent,
registerAgent = MakePlugin[Agent](true)
)
// SiteURL The site url is the domain address of the current site. e.g. http://localhost:8080
// When some Agent plugins want to redirect to the origin site, it can use this function to get the site url.
func SiteURL() string {
return constant.DefaultSiteURL
}

View File

@ -12,6 +12,7 @@ const (
ConfigTypeUpload ConfigType = "upload"
ConfigTypeTimezone ConfigType = "timezone"
ConfigTypeSwitch ConfigType = "switch"
ConfigTypeButton ConfigType = "button"
)
const (
@ -43,10 +44,13 @@ type ConfigField struct {
}
type ConfigFieldUIOptions struct {
Placeholder Translator `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType InputType `json:"input_type,omitempty"`
Label Translator `json:"label,omitempty"`
Placeholder Translator `json:"placeholder,omitempty"`
Rows string `json:"rows,omitempty"`
InputType InputType `json:"input_type,omitempty"`
Label Translator `json:"label,omitempty"`
Action *UIOptionAction `json:"action,omitempty"`
Variant string `json:"variant,omitempty"`
Text Translator `json:"text,omitempty"`
}
type ConfigFieldOption struct {
@ -54,6 +58,31 @@ type ConfigFieldOption struct {
Value string `json:"value"`
}
type UIOptionAction struct {
Url string `json:"url"`
Method string `json:"method,omitempty"`
Loading *LoadingAction `json:"loading,omitempty"`
OnComplete *OnCompleteAction `json:"on_complete,omitempty"`
}
const (
LoadingActionStateNone LoadingActionType = "none"
LoadingActionStatePending LoadingActionType = "pending"
LoadingActionStateComplete LoadingActionType = "completed"
)
type LoadingActionType string
type LoadingAction struct {
Text Translator `json:"text"`
State LoadingActionType `json:"state"`
}
type OnCompleteAction struct {
ToastReturnMessage bool `json:"toast_return_message"`
RefreshFormConfig bool `json:"refresh_form_config"`
}
type Config interface {
Base

View File

@ -52,6 +52,10 @@ func Register(p Base) {
if _, ok := p.(UserCenter); ok {
registerUserCenter(p.(UserCenter))
}
if _, ok := p.(Agent); ok {
registerAgent(p.(Agent))
}
}
type Stack[T Base] struct {
@ -132,7 +136,7 @@ type TranslateFn func(ctx *GinContext) string
// Translator contains a function that translates the key to the current language of the context
type Translator struct {
fn TranslateFn
Fn TranslateFn
}
// MakeTranslator generates a translator from the key
@ -140,13 +144,13 @@ func MakeTranslator(key string) Translator {
t := func(ctx *GinContext) string {
return Translate(ctx, key)
}
return Translator{fn: t}
return Translator{Fn: t}
}
// Translate translates the key to the current language of the context
func (t Translator) Translate(ctx *GinContext) string {
if &t == nil || t.fn == nil {
if &t == nil || t.Fn == nil {
return ""
}
return t.fn(ctx)
return t.Fn(ctx)
}

View File

@ -12,21 +12,29 @@ type UserCenter interface {
SignUpCallback(ctx *GinContext) (userInfo *UserCenterBasicUserInfo, err error)
// UserInfo returns the user information
UserInfo(externalID string) (userInfo *UserCenterBasicUserInfo, err error)
// UserStatus returns the latest user status
UserStatus(externalID string) (userStatus UserStatus)
// UserList returns the user list information
UserList(externalIDs []string) (userInfo []*UserCenterBasicUserInfo, err error)
// UserSettings returns the user settings
UserSettings(externalID string) (userSettings *SettingInfo, err error)
// PersonalBranding returns the personal branding information
PersonalBranding(externalID string) (branding []*PersonalBranding)
// AfterLogin is called after the user logs in
AfterLogin(externalID, accessToken string)
}
type UserCenterDesc struct {
Name string `json:"name"`
Icon string `json:"icon"`
Url string `json:"url"`
LoginRedirectURL string `json:"login_redirect_url"`
SignUpRedirectURL string `json:"sign_up_redirect_url"`
RankAgentEnabled bool `json:"rank_agent_enabled"`
Name string `json:"name"`
DisplayName Translator `json:"display_name"`
Icon string `json:"icon"`
Url string `json:"url"`
LoginRedirectURL string `json:"login_redirect_url"`
SignUpRedirectURL string `json:"sign_up_redirect_url"`
RankAgentEnabled bool `json:"rank_agent_enabled"`
UserStatusAgentEnabled bool `json:"user_status_agent_enabled"`
MustAuthEmailEnabled bool `json:"must_auth_email_enabled"`
EnabledOriginalUserSystem bool `json:"enabled_original_user_system"`
}
type UserStatus int
@ -45,6 +53,7 @@ type UserCenterBasicUserInfo struct {
Rank int `json:"rank"`
Avatar string `json:"avatar"`
Mobile string `json:"mobile"`
Bio string `json:"bio"`
Status UserStatus `json:"status"`
}

View File

@ -1,26 +1,21 @@
#!/bin/bash
set -e
echo "begin build plugin"
plugin_file=./script/plugin_list
if [ ! -f "$plugin_file" ]; then
echo "plugin_list is not exist"
exit 0
fi
num=0
for line in `cat $plugin_file`
do
account=$line
accounts[$num]=$account
((num++))
done
if [ $num -eq 0 ]; then
echo "plugin_list is null"
exit 0
fi
echo "plugin_list exist"
cmd="./answer build "
for repo in ${accounts[@]}
for repo in `cat $plugin_file`
do
echo ${repo}
cmd=$cmd" --with "${repo}
echo ${repo}
cmd=$cmd" --with "${repo}
done
echo "cmd is "$cmd
$cmd
if [ ! -f "./new_answer" ]; then
echo "new_answer is not exist build failed"