Merge remote-tracking branch 'origin/main' into github-main

This commit is contained in:
LinkinStar
2022-12-18 16:17:35 +08:00
291 changed files with 13888 additions and 3220 deletions
+3
View File
@@ -9,6 +9,7 @@
/.fleet
/.vscode/*.log
/cmd/answer/*.sh
/cmd/answer/answer
/cmd/answer/uploads/*
/cmd/logs
/configs/config-dev.yaml
@@ -22,3 +23,5 @@ tmp
vendor/
/answer-data/
/answer
dist/
+4 -7
View File
@@ -13,8 +13,6 @@ stages:
"compile the html and other static files":
image: node:16
stage: compile-html
tags:
- runner-nanjing
before_script:
- npm config set registry https://repo.huaweicloud.com/repository/npm/
- make install-ui-packages
@@ -27,10 +25,9 @@ stages:
"compile the golang project":
image: golang:1.18
stage: compile-golang
before_script:
- export GOPROXY=https://goproxy.cn,direct
# before_script:
# - export GOPROXY=https://goproxy.cn,direct
script:
- make generate
- make build
artifacts:
paths:
@@ -38,8 +35,8 @@ stages:
"build docker images and push":
stage: push
before_script:
- export GOPROXY=https://goproxy.cn,direct
# before_script:
# - export GOPROXY=https://goproxy.cn,direct
extends: .docker-build-push
only:
- test
+90
View File
@@ -0,0 +1,90 @@
env:
- GO11MODULE=on
- GO111MODULE=on
- GOPROXY=https://goproxy.io
- CGO_ENABLED=1
before:
hooks:
- go mod tidy
builds:
- id: build-amd64
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
goos:
- linux
goarch:
- amd64
# linux windows need cgomingw64-gcc
- id: build-windows
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
env:
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
goos:
- windows
goarch:
- amd64
# linux arm64 need cgo arm64
- id: build-arm64
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
env:
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
goos:
- linux
goarch:
- arm64
- id: build-darwin-arm64
main: ./cmd/answer/.
binary: answer
env:
- CC=oa64-clang
- CXX=oa64-clang++
goos:
- darwin
goarch:
- arm64
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
flags: -v
- id: build-darwin-amd64
main: ./cmd/answer/.
binary: answer
env:
- CC=o64-clang
- CXX=o64-clang++
goos:
- darwin
goarch:
- amd64
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
flags: -v
archives:
- replacements:
darwin: Darwin
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
# sudo apt-get install build-essential
# sudo apt-get install gcc-multilib g++-multilib
# sudo apt-get install gcc-mingw-w64
# sudo apt-get -y install gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf
# sudo apt-get install clang llvm
# goreleaser release --snapshot --rm-dist
+2 -1
View File
@@ -1,6 +1,6 @@
.PHONY: build clean ui
VERSION=0.5.0
VERSION=1.0.0
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
@@ -22,6 +22,7 @@ universal:
generate:
go get github.com/google/wire/cmd/wire@latest
go install github.com/golang/mock/mockgen@v1.6.0
go generate ./...
go mod tidy
+4 -1
View File
@@ -6,6 +6,7 @@ import (
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/cron"
"github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
@@ -45,6 +46,7 @@ func runApp() {
if err != nil {
panic(err)
}
conf.GetPathIgnoreList()
app, cleanup, err := initApplication(
c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger())
if err != nil {
@@ -59,7 +61,8 @@ func runApp() {
}
}
func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application {
func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application {
manager.Run()
return pacman.NewApp(
pacman.WithName(Name),
pacman.WithVersion(Version),
+4
View File
@@ -7,11 +7,13 @@ package main
import (
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/cron"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/server"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/controller_backyard"
"github.com/answerdev/answer/internal/repo"
"github.com/answerdev/answer/internal/router"
@@ -37,7 +39,9 @@ func initApplication(
router.ProviderSetRouter,
controller.ProviderSetController,
controller_backyard.ProviderSetController,
templaterender.ProviderSetTemplateRenderController,
service.ProviderSetService,
cron.ProviderSetService,
repo.ProviderSetRepo,
translator.ProviderSet,
middleware.ProviderSetMiddleware,
+27 -12
View File
@@ -8,11 +8,13 @@ package main
import (
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/cron"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/server"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/controller_backyard"
"github.com/answerdev/answer/internal/repo/activity"
"github.com/answerdev/answer/internal/repo/activity_common"
@@ -31,6 +33,7 @@ import (
"github.com/answerdev/answer/internal/repo/reason"
"github.com/answerdev/answer/internal/repo/report"
"github.com/answerdev/answer/internal/repo/revision"
"github.com/answerdev/answer/internal/repo/role"
"github.com/answerdev/answer/internal/repo/search_common"
"github.com/answerdev/answer/internal/repo/site_info"
"github.com/answerdev/answer/internal/repo/tag"
@@ -61,6 +64,7 @@ import (
"github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/revision_common"
role2 "github.com/answerdev/answer/internal/service/role"
"github.com/answerdev/answer/internal/service/search_parser"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo"
@@ -109,14 +113,18 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
emailRepo := export.NewEmailRepo(dataData)
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService)
userRoleRelRepo := role.NewUserRoleRelRepo(dataData)
roleRepo := role.NewRoleRepo(dataData)
roleService := role2.NewRoleService(roleRepo)
userRoleRelService := role2.NewUserRoleRelService(userRoleRelRepo, roleService)
userCommon := usercommon.NewUserCommon(userRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, serviceConf, siteInfoCommonService, userRoleRelService, userCommon)
captchaRepo := captcha.NewCaptchaRepo(dataData)
captchaService := action.NewCaptchaService(captchaRepo)
uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService)
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService, siteInfoCommonService)
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)
userCommon := usercommon.NewUserCommon(userRepo)
answerRepo := answer.NewAnswerRepo(dataData, uniqueIDRepo, userRankRepo, activityRepo)
questionRepo := question.NewQuestionRepo(dataData, uniqueIDRepo)
tagCommonRepo := tag_common.NewTagCommonRepo(dataData, uniqueIDRepo)
@@ -128,7 +136,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService)
voteRepo := activity_common.NewVoteRepo(dataData, activityRepo)
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, configRepo)
rolePowerRelRepo := role.NewRolePowerRelRepo(dataData)
rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configRepo)
commentController := controller.NewCommentController(commentService, rankService)
reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
reportService := report2.NewReportService(reportRepo, objService)
@@ -154,7 +164,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo)
questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData)
questionController := controller.NewQuestionController(questionService, rankService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
@@ -171,13 +181,13 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reportBackyardService := report_backyard.NewReportBackyardService(reportRepo, userCommon, commonRepo, answerRepo, questionRepo, commentCommonRepo, reportHandle, configRepo)
controller_backyardReportController := controller_backyard.NewReportController(reportBackyardService)
userBackyardRepo := user.NewUserBackyardRepo(dataData, authRepo)
userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo)
userBackyardService := user_backyard.NewUserBackyardService(userBackyardRepo, userRoleRelService, authService, userCommon)
userBackyardController := controller_backyard.NewUserBackyardController(userBackyardService)
reasonRepo := reason.NewReasonRepo(configRepo)
reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService)
themeController := controller_backyard.NewThemeController()
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService, tagCommonService)
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService)
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData)
@@ -191,13 +201,18 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo)
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService)
activityController := controller.NewActivityController(activityCommon, activityService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController)
roleController := controller_backyard.NewRoleController(roleService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController, roleController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter()
authUserMiddleware := middleware.NewAuthUserMiddleware(authService)
uiRouter := router.NewUIRouter(siteinfoController, siteInfoCommonService)
authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)
avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware)
application := newApplication(serverConf, ginEngine)
templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, dataData, siteInfoCommonService)
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService)
templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter)
scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService)
application := newApplication(serverConf, ginEngine, scheduledTaskManager)
return application, func() {
cleanup2()
cleanup()
+3
View File
@@ -4,3 +4,6 @@ import _ "embed"
//go:embed config.yaml
var Config []byte
//go:embed path_ignore.yaml
var PathIgnore []byte
+13
View File
@@ -0,0 +1,13 @@
# url path reserves the keywords list
users:
- settings
- login
- register
- account-recovery
- change-email
- password-reset
- account-activation
- confirm-new-email
- account-suspended
questions:
- ask
+935 -14
View File
File diff suppressed because it is too large Load Diff
+935 -14
View File
File diff suppressed because it is too large Load Diff
+586 -10
View File
@@ -1,11 +1,4 @@
definitions:
entity.AdminSetAnswerStatusRequest:
properties:
answer_id:
type: string
status:
type: string
type: object
handler.RespBody:
properties:
code:
@@ -95,6 +88,8 @@ definitions:
type: string
display_name:
type: string
main_tag_slug_name:
type: string
object_type:
type: string
question_id:
@@ -118,6 +113,8 @@ definitions:
type: string
created_at:
type: integer
id:
type: string
object_id:
type: string
object_type:
@@ -179,6 +176,30 @@ definitions:
- object_id
- report_type
type: object
schema.AddUserReq:
properties:
display_name:
maxLength: 30
type: string
email:
maxLength: 500
type: string
password:
maxLength: 32
minLength: 8
type: string
required:
- display_name
- email
- password
type: object
schema.AdminSetAnswerStatusRequest:
properties:
answer_id:
type: string
status:
type: string
type: object
schema.AdminSetQuestionStatusRequest:
properties:
question_id:
@@ -203,7 +224,6 @@ definitions:
answer_id:
type: string
question_id:
description: question_id
type: string
type: object
schema.AnswerUpdateReq:
@@ -562,6 +582,15 @@ definitions:
user_info:
$ref: '#/definitions/schema.UserBasicInfo'
type: object
schema.GetRoleResp:
properties:
description:
type: string
id:
type: integer
name:
type: string
type: object
schema.GetSMTPConfigResp:
properties:
encryption:
@@ -638,6 +667,9 @@ definitions:
created_at:
description: created time
type: integer
description:
description: description text
type: string
display_name:
description: display name
type: string
@@ -724,6 +756,12 @@ definitions:
rank:
description: rank
type: integer
role_id:
description: role id
type: integer
role_name:
description: role name
type: string
status:
description: user status(normal,suspended,deleted,inactive)
type: string
@@ -1052,6 +1090,11 @@ definitions:
required:
- tag_id
type: object
schema.ReopenQuestionReq:
properties:
question_id:
type: string
type: object
schema.ReportHandleReq:
properties:
flagged_content:
@@ -1163,6 +1206,36 @@ definitions:
- logo
- square_icon
type: object
schema.SiteCustomCssHTMLReq:
properties:
custom_css:
maxLength: 65536
type: string
custom_footer:
maxLength: 65536
type: string
custom_head:
maxLength: 65536
type: string
custom_header:
maxLength: 65536
type: string
type: object
schema.SiteCustomCssHTMLResp:
properties:
custom_css:
maxLength: 65536
type: string
custom_footer:
maxLength: 65536
type: string
custom_head:
maxLength: 65536
type: string
custom_header:
maxLength: 65536
type: string
type: object
schema.SiteGeneralReq:
properties:
contact_email:
@@ -1207,6 +1280,23 @@ definitions:
- name
- site_url
type: object
schema.SiteInfoResp:
properties:
branding:
$ref: '#/definitions/schema.SiteBrandingResp'
custom_css_html:
$ref: '#/definitions/schema.SiteCustomCssHTMLResp'
general:
$ref: '#/definitions/schema.SiteGeneralResp'
interface:
$ref: '#/definitions/schema.SiteInterfaceResp'
login:
$ref: '#/definitions/schema.SiteLoginResp'
site__seo:
$ref: '#/definitions/schema.SiteSeoReq'
theme:
$ref: '#/definitions/schema.SiteThemeResp'
type: object
schema.SiteInterfaceReq:
properties:
language:
@@ -1261,6 +1351,67 @@ definitions:
terms_of_service_parsed_text:
type: string
type: object
schema.SiteLoginReq:
properties:
allow_new_registrations:
type: boolean
login_required:
type: boolean
type: object
schema.SiteLoginResp:
properties:
allow_new_registrations:
type: boolean
login_required:
type: boolean
type: object
schema.SiteSeoReq:
properties:
permalink:
maximum: 3
minimum: 0
type: integer
robots:
type: string
required:
- permalink
- robots
type: object
schema.SiteSeoResp:
properties:
permalink:
maximum: 3
minimum: 0
type: integer
robots:
type: string
required:
- permalink
- robots
type: object
schema.SiteThemeReq:
properties:
theme:
maxLength: 255
type: string
theme_config:
additionalProperties: true
type: object
required:
- theme
type: object
schema.SiteThemeResp:
properties:
theme:
type: string
theme_config:
additionalProperties: true
type: object
theme_options:
items:
$ref: '#/definitions/schema.ThemeOption'
type: array
type: object
schema.SiteWriteReq:
properties:
recommend_tags:
@@ -1335,6 +1486,13 @@ definitions:
description: tag id
type: string
type: object
schema.ThemeOption:
properties:
label:
type: string
value:
type: string
type: object
schema.UnreviewedRevisionInfoInfo:
properties:
content:
@@ -1483,6 +1641,30 @@ definitions:
required:
- language
type: object
schema.UpdateUserPasswordReq:
properties:
password:
maxLength: 32
minLength: 8
type: string
user_id:
type: string
required:
- password
- user_id
type: object
schema.UpdateUserRoleReq:
properties:
role_id:
description: role id
type: integer
user_id:
description: user id
type: string
required:
- role_id
- user_id
type: object
schema.UpdateUserStatusReq:
properties:
status:
@@ -1508,6 +1690,9 @@ definitions:
display_name:
description: display_name
type: string
id:
description: user_id
type: string
ip_info:
description: ip info
type: string
@@ -1589,6 +1774,39 @@ definitions:
notice_switch:
type: boolean
type: object
schema.UserRankingResp:
properties:
staffs:
items:
$ref: '#/definitions/schema.UserRankingSimpleInfo'
type: array
users_with_the_most_reputation:
items:
$ref: '#/definitions/schema.UserRankingSimpleInfo'
type: array
users_with_the_most_vote:
items:
$ref: '#/definitions/schema.UserRankingSimpleInfo'
type: array
type: object
schema.UserRankingSimpleInfo:
properties:
avatar:
description: avatar
type: string
display_name:
description: display name
type: string
rank:
description: rank
type: integer
username:
description: username
type: string
vote_count:
description: vote
type: integer
type: object
schema.UserRePassWordRequest:
properties:
code:
@@ -1605,6 +1823,12 @@ definitions:
type: object
schema.UserRegisterReq:
properties:
captcha_code:
description: captcha_code
type: string
captcha_id:
description: captcha_id
type: string
e_mail:
description: email
maxLength: 500
@@ -1722,7 +1946,7 @@ paths:
name: data
required: true
schema:
$ref: '#/definitions/entity.AdminSetAnswerStatusRequest'
$ref: '#/definitions/schema.AdminSetAnswerStatusRequest'
produces:
- application/json
responses:
@@ -1935,6 +2159,26 @@ paths:
summary: list report page
tags:
- admin
/answer/admin/api/roles:
get:
description: get role list
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
items:
$ref: '#/definitions/schema.GetRoleResp'
type: array
type: object
summary: get role list
tags:
- admin
/answer/admin/api/setting/smtp:
get:
description: GetSMTPConfig get smtp config
@@ -2017,6 +2261,47 @@ paths:
summary: update site info branding
tags:
- admin
/answer/admin/api/siteinfo/custom-css-html:
get:
description: get site info custom html css config
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteCustomCssHTMLResp'
type: object
security:
- ApiKeyAuth: []
summary: get site info custom html css config
tags:
- admin
put:
description: update site custom css html config
parameters:
- description: login info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteCustomCssHTMLReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site custom css html config
tags:
- admin
/answer/admin/api/siteinfo/general:
get:
description: get site general information
@@ -2140,6 +2425,129 @@ paths:
summary: update site legal info
tags:
- admin
/answer/admin/api/siteinfo/login:
get:
description: get site info login config
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteLoginResp'
type: object
security:
- ApiKeyAuth: []
summary: get site info login config
tags:
- admin
put:
description: update site login
parameters:
- description: login info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteLoginReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site login
tags:
- admin
/answer/admin/api/siteinfo/seo:
get:
description: get site seo information
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteSeoResp'
type: object
security:
- ApiKeyAuth: []
summary: get site seo information
tags:
- admin
put:
description: update site seo information
parameters:
- description: seo
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteSeoReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site seo information
tags:
- admin
/answer/admin/api/siteinfo/theme:
get:
description: get site info theme config
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteThemeResp'
type: object
security:
- ApiKeyAuth: []
summary: get site info theme config
tags:
- admin
put:
description: update site custom css html config
parameters:
- description: login info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteThemeReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site custom css html config
tags:
- admin
/answer/admin/api/siteinfo/write:
get:
description: get site interface
@@ -2196,6 +2604,78 @@ paths:
summary: Get theme options
tags:
- admin
/answer/admin/api/user:
post:
consumes:
- application/json
description: add user
parameters:
- description: user
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.AddUserReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: add user
tags:
- admin
/answer/admin/api/user/password:
put:
consumes:
- application/json
description: update user password
parameters:
- description: user
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdateUserPasswordReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update user password
tags:
- admin
/answer/admin/api/user/role:
put:
consumes:
- application/json
description: update user role
parameters:
- description: user
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdateUserRoleReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update user role
tags:
- admin
/answer/admin/api/user/status:
put:
consumes:
@@ -2236,6 +2716,10 @@ paths:
in: query
name: query
type: string
- description: staff user
in: query
name: staff
type: boolean
- description: user status
enum:
- suspended
@@ -3279,6 +3763,30 @@ paths:
summary: SearchQuestionList
tags:
- api-question
/answer/api/v1/question/reopen:
put:
consumes:
- application/json
description: reopen question
parameters:
- description: question
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.ReopenQuestionReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: reopen question
tags:
- api-question
/answer/api/v1/question/search:
post:
consumes:
@@ -3646,7 +4154,7 @@ paths:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteGeneralResp'
$ref: '#/definitions/schema.SiteInfoResp'
type: object
summary: get site info
tags:
@@ -4213,6 +4721,48 @@ paths:
summary: RetrievePassWord
tags:
- User
/answer/api/v1/user/ranking:
get:
consumes:
- application/json
description: get user ranking
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.UserRankingResp'
type: object
security:
- ApiKeyAuth: []
summary: get user ranking
tags:
- User
/answer/api/v1/user/register/captcha:
get:
consumes:
- application/json
description: UserRegisterCaptcha
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.GetUserResp'
type: object
summary: UserRegisterCaptcha
tags:
- User
/answer/api/v1/user/register/email:
post:
consumes:
@@ -4298,6 +4848,19 @@ paths:
summary: vote up
tags:
- Activity
/custom.css:
get:
description: get site robots information
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: get site robots information
tags:
- site
/installation/base-info:
post:
consumes:
@@ -4453,6 +5016,19 @@ paths:
summary: UserList
tags:
- api-question
/robots.txt:
get:
description: get site robots information
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: get site robots information
tags:
- site
securityDefinitions:
ApiKeyAuth:
in: header
+9 -6
View File
@@ -6,6 +6,7 @@ require (
github.com/Chain-Zhang/pinyin v0.1.3
github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267
github.com/bwmarrin/snowflake v0.3.0
github.com/davecgh/go-spew v1.1.1
github.com/disintegration/imaging v1.6.2
github.com/gin-gonic/gin v1.8.1
github.com/go-playground/locales v0.14.0
@@ -13,8 +14,11 @@ require (
github.com/go-playground/validator/v10 v10.11.1
github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-json v0.9.11
github.com/golang/mock v1.4.4
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c
github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0
github.com/gosimple/slug v1.13.1
github.com/grokify/html-strip-tags-go v0.0.1
github.com/jinzhu/copier v0.3.5
github.com/jinzhu/now v1.1.5
@@ -22,10 +26,11 @@ require (
github.com/mattn/go-sqlite3 v1.14.16
github.com/mojocn/base64Captcha v1.3.5
github.com/ory/dockertest/v3 v3.9.1
github.com/robfig/cron/v3 v3.0.1
github.com/segmentfault/pacman v1.0.1
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
github.com/spf13/cobra v1.6.1
@@ -45,12 +50,12 @@ require (
require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/LinkinStars/go-i18n/v2 v2.2.2 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v20.10.14+incompatible // indirect
github.com/docker/docker v20.10.7+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
@@ -65,7 +70,7 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/subcommands v1.0.1 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
@@ -81,7 +86,6 @@ require (
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nicksnyder/go-i18n/v2 v2.2.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
@@ -106,9 +110,8 @@ require (
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/image v0.1.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/tools v0.2.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+15 -7
View File
@@ -50,6 +50,8 @@ github.com/Chain-Zhang/pinyin v0.1.3/go.mod h1:5iHpt9p4znrnaP59/hfPMnAojajkDxQaP
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/LinkinStars/go-i18n/v2 v2.2.2 h1:ZfjpzbW13dv6btv3RALKZkpN9A+7K1JA//2QcNeWaxU=
github.com/LinkinStars/go-i18n/v2 v2.2.2/go.mod h1:hLglSJ4/3M0Y7ZVcoEJI+OwqkglHCA32DdjuJJR2LbM=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
@@ -233,6 +235,7 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -253,6 +256,8 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c h1:iyaGYbCmcYK0Ja9a3OUa2Fo+EaN0cbLu0eKpBwPFzc8=
github.com/gomarkdown/markdown v0.0.0-20221013030248-663e2500819c/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -284,7 +289,6 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe
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=
github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -300,6 +304,10 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q=
github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0=
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@@ -499,8 +507,6 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nicksnyder/go-i18n/v2 v2.2.0 h1:MNXbyPvd141JJqlU6gJKrczThxJy+kdCNivxZpBQFkw=
github.com/nicksnyder/go-i18n/v2 v2.2.0/go.mod h1:4OtLfzqyAxsscyCb//3gfqSvBc81gImX91LrZzczN1o=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
@@ -576,6 +582,8 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
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/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
@@ -597,8 +605,8 @@ github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068 h1:ln/qgrC62e7/XHGPiikWFV4dyYgCaWeZYkmSGqrHZp4=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E=
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc=
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk=
github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A=
@@ -780,7 +788,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -930,8 +937,9 @@ 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.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 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+158 -38
View File
@@ -13,6 +13,22 @@ backend:
database_error:
other: "Data server error."
role:
name:
user:
other: "User"
admin:
other: "Admin"
moderator:
other: "Moderator"
description:
user:
other: "Default with no special access."
admin:
other: "Have the full power to access the site."
moderator:
other: "Has access to all posts except admin settings."
email:
other: "Email"
password:
@@ -93,6 +109,8 @@ backend:
other: "Should not contain synonym tags."
cannot_update:
other: "No permission to update."
cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself."
theme:
not_found:
other: "Theme not found."
@@ -125,36 +143,40 @@ backend:
install:
create_config_failed:
other: "Cant create the config.yaml file."
cannot_update_your_role:
other: "You cannot modify your role."
not_allowed_registration:
other: "Currently the site is not open for registration"
report:
spam:
name:
other: "spam"
description:
desc:
other: "This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."
rude:
name:
other: "rude or abusive"
description:
desc:
other: "A reasonable person would find this content inappropriate for respectful discourse."
duplicate:
name:
other: "a duplicate"
description:
desc:
other: "This question has been asked before and already has an answer."
not_answer:
name:
other: "not an answer"
description:
desc:
other: "This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether."
not_need:
name:
other: "no longer needed"
description:
desc:
other: "This comment is outdated, conversational or not relevant to this post."
other:
name:
other: "something else"
description:
desc:
other: "This post requires staff attention for another reason not listed above."
question:
@@ -162,22 +184,22 @@ backend:
duplicate:
name:
other: "spam"
description:
desc:
other: "This question has been asked before and already has an answer."
guideline:
name:
other: "a community-specific reason"
description:
desc:
other: "This question doesn't meet a community guideline."
multiple:
name:
other: "needs details or clarity"
description:
desc:
other: "This question currently includes multiple questions in one. It should focus on one problem only."
other:
name:
other: "something else"
description:
desc:
other: "This post requires another reason not listed above."
notification:
@@ -206,11 +228,12 @@ backend:
other: "Your answer has been deleted"
your_comment_was_deleted:
other: "Your comment has been deleted"
# The following fields are used for interface presentation(Front-end)
ui:
how_to_format:
title: How to Format
description: >-
desc: >-
<ul class="mb-0"><li><p class="mb-2">to make links</p><pre
class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[Title](https://url.com)</code></pre></li><li><p
class="mb-2">put returns between paragraphs</p></li><li><p
@@ -248,6 +271,7 @@ ui:
install: Answer Installation
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
notifications:
title: Notifications
inbox: Inbox
@@ -319,7 +343,7 @@ ui:
empty: File cannot be empty.
only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB.
description:
desc:
label: Description (optional)
tab_url: Image URL
form_url:
@@ -391,12 +415,12 @@ ui:
range: Display name up to 35 characters.
slug_name:
label: URL Slug
description: 'Must use the character set "a-z", "0-9", "+ # - ."'
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
msg:
empty: URL slug cannot be empty.
range: URL slug up to 35 characters.
character: URL slug contains unallowed character set.
description:
desc:
label: Description (optional)
btn_cancel: Cancel
btn_submit: Submit
@@ -431,7 +455,7 @@ ui:
slug_name:
label: URL Slug
info: 'Must use the character set "a-z", "0-9", "+ # - ."'
description:
desc:
label: Description
edit_summary:
label: Edit Summary
@@ -492,7 +516,7 @@ ui:
button_following: Following
tag_label: questions
search_placeholder: Filter by tag name
no_description: The tag has no description.
no_desc: The tag has no description.
more: More
ask:
title: Add Question
@@ -551,7 +575,7 @@ ui:
placeholder: Search
footer:
build_on: >-
Built on <1> Answer </1>- the open-source software that power Q&A
Built on <1> Answer </1>- the open-source software that powers Q&A
communities.<br />Made with love © {{cc}}.
upload_img:
name: Change
@@ -640,7 +664,8 @@ ui:
account: Account
interface: Interface
profile:
btn_name: Update profile
heading: Profile
btn_name: Save
display_name:
label: Display Name
msg: Display name cannot be empty.
@@ -658,7 +683,7 @@ ui:
custom: Custom
btn_refresh: Refresh
custom_text: You can upload your image.
default: Default
default: System
msg: Please upload an avatar
bio:
label: About Me (optional)
@@ -670,10 +695,12 @@ ui:
label: Location (optional)
placeholder: "City, Country"
notification:
heading: Notifications
email:
label: Email Notifications
radio: "Answers to your questions, comments, and more"
account:
heading: Account
change_email_btn: Change email
change_pass_btn: Change password
change_email_info: >-
@@ -694,6 +721,7 @@ ui:
pass_confirm:
label: Confirm New Password
interface:
heading: Interface
lang:
label: Interface Language
text: User interface language. It will change when you refresh the page.
@@ -701,7 +729,7 @@ ui:
update: update success
update_password: Password changed successfully.
flag_success: Thanks for flagging.
fobidden_operate_self: Forbidden to operate on yourself
forbidden_operate_self: Forbidden to operate on yourself
review: Your revision will show after review.
related_question:
title: Related Questions
@@ -727,12 +755,17 @@ ui:
write_answer:
title: Your Answer
btn_name: Post your answer
add_another_answer: Add another answer
confirm_title: Continue to answer
continue: Continue
confirm_info: >-
<p>Are you sure you want to add another answer?</p><p>You could use the
edit link to refine and improve your existing answer, instead.</p>
empty: Answer cannot be empty.
reopen:
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
delete:
title: Delete this post
question: >-
@@ -893,7 +926,7 @@ ui:
config_yaml:
title: Create config.yaml
label: The config.yaml file created.
description: >-
desc: >-
You can create the <1>config.yaml</1> file manually in the
<1>/var/wwww/xxx/</1> directory and paste the following text into it.
info: "After youve done that, click “Next” button."
@@ -930,31 +963,31 @@ ui:
empty: Email cannot be empty.
incorrect: Email incorrect format.
ready_title: Your Answer is Ready!
ready_description: >-
ready_desc: >-
If you ever feel like changing more settings, visit <1>admin section</1>;
find it in the site menu.
good_luck: "Have fun, and good luck!"
warn_title: Warning
warn_description: >-
warn_desc: >-
The file <1>config.yaml</1> already exists. If you need to reset any of the
configuration items in this file, please delete it first.
install_now: You may try <1>installing now</1>.
installed: Already installed
installed_description: >-
installed_desc: >-
You appear to have already installed. To reinstall please clear your old
database tables first.
db_failed: Database connection failed
db_failed_description: >-
db_failed_desc: >-
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your hosts database server is down.
page_404:
description: "Unfortunately, this page doesn't exist."
desc: "Unfortunately, this page doesn't exist."
back_home: Back to homepage
page_50X:
description: The server encountered an error and could not complete your request.
desc: The server encountered an error and could not complete your request.
back_home: Back to homepage
page_maintenance:
description: "We are under maintenance, well be back soon."
desc: "We are under maintenance, well be back soon."
nav_menus:
dashboard: Dashboard
contents: Contents
@@ -971,6 +1004,11 @@ ui:
write: Write
tos: Terms of Service
privacy: Privacy
seo: SEO
customize: Customize
themes: Themes
css-html: CSS/HTML
login: Login
admin:
admin_header:
title: Admin
@@ -1020,13 +1058,13 @@ ui:
btn_cancel: Cancel
btn_submit: Submit
normal_name: normal
normal_description: A normal user can ask and answer questions.
normal_desc: A normal user can ask and answer questions.
suspended_name: suspended
suspended_description: A suspended user can't log in.
suspended_desc: A suspended user can't log in.
deleted_name: deleted
deleted_description: "Delete profile, authentication associations."
deleted_desc: "Delete profile, authentication associations."
inactive_name: inactive
inactive_description: An inactive user must re-validate their email.
inactive_desc: An inactive user must re-validate their email.
confirm_title: Delete this user
confirm_content: Are you sure you want to delete this user? This is permanent!
confirm_btn: Delete
@@ -1035,11 +1073,11 @@ ui:
status_modal:
title: "Change {{ type }} status to..."
normal_name: normal
normal_description: A normal post available to everyone.
normal_desc: A normal post available to everyone.
closed_name: closed
closed_description: "A closed question can't answer, but still can edit, vote and comment."
closed_desc: "A closed question can't answer, but still can edit, vote and comment."
deleted_name: deleted
deleted_description: All reputation gained and lost will be restored.
deleted_desc: All reputation gained and lost will be restored.
btn_cancel: Cancel
btn_submit: Submit
btn_next: Next
@@ -1074,6 +1112,34 @@ ui:
change_status: Change status
change_role: Change role
show_logs: Show logs
add_user: Add user
new_password_modal:
title: Set new password
form:
fields:
password:
label: Password
text: The user will be logged out and need to login again.
msg: Password must be at 8 - 32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
user_modal:
title: Add new user
form:
fields:
display_name:
label: Display Name
msg: display_name must be at 4 - 30 characters in length.
email:
label: Email
msg: Email is not valid.
password:
label: Password
msg: Password must be at 8 - 32 characters in length.
btn_cancel: Cancel
btn_submit: Submit
questions:
page_title: Questions
normal: Normal
@@ -1111,11 +1177,11 @@ ui:
msg: Site url cannot be empty.
validate: Please enter a valid URL.
text: The address of your site.
short_description:
short_desc:
label: Short Site Description (optional)
msg: Short site description cannot be empty.
text: "Short description, as used in the title tag on homepage."
description:
desc:
label: Site Description (optional)
msg: Site description cannot be empty.
text: "Describe this site in one sentence, as used in the meta description tag."
@@ -1177,7 +1243,8 @@ ui:
text: Provide email address that will receive test sends.
msg: Test email recipients is invalid
smtp_authentication:
label: SMTP Authentication
label: Enable authentication
title: SMTP Authentication
msg: SMTP authentication cannot be empty.
"yes": "Yes"
"no": "No"
@@ -1217,6 +1284,50 @@ ui:
reserved_tags:
label: Reserved Tags
text: "Reserved tags can only be added to a post by moderator."
seo:
page_title: SEO
permalink:
label: Permalink
text: Custom URL structures can improve the usability, and forward-compatibility of your links.
robots:
label: robots.txt
text: This will permanently override any related site settings.
themes:
page_title: Themes
themes:
label: Themes
text: Select an existing theme.
navbar_style:
label: Navbar Style
text: Select an existing theme.
primary_color:
label: Primary Color
text: Modify the colors used by your themes
css_and_html:
page_title: CSS and HTML
custom_css:
label: Custom CSS
text: This will insert as <link>
head:
label: Head
text: This will insert before </head>
header:
label: Header
text: This will insert after <body>
footer:
label: Footer
text: This will insert before </html>.
login:
page_title: Login
membership:
title: Membership
label: Allow new registrations
text: Turn off to prevent anyone from creating a new account.
private:
title: Private
label: Login required
text: Only logged in users can access this community.
form:
empty: cannot be empty
invalid: is invalid
@@ -1260,3 +1371,12 @@ ui:
by: By
comment: Comment
no_data: "We couldn't find anything."
users:
title: Users
users_with_the_most_reputation: Users with the highest reputation scores
users_with_the_most_vote: Users who voted the most
staffs: Our community staff
reputation: reputation
votes: votes
+40 -18
View File
@@ -11,6 +11,22 @@ backend:
database_error:
other: "数据服务异常"
role:
name:
user:
other: "用户"
admin:
other: "管理员"
moderator:
other: "版主"
description:
user:
other: "默认没有特殊访问权限。"
admin:
other: "拥有进入网站的全部权限。"
moderator:
other: "有权访问所有的帖子,无法进入管理员设置页面。"
email:
other: "邮箱"
password:
@@ -81,6 +97,8 @@ backend:
other: "不应包含同义词标签。"
cannot_update:
other: "没有更新标签权限。"
cannot_set_synonym_as_itself:
other: "你无法将当前标签的同义词设置为当前标签自己"
theme:
not_found:
other: "主题未找到"
@@ -97,6 +115,10 @@ backend:
other: "用户名已被使用"
set_avatar:
other: "头像设置错误"
cannot_update_your_role:
other: "你无法修改自己的角色"
not_allowed_registration:
other: "目前该网站尚未开放注册"
revision:
review_underway:
other: "目前无法编辑,有一个版本在审阅队列中。"
@@ -188,7 +210,7 @@ backend:
ui:
how_to_format:
title: 如何设定文本格式
description: >-
desc: >-
<ul class="mb-0"><li><p class="mb-2">添加链接:</p><pre
class="mb-2"><code>&lt;https://url.com&gt;<br/><br/>[标题](https://url.com)</code></pre></li><li><p
class="mb-2">段落之间使用空行分隔</p></li><li><p class="mb-2"><em>_斜体_</em> 或者
@@ -293,7 +315,7 @@ ui:
empty: 请选择图片文件。
only_image: 只能上传图片文件。
max_size: 图片文件大小不能超过 4 MB。
description:
desc:
label: 图片描述(可选)
tab_url: 网络图片
form_url:
@@ -365,12 +387,12 @@ ui:
range: 不能超过 35 个字符
slug_name:
label: URL 固定链接
description: '必须由 "a-z", "0-9", "+ # - ." 组成'
desc: '必须由 "a-z", "0-9", "+ # - ." 组成'
msg:
empty: 不能为空
range: 不能超过 35 个字符
character: 包含非法字符
description:
desc:
label: 标签描述(可选)
btn_cancel: 取消
btn_submit: 提交
@@ -402,7 +424,7 @@ ui:
slug_name:
label: URL 固定链接
info: '必须由 "a-z", "0-9", "+ # - ." 组成'
description:
desc:
label: 描述
edit_summary:
label: 编辑概要
@@ -453,7 +475,7 @@ ui:
button_following: 已关注
tag_label: 个问题
search_placeholder: 通过标签名过滤
no_description: 此标签无描述。
no_desc: 此标签无描述。
more: 更多
ask:
title: 提交新的问题
@@ -505,7 +527,7 @@ ui:
placeholder: 搜索
footer:
build_on: >-
Built on <1> Answer </1>- the open-source software that power Q&A
Built on <1> Answer </1>- the open-source software that powers Q&A
communities<br />Made with love © 2022 Answer
upload_img:
name: 更改图片
@@ -767,10 +789,10 @@ ui:
x_answers: 个回答
x_questions: 个问题
page_404:
description: 页面不存在
desc: 页面不存在
back_home: 回到主页
page_50X:
description: 服务器遇到了一个错误,无法完成你的请求。
desc: 服务器遇到了一个错误,无法完成你的请求。
back_home: 回到主页
nav_menus:
dashboard: 后台管理
@@ -803,13 +825,13 @@ ui:
btn_cancel: 取消
btn_submit: 提交
normal_name: 正常
normal_description: 正常状态的用户可以提问和回答。
normal_desc: 正常状态的用户可以提问和回答。
suspended_name: 封禁
suspended_description: 被封禁的用户将无法登录。
suspended_desc: 被封禁的用户将无法登录。
deleted_name: 删除
deleted_description: 删除用户的个人信息,认证等等。
deleted_desc: 删除用户的个人信息,认证等等。
inactive_name: 不活跃
inactive_description: 不活跃的用户必须重新验证邮箱。
inactive_desc: 不活跃的用户必须重新验证邮箱。
confirm_title: 删除此用户
confirm_content: 确定要删除此用户?此操作无法撤销!
confirm_btn: 删除
@@ -818,11 +840,11 @@ ui:
status_modal:
title: '更改 {{ type }} 状态为...'
normal_name: 正常
normal_description: 所有用户都可以访问
normal_desc: 所有用户都可以访问
closed_name: 关闭
closed_description: 不能回答,但仍然可以编辑、投票和评论。
closed_desc: 不能回答,但仍然可以编辑、投票和评论。
deleted_name: 删除
deleted_description: 所有获得/损失的声望将会恢复。
deleted_desc: 所有获得/损失的声望将会恢复。
btn_cancel: 取消
btn_submit: 提交
btn_next: 下一步
@@ -870,11 +892,11 @@ ui:
label: 站点名称
msg: 不能为空
text: 站点的名称,作为站点的标题(HTML 的 title 标签)。
short_description:
short_desc:
label: 简短的站点标语 (可选)
msg: 不能为空
text: 简短的标语,作为网站主页的标题(HTML 的 title 标签)。
description:
desc:
label: 网站描述 (可选)
msg: 不能为空
text: 使用一句话描述本站,作为网站的描述(HTML 的 meta 标签)。
+22
View File
@@ -4,6 +4,8 @@ import (
"bytes"
"path/filepath"
"github.com/answerdev/answer/configs"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/server"
"github.com/answerdev/answer/internal/base/translator"
@@ -12,6 +14,7 @@ import (
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/pkg/writer"
"github.com/segmentfault/pacman/contrib/conf/viper"
"github.com/segmentfault/pacman/log"
"gopkg.in/yaml.v3"
)
@@ -25,6 +28,10 @@ type AllConfig struct {
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"`
}
type PathIgnore struct {
Users []string `yaml:"users"`
}
// Server server config
type Server struct {
HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"`
@@ -62,3 +69,18 @@ func RewriteConfig(configFilePath string, allConfig *AllConfig) error {
}
return writer.ReplaceFile(configFilePath, buf.String())
}
func GetPathIgnoreList() map[string]bool {
list := make(map[string]bool, 0)
data := &PathIgnore{}
err := yaml.Unmarshal(configs.PathIgnore, data)
if err != nil {
log.Error(err)
return list
}
for _, item := range data.Users {
list[item] = true
}
constant.PathIgnoreMap = list
return list
}
+18 -5
View File
@@ -11,6 +11,9 @@ const (
AdminTokenCacheKey = "answer:admin:token:"
AdminTokenCacheTime = 7 * 24 * time.Hour
AcceptLanguageFlag = "Accept-Language"
UserTokenMappingCacheKey = "answer:user-token:mapping:"
SiteInfoCacheKey = "answer:site-info:"
SiteInfoCacheTime = 1 * time.Hour
)
const (
@@ -29,6 +32,8 @@ const (
var (
Version string = ""
PathIgnoreMap map[string]bool
ObjectTypeStrMapping = map[string]int{
QuestionObjectType: 1,
AnswerObjectType: 2,
@@ -51,10 +56,18 @@ var (
)
const (
SiteTypeGeneral = "general"
SiteTypeInterface = "interface"
SiteTypeBranding = "branding"
SiteTypeWrite = "write"
SiteTypeLegal = "legal"
SiteTypeGeneral = "general"
SiteTypeInterface = "interface"
SiteTypeBranding = "branding"
SiteTypeWrite = "write"
SiteTypeLegal = "legal"
SiteTypeSeo = "seo"
SiteTypeLogin = "login"
SiteTypeCustomCssHTML = "css-html"
SiteTypeTheme = "theme"
)
func ExistInPathIgnore(name string) bool {
_, ok := PathIgnoreMap[name]
return ok
}
+44
View File
@@ -0,0 +1,44 @@
package cron
import (
"context"
"fmt"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/robfig/cron/v3"
"github.com/segmentfault/pacman/log"
)
// ScheduledTaskManager scheduled task manager
type ScheduledTaskManager struct {
siteInfoService *siteinfo_common.SiteInfoCommonService
questionService *service.QuestionService
}
// NewScheduledTaskManager new scheduled task manager
func NewScheduledTaskManager(
siteInfoService *siteinfo_common.SiteInfoCommonService,
questionService *service.QuestionService,
) *ScheduledTaskManager {
manager := &ScheduledTaskManager{
siteInfoService: siteInfoService,
questionService: questionService,
}
return manager
}
func (s *ScheduledTaskManager) Run() {
fmt.Println("start cron")
s.questionService.SitemapCron(context.Background())
c := cron.New()
_, err := c.AddFunc("0 */1 * * *", func() {
ctx := context.Background()
fmt.Println("sitemap cron execution")
s.questionService.SitemapCron(ctx)
})
if err != nil {
log.Error(err)
}
c.Start()
}
+10
View File
@@ -0,0 +1,10 @@
package cron
import (
"github.com/google/wire"
)
// ProviderSetService is providers.
var ProviderSetService = wire.NewSet(
NewScheduledTaskManager,
)
+14
View File
@@ -59,3 +59,17 @@ func BindAndCheck(ctx *gin.Context, data interface{}) bool {
}
return false
}
// BindAndCheckReturnErr bind request and check
func BindAndCheckReturnErr(ctx *gin.Context, data interface{}) (errFields []*validator.FormErrorField) {
lang := GetLang(ctx)
if err := ctx.ShouldBind(data); err != nil {
log.Errorf("http_handle BindAndCheck fail, %s", err.Error())
HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil)
ctx.Abort()
return nil
}
errFields, _ = validator.GetValidatorByLang(lang.Abbr()).Check(data)
return errFields
}
@@ -0,0 +1,13 @@
package middleware
import (
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/gin-gonic/gin"
)
// ExtractAndSetAcceptLanguage extract accept language from header and set to context
func ExtractAndSetAcceptLanguage(ctx *gin.Context) {
lang := handler.GetLang(ctx)
ctx.Set(constant.AcceptLanguageFlag, lang)
}
+31 -3
View File
@@ -4,6 +4,7 @@ import (
"strings"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
@@ -18,13 +19,17 @@ var ctxUUIDKey = "ctxUuidKey"
// AuthUserMiddleware auth user middleware
type AuthUserMiddleware struct {
authService *auth.AuthService
authService *auth.AuthService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
}
// NewAuthUserMiddleware new auth user middleware
func NewAuthUserMiddleware(authService *auth.AuthService) *AuthUserMiddleware {
func NewAuthUserMiddleware(
authService *auth.AuthService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService) *AuthUserMiddleware {
return &AuthUserMiddleware{
authService: authService,
authService: authService,
siteInfoCommonService: siteInfoCommonService,
}
}
@@ -48,6 +53,29 @@ func (am *AuthUserMiddleware) Auth() gin.HandlerFunc {
}
}
// EjectUserBySiteInfo if admin config the site can access by nologin user, eject user.
func (am *AuthUserMiddleware) EjectUserBySiteInfo() gin.HandlerFunc {
return func(ctx *gin.Context) {
mustLogin := false
siteInfo, _ := am.siteInfoCommonService.GetSiteLogin(ctx)
if siteInfo != nil {
mustLogin = siteInfo.LoginRequired
}
if !mustLogin {
ctx.Next()
return
}
_, isLogin := ctx.Get(ctxUUIDKey)
if !isLogin {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
ctx.Abort()
return
}
ctx.Next()
}
}
// MustAuth auth user info. If the user does not log in, an unauthenticated error is displayed
func (am *AuthUserMiddleware) MustAuth() gin.HandlerFunc {
return func(ctx *gin.Context) {
+7 -3
View File
@@ -2,8 +2,8 @@ package middleware
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"strings"
@@ -11,6 +11,7 @@ import (
"github.com/answerdev/answer/internal/service/uploader"
"github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
type AvatarMiddleware struct {
@@ -44,7 +45,7 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
filePath := fmt.Sprintf("%s/avatar/%s", uploadPath, urlfileName)
var avatarfile []byte
if size == 0 {
avatarfile, err = ioutil.ReadFile(filePath)
avatarfile, err = os.ReadFile(filePath)
} else {
avatarfile, err = am.uploaderService.AvatarThumbFile(ctx, uploadPath, urlfileName, size)
}
@@ -52,7 +53,10 @@ func (am *AvatarMiddleware) AvatarThumb() gin.HandlerFunc {
ctx.Next()
return
}
ctx.Writer.WriteString(string(avatarfile))
_, err = ctx.Writer.WriteString(string(avatarfile))
if err != nil {
log.Error(err)
}
ctx.Abort()
return
+3
View File
@@ -57,4 +57,7 @@ const (
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration"
)
+16 -2
View File
@@ -1,9 +1,13 @@
package server
import (
"html/template"
"io/fs"
brotli "github.com/anargu/gin-brotli"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/router"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
)
@@ -15,6 +19,7 @@ func NewHTTPServer(debug bool,
viewRouter *router.UIRouter,
authUserMiddleware *middleware.AuthUserMiddleware,
avatarMiddleware *middleware.AvatarMiddleware,
templateRouter *router.TemplateRouter,
) *gin.Engine {
if debug {
@@ -23,9 +28,13 @@ func NewHTTPServer(debug bool,
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(brotli.Brotli(brotli.DefaultCompression))
r.Use(brotli.Brotli(brotli.DefaultCompression), middleware.ExtractAndSetAcceptLanguage)
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
html, _ := fs.Sub(ui.Template, "template")
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
r.SetHTMLTemplate(htmlTemplate)
viewRouter.Register(r)
rootGroup := r.Group("")
@@ -34,9 +43,13 @@ func NewHTTPServer(debug bool,
static.Use(avatarMiddleware.AvatarThumb())
staticRouter.RegisterStaticRouter(static)
// The route must be available without logging in
mustUnAuthV1 := r.Group("/answer/api/v1")
answerRouter.RegisterMustUnAuthAnswerAPIRouter(mustUnAuthV1)
// register api that no need to login
unAuthV1 := r.Group("/answer/api/v1")
unAuthV1.Use(authUserMiddleware.Auth())
unAuthV1.Use(authUserMiddleware.Auth(), authUserMiddleware.EjectUserBySiteInfo())
answerRouter.RegisterUnAuthAnswerAPIRouter(unAuthV1)
// register api that must be authenticated
@@ -48,5 +61,6 @@ func NewHTTPServer(debug bool,
cmsauthV1.Use(authUserMiddleware.CmsAuth())
answerRouter.RegisterAnswerCmsAPIRouter(cmsauthV1)
templateRouter.RegisterTemplateRouter(rootGroup)
return r
}
+117
View File
@@ -0,0 +1,117 @@
package server
import (
"html/template"
"math"
"regexp"
"strconv"
"strings"
"time"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/day"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/segmentfault/pacman/i18n"
)
var funcMap = template.FuncMap{
"replaceHTMLTag": func(src string, tags ...string) string {
p := `(?U)<(\d+)>.+</(\d+)>`
re := regexp.MustCompile(p)
ms := re.FindAllStringSubmatch(src, -1)
for _, mi := range ms {
if mi[1] == mi[2] {
i, err := strconv.Atoi(mi[1])
if err != nil || len(tags) < i {
break
}
src = strings.ReplaceAll(src, mi[0], tags[i-1])
}
}
return src
},
"join": func(sep string, elems ...string) string {
return strings.Join(elems, sep)
},
"templateHTML": func(data string) template.HTML {
return template.HTML(data)
},
"translator": func(la i18n.Language, data string, params ...interface{}) string {
trans := translator.GlobalTrans.Tr(la, data)
if len(params) > 0 && len(params)%2 == 0 {
for i := 0; i < len(params); i += 2 {
k := converter.InterfaceToString(params[i])
v := converter.InterfaceToString(params[i+1])
trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v)
}
}
return trans
},
"timeFormatISO": func(tz string, timestamp int64) string {
_, _ = time.LoadLocation(tz)
return time.Unix(timestamp, 0).Format("2006-01-02T15:04:05.000Z")
},
"translatorTimeFormatLongDate": func(la i18n.Language, tz string, timestamp int64) string {
trans := translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_time")
return day.Format(timestamp, trans, tz)
},
"translatorTimeFormat": func(la i18n.Language, tz string, timestamp int64) string {
var (
now = time.Now().Unix()
between int64 = 0
trans string
)
_, _ = time.LoadLocation(tz)
if now > timestamp {
between = now - timestamp
}
if between <= 1 {
return translator.GlobalTrans.Tr(la, "ui.dates.now")
}
if between > 1 && between < 60 {
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_seconds_ago")
return strings.ReplaceAll(trans, "{{count}}", converter.IntToString(between))
}
if between >= 60 && between < 3600 {
min := math.Floor(float64(between / 60))
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_minutes_ago")
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(min, 'f', 0, 64))
}
if between >= 3600 && between < 3600*24 {
h := math.Floor(float64(between / 3600))
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_hours_ago")
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(h, 'f', 0, 64))
}
if between >= 3600*24 &&
between < 3600*24*366 &&
time.Unix(timestamp, 0).Format("2006") == time.Unix(now, 0).Format("2006") {
trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date")
return day.Format(timestamp, trans, tz)
}
trans = translator.GlobalTrans.Tr(la, "ui.dates.long_date_with_year")
return day.Format(timestamp, trans, tz)
},
"wrapComments": func(comments []*schema.GetCommentResp, la i18n.Language, tz string) map[string]interface{} {
return map[string]interface{}{
"comments": comments,
"language": la,
"timezone": tz,
}
},
"urlTitle": func(title string) string {
return htmltext.UrlTitle(title)
},
}
+13 -5
View File
@@ -51,14 +51,22 @@ func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
return nil, fmt.Errorf("read file failed: %s %s", file.Name(), err)
}
// only parse the backend translation
translation := struct {
Content map[string]interface{} `yaml:"backend"`
// parse the backend translation
originalTr := struct {
Backend map[string]map[string]interface{} `yaml:"backend"`
UI map[string]interface{} `yaml:"ui"`
}{}
if err = yaml.Unmarshal(buf, &translation); err != nil {
if err = yaml.Unmarshal(buf, &originalTr); err != nil {
return nil, err
}
content, err := yaml.Marshal(translation.Content)
translation := make(map[string]interface{}, 0)
for k, v := range originalTr.Backend {
translation[k] = v
}
translation["backend"] = originalTr.Backend
translation["ui"] = originalTr.UI
content, err := yaml.Marshal(translation)
if err != nil {
return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err)
}
+6 -2
View File
@@ -124,9 +124,13 @@ func (m *MyValidator) Check(value interface{}) (errFields []*FormErrorField, err
if v, ok := value.(Checker); ok {
errFields, err = v.Check()
if err != nil {
return errFields, err
if err == nil {
return nil, nil
}
for _, errField := range errFields {
errField.ErrorMsg = translator.GlobalTrans.Tr(m.Lang, errField.ErrorMsg)
}
return errFields, err
}
return nil, nil
}
+15 -12
View File
@@ -9,6 +9,7 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/dashboard"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
@@ -51,7 +52,7 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerDeleteRank, req.ID)
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerDelete, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -110,7 +111,7 @@ func (ac *AnswerController) Add(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerAddRank, "")
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAdd, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -159,15 +160,17 @@ func (ac *AnswerController) Update(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.AnswerEditRank,
rank.AnswerEditWithoutReviewRank,
}, req.ID)
permission.AnswerEdit,
permission.AnswerEditWithoutReview,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.NoNeedReview = canList[1]
objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
req.CanEdit = canList[0] || objectOwner
req.NoNeedReview = canList[1] || objectOwner
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
@@ -208,9 +211,9 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.AnswerEditRank,
rank.AnswerDeleteRank,
}, "")
permission.AnswerEdit,
permission.AnswerDelete,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -246,7 +249,7 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, rank.AnswerAcceptRank, req.QuestionID)
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAccept, req.QuestionID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -267,7 +270,7 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) {
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body entity.AdminSetAnswerStatusRequest true "AdminSetAnswerStatusRequest"
// @Param data body schema.AdminSetAnswerStatusRequest true "AdminSetAnswerStatusRequest"
// @Router /answer/admin/api/answer/status [put]
// @Success 200 {object} handler.RespBody
func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) {
+13 -12
View File
@@ -6,6 +6,7 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/comment"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
@@ -42,10 +43,10 @@ func (cc *CommentController) AddComment(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.CommentAddRank,
rank.CommentEditRank,
rank.CommentDeleteRank,
}, "")
permission.CommentAdd,
permission.CommentEdit,
permission.CommentDelete,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -79,7 +80,7 @@ func (cc *CommentController) RemoveComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, rank.CommentDeleteRank, req.CommentID)
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentDelete, req.CommentID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -110,7 +111,7 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, rank.CommentEditRank, req.CommentID)
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentEdit, req.CommentID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -143,9 +144,9 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.CommentEditRank,
rank.CommentDeleteRank,
}, "")
permission.CommentEdit,
permission.CommentDelete,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -195,9 +196,9 @@ func (cc *CommentController) GetComment(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.CommentEditRank,
rank.CommentDeleteRank,
}, "")
permission.CommentEdit,
permission.CommentDelete,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
+1
View File
@@ -23,4 +23,5 @@ var ProviderSetController = wire.NewSet(
NewDashboardController,
NewUploadController,
NewActivityController,
NewTemplateController,
)
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/notification"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin"
)
@@ -43,10 +44,10 @@ func (nc *NotificationController) GetRedDot(ctx *gin.Context) {
req.UserID = userID
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAuditRank,
rank.AnswerAuditRank,
rank.TagAuditRank,
}, "")
permission.QuestionAudit,
permission.AnswerAudit,
permission.TagAudit,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -76,10 +77,10 @@ func (nc *NotificationController) ClearRedDot(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := nc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAuditRank,
rank.AnswerAuditRank,
rank.TagAuditRank,
}, "")
permission.QuestionAudit,
permission.AnswerAudit,
permission.TagAudit,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
+105 -26
View File
@@ -6,9 +6,11 @@ import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin"
@@ -43,7 +45,7 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, rank.QuestionDeleteRank, req.ID)
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionDelete, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -73,8 +75,47 @@ func (qc *QuestionController) CloseQuestion(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
err := qc.questionService.CloseQuestion(ctx, req)
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionClose, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err = qc.questionService.CloseQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// ReopenQuestion reopen question
// @Summary reopen question
// @Description reopen question
// @Tags api-question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.ReopenQuestionReq true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question/reopen [put]
func (qc *QuestionController) ReopenQuestion(ctx *gin.Context) {
req := &schema.ReopenQuestionReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := qc.rankService.CheckOperationPermission(ctx, req.UserID, permission.QuestionReopen, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err = qc.questionService.ReopenQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
@@ -93,16 +134,21 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
req := schema.QuestionPermission{}
canList, err := qc.rankService.CheckOperationPermissions(ctx, userID, []string{
rank.QuestionEditRank,
rank.QuestionDeleteRank,
}, id)
permission.QuestionEdit,
permission.QuestionDelete,
permission.QuestionClose,
permission.QuestionReopen,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id)
req.CanEdit = canList[0] || objectOwner
req.CanDelete = canList[1]
req.CanClose = middleware.GetIsAdminFromContext(ctx)
req.CanClose = canList[2]
req.CanReopen = canList[3]
info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)
if err != nil {
@@ -202,16 +248,20 @@ func (qc *QuestionController) SearchList(c *gin.Context) {
// @Router /answer/api/v1/question [post]
func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
req := &schema.QuestionAdd{}
if handler.BindAndCheck(ctx, req) {
errFields := handler.BindAndCheckReturnErr(ctx, req)
if ctx.IsAborted() {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAddRank,
rank.QuestionEditRank,
rank.QuestionDeleteRank,
}, "")
permission.QuestionAdd,
permission.QuestionEdit,
permission.QuestionDelete,
permission.QuestionClose,
permission.QuestionReopen,
permission.TagUseReservedTag,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -219,13 +269,26 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
req.CanAdd = canList[0]
req.CanEdit = canList[1]
req.CanDelete = canList[2]
req.CanClose = middleware.GetIsAdminFromContext(ctx)
req.CanClose = canList[3]
req.CanReopen = canList[4]
req.CanUseReservedTag = canList[5]
if !req.CanAdd {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
resp, err := qc.questionService.AddQuestion(ctx, req)
if err != nil {
errlist, ok := resp.([]*validator.FormErrorField)
if ok {
errFields = append(errFields, errlist...)
}
}
if len(errFields) > 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
return
}
handler.HandleResponse(ctx, err, resp)
}
@@ -241,33 +304,49 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
// @Router /answer/api/v1/question [put]
func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
req := &schema.QuestionUpdate{}
if handler.BindAndCheck(ctx, req) {
errFields := handler.BindAndCheckReturnErr(ctx, req)
if ctx.IsAborted() {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionEditRank,
rank.QuestionDeleteRank,
rank.QuestionEditWithoutReviewRank,
}, req.ID)
permission.QuestionEdit,
permission.QuestionDelete,
permission.QuestionEditWithoutReview,
permission.TagUseReservedTag,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.CanDelete = canList[1]
req.NoNeedReview = canList[2]
req.CanClose = middleware.GetIsAdminFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
req.CanEdit = canList[0] || objectOwner
req.CanDelete = canList[1]
req.NoNeedReview = canList[2] || objectOwner
req.CanUseReservedTag = canList[3]
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
_, err = qc.questionService.UpdateQuestion(ctx, req)
handler.HandleResponse(ctx, err, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
errlist, err := qc.questionService.UpdateQuestionCheckTags(ctx, req)
if err != nil {
errFields = append(errFields, errlist...)
}
if len(errFields) > 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
return
}
resp, err := qc.questionService.UpdateQuestion(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, resp)
return
}
handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
}
// CloseMsgList close question msg list
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/report"
"github.com/gin-gonic/gin"
@@ -40,7 +41,7 @@ func (rc *ReportController) AddReport(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, rank.ReportAddRank, "")
can, err := rc.rankService.CheckOperationPermission(ctx, req.UserID, permission.ReportAdd, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
+12 -11
View File
@@ -7,6 +7,7 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/pkg/obj"
"github.com/gin-gonic/gin"
@@ -70,10 +71,10 @@ func (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAuditRank,
rank.AnswerAuditRank,
rank.TagAuditRank,
}, "")
permission.QuestionAudit,
permission.AnswerAudit,
permission.TagAudit,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -102,10 +103,10 @@ func (rc *RevisionController) RevisionAudit(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := rc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.QuestionAuditRank,
rank.AnswerAuditRank,
rank.TagAuditRank,
}, "")
permission.QuestionAudit,
permission.AnswerAudit,
permission.TagAudit,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -139,11 +140,11 @@ func (rc *RevisionController) CheckCanUpdateRevision(ctx *gin.Context) {
objectTypeStr, _ := obj.GetObjectTypeStrByObjectID(req.ID)
switch objectTypeStr {
case constant.QuestionObjectType:
action = rank.QuestionEditRank
action = permission.QuestionEdit
case constant.AnswerObjectType:
action = rank.AnswerEditRank
action = permission.AnswerEdit
case constant.TagObjectType:
action = rank.TagEditRank
action = permission.TagEdit
default:
handler.HandleResponse(ctx, errors.BadRequest(reason.ObjectNotFound), nil)
return
+55 -1
View File
@@ -1,6 +1,9 @@
package controller
import (
"net/http"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
@@ -24,7 +27,7 @@ func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonServic
// @Description get site info
// @Tags site
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}
// @Success 200 {object} handler.RespBody{data=schema.SiteInfoResp}
// @Router /answer/api/v1/siteinfo [get]
func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
var err error
@@ -42,6 +45,26 @@ func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
if err != nil {
log.Error(err)
}
resp.Login, err = sc.siteInfoService.GetSiteLogin(ctx)
if err != nil {
log.Error(err)
}
resp.Theme, err = sc.siteInfoService.GetSiteTheme(ctx)
if err != nil {
log.Error(err)
}
resp.CustomCssHtml, err = sc.siteInfoService.GetSiteCustomCssHTML(ctx)
if err != nil {
log.Error(err)
}
resp.SiteSeo, err = sc.siteInfoService.GetSiteSeo(ctx)
if err != nil {
log.Error(err)
}
handler.HandleResponse(ctx, nil, resp)
}
@@ -73,3 +96,34 @@ func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) {
}
handler.HandleResponse(ctx, nil, resp)
}
// GetManifestJson get manifest.json
func (sc *SiteinfoController) GetManifestJson(ctx *gin.Context) {
favicon := "favicon.ico"
resp := &schema.GetManifestJsonResp{
ManifestVersion: 3,
Version: constant.Version,
ShortName: "Answer",
Name: "Answer.dev",
Icons: map[string]string{
"16": favicon,
"32": favicon,
"48": favicon,
"128": favicon,
},
StartUrl: ".",
Display: "standalone",
ThemeColor: "#000000",
BackgroundColor: "#ffffff",
}
branding, err := sc.siteInfoService.GetSiteBranding(ctx)
if err != nil {
log.Error(err)
} else if len(branding.Favicon) > 0 {
resp.Icons["16"] = branding.Favicon
resp.Icons["32"] = branding.Favicon
resp.Icons["48"] = branding.Favicon
resp.Icons["128"] = branding.Favicon
}
ctx.JSON(http.StatusOK, resp)
}
+11 -12
View File
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/permission"
"github.com/answerdev/answer/internal/service/rank"
"github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/tag_common"
@@ -63,7 +64,7 @@ func (tc *TagController) RemoveTag(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, rank.TagDeleteRank, "")
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagDelete, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -94,9 +95,9 @@ func (tc *TagController) UpdateTag(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.TagEditRank,
rank.TagEditWithoutReviewRank,
}, "")
permission.TagEdit,
permission.TagEditWithoutReview,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -133,9 +134,9 @@ func (tc *TagController) GetTagInfo(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.TagEditRank,
rank.TagDeleteRank,
}, "")
permission.TagEdit,
permission.TagDelete,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@@ -199,14 +200,12 @@ func (tc *TagController) GetTagSynonyms(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
rank.TagSynonymRank,
}, "")
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.CanEdit = can
resp, err := tc.tagService.GetTagSynonyms(ctx, req)
handler.HandleResponse(ctx, err, resp)
@@ -228,7 +227,7 @@ func (tc *TagController) UpdateTagSynonym(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, rank.TagSynonymRank, "")
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
+487
View File
@@ -0,0 +1,487 @@
package controller
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"regexp"
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
templaterender "github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/obj"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
type TemplateController struct {
scriptPath string
cssPath string
templateRenderController *templaterender.TemplateRenderController
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewTemplateController new controller
func NewTemplateController(
templateRenderController *templaterender.TemplateRenderController,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *TemplateController {
script, css := GetStyle()
return &TemplateController{
scriptPath: script,
cssPath: css,
templateRenderController: templateRenderController,
siteInfoService: siteInfoService,
}
}
func GetStyle() (script, css string) {
file, err := ui.Build.ReadFile("build/index.html")
if err != nil {
return
}
scriptRegexp := regexp.MustCompile(`<script defer="defer" src="(.*)"></script>`)
scriptData := scriptRegexp.FindStringSubmatch(string(file))
cssRegexp := regexp.MustCompile(`<link href="(.*)" rel="stylesheet">`)
cssListData := cssRegexp.FindStringSubmatch(string(file))
if len(scriptData) == 2 {
script = scriptData[1]
}
if len(cssListData) == 2 {
css = cssListData[1]
}
return
}
func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInfoResp {
var err error
resp := &schema.TemplateSiteInfoResp{}
resp.General, err = tc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Error(err)
}
resp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx)
if err != nil {
log.Error(err)
}
resp.Branding, err = tc.siteInfoService.GetSiteBranding(ctx)
if err != nil {
log.Error(err)
}
resp.SiteSeo, err = tc.siteInfoService.GetSiteSeo(ctx)
if err != nil {
log.Error(err)
}
resp.CustomCssHtml, err = tc.siteInfoService.GetSiteCustomCssHTML(ctx)
if err != nil {
log.Error(err)
}
resp.Year = fmt.Sprintf("%d", time.Now().Year())
return resp
}
// Index question list
func (tc *TemplateController) Index(ctx *gin.Context) {
req := &schema.QuestionSearch{
Order: "newest",
}
if handler.BindAndCheck(ctx, req) {
tc.Page404(ctx)
return
}
var page = req.Page
data, count, err := tc.templateRenderController.Index(ctx, req)
if err != nil {
tc.Page404(ctx)
return
}
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = siteInfo.General.SiteUrl
UrlUseTitle := false
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = ""
tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
"data": data,
"useTitle": UrlUseTitle,
"page": templaterender.Paginator(page, req.PageSize, count),
"path": "questions",
})
}
func (tc *TemplateController) QuestionList(ctx *gin.Context) {
req := &schema.QuestionSearch{
Order: "newest",
}
if handler.BindAndCheck(ctx, req) {
tc.Page404(ctx)
return
}
var page = req.Page
data, count, err := tc.templateRenderController.Index(ctx, req)
if err != nil {
tc.Page404(ctx)
return
}
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl)
UrlUseTitle := false
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = fmt.Sprintf("Questions - %s", siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
"data": data,
"useTitle": UrlUseTitle,
"page": templaterender.Paginator(page, req.PageSize, count),
})
}
func (tc *TemplateController) QuestionInfo301Jump(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
id := ctx.Param("id")
title := ctx.Param("title")
titleIsAnswerID := false
objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(title)
if objectTypeerr == nil {
if objectType == constant.AnswerObjectType {
titleIsAnswerID = true
}
}
url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID {
//not have title
if titleIsAnswerID || len(title) == 0 {
return false, ""
}
return true, url
} else {
//have title
if len(title) > 0 && !titleIsAnswerID && correctTitle {
return false, ""
}
detail, err := tc.templateRenderController.QuestionDetail(ctx, id)
if err != nil {
tc.Page404(ctx)
return
}
url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title))
return true, url
}
}
// QuestionInfo question and answers info
func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
id := ctx.Param("id")
title := ctx.Param("title")
answerid := ctx.Param("answerid")
if id == "ask" {
file, err := ui.Build.ReadFile("build/index.html")
if err != nil {
log.Error(err)
tc.Page404(ctx)
return
}
ctx.Header("content-type", "text/html;charset=utf-8")
ctx.String(http.StatusOK, string(file))
return
}
correctTitle := false
detail, err := tc.templateRenderController.QuestionDetail(ctx, id)
if err != nil {
tc.Page404(ctx)
return
}
encodeTitle := htmltext.UrlTitle(detail.Title)
if encodeTitle == title {
correctTitle = true
}
siteInfo := tc.SiteInfo(ctx)
jump, jumpurl := tc.QuestionInfo301Jump(ctx, siteInfo, correctTitle)
if jump {
ctx.Redirect(http.StatusMovedPermanently, jumpurl)
return
}
// answers
answerReq := &schema.AnswerListReq{
QuestionID: id,
Order: "",
Page: 1,
PageSize: 999,
UserID: "",
}
answers, answerCount, err := tc.templateRenderController.AnswerList(ctx, answerReq)
if err != nil {
tc.Page404(ctx)
return
}
// comments
objectIDs := []string{id}
for _, answer := range answers {
objectIDs = append(objectIDs, answer.ID)
}
comments, err := tc.templateRenderController.CommentList(ctx, objectIDs)
if err != nil {
tc.Page404(ctx)
return
}
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle)
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionID {
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
}
jsonLD := &schema.QAPageJsonLD{}
jsonLD.Context = "https://schema.org"
jsonLD.Type = "QAPage"
jsonLD.MainEntity.Type = "Question"
jsonLD.MainEntity.Name = detail.Title
jsonLD.MainEntity.Text = detail.HTML
jsonLD.MainEntity.AnswerCount = int(answerCount)
jsonLD.MainEntity.UpvoteCount = detail.VoteCount
jsonLD.MainEntity.DateCreated = time.Unix(detail.CreateTime, 0)
jsonLD.MainEntity.Author.Type = "Person"
jsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName
answerList := make([]*schema.SuggestedAnswerItem, 0)
for _, answer := range answers {
if answer.Adopted == schema.AnswerAdoptedEnable {
acceptedAnswerItem := &schema.AcceptedAnswerItem{}
acceptedAnswerItem.Type = "Answer"
acceptedAnswerItem.Text = answer.HTML
acceptedAnswerItem.DateCreated = time.Unix(answer.CreateTime, 0)
acceptedAnswerItem.UpvoteCount = answer.VoteCount
acceptedAnswerItem.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
acceptedAnswerItem.Author.Type = "Person"
acceptedAnswerItem.Author.Name = answer.UserInfo.DisplayName
jsonLD.MainEntity.AcceptedAnswer = acceptedAnswerItem
} else {
item := &schema.SuggestedAnswerItem{}
item.Type = "Answer"
item.Text = answer.HTML
item.DateCreated = time.Unix(answer.CreateTime, 0)
item.UpvoteCount = answer.VoteCount
item.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
item.Author.Type = "Person"
item.Author.Name = answer.UserInfo.DisplayName
answerList = append(answerList, item)
}
}
jsonLD.MainEntity.SuggestedAnswer = answerList
jsonLDStr, err := json.Marshal(jsonLD)
if err == nil {
siteInfo.JsonLD = `<script data-react-helmet="true" type="application/ld+json">` + string(jsonLDStr) + ` </script>`
}
siteInfo.Description = htmltext.FetchExcerpt(detail.HTML, "...", 240)
tags := make([]string, 0)
for _, tag := range detail.Tags {
tags = append(tags, tag.DisplayName)
}
siteInfo.Keywords = strings.Replace(strings.Trim(fmt.Sprint(tags), "[]"), " ", ",", -1)
siteInfo.Title = fmt.Sprintf("%s - %s", detail.Title, siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "question-detail.html", siteInfo, gin.H{
"id": id,
"answerid": answerid,
"detail": detail,
"answers": answers,
"comments": comments,
})
}
// TagList tags list
func (tc *TemplateController) TagList(ctx *gin.Context) {
req := &schema.GetTagWithPageReq{}
if handler.BindAndCheck(ctx, req) {
return
}
data, err := tc.templateRenderController.TagList(ctx, req)
if err != nil {
tc.Page404(ctx)
return
}
page := templaterender.Paginator(req.Page, req.PageSize, data.Count)
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl)
siteInfo.Title = fmt.Sprintf("%s - %s", "Tags", siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{
"page": page,
"data": data,
})
}
// TagInfo taginfo
func (tc *TemplateController) TagInfo(ctx *gin.Context) {
tag := ctx.Param("tag")
req := &schema.GetTamplateTagInfoReq{}
if handler.BindAndCheck(ctx, req) {
tc.Page404(ctx)
return
}
nowPage := req.Page
req.Name = tag
taginifo, questionList, questionCount, err := tc.templateRenderController.TagInfo(ctx, req)
if err != nil {
tc.Page404(ctx)
return
}
page := templaterender.Paginator(nowPage, req.PageSize, questionCount)
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag)
siteInfo.Description = htmltext.FetchExcerpt(taginifo.ParsedText, "...", 240)
if len(taginifo.ParsedText) == 0 {
siteInfo.Description = "The tag has no description."
}
siteInfo.Keywords = taginifo.DisplayName
UrlUseTitle := false
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = fmt.Sprintf("'%s' Questions - %s", taginifo.DisplayName, siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "tag-detail.html", siteInfo, gin.H{
"tag": taginifo,
"questionList": questionList,
"questionCount": questionCount,
"useTitle": UrlUseTitle,
"page": page,
})
}
// UserInfo user info
func (tc *TemplateController) UserInfo(ctx *gin.Context) {
username := ctx.Param("username")
if username == "" {
tc.Page404(ctx)
return
}
exist := constant.ExistInPathIgnore(username)
if exist {
file, err := ui.Build.ReadFile("build/index.html")
if err != nil {
log.Error(err)
tc.Page404(ctx)
return
}
ctx.Header("content-type", "text/html;charset=utf-8")
ctx.String(http.StatusOK, string(file))
return
}
req := &schema.GetOtherUserInfoByUsernameReq{}
req.Username = username
userinfo, err := tc.templateRenderController.UserInfo(ctx, req)
if err != nil {
tc.Page404(ctx)
return
}
if !userinfo.Has {
tc.Page404(ctx)
return
}
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username)
siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{
"userinfo": userinfo,
"bio": template.HTML(userinfo.Info.BioHTML),
})
}
func (tc *TemplateController) Page404(ctx *gin.Context) {
tc.html(ctx, http.StatusNotFound, "404.html", tc.SiteInfo(ctx), gin.H{})
}
func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteInfo *schema.TemplateSiteInfoResp, data gin.H) {
data["siteinfo"] = siteInfo
data["scriptPath"] = tc.scriptPath
data["cssPath"] = tc.cssPath
data["keywords"] = siteInfo.Keywords
if siteInfo.Description == "" {
siteInfo.Description = siteInfo.General.Description
}
data["title"] = siteInfo.Title
if siteInfo.Title == "" {
data["title"] = siteInfo.General.Name
}
data["description"] = siteInfo.Description
data["language"] = handler.GetLang(ctx)
data["timezone"] = siteInfo.Interface.TimeZone
data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
_, ok := data["path"]
if !ok {
data["path"] = ""
}
ctx.HTML(code, tpl, data)
}
func (tc *TemplateController) Sitemap(ctx *gin.Context) {
if tc.checkPrivateMode(ctx) {
tc.Page404(ctx)
return
}
tc.templateRenderController.Sitemap(ctx)
}
func (tc *TemplateController) SitemapPage(ctx *gin.Context) {
if tc.checkPrivateMode(ctx) {
tc.Page404(ctx)
return
}
page := 0
pageParam := ctx.Param("page")
pageRegexp := regexp.MustCompile(`question-(.*).xml`)
pageStr := pageRegexp.FindStringSubmatch(pageParam)
if len(pageStr) != 2 {
tc.Page404(ctx)
return
}
page = converter.StringToInt(pageStr[1])
if page == 0 {
tc.Page404(ctx)
return
}
err := tc.templateRenderController.SitemapPage(ctx, page)
if err != nil {
tc.Page404(ctx)
return
}
}
func (tc *TemplateController) checkPrivateMode(ctx *gin.Context) bool {
resp, err := tc.siteInfoService.GetSiteLogin(ctx)
if err != nil {
log.Error(err)
return false
}
if resp.LoginRequired {
return true
}
return false
}
@@ -0,0 +1,11 @@
package templaterender
import (
"context"
"github.com/answerdev/answer/internal/schema"
)
func (t *TemplateRenderController) AnswerList(ctx context.Context, req *schema.AnswerListReq) ([]*schema.AnswerInfo, int64, error) {
return t.answerService.SearchList(ctx, req)
}
@@ -0,0 +1,38 @@
package templaterender
import (
"context"
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/schema"
)
func (t *TemplateRenderController) CommentList(
ctx context.Context,
objectIDs []string,
) (
comments map[string][]*schema.GetCommentResp,
err error,
) {
comments = make(map[string][]*schema.GetCommentResp, len(objectIDs))
for _, objectID := range objectIDs {
var (
req = &schema.GetCommentWithPageReq{
Page: 1,
PageSize: 3,
ObjectID: objectID,
QueryCond: "vote",
UserID: "",
}
pageModel *pager.PageModel
)
pageModel, err = t.commentService.GetCommentWithPage(ctx, req)
if err != nil {
return
}
li := pageModel.List
comments[objectID] = li.([]*schema.GetCommentResp)
}
return
}
@@ -0,0 +1,106 @@
package templaterender
import (
"math"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/service/comment"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/tag"
"github.com/google/wire"
)
// ProviderSetTemplateRenderController is template render controller providers.
var ProviderSetTemplateRenderController = wire.NewSet(
NewTemplateRenderController,
)
type TemplateRenderController struct {
questionService *service.QuestionService
userService *service.UserService
tagService *tag.TagService
answerService *service.AnswerService
commentService *comment.CommentService
data *data.Data
siteInfoService *siteinfo_common.SiteInfoCommonService
}
func NewTemplateRenderController(
questionService *service.QuestionService,
userService *service.UserService,
tagService *tag.TagService,
answerService *service.AnswerService,
commentService *comment.CommentService,
data *data.Data,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *TemplateRenderController {
return &TemplateRenderController{
questionService: questionService,
userService: userService,
tagService: tagService,
answerService: answerService,
commentService: commentService,
data: data,
siteInfoService: siteInfoService,
}
}
// Paginator page
// page : now page
// pageSize : Number per page
// nums : Total
// Returns the contents of the page in the format of 1, 2, 3, 4, and 5. If the contents are less than 5 pages, the page number is returned
func Paginator(page, pageSize int, nums int64) *schema.Paginator {
if pageSize == 0 {
pageSize = 10
}
var prevpage int //Previous page address
var nextpage int //Address on the last page
//Generate the total number of pages based on the total number of nums and the number of prepage pages
totalpages := int(math.Ceil(float64(nums) / float64(pageSize))) //Total number of Pages
if page > totalpages {
page = totalpages
}
if page <= 0 {
page = 1
}
var pages []int
switch {
case page >= totalpages-5 && totalpages > 5: //The last 5 pages
start := totalpages - 5 + 1
prevpage = page - 1
nextpage = int(math.Min(float64(totalpages), float64(page+1)))
pages = make([]int, 5)
for i := range pages {
pages[i] = start + i
}
case page >= 3 && totalpages > 5:
start := page - 3 + 1
pages = make([]int, 5)
prevpage = page - 3
for i := range pages {
pages[i] = start + i
}
prevpage = page - 1
nextpage = page + 1
default:
pages = make([]int, int(math.Min(5, float64(totalpages))))
for i := range pages {
pages[i] = i + 1
}
prevpage = int(math.Max(float64(1), float64(page-1)))
nextpage = page + 1
}
paginator := &schema.Paginator{}
paginator.Pages = pages
paginator.Totalpages = totalpages
paginator.Prevpage = prevpage
paginator.Nextpage = nextpage
paginator.Currpage = page
return paginator
}
@@ -0,0 +1 @@
package templaterender
@@ -0,0 +1,91 @@
package templaterender
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionSearch) ([]*schema.QuestionInfo, int64, error) {
return t.questionService.SearchList(ctx, req, req.UserID)
}
func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfo, err error) {
return t.questionService.GetQuestion(ctx, id, "", schema.QuestionPermission{})
}
func (t *TemplateRenderController) Sitemap(ctx *gin.Context) {
general, err := t.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Error("get site general failed:", err)
return
}
sitemapInfo := &schema.SiteMapList{}
infoStr, err := t.data.Cache.GetString(ctx, schema.SitemapCachekey)
if err != nil {
log.Errorf("get Cache failed: %s", err)
return
}
if err = json.Unmarshal([]byte(infoStr), sitemapInfo); err != nil {
log.Errorf("get sitemap info failed: %s", err)
return
}
if len(sitemapInfo.QuestionIDs) > 0 {
//question url list
ctx.Header("Content-Type", "application/xml")
ctx.HTML(
http.StatusOK, "sitemap.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"list": sitemapInfo.QuestionIDs,
"general": general,
},
)
} else {
//question list page
ctx.Header("Content-Type", "application/xml")
ctx.HTML(
http.StatusOK, "sitemap-list.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"page": sitemapInfo.MaxPageNum,
"general": general,
},
)
return
}
}
func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error {
sitemapInfo := &schema.SiteMapPageList{}
general, err := t.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Error("get site general failed:", err)
return err
}
cachekey := fmt.Sprintf(schema.SitemapPageCachekey, page)
infoStr, err := t.data.Cache.GetString(ctx, cachekey)
if err != nil {
//If there is no cache, return directly.
return nil
}
if err = json.Unmarshal([]byte(infoStr), sitemapInfo); err != nil {
log.Errorf("get sitemap info failed: %s", err)
return err
}
ctx.Header("Content-Type", "application/xml")
ctx.HTML(
http.StatusOK, "sitemap.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"list": sitemapInfo.PageData,
"general": general,
},
)
return nil
}
@@ -0,0 +1,35 @@
package templaterender
import (
"github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/schema"
"github.com/jinzhu/copier"
"golang.org/x/net/context"
)
func (q *TemplateRenderController) TagList(ctx context.Context, req *schema.GetTagWithPageReq) (resp *pager.PageModel, err error) {
resp, err = q.tagService.GetTagWithPage(ctx, req)
if err != nil {
return
}
return
}
func (q *TemplateRenderController) TagInfo(ctx context.Context, req *schema.GetTamplateTagInfoReq) (resp *schema.GetTagResp, questionList []*schema.QuestionInfo, questionCount int64, err error) {
dto := &schema.GetTagInfoReq{}
_ = copier.Copy(dto, req)
resp, err = q.tagService.GetTagInfo(ctx, dto)
if err != nil {
return
}
searchQuestion := &schema.QuestionSearch{}
searchQuestion.Page = req.Page
searchQuestion.PageSize = req.PageSize
searchQuestion.Order = "newest"
searchQuestion.Tag = req.Name
questionList, questionCount, err = q.questionService.SearchList(ctx, searchQuestion, "")
if err != nil {
return
}
return resp, questionList, questionCount, err
}
@@ -0,0 +1,10 @@
package templaterender
import (
"github.com/answerdev/answer/internal/schema"
"golang.org/x/net/context"
)
func (q *TemplateRenderController) UserInfo(ctx context.Context, req *schema.GetOtherUserInfoByUsernameReq) (resp *schema.GetOtherUserInfoResp, err error) {
return q.userService.GetOtherUserInfoByUsername(ctx, req.Username)
}
+80 -18
View File
@@ -11,18 +11,21 @@ import (
"github.com/answerdev/answer/internal/service/action"
"github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/uploader"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// UserController user controller
type UserController struct {
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
uploaderService *uploader.UploaderService
emailService *export.EmailService
userService *service.UserService
authService *auth.AuthService
actionService *action.CaptchaService
uploaderService *uploader.UploaderService
emailService *export.EmailService
siteInfoCommonService *siteinfo_common.SiteInfoCommonService
}
// NewUserController new controller
@@ -32,13 +35,15 @@ func NewUserController(
actionService *action.CaptchaService,
emailService *export.EmailService,
uploaderService *uploader.UploaderService,
siteInfoCommonService *siteinfo_common.SiteInfoCommonService,
) *UserController {
return &UserController{
authService: authService,
userService: userService,
actionService: actionService,
uploaderService: uploaderService,
emailService: emailService,
authService: authService,
userService: userService,
actionService: actionService,
uploaderService: uploaderService,
emailService: emailService,
siteInfoCommonService: siteInfoCommonService,
}
}
@@ -52,16 +57,20 @@ func NewUserController(
// @Success 200 {object} handler.RespBody{data=schema.GetUserToSetShowResp}
// @Router /answer/api/v1/user/info [get]
func (uc *UserController) GetUserInfoByUserID(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
token := middleware.ExtractToken(ctx)
// if user is no login return null in data
if len(token) == 0 || len(userID) == 0 {
if len(token) == 0 {
handler.HandleResponse(ctx, nil, nil)
return
}
resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userID)
// if user is no login return null in data
userInfo, _ := uc.authService.GetUserCacheInfo(ctx, token)
if userInfo == nil {
handler.HandleResponse(ctx, nil, nil)
return
}
resp, err := uc.userService.GetUserInfoByUserID(ctx, token, userInfo.UserID)
handler.HandleResponse(ctx, err, resp)
}
@@ -189,6 +198,10 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) {
// @Router /answer/api/v1/user/logout [get]
func (uc *UserController) UserLogout(ctx *gin.Context) {
accessToken := middleware.ExtractToken(ctx)
if len(accessToken) == 0 {
handler.HandleResponse(ctx, nil, nil)
return
}
_ = uc.authService.RemoveUserCacheInfo(ctx, accessToken)
handler.HandleResponse(ctx, nil, nil)
}
@@ -203,11 +216,31 @@ func (uc *UserController) UserLogout(ctx *gin.Context) {
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/register/email [post]
func (uc *UserController) UserRegisterByEmail(ctx *gin.Context) {
// check whether site allow register or not
siteInfo, err := uc.siteInfoCommonService.GetSiteLogin(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !siteInfo.AllowNewRegistrations {
handler.HandleResponse(ctx, errors.BadRequest(reason.NotAllowedRegistration), nil)
return
}
req := &schema.UserRegisterReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.IP = ctx.ClientIP()
captchaPass := uc.actionService.UserRegisterVerifyCaptcha(ctx, req.CaptchaID, req.CaptchaCode)
if !captchaPass {
errFields := append([]*validator.FormErrorField{}, &validator.FormErrorField{
ErrorField: "captcha_code",
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
resp, err := uc.userService.UserRegisterByEmail(ctx, req)
handler.HandleResponse(ctx, err, resp)
@@ -275,11 +308,13 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
ErrorMsg: translator.GlobalTrans.Tr(handler.GetLang(ctx), reason.CaptchaVerificationFailed),
})
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), errFields)
return
}
uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
err := uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID)
_, err := uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
if err != nil {
log.Error(err)
}
err = uc.userService.UserVerifyEmailSend(ctx, userInfo.UserID)
handler.HandleResponse(ctx, err, nil)
}
@@ -386,6 +421,19 @@ func (uc *UserController) ActionRecord(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// UserRegisterCaptcha godoc
// @Summary UserRegisterCaptcha
// @Description UserRegisterCaptcha
// @Tags User
// @Accept json
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/register/captcha [get]
func (uc *UserController) UserRegisterCaptcha(ctx *gin.Context) {
resp, err := uc.actionService.UserRegisterCaptcha(ctx)
handler.HandleResponse(ctx, err, resp)
}
// UserNoticeSet godoc
// @Summary UserNoticeSet
// @Description UserNoticeSet
@@ -473,3 +521,17 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) {
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
handler.HandleResponse(ctx, err, nil)
}
// UserRanking get user ranking
// @Summary get user ranking
// @Description get user ranking
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} handler.RespBody{data=schema.UserRankingResp}
// @Router /answer/api/v1/user/ranking [get]
func (uc *UserController) UserRanking(ctx *gin.Context) {
resp, err := uc.userService.UserRanking(ctx)
handler.HandleResponse(ctx, err, resp)
}
@@ -8,4 +8,5 @@ var ProviderSetController = wire.NewSet(
NewUserBackyardController,
NewThemeController,
NewSiteInfoController,
NewRoleController,
)
@@ -0,0 +1,34 @@
package controller_backyard
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema"
service "github.com/answerdev/answer/internal/service/role"
"github.com/gin-gonic/gin"
)
// RoleController role controller
type RoleController struct {
roleService *service.RoleService
}
// NewRoleController new controller
func NewRoleController(roleService *service.RoleService) *RoleController {
return &RoleController{roleService: roleService}
}
// GetRoleList get role list
// @Summary get role list
// @Description get role list
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]schema.GetRoleResp}
// @Router /answer/admin/api/roles [get]
func (rc *RoleController) GetRoleList(ctx *gin.Context) {
req := &schema.GetRoleResp{}
if handler.BindAndCheck(ctx, req) {
return
}
resp, err := rc.roleService.GetRoleList(ctx)
handler.HandleResponse(ctx, err, resp)
}
@@ -1,6 +1,8 @@
package controller_backyard
import (
"net/http"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
@@ -85,6 +87,109 @@ func (sc *SiteInfoController) GetSiteLegal(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// GetSeo get site seo information
// @Summary get site seo information
// @Description get site seo information
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteSeoResp}
// @Router /answer/admin/api/siteinfo/seo [get]
func (sc *SiteInfoController) GetSeo(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSeo(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetSiteLogin get site info login config
// @Summary get site info login config
// @Description get site info login config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteLoginResp}
// @Router /answer/admin/api/siteinfo/login [get]
func (sc *SiteInfoController) GetSiteLogin(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteLogin(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetSiteCustomCssHTML get site info custom html css config
// @Summary get site info custom html css config
// @Description get site info custom html css config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteCustomCssHTMLResp}
// @Router /answer/admin/api/siteinfo/custom-css-html [get]
func (sc *SiteInfoController) GetSiteCustomCssHTML(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetSiteTheme get site info theme config
// @Summary get site info theme config
// @Description get site info theme config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteThemeResp}
// @Router /answer/admin/api/siteinfo/theme [get]
func (sc *SiteInfoController) GetSiteTheme(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteTheme(ctx)
handler.HandleResponse(ctx, err, resp)
}
// GetRobots get site robots information
// @Summary get site robots information
// @Description get site robots information
// @Tags site
// @Produce json
// @Success 200 {string} txt ""
// @Router /robots.txt [get]
func (sc *SiteInfoController) GetRobots(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSeo(ctx)
if err != nil {
ctx.String(http.StatusOK, "")
return
}
ctx.String(http.StatusOK, resp.Robots)
}
// GetRobots get site robots information
// @Summary get site robots information
// @Description get site robots information
// @Tags site
// @Produce json
// @Success 200 {string} txt ""
// @Router /custom.css [get]
func (sc *SiteInfoController) GetCss(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteCustomCssHTML(ctx)
if err != nil {
ctx.String(http.StatusOK, "")
return
}
ctx.Header("content-type", "text/css;charset=utf-8")
ctx.String(http.StatusOK, resp.CustomCss)
}
// UpdateSeo update site seo information
// @Summary update site seo information
// @Description update site seo information
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteSeoReq true "seo"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/seo [put]
func (sc *SiteInfoController) UpdateSeo(ctx *gin.Context) {
req := schema.SiteSeoReq{}
if handler.BindAndCheck(ctx, &req) {
return
}
err := sc.siteInfoService.SaveSeo(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UpdateGeneral update site general information
// @Summary update site general information
// @Description update site general information
@@ -177,6 +282,60 @@ func (sc *SiteInfoController) UpdateSiteLegal(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UpdateSiteLogin update site login
// @Summary update site login
// @Description update site login
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteLoginReq true "login info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/login [put]
func (sc *SiteInfoController) UpdateSiteLogin(ctx *gin.Context) {
req := &schema.SiteLoginReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteLogin(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UpdateSiteCustomCssHTML update site custom css html config
// @Summary update site custom css html config
// @Description update site custom css html config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteCustomCssHTMLReq true "login info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/custom-css-html [put]
func (sc *SiteInfoController) UpdateSiteCustomCssHTML(ctx *gin.Context) {
req := &schema.SiteCustomCssHTMLReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteCustomCssHTML(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// SaveSiteTheme update site custom css html config
// @Summary update site custom css html config
// @Description update site custom css html config
// @Security ApiKeyAuth
// @Tags admin
// @Produce json
// @Param data body schema.SiteThemeReq true "login info"
// @Success 200 {object} handler.RespBody{}
// @Router /answer/admin/api/siteinfo/theme [put]
func (sc *SiteInfoController) SaveSiteTheme(ctx *gin.Context) {
req := &schema.SiteThemeReq{}
if handler.BindAndCheck(ctx, req) {
return
}
err := sc.siteInfoService.SaveSiteTheme(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetSMTPConfig get smtp config
// @Summary GetSMTPConfig get smtp config
// @Description GetSMTPConfig get smtp config
@@ -2,6 +2,7 @@ package controller_backyard
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/user_backyard"
"github.com/gin-gonic/gin"
@@ -37,6 +38,72 @@ func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// UpdateUserRole update user role
// @Summary update user role
// @Description update user role
// @Security ApiKeyAuth
// @Tags admin
// @Accept json
// @Produce json
// @Param data body schema.UpdateUserRoleReq true "user"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/role [put]
func (uc *UserBackyardController) UpdateUserRole(ctx *gin.Context) {
req := &schema.UpdateUserRoleReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.UpdateUserRole(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// AddUser add user
// @Summary add user
// @Description add user
// @Security ApiKeyAuth
// @Tags admin
// @Accept json
// @Produce json
// @Param data body schema.AddUserReq true "user"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user [post]
func (uc *UserBackyardController) AddUser(ctx *gin.Context) {
req := &schema.AddUserReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.AddUser(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UpdateUserPassword update user password
// @Summary update user password
// @Description update user password
// @Security ApiKeyAuth
// @Tags admin
// @Accept json
// @Produce json
// @Param data body schema.UpdateUserPasswordReq true "user"
// @Success 200 {object} handler.RespBody
// @Router /answer/admin/api/user/password [put]
func (uc *UserBackyardController) UpdateUserPassword(ctx *gin.Context) {
req := &schema.UpdateUserPasswordReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.UpdateUserPassword(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// GetUserPage get user page
// @Summary get user page
// @Description get user page
@@ -46,6 +113,7 @@ func (uc *UserBackyardController) UpdateUserStatus(ctx *gin.Context) {
// @Param page query int false "page size"
// @Param page_size query int false "page size"
// @Param query query string false "search query: email, username or id:[id]"
// @Param staff query bool false "staff user"
// @Param status query string false "user status" Enums(suspended, deleted, inactive)
// @Success 200 {object} handler.RespBody{data=pager.PageModel{records=[]schema.GetUserPageResp}}
// @Router /answer/admin/api/users/page [get]
+10
View File
@@ -28,6 +28,16 @@ type ActivityRankSum struct {
Rank int `xorm:"not null default 0 INT(11) rank"`
}
type ActivityUserRankStat struct {
UserID string `xorm:"user_id"`
Rank int `xorm:"rank_amount"`
}
type ActivityUserVoteStat struct {
UserID string `xorm:"user_id"`
VoteCount int `xorm:"vote_count"`
}
// TableName activity table name
func (Activity) TableName() string {
return "activity"
+18
View File
@@ -0,0 +1,18 @@
package entity
import "time"
// Power power
type Power struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
Name string `xorm:"not null default '' VARCHAR(50) name"`
PowerType string `xorm:"not null default '' VARCHAR(100) power_type"`
Description string `xorm:"not null default '' VARCHAR(200) description"`
}
// TableName power table name
func (Power) TableName() string {
return "power"
}
+17
View File
@@ -0,0 +1,17 @@
package entity
import "time"
// Role role
type Role struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
Name string `xorm:"not null default '' VARCHAR(50) name"`
Description string `xorm:"not null default '' VARCHAR(200) description"`
}
// TableName user table name
func (Role) TableName() string {
return "role"
}
+17
View File
@@ -0,0 +1,17 @@
package entity
import "time"
// RolePowerRel role power rel
type RolePowerRel struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
RoleID int `xorm:"not null default 0 INT(11) role_id"`
PowerType string `xorm:"not null default '' VARCHAR(200) power_type"`
}
// TableName role power rel table name
func (RolePowerRel) TableName() string {
return "role_power_rel"
}
+17
View File
@@ -0,0 +1,17 @@
package entity
import "time"
// UserRoleRel role
type UserRoleRel struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
CreatedAt time.Time `xorm:"created TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated TIMESTAMP updated_at"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
RoleID int `xorm:"not null default 0 INT(11) role_id"`
}
// TableName user role rel table name
func (UserRoleRel) TableName() string {
return "user_role_rel"
}
-1
View File
@@ -187,5 +187,4 @@ func InitBaseInfo(ctx *gin.Context) {
time.Sleep(1 * time.Second)
os.Exit(0)
}()
return
}
+231 -22
View File
@@ -6,10 +6,28 @@ import (
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
"golang.org/x/crypto/bcrypt"
"xorm.io/xorm"
)
const (
defaultSEORobotTxt = `User-agent: *
Disallow: /admin
Disallow: /search
Disallow: /install
Disallow: /review
Disallow: /users/login
Disallow: /users/register
Disallow: /users/account-recovery
Disallow: /users/oauth/*
Disallow: /users/*/*
Disallow: /answer/api
Disallow: /*?code*
Sitemap: `
)
var tables = []interface{}{
&entity.Activity{},
&entity.Answer{},
@@ -28,6 +46,10 @@ var tables = []interface{}{
&entity.Uniqid{},
&entity.User{},
&entity.Version{},
&entity.Role{},
&entity.RolePowerRel{},
&entity.Power{},
&entity.UserRoleRel{},
}
// InitDB init db
@@ -51,6 +73,10 @@ func InitDB(dataConf *data.Database) (err error) {
if err != nil {
return fmt.Errorf("sync table failed: %s", err)
}
_, err = engine.InsertOne(&entity.Version{ID: 1, VersionNumber: ExpectedVersion()})
if err != nil {
return fmt.Errorf("init version table failed: %s", err)
}
err = initAdminUser(engine)
if err != nil {
@@ -61,11 +87,17 @@ func InitDB(dataConf *data.Database) (err error) {
if err != nil {
return fmt.Errorf("init config table: %s", err)
}
err = initRolePower(engine)
if err != nil {
return fmt.Errorf("init role and power failed: %s", err)
}
return nil
}
func initAdminUser(engine *xorm.Engine) error {
_, err := engine.InsertOne(&entity.User{
ID: "1",
Username: "admin",
Pass: "$2a$10$.gnUnpW.8ssRNaEvx.XwvOR2NuPsGzFLWWX2rqSIVAdIvLNZZYs5y", // admin
EMail: "admin@admin.com",
@@ -74,7 +106,6 @@ func initAdminUser(engine *xorm.Engine) error {
Status: 1,
Rank: 1,
DisplayName: "admin",
IsAdmin: true,
})
return err
}
@@ -106,13 +137,43 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
Content: string(generalDataBytes),
Status: 1,
})
if err != nil {
return err
}
loginConfig := map[string]bool{
"allow_new_registrations": true,
"login_required": false,
}
loginConfigDataBytes, _ := json.Marshal(loginConfig)
_, err = engine.InsertOne(&entity.SiteInfo{
Type: "login",
Content: string(loginConfigDataBytes),
Status: 1,
})
if err != nil {
return err
}
seoData := map[string]string{
"robots": defaultSEORobotTxt + siteURL + "/sitemap.xml",
}
seoDataBytes, _ := json.Marshal(seoData)
_, err = engine.InsertOne(&entity.SiteInfo{
Type: "seo",
Content: string(seoDataBytes),
Status: 1,
})
if err != nil {
return err
}
return err
}
func updateAdminInfo(engine *xorm.Engine, adminName, adminPassword, adminEmail string) error {
generateFromPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("")
return err
}
adminPassword = string(generateFromPassword)
@@ -191,26 +252,26 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 32, Key: "question.follow", Value: `0`},
{ID: 33, Key: "email.config", Value: `{"from_name":"","from_email":"","smtp_host":"","smtp_port":465,"smtp_password":"","smtp_username":"","smtp_authentication":true,"encryption":"","register_title":"[{{.SiteName}}] Confirm your new account","register_body":"Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n","pass_reset_title":"[{{.SiteName }}] Password reset","pass_reset_body":"Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n","change_title":"[{{.SiteName}}] Confirm your new email address","change_body":"Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n","test_title":"[{{.SiteName}}] Test Email","test_body":"This is a test email."}`},
{ID: 35, Key: "tag.follow", Value: `0`},
{ID: 36, Key: "rank.question.add", Value: `0`},
{ID: 37, Key: "rank.question.edit", Value: `0`},
{ID: 38, Key: "rank.question.delete", Value: `0`},
{ID: 39, Key: "rank.question.vote_up", Value: `0`},
{ID: 40, Key: "rank.question.vote_down", Value: `0`},
{ID: 41, Key: "rank.answer.add", Value: `0`},
{ID: 42, Key: "rank.answer.edit", Value: `0`},
{ID: 43, Key: "rank.answer.delete", Value: `0`},
{ID: 44, Key: "rank.answer.accept", Value: `0`},
{ID: 45, Key: "rank.answer.vote_up", Value: `0`},
{ID: 46, Key: "rank.answer.vote_down", Value: `0`},
{ID: 47, Key: "rank.comment.add", Value: `0`},
{ID: 48, Key: "rank.comment.edit", Value: `0`},
{ID: 49, Key: "rank.comment.delete", Value: `0`},
{ID: 50, Key: "rank.report.add", Value: `0`},
{ID: 51, Key: "rank.tag.add", Value: `0`},
{ID: 52, Key: "rank.tag.edit", Value: `0`},
{ID: 53, Key: "rank.tag.delete", Value: `0`},
{ID: 54, Key: "rank.tag.synonym", Value: `0`},
{ID: 55, Key: "rank.link.url_limit", Value: `0`},
{ID: 36, Key: "rank.question.add", Value: `1`},
{ID: 37, Key: "rank.question.edit", Value: `200`},
{ID: 38, Key: "rank.question.delete", Value: `-1`},
{ID: 39, Key: "rank.question.vote_up", Value: `15`},
{ID: 40, Key: "rank.question.vote_down", Value: `125`},
{ID: 41, Key: "rank.answer.add", Value: `1`},
{ID: 42, Key: "rank.answer.edit", Value: `200`},
{ID: 43, Key: "rank.answer.delete", Value: `-1`},
{ID: 44, Key: "rank.answer.accept", Value: `1`},
{ID: 45, Key: "rank.answer.vote_up", Value: `15`},
{ID: 46, Key: "rank.answer.vote_down", Value: `125`},
{ID: 47, Key: "rank.comment.add", Value: `1`},
{ID: 48, Key: "rank.comment.edit", Value: `-1`},
{ID: 49, Key: "rank.comment.delete", Value: `-1`},
{ID: 50, Key: "rank.report.add", Value: `1`},
{ID: 51, Key: "rank.tag.add", Value: `1`},
{ID: 52, Key: "rank.tag.edit", Value: `100`},
{ID: 53, Key: "rank.tag.delete", Value: `-1`},
{ID: 54, Key: "rank.tag.synonym", Value: `20000`},
{ID: 55, Key: "rank.link.url_limit", Value: `10`},
{ID: 56, Key: "rank.vote.detail", Value: `0`},
{ID: 57, Key: "reason.spam", Value: `{"name":"spam","description":"This post is an advertisement, or vandalism. It is not useful or relevant to the current topic."}`},
{ID: 58, Key: "reason.rude_or_abusive", Value: `{"name":"rude or abusive","description":"A reasonable person would find this content inappropriate for respectful discourse."}`},
@@ -262,7 +323,155 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 104, Key: "tag.rollback", Value: `0`},
{ID: 105, Key: "tag.deleted", Value: `0`},
{ID: 106, Key: "tag.undeleted", Value: `0`},
{ID: 107, Key: "rank.comment.vote_up", Value: `1`},
{ID: 108, Key: "rank.comment.vote_down", Value: `1`},
{ID: 109, Key: "rank.question.edit_without_review", Value: `2000`},
{ID: 110, Key: "rank.answer.edit_without_review", Value: `2000`},
{ID: 111, Key: "rank.tag.edit_without_review", Value: `20000`},
{ID: 112, Key: "rank.answer.audit", Value: `2000`},
{ID: 113, Key: "rank.question.audit", Value: `2000`},
{ID: 114, Key: "rank.tag.audit", Value: `20000`},
{ID: 115, Key: "rank.question.close", Value: `-1`},
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
}
_, err := engine.Insert(defaultConfigTable)
return err
}
func initRolePower(engine *xorm.Engine) (err error) {
roles := []*entity.Role{
{ID: 1, Name: "User", Description: "Default with no special access."},
{ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
{ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
}
_, err = engine.Insert(roles)
if err != nil {
return err
}
powers := []*entity.Power{
{ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
{ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
{ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
{ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
{ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
{ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
{ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
{ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
{ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
{ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
{ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
{ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
{ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
{ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
{ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
{ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
{ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
{ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
{ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
{ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
{ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
{ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
{ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
{ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
{ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
{ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
{ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
{ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
{ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
{ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
{ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
{ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
{ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
}
_, err = engine.Insert(powers)
if err != nil {
return err
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AdminAccess},
{RoleID: 2, PowerType: permission.QuestionAdd},
{RoleID: 2, PowerType: permission.QuestionEdit},
{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 2, PowerType: permission.QuestionDelete},
{RoleID: 2, PowerType: permission.QuestionClose},
{RoleID: 2, PowerType: permission.QuestionReopen},
{RoleID: 2, PowerType: permission.QuestionVoteUp},
{RoleID: 2, PowerType: permission.QuestionVoteDown},
{RoleID: 2, PowerType: permission.AnswerAdd},
{RoleID: 2, PowerType: permission.AnswerEdit},
{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 2, PowerType: permission.AnswerDelete},
{RoleID: 2, PowerType: permission.AnswerAccept},
{RoleID: 2, PowerType: permission.AnswerVoteUp},
{RoleID: 2, PowerType: permission.AnswerVoteDown},
{RoleID: 2, PowerType: permission.CommentAdd},
{RoleID: 2, PowerType: permission.CommentEdit},
{RoleID: 2, PowerType: permission.CommentDelete},
{RoleID: 2, PowerType: permission.CommentVoteUp},
{RoleID: 2, PowerType: permission.CommentVoteDown},
{RoleID: 2, PowerType: permission.ReportAdd},
{RoleID: 2, PowerType: permission.TagAdd},
{RoleID: 2, PowerType: permission.TagEdit},
{RoleID: 2, PowerType: permission.TagEditSlugName},
{RoleID: 2, PowerType: permission.TagEditWithoutReview},
{RoleID: 2, PowerType: permission.TagDelete},
{RoleID: 2, PowerType: permission.TagSynonym},
{RoleID: 2, PowerType: permission.LinkUrlLimit},
{RoleID: 2, PowerType: permission.VoteDetail},
{RoleID: 2, PowerType: permission.AnswerAudit},
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 3, PowerType: permission.QuestionDelete},
{RoleID: 3, PowerType: permission.QuestionClose},
{RoleID: 3, PowerType: permission.QuestionReopen},
{RoleID: 3, PowerType: permission.QuestionVoteUp},
{RoleID: 3, PowerType: permission.QuestionVoteDown},
{RoleID: 3, PowerType: permission.AnswerAdd},
{RoleID: 3, PowerType: permission.AnswerEdit},
{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 3, PowerType: permission.AnswerDelete},
{RoleID: 3, PowerType: permission.AnswerAccept},
{RoleID: 3, PowerType: permission.AnswerVoteUp},
{RoleID: 3, PowerType: permission.AnswerVoteDown},
{RoleID: 3, PowerType: permission.CommentAdd},
{RoleID: 3, PowerType: permission.CommentEdit},
{RoleID: 3, PowerType: permission.CommentDelete},
{RoleID: 3, PowerType: permission.CommentVoteUp},
{RoleID: 3, PowerType: permission.CommentVoteDown},
{RoleID: 3, PowerType: permission.ReportAdd},
{RoleID: 3, PowerType: permission.TagAdd},
{RoleID: 3, PowerType: permission.TagEdit},
{RoleID: 3, PowerType: permission.TagEditSlugName},
{RoleID: 3, PowerType: permission.TagEditWithoutReview},
{RoleID: 3, PowerType: permission.TagDelete},
{RoleID: 3, PowerType: permission.TagSynonym},
{RoleID: 3, PowerType: permission.LinkUrlLimit},
{RoleID: 3, PowerType: permission.VoteDetail},
{RoleID: 3, PowerType: permission.AnswerAudit},
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
}
_, err = engine.Insert(rolePowerRels)
if err != nil {
return err
}
adminUserRoleRel := &entity.UserRoleRel{
UserID: "1",
RoleID: 2,
}
_, err = engine.Insert(adminUserRoleRel)
if err != nil {
return err
}
return nil
}
+2
View File
@@ -45,6 +45,8 @@ var migrations = []Migration{
NewMigration("add user language", addUserLanguage),
NewMigration("add recommend and reserved tag fields", addTagRecommendedAndReserved),
NewMigration("add activity timeline", addActivityTimeline),
NewMigration("add user role", addRoleFeatures),
NewMigration("add theme and private mode", addThemeAndPrivateMode),
}
// GetCurrentDBVersion returns the current db version
+2
View File
@@ -6,6 +6,8 @@ import (
func addUserLanguage(x *xorm.Engine) error {
type User struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
Username string `xorm:"not null default '' VARCHAR(50) UNIQUE username"`
Language string `xorm:"not null default '' VARCHAR(100) language"`
}
return x.Sync(new(User))
+4 -2
View File
@@ -6,8 +6,10 @@ import (
func addTagRecommendedAndReserved(x *xorm.Engine) error {
type Tag struct {
Recommend bool `xorm:"not null default false BOOL recommend"`
Reserved bool `xorm:"not null default false BOOL reserved"`
ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
Recommend bool `xorm:"not null default false BOOL recommend"`
Reserved bool `xorm:"not null default false BOOL reserved"`
}
return x.Sync(new(Tag))
}
+15 -2
View File
@@ -95,6 +95,7 @@ ON "question" (
// only increasing field length to 128
type Config struct {
ID int `xorm:"not null pk autoincr INT(11) id"`
Key string `xorm:"unique VARCHAR(128) key"`
}
if err := x.Sync(new(Config)); err != nil {
@@ -172,22 +173,34 @@ ON "question" (
}
type Revision struct {
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
ObjectID string `xorm:"not null default 0 BIGINT(20) INDEX object_id"`
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
}
type Activity struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
CancelledAt time.Time `xorm:"TIMESTAMP cancelled_at"`
UserID string `xorm:"not null index BIGINT(20) user_id"`
TriggerUserID int64 `xorm:"not null default 0 index BIGINT(20) trigger_user_id"`
ObjectID string `xorm:"not null default 0 index BIGINT(20) object_id"`
RevisionID int64 `xorm:"not null default 0 BIGINT(20) revision_id"`
OriginalObjectID string `xorm:"not null default 0 BIGINT(20) original_object_id"`
}
type Tag struct {
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
UserID string `xorm:"not null default 0 BIGINT(20) user_id"`
}
type Question struct {
ID string `xorm:"not null pk BIGINT(20) id"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
}
type Answer struct {
ID string `xorm:"not null pk autoincr BIGINT(20) id"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
}
+215
View File
@@ -0,0 +1,215 @@
package migrations
import (
"fmt"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addRoleFeatures(x *xorm.Engine) error {
err := x.Sync(new(entity.Role), new(entity.RolePowerRel), new(entity.Power), new(entity.UserRoleRel))
if err != nil {
return err
}
roles := []*entity.Role{
{ID: 1, Name: "User", Description: "Default with no special access."},
{ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
{ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
}
// insert default roles
for _, role := range roles {
exist, err := x.Get(&entity.Role{ID: role.ID, Name: role.Name})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Insert(role)
if err != nil {
return err
}
}
powers := []*entity.Power{
{ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
{ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
{ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
{ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
{ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
{ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
{ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
{ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
{ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
{ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
{ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
{ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
{ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
{ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
{ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
{ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
{ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
{ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
{ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
{ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
{ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
{ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
{ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
{ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
{ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
{ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
{ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
{ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
{ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
{ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
{ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
{ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
{ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
}
// insert default powers
for _, power := range powers {
exist, err := x.Get(&entity.Power{ID: power.ID})
if err != nil {
return err
}
if exist {
_, err = x.ID(power.ID).Update(power)
} else {
_, err = x.Insert(power)
}
if err != nil {
return err
}
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AdminAccess},
{RoleID: 2, PowerType: permission.QuestionAdd},
{RoleID: 2, PowerType: permission.QuestionEdit},
{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 2, PowerType: permission.QuestionDelete},
{RoleID: 2, PowerType: permission.QuestionClose},
{RoleID: 2, PowerType: permission.QuestionReopen},
{RoleID: 2, PowerType: permission.QuestionVoteUp},
{RoleID: 2, PowerType: permission.QuestionVoteDown},
{RoleID: 2, PowerType: permission.AnswerAdd},
{RoleID: 2, PowerType: permission.AnswerEdit},
{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 2, PowerType: permission.AnswerDelete},
{RoleID: 2, PowerType: permission.AnswerAccept},
{RoleID: 2, PowerType: permission.AnswerVoteUp},
{RoleID: 2, PowerType: permission.AnswerVoteDown},
{RoleID: 2, PowerType: permission.CommentAdd},
{RoleID: 2, PowerType: permission.CommentEdit},
{RoleID: 2, PowerType: permission.CommentDelete},
{RoleID: 2, PowerType: permission.CommentVoteUp},
{RoleID: 2, PowerType: permission.CommentVoteDown},
{RoleID: 2, PowerType: permission.ReportAdd},
{RoleID: 2, PowerType: permission.TagAdd},
{RoleID: 2, PowerType: permission.TagEdit},
{RoleID: 2, PowerType: permission.TagEditSlugName},
{RoleID: 2, PowerType: permission.TagEditWithoutReview},
{RoleID: 2, PowerType: permission.TagDelete},
{RoleID: 2, PowerType: permission.TagSynonym},
{RoleID: 2, PowerType: permission.LinkUrlLimit},
{RoleID: 2, PowerType: permission.VoteDetail},
{RoleID: 2, PowerType: permission.AnswerAudit},
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 3, PowerType: permission.QuestionDelete},
{RoleID: 3, PowerType: permission.QuestionClose},
{RoleID: 3, PowerType: permission.QuestionReopen},
{RoleID: 3, PowerType: permission.QuestionVoteUp},
{RoleID: 3, PowerType: permission.QuestionVoteDown},
{RoleID: 3, PowerType: permission.AnswerAdd},
{RoleID: 3, PowerType: permission.AnswerEdit},
{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 3, PowerType: permission.AnswerDelete},
{RoleID: 3, PowerType: permission.AnswerAccept},
{RoleID: 3, PowerType: permission.AnswerVoteUp},
{RoleID: 3, PowerType: permission.AnswerVoteDown},
{RoleID: 3, PowerType: permission.CommentAdd},
{RoleID: 3, PowerType: permission.CommentEdit},
{RoleID: 3, PowerType: permission.CommentDelete},
{RoleID: 3, PowerType: permission.CommentVoteUp},
{RoleID: 3, PowerType: permission.CommentVoteDown},
{RoleID: 3, PowerType: permission.ReportAdd},
{RoleID: 3, PowerType: permission.TagAdd},
{RoleID: 3, PowerType: permission.TagEdit},
{RoleID: 3, PowerType: permission.TagEditSlugName},
{RoleID: 3, PowerType: permission.TagEditWithoutReview},
{RoleID: 3, PowerType: permission.TagDelete},
{RoleID: 3, PowerType: permission.TagSynonym},
{RoleID: 3, PowerType: permission.LinkUrlLimit},
{RoleID: 3, PowerType: permission.VoteDetail},
{RoleID: 3, PowerType: permission.AnswerAudit},
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
}
// insert default powers
for _, rel := range rolePowerRels {
exist, err := x.Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Insert(rel)
if err != nil {
return err
}
}
adminUserRoleRel := &entity.UserRoleRel{
UserID: "1",
RoleID: 2,
}
exist, err := x.Get(adminUserRoleRel)
if err != nil {
return err
}
if !exist {
_, err = x.Insert(adminUserRoleRel)
if err != nil {
return err
}
}
defaultConfigTable := []*entity.Config{
{ID: 115, Key: "rank.question.close", Value: `-1`},
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package migrations
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/entity"
"xorm.io/xorm"
)
func addThemeAndPrivateMode(x *xorm.Engine) error {
loginConfig := map[string]bool{
"allow_new_registrations": true,
"login_required": false,
}
loginConfigDataBytes, _ := json.Marshal(loginConfig)
siteInfo := &entity.SiteInfo{
Type: "login",
Content: string(loginConfigDataBytes),
Status: 1,
}
exist, err := x.Get(&entity.SiteInfo{Type: siteInfo.Type})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if !exist {
_, err = x.InsertOne(siteInfo)
}
return err
}
+3 -6
View File
@@ -27,15 +27,12 @@ type AnswerActivityRepo struct {
}
const (
acceptAction = "accept"
acceptedAction = "accepted"
acceptCancelAction = "accept_cancel"
acceptedCancelAction = "accepted_cancel"
acceptAction = "accept"
acceptedAction = "accepted"
)
var (
acceptActionList = []string{acceptAction, acceptedAction}
acceptCancelActionList = []string{acceptCancelAction, acceptedCancelAction}
acceptActionList = []string{acceptAction, acceptedAction}
)
// NewAnswerActivityRepo new repository
+12 -5
View File
@@ -70,6 +70,7 @@ 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)
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
result = nil
for _, action := range actions {
@@ -126,8 +127,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
if isReachStandard {
insertActivity.Rank = 0
}
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
notificationUserIDs = append(notificationUserIDs, activityUserID)
}
if has {
@@ -165,11 +165,15 @@ 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 {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
}
return
}
func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
resp = &schema.VoteResp{}
notificationUserIDs := make([]string, 0)
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
for _, action := range actions {
var (
@@ -216,13 +220,12 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj
}
// trigger user rank and send notification
if hasRank != 0 {
if hasRank != 0 && existsActivity.Rank > 0 {
_, err = vr.userRankRepo.TriggerUserRank(ctx, session, activityUserID, -deltaRank, activityType)
if err != nil {
return
}
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
notificationUserIDs = append(notificationUserIDs, activityUserID)
}
// update votes
@@ -245,6 +248,10 @@ func (vr *VoteRepo) voteCancel(ctx context.Context, objectID string, userID, obj
}
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
for _, activityUserID := range notificationUserIDs {
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
}
return
}
@@ -3,9 +3,11 @@ package activity_common
import (
"context"
"fmt"
"time"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/activity_common"
"github.com/answerdev/answer/internal/service/activity_type"
"github.com/answerdev/answer/pkg/obj"
"xorm.io/builder"
"xorm.io/xorm"
@@ -106,3 +108,49 @@ func (ar *ActivityRepo) AddActivity(ctx context.Context, activity *entity.Activi
}
return
}
// GetUsersWhoHasGainedTheMostReputation get users who has gained the most reputation over a period of time
func (ar *ActivityRepo) GetUsersWhoHasGainedTheMostReputation(
ctx context.Context, startTime, endTime time.Time, limit int) (rankStat []*entity.ActivityUserRankStat, err error) {
rankStat = make([]*entity.ActivityUserRankStat, 0)
session := ar.data.DB.Select("user_id, SUM(rank) AS rank_amount").Table("activity")
session.Where("has_rank = 1 AND cancelled = 0")
session.Where("created_at >= ?", startTime)
session.Where("created_at <= ?", endTime)
session.GroupBy("user_id")
session.Desc("rank_amount")
session.Limit(limit)
err = session.Find(&rankStat)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUsersWhoHasVoteMost get users who has vote most
func (ar *ActivityRepo) GetUsersWhoHasVoteMost(
ctx context.Context, startTime, endTime time.Time, limit int) (voteStat []*entity.ActivityUserVoteStat, err error) {
voteStat = make([]*entity.ActivityUserVoteStat, 0)
actIDs := make([]int, 0)
for _, act := range activity_type.ActivityTypeList {
configType, err := ar.configRepo.GetConfigType(act)
if err == nil {
actIDs = append(actIDs, configType)
}
}
session := ar.data.DB.Select("user_id, COUNT(*) AS vote_count").Table("activity")
session.Where("cancelled = 0")
session.In("activity_type", actIDs)
session.Where("created_at >= ?", startTime)
session.Where("created_at <= ?", endTime)
session.GroupBy("user_id")
session.Desc("vote_count")
session.Limit(limit)
err = session.Find(&voteStat)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+42
View File
@@ -10,6 +10,7 @@ import (
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/auth"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// authRepo auth repository
@@ -42,6 +43,9 @@ func (ar *authRepo) SetUserCacheInfo(ctx context.Context, accessToken string, us
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if err := ar.AddUserTokenMapping(ctx, userInfo.UserID, accessToken); err != nil {
log.Error(err)
}
return nil
}
@@ -130,6 +134,44 @@ func (ar *authRepo) RemoveBackyardUserCacheInfo(ctx context.Context, accessToken
return nil
}
// AddUserTokenMapping add user token mapping
func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error) {
key := constant.UserTokenMappingCacheKey + userID
resp, _ := ar.data.Cache.GetString(ctx, key)
mapping := make(map[string]bool, 0)
if len(resp) > 0 {
_ = json.Unmarshal([]byte(resp), &mapping)
}
mapping[accessToken] = true
content, _ := json.Marshal(mapping)
return ar.data.Cache.SetString(ctx, key, string(content), constant.UserTokenCacheTime)
}
// RemoveAllUserTokens Log out all users under this user id
func (ar *authRepo) RemoveAllUserTokens(ctx context.Context, userID string) {
key := constant.UserTokenMappingCacheKey + userID
resp, _ := ar.data.Cache.GetString(ctx, key)
mapping := make(map[string]bool, 0)
if len(resp) > 0 {
_ = json.Unmarshal([]byte(resp), &mapping)
log.Debugf("find %d user tokens by user id %s", len(mapping), userID)
}
for token := range mapping {
if err := ar.RemoveUserCacheInfo(ctx, token); err != nil {
log.Error(err)
} else {
log.Debugf("del user %s token success")
}
}
if err := ar.RemoveUserStatus(ctx, userID); err != nil {
log.Error(err)
}
if err := ar.data.Cache.Del(ctx, key); err != nil {
log.Error(err)
}
}
// NewAuthRepo new repository
func NewAuthRepo(data *data.Data) auth.AuthRepo {
return &authRepo{
+2 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/action"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// captchaRepo captcha repository
@@ -65,7 +66,7 @@ func (cr *captchaRepo) SetCaptcha(ctx context.Context, key, captcha string) (err
func (cr *captchaRepo) GetCaptcha(ctx context.Context, key string) (captcha string, err error) {
captcha, err = cr.data.Cache.GetString(ctx, key)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
log.Debug(err)
}
// TODO: cache reflect should return empty when key not found
return captcha, nil
+5
View File
@@ -19,6 +19,7 @@ import (
"github.com/answerdev/answer/internal/repo/reason"
"github.com/answerdev/answer/internal/repo/report"
"github.com/answerdev/answer/internal/repo/revision"
"github.com/answerdev/answer/internal/repo/role"
"github.com/answerdev/answer/internal/repo/search_common"
"github.com/answerdev/answer/internal/repo/site_info"
"github.com/answerdev/answer/internal/repo/tag"
@@ -67,4 +68,8 @@ var ProviderSetRepo = wire.NewSet(
reason.NewReasonRepo,
site_info.NewSiteInfo,
notification.NewNotificationRepo,
role.NewRoleRepo,
role.NewUserRoleRelRepo,
role.NewRolePowerRelRepo,
role.NewPowerRepo,
)
+32
View File
@@ -2,6 +2,7 @@ package question
import (
"context"
"fmt"
"strings"
"time"
"unicode"
@@ -16,6 +17,7 @@ import (
"github.com/answerdev/answer/internal/schema"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/unique"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/segmentfault/pacman/errors"
)
@@ -173,6 +175,36 @@ func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err
return
}
func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error) {
questionIDList = make([]*schema.SiteMapQuestionInfo, 0)
rows := make([]*entity.Question, 0)
if page > 0 {
page = page - 1
} else {
page = 0
}
if pageSize == 0 {
pageSize = constant.DefaultPageSize
}
offset := page * pageSize
session := qr.data.DB.Table("question")
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
session = session.Limit(pageSize, offset)
session = session.OrderBy("question.created_at asc")
err = session.Select("id,title,post_update_time").Find(&rows)
if err != nil {
return questionIDList, err
}
for _, question := range rows {
item := &schema.SiteMapQuestionInfo{}
item.ID = question.ID
item.Title = htmltext.UrlTitle(question.Title)
item.UpdateTime = fmt.Sprintf("%v", question.PostUpdateTime.UTC())
questionIDList = append(questionIDList, item)
}
return questionIDList, nil
}
// GetQuestionPage get question page
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) {
questionList = make([]*entity.Question, 0)
+2 -2
View File
@@ -43,7 +43,7 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
if deltaRank < 0 {
// if user rank is lower than 1 after this action, then user rank will be set to 1 only.
var isReachMin bool
isReachMin, err = ur.checkUserMinRank(ctx, session, userID, activityType)
isReachMin, err = ur.checkUserMinRank(ctx, session, userID, deltaRank)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@@ -52,7 +52,7 @@ func (ur *UserRankRepo) TriggerUserRank(ctx context.Context,
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return false, nil
return true, nil
}
} else {
isReachStandard, err = ur.checkUserTodayRank(ctx, session, userID, activityType)
@@ -33,7 +33,6 @@ func Test_commentRepo_AddComment(t *testing.T) {
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
assert.NoError(t, err)
return
}
func Test_commentRepo_GetCommentPage(t *testing.T) {
@@ -55,7 +54,6 @@ func Test_commentRepo_GetCommentPage(t *testing.T) {
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
assert.NoError(t, err)
return
}
func Test_commentRepo_UpdateComment(t *testing.T) {
@@ -77,5 +75,4 @@ func Test_commentRepo_UpdateComment(t *testing.T) {
err = commentRepo.RemoveComment(context.TODO(), testCommentEntity.ID)
assert.NoError(t, err)
return
}
@@ -3,12 +3,13 @@ package repo_test
import (
"context"
"encoding/json"
"testing"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/repo/question"
"github.com/answerdev/answer/internal/repo/revision"
"github.com/answerdev/answer/internal/repo/unique"
"github.com/stretchr/testify/assert"
"testing"
)
var q = &entity.Question{
@@ -53,6 +54,7 @@ func Test_revisionRepo_AddRevision(t *testing.T) {
assert.NotEqual(t, "", q.ID)
content, err := json.Marshal(q)
assert.NoError(t, err)
// auto update false
rev := getRev(q.ID, q.Title, string(content))
err = revisionRepo.AddRevision(context.TODO(), rev, false)
+1 -1
View File
@@ -92,7 +92,7 @@ func Test_tagRepo_GetTagListByName(t *testing.T) {
tagOnce.Do(addTagList)
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, 1, false)
gotTags, err := tagCommonRepo.GetTagListByName(context.TODO(), testTagList[0].SlugName, false)
assert.NoError(t, err)
assert.Equal(t, testTagList[0].SlugName, gotTags[0].SlugName)
}
@@ -20,7 +20,7 @@ func Test_userBackyardRepo_GetUserInfo(t *testing.T) {
func Test_userBackyardRepo_GetUserPage(t *testing.T) {
userBackyardRepo := user.NewUserBackyardRepo(testDataSource, auth.NewAuthRepo(testDataSource))
got, total, err := userBackyardRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "")
got, total, err := userBackyardRepo.GetUserPage(context.TODO(), 1, 1, &entity.User{Username: "admin"}, "", false)
assert.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Equal(t, "1", got[0].ID)
+14 -15
View File
@@ -76,28 +76,27 @@ func (rr *reportRepo) GetReportListPage(ctx context.Context, dto schema.GetRepor
}
// GetByID get report by ID
func (ar *reportRepo) GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error) {
report = entity.Report{}
exist, err = ar.data.DB.ID(id).Get(&report)
return
}
// UpdateByID handle report by ID
func (ar *reportRepo) UpdateByID(
ctx context.Context,
id string,
handleData entity.Report,
) (err error) {
_, err = ar.data.DB.ID(id).Update(&handleData)
func (rr *reportRepo) GetByID(ctx context.Context, id string) (report *entity.Report, exist bool, err error) {
report = &entity.Report{}
exist, err = rr.data.DB.ID(id).Get(report)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (vr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) {
// UpdateByID handle report by ID
func (rr *reportRepo) UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error) {
_, err = rr.data.DB.ID(id).Update(&handleData)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
func (rr *reportRepo) GetReportCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.Report, 0)
count, err = vr.data.DB.Where("status =?", entity.ReportStatusPending).FindAndCount(&list)
count, err = rr.data.DB.Where("status =?", entity.ReportStatusPending).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
+33
View File
@@ -0,0 +1,33 @@
package role
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/role"
"github.com/segmentfault/pacman/errors"
)
// powerRepo power repository
type powerRepo struct {
data *data.Data
}
// NewPowerRepo new repository
func NewPowerRepo(data *data.Data) role.PowerRepo {
return &powerRepo{
data: data,
}
}
// GetPowerList get list all
func (pr *powerRepo) GetPowerList(ctx context.Context, power *entity.Power) (powerList []*entity.Power, err error) {
powerList = make([]*entity.Power, 0)
err = pr.data.DB.Find(powerList, power)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+34
View File
@@ -0,0 +1,34 @@
package role
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/role"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
)
// rolePowerRelRepo rolePowerRel repository
type rolePowerRelRepo struct {
data *data.Data
}
// NewRolePowerRelRepo new repository
func NewRolePowerRelRepo(data *data.Data) role.RolePowerRelRepo {
return &rolePowerRelRepo{
data: data,
}
}
// GetRolePowerTypeList get role power type list
func (rr *rolePowerRelRepo) GetRolePowerTypeList(ctx context.Context, roleID int) (powers []string, err error) {
powers = make([]string, 0)
err = rr.data.DB.Table("role_power_rel").
Cols("power_type").Where(builder.Eq{"role_id": roleID}).Find(&powers)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+46
View File
@@ -0,0 +1,46 @@
package role
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
service "github.com/answerdev/answer/internal/service/role"
"github.com/segmentfault/pacman/errors"
)
// roleRepo role repository
type roleRepo struct {
data *data.Data
}
// NewRoleRepo new repository
func NewRoleRepo(data *data.Data) service.RoleRepo {
return &roleRepo{
data: data,
}
}
// GetRoleAllList get role list all
func (rr *roleRepo) GetRoleAllList(ctx context.Context) (roleList []*entity.Role, err error) {
roleList = make([]*entity.Role, 0)
err = rr.data.DB.Find(&roleList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetRoleAllMapping get role all mapping
func (rr *roleRepo) GetRoleAllMapping(ctx context.Context) (roleMapping map[int]*entity.Role, err error) {
roleList, err := rr.GetRoleAllList(ctx)
if err != nil {
return nil, err
}
roleMapping = make(map[int]*entity.Role, 0)
for _, role := range roleList {
roleMapping[role.ID] = role
}
return roleMapping, nil
}
+83
View File
@@ -0,0 +1,83 @@
package role
import (
"context"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/role"
"github.com/segmentfault/pacman/errors"
"xorm.io/builder"
"xorm.io/xorm"
)
// userRoleRelRepo userRoleRel repository
type userRoleRelRepo struct {
data *data.Data
}
// NewUserRoleRelRepo new repository
func NewUserRoleRelRepo(data *data.Data) role.UserRoleRelRepo {
return &userRoleRelRepo{
data: data,
}
}
// SaveUserRoleRel save user role rel
func (ur *userRoleRelRepo) SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) {
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) {
item := &entity.UserRoleRel{UserID: userID}
exist, err := ur.data.DB.Get(item)
if err != nil {
return nil, err
}
if exist {
item.RoleID = roleID
_, err = ur.data.DB.ID(item.ID).Update(item)
} else {
_, err = ur.data.DB.Insert(&entity.UserRoleRel{UserID: userID, RoleID: roleID})
}
if err != nil {
return nil, err
}
return nil, nil
})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserRoleRelList get user role all
func (ur *userRoleRelRepo) GetUserRoleRelList(ctx context.Context, userIDs []string) (
userRoleRelList []*entity.UserRoleRel, err error) {
userRoleRelList = make([]*entity.UserRoleRel, 0)
err = ur.data.DB.In("user_id", userIDs).Find(&userRoleRelList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserRoleRelListByRoleID get user role all by role id
func (ur *userRoleRelRepo) GetUserRoleRelListByRoleID(ctx context.Context, roleIDs []int) (
userRoleRelList []*entity.UserRoleRel, err error) {
userRoleRelList = make([]*entity.UserRoleRel, 0)
err = ur.data.DB.In("role_id", roleIDs).Find(&userRoleRelList)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserRoleRel get user role
func (ur *userRoleRelRepo) GetUserRoleRel(ctx context.Context, userID string) (
rolePowerRel *entity.UserRoleRel, exist bool, err error) {
rolePowerRel = &entity.UserRoleRel{}
exist, err = ur.data.DB.Where(builder.Eq{"user_id": userID}).Get(rolePowerRel)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
+9 -5
View File
@@ -69,9 +69,8 @@ func NewSearchRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo, userCommon
// SearchContents search question and answer data
func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs []string, userID string, votes int, page, size int, order string) (resp []schema.SearchResp, total int64, err error) {
if words = filterWords(words); len(words) == 0 {
return
}
words = filterWords(words)
var (
b *builder.Builder
ub *builder.Builder
@@ -80,9 +79,14 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
argsQ = []interface{}{}
argsA = []interface{}{}
)
if order == "relevance" {
qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs)
afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
if len(words) > 0 {
qfs, argsQ = addRelevanceField([]string{"title", "original_text"}, words, qfs)
afs, argsA = addRelevanceField([]string{"`answer`.`original_text`"}, words, afs)
} else {
order = "newest"
}
}
b = builder.MySQL().Select(qfs...).From("`question`")
+38 -12
View File
@@ -2,12 +2,15 @@ package site_info
import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
"xorm.io/builder"
)
@@ -23,32 +26,55 @@ func NewSiteInfo(data *data.Data) siteinfo_common.SiteInfoRepo {
// SaveByType save site setting by type
func (sr *siteInfoRepo) SaveByType(ctx context.Context, siteType string, data *entity.SiteInfo) (err error) {
var (
old = &entity.SiteInfo{}
exist bool
)
exist, _ = sr.data.DB.Where(builder.Eq{"type": siteType}).Get(old)
old := &entity.SiteInfo{}
exist, err := sr.data.DB.Where(builder.Eq{"type": siteType}).Get(old)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
_, err = sr.data.DB.ID(old.ID).Update(data)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
} else {
_, err = sr.data.DB.Insert(data)
}
_, err = sr.data.DB.Insert(data)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
sr.setCache(ctx, siteType, data)
return
}
// GetByType get site info by type
func (sr *siteInfoRepo) GetByType(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo, exist bool, err error) {
siteInfo = sr.getCache(ctx, siteType)
if siteInfo != nil {
return siteInfo, true, nil
}
siteInfo = &entity.SiteInfo{}
exist, err = sr.data.DB.Where(builder.Eq{"type": siteType}).Get(siteInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
sr.setCache(ctx, siteType, siteInfo)
}
return
}
func (sr *siteInfoRepo) getCache(ctx context.Context, siteType string) (siteInfo *entity.SiteInfo) {
siteInfo = &entity.SiteInfo{}
siteInfoCache, err := sr.data.Cache.GetString(ctx, constant.SiteInfoCacheKey+siteType)
if err != nil {
return nil
}
_ = json.Unmarshal([]byte(siteInfoCache), siteInfo)
return siteInfo
}
func (sr *siteInfoRepo) setCache(ctx context.Context, siteType string, siteInfo *entity.SiteInfo) {
siteInfoCache, _ := json.Marshal(siteInfo)
err := sr.data.Cache.SetString(ctx,
constant.SiteInfoCacheKey+siteType, string(siteInfoCache), constant.SiteInfoCacheTime)
if err != nil {
log.Error(err)
}
}
+8 -8
View File
@@ -55,7 +55,7 @@ func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string)
}
// GetTagListByName get tag list all like name
func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error) {
func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, hasReserved bool) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
@@ -65,13 +65,13 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limi
cond.Recommend = true
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
session.Limit(limit).Asc("slug_name")
if !hasReserved {
cond.Reserved = false
session.UseBool("recommend", "reserved")
} else {
session.UseBool("recommend")
}
session.Asc("slug_name")
// if !hasReserved {
// cond.Reserved = false
// session.UseBool("recommend", "reserved")
// } else {
session.UseBool("recommend")
// }
err = session.OrderBy("recommend desc,reserved desc,id desc").Find(&tagList, cond)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+42 -38
View File
@@ -3,10 +3,7 @@ package user
import (
"context"
"encoding/json"
"net/mail"
"strings"
"time"
"unicode"
"xorm.io/builder"
@@ -64,6 +61,24 @@ func (ur *userBackyardRepo) UpdateUserStatus(ctx context.Context, userID string,
return
}
// AddUser add user
func (ur *userBackyardRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
_, err = ur.data.DB.Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateUserPassword update user password
func (ur *userBackyardRepo) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) {
_, err = ur.data.DB.ID(userID).Update(&entity.User{Pass: password})
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserInfo get user info
func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) {
user = &entity.User{}
@@ -74,50 +89,39 @@ func (ur *userBackyardRepo) GetUserInfo(ctx context.Context, userID string) (use
return
}
// GetUserInfoByEmail get user info
func (ur *userBackyardRepo) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) {
userInfo := &entity.User{}
exist, err = ur.data.DB.Where("e_mail = ?", email).
Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetUserPage get user page
func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, query string) (users []*entity.User, total int64, err error) {
func (ur *userBackyardRepo) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User,
usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) {
users = make([]*entity.User, 0)
session := ur.data.DB.NewSession()
switch user.Status {
case entity.UserStatusDeleted:
session.Desc("deleted_at")
session.Desc("user.deleted_at")
case entity.UserStatusSuspended:
session.Desc("suspended_at")
session.Desc("user.suspended_at")
default:
session.Desc("created_at")
session.Desc("user.created_at")
}
if len(query) > 0 {
if email, e := mail.ParseAddress(query); e == nil {
session.And(builder.Eq{"e_mail": email.Address})
} else {
var (
idSearch = false
id = ""
)
if strings.Contains(query, "user:") {
idSearch = true
id = strings.TrimSpace(strings.TrimPrefix(query, "user:"))
for _, r := range id {
if !unicode.IsDigit(r) {
idSearch = false
break
}
}
}
if idSearch {
session.And(builder.Eq{
"id": id,
})
} else {
session.And(builder.Or(
builder.Like{"username", query},
builder.Like{"display_name", query},
))
}
}
if len(usernameOrDisplayName) > 0 {
session.And(builder.Or(
builder.Like{"user.username", usernameOrDisplayName},
builder.Like{"user.display_name", usernameOrDisplayName},
))
}
if isStaff {
session.Join("INNER", "user_role_rel", "user.id = user_role_rel.user_id AND user_role_rel.role_id > 1")
}
total, err = pager.Help(page, pageSize, &users, user, session)
+3 -2
View File
@@ -28,7 +28,7 @@ func NewUserRepo(data *data.Data, configRepo config.ConfigRepo) usercommon.UserR
// AddUser add user
func (ur *userRepo) AddUser(ctx context.Context, user *entity.User) (err error) {
_, err = ur.data.DB.UseBool("is_admin").Insert(user)
_, err = ur.data.DB.Insert(user)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@@ -151,7 +151,8 @@ func (ur *userRepo) GetByUsername(ctx context.Context, username string) (userInf
// GetByEmail get user by email
func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) {
userInfo = &entity.User{}
exist, err = ur.data.DB.Where("e_mail = ?", email).Get(userInfo)
exist, err = ur.data.DB.Where("e_mail = ?", email).
Where("status != ?", entity.UserStatusDeleted).Get(userInfo)
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
+39 -16
View File
@@ -30,6 +30,7 @@ type AnswerAPIRouter struct {
dashboardController *controller.DashboardController
uploadController *controller.UploadController
activityController *controller.ActivityController
roleController *controller_backyard.RoleController
}
func NewAnswerAPIRouter(
@@ -56,6 +57,7 @@ func NewAnswerAPIRouter(
dashboardController *controller.DashboardController,
uploadController *controller.UploadController,
activityController *controller.ActivityController,
roleController *controller_backyard.RoleController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@@ -81,32 +83,38 @@ func NewAnswerAPIRouter(
dashboardController: dashboardController,
uploadController: uploadController,
activityController: activityController,
roleController: roleController,
}
}
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
func (a *AnswerAPIRouter) RegisterMustUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// i18n
r.GET("/language/config", a.langController.GetLangMapping)
r.GET("/language/options", a.langController.GetUserLangOptions)
// comment
r.GET("/comment/page", a.commentController.GetCommentWithPage)
r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage)
r.GET("/comment", a.commentController.GetComment)
//siteinfo
r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
// user
r.GET("/user/info", a.userController.GetUserInfoByUserID)
r.GET("/user/action/record", a.userController.ActionRecord)
r.POST("/user/login/email", a.userController.UserEmailLogin)
r.POST("/user/register/email", a.userController.UserRegisterByEmail)
r.GET("/user/register/captcha", a.userController.UserRegisterCaptcha)
r.POST("/user/email/verification", a.userController.UserVerifyEmail)
r.PUT("/user/email", a.userController.UserChangeEmailVerify)
r.GET("/user/action/record", a.userController.ActionRecord)
r.POST("/user/password/reset", a.userController.RetrievePassWord)
r.POST("/user/password/replacement", a.userController.UseRePassWord)
r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend)
r.GET("/user/info", a.userController.GetUserInfoByUserID)
}
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// user
r.GET("/user/logout", a.userController.UserLogout)
r.PUT("/user/email", a.userController.UserChangeEmailVerify)
r.POST("/user/email/change/code", a.userController.UserChangeEmailSendCode)
r.POST("/user/email/verification/send", a.userController.UserVerifyEmailSend)
r.GET("/personal/user/info", a.userController.GetOtherUserInfoByUsername)
r.GET("/user/ranking", a.userController.UserRanking)
//answer
r.GET("/answer/info", a.answerController.Get)
@@ -121,6 +129,11 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/personal/qa/top", a.questionController.UserTop)
r.GET("/personal/question/page", a.questionController.UserList)
// comment
r.GET("/comment/page", a.commentController.GetCommentWithPage)
r.GET("/personal/comment/page", a.commentController.GetCommentPersonalWithPage)
r.GET("/comment", a.commentController.GetComment)
//revision
r.GET("/revisions", a.revisionController.GetRevisionList)
@@ -136,11 +149,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
//rank
r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage)
//siteinfo
r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
r.GET("/siteinfo/legal", a.siteinfoController.GetSiteLegalInfo)
}
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
@@ -180,6 +188,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.PUT("/question", a.questionController.UpdateQuestion)
r.DELETE("/question", a.questionController.RemoveQuestion)
r.PUT("/question/status", a.questionController.CloseQuestion)
r.PUT("/question/reopen", a.questionController.ReopenQuestion)
r.GET("/question/similar", a.questionController.SearchByTitleLike)
// answer
@@ -229,6 +238,9 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
// user
r.GET("/users/page", a.backyardUserController.GetUserPage)
r.PUT("/user/status", a.backyardUserController.UpdateUserStatus)
r.PUT("/user/role", a.backyardUserController.UpdateUserRole)
r.POST("/user", a.backyardUserController.AddUser)
r.PUT("/user/password", a.backyardUserController.UpdateUserPassword)
// reason
r.GET("/reasons", a.reasonController.Reasons)
@@ -245,14 +257,25 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
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.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
r.PUT("/siteinfo/branding", a.siteInfoController.UpdateBranding)
r.PUT("/siteinfo/write", a.siteInfoController.UpdateSiteWrite)
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.PUT("/siteinfo/seo", a.siteInfoController.UpdateSeo)
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
//dashboard
// dashboard
r.GET("/dashboard", a.dashboardController.DashboardInfo)
// roles
r.GET("/roles", a.roleController.GetRoleList)
}
+1 -1
View File
@@ -3,4 +3,4 @@ package router
import "github.com/google/wire"
// ProviderSetRouter is providers.
var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter)
var ProviderSetRouter = wire.NewSet(NewAnswerAPIRouter, NewSwaggerRouter, NewStaticRouter, NewUIRouter, NewTemplateRouter)
+49
View File
@@ -0,0 +1,49 @@
package router
import (
"github.com/answerdev/answer/internal/controller"
templaterender "github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/controller_backyard"
"github.com/gin-gonic/gin"
)
type TemplateRouter struct {
templateController *controller.TemplateController
templateRenderController *templaterender.TemplateRenderController
siteInfoController *controller_backyard.SiteInfoController
}
func NewTemplateRouter(
templateController *controller.TemplateController,
templateRenderController *templaterender.TemplateRenderController,
siteInfoController *controller_backyard.SiteInfoController,
) *TemplateRouter {
return &TemplateRouter{
templateController: templateController,
templateRenderController: templateRenderController,
siteInfoController: siteInfoController,
}
}
// TemplateRouter template router
func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
r.GET("/sitemap.xml", a.templateController.Sitemap)
r.GET("/sitemap/:page", a.templateController.SitemapPage)
r.GET("/robots.txt", a.siteInfoController.GetRobots)
r.GET("/custom.css", a.siteInfoController.GetCss)
r.GET("/", a.templateController.Index)
r.GET("/index", a.templateController.Index)
r.GET("/questions", a.templateController.QuestionList)
r.GET("/questions/:id", a.templateController.QuestionInfo)
r.GET("/questions/:id/:title", a.templateController.QuestionInfo)
r.GET("/questions/:id/:title/:answerid", a.templateController.QuestionInfo)
r.GET("/tags", a.templateController.TagList)
r.GET("/tags/:tag", a.templateController.TagInfo)
r.GET("/users/:username", a.templateController.UserInfo)
r.GET("/404", a.templateController.Page404)
}
+28 -5
View File
@@ -7,6 +7,9 @@ import (
"net/http"
"os"
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
@@ -18,11 +21,19 @@ const UIStaticPath = "build/static"
// UIRouter is an interface that provides ui static file routers
type UIRouter struct {
siteInfoController *controller.SiteinfoController
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewUIRouter creates a new UIRouter instance with the embed resources
func NewUIRouter() *UIRouter {
return &UIRouter{}
func NewUIRouter(
siteInfoController *controller.SiteinfoController,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *UIRouter {
return &UIRouter{
siteInfoController: siteInfoController,
siteInfoService: siteInfoService,
}
}
// _resource is an interface that provides static file, it's a private interface
@@ -72,10 +83,22 @@ func (a *UIRouter) Register(r *gin.Engine) {
filePath := ""
switch urlPath {
case "/favicon.ico":
c.Header("content-type", "image/vnd.microsoft.icon")
filePath = UIRootFilePath + urlPath
branding, err := a.siteInfoService.GetSiteBranding(c)
if err != nil {
log.Error(err)
}
if branding.Favicon != "" {
c.String(http.StatusOK, htmltext.GetPicByUrl(branding.Favicon))
return
} else {
c.Header("content-type", "image/vnd.microsoft.icon")
filePath = UIRootFilePath + urlPath
}
case "/manifest.json":
filePath = UIRootFilePath + urlPath
// filePath = UIRootFilePath + urlPath
a.siteInfoController.GetManifestJson(c)
return
case "/install":
// if answer is running by run command user can not access install page.
c.Redirect(http.StatusFound, "/")
-36
View File
@@ -1,36 +0,0 @@
package router
import (
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"testing"
)
func TestUIRouter_Register(t *testing.T) {
r := gin.Default()
NewUIRouter().Register(r)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestUIRouter_Static(t *testing.T) {
r := gin.Default()
NewUIRouter().Register(r)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/static/version.txt", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "OK", w.Body.String())
}
+3 -3
View File
@@ -39,18 +39,18 @@ type ActObjectTimeline struct {
ObjectType string `json:"object_type"`
Cancelled bool `json:"cancelled"`
CancelledAt int64 `json:"cancelled_at"`
UserID string `json:"-"`
UserID string `json:"id"`
}
// ActObjectInfo act object info
type ActObjectInfo struct {
ObjectType string `json:"object_type"`
Title string `json:"title"`
ObjectType string `json:"object_type"`
QuestionID string `json:"question_id"`
AnswerID string `json:"answer_id"`
MainTagSlugName string `json:"main_tag_slug_name"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
MainTagSlugName string `json:"main_tag_slug_name"`
}
// GetObjectTimelineDetailReq get object timeline detail request
+15
View File
@@ -1,5 +1,10 @@
package schema
import (
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/pkg/converter"
)
// RemoveAnswerReq delete answer request
type RemoveAnswerReq struct {
// answer id
@@ -21,6 +26,11 @@ type AnswerAddReq struct {
UserID string `json:"-" ` // user_id
}
func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) {
req.HTML = converter.Markdown2HTML(req.Content)
return nil, nil
}
type AnswerUpdateReq struct {
ID string `json:"id"` // id
QuestionID string `json:"question_id" ` // question_id
@@ -34,6 +44,11 @@ type AnswerUpdateReq struct {
CanEdit bool `json:"-"`
}
func (req *AnswerUpdateReq) Check() (errFields []*validator.FormErrorField, err error) {
req.HTML = converter.Markdown2HTML(req.Content)
return nil, nil
}
// AnswerUpdateResp answer update resp
type AnswerUpdateResp struct {
WaitForReview bool `json:"wait_for_review"`
+31
View File
@@ -30,6 +30,8 @@ type GetUserPageReq struct {
Query string `validate:"omitempty,gt=0,lte=100" form:"query"`
// user status
Status string `validate:"omitempty,oneof=suspended deleted inactive" form:"status"`
// staff, if staff is true means query admin or moderator
Staff bool `validate:"omitempty" form:"staff"`
}
func (r *GetUserPageReq) IsSuspended() bool { return r.Status == UserSuspended }
@@ -58,6 +60,10 @@ type GetUserPageResp struct {
DisplayName string `json:"display_name"`
// avatar
Avatar string `json:"avatar"`
// role id
RoleID int `json:"role_id"`
// role name
RoleName string `json:"role_name"`
}
// GetUserInfoReq get user request
@@ -68,3 +74,28 @@ type GetUserInfoReq struct {
// GetUserInfoResp get user response
type GetUserInfoResp struct {
}
// UpdateUserRoleReq update user role request
type UpdateUserRoleReq struct {
// user id
UserID string `validate:"required" json:"user_id"`
// role id
RoleID int `validate:"required" json:"role_id"`
// login user id
LoginUserID string `json:"-"`
}
// AddUserReq add user request
type AddUserReq struct {
DisplayName string `validate:"required,gt=4,lte=30" json:"display_name"`
Email string `validate:"required,email,gt=0,lte=500" json:"email"`
Password string `validate:"required,gte=8,lte=32" json:"password"`
LoginUserID string `json:"-"`
}
// UpdateUserPasswordReq update user password request
type UpdateUserPasswordReq struct {
UserID string `validate:"required" json:"user_id"`
Password string `validate:"required,gte=8,lte=32" json:"password"`
LoginUserID string `json:"-"`
}
+12
View File
@@ -1,7 +1,9 @@
package schema
import (
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/converter"
"github.com/jinzhu/copier"
)
@@ -27,6 +29,11 @@ type AddCommentReq struct {
CanDelete bool `json:"-"`
}
func (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
req.ParsedText = converter.Markdown2HTML(req.OriginalText)
return nil, nil
}
// RemoveCommentReq remove comment
type RemoveCommentReq struct {
// comment id
@@ -47,6 +54,11 @@ type UpdateCommentReq struct {
UserID string `json:"-"`
}
func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
req.ParsedText = converter.Markdown2HTML(req.OriginalText)
return nil, nil
}
// GetCommentListReq get comment list all request
type GetCommentListReq struct {
// user id
+58 -7
View File
@@ -1,19 +1,29 @@
package schema
import (
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/pkg/converter"
)
const (
SitemapMaxSize = 50000
SitemapCachekey = "answer@sitemap"
SitemapPageCachekey = "answer@sitemap@page%d"
)
// RemoveQuestionReq delete question request
type RemoveQuestionReq struct {
// question id
ID string `validate:"required" comment:"question id" json:"id"`
ID string `validate:"required" json:"id"`
UserID string `json:"-" ` // user_id
IsAdmin bool `json:"-"`
}
type CloseQuestionReq struct {
ID string `validate:"required" comment:"question id" json:"id"`
UserID string `json:"-" ` // user_id
CloseType int `json:"close_type" ` // close_type
CloseMsg string `json:"close_msg" ` // close_type
IsAdmin bool `json:"-"`
ID string `validate:"required" json:"id"`
CloseType int `json:"close_type"` // close_type
CloseMsg string `json:"close_msg"` // close_type
UserID string `json:"-"` // user_id
}
type CloseQuestionMeta struct {
@@ -21,6 +31,12 @@ type CloseQuestionMeta struct {
CloseMsg string `json:"close_msg"`
}
// ReopenQuestionReq reopen question request
type ReopenQuestionReq struct {
QuestionID string `json:"question_id"`
UserID string `json:"-"`
}
type QuestionAdd struct {
// question title
Title string `validate:"required,gte=6,lte=150" json:"title"`
@@ -35,6 +51,16 @@ type QuestionAdd struct {
QuestionPermission
}
func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err error) {
req.HTML = converter.Markdown2HTML(req.Content)
for _, tag := range req.Tags {
if len(tag.OriginalText) > 0 {
tag.ParsedText = converter.Markdown2HTML(tag.OriginalText)
}
}
return nil, nil
}
type QuestionPermission struct {
// whether user can add it
CanAdd bool `json:"-"`
@@ -44,6 +70,10 @@ type QuestionPermission struct {
CanDelete bool `json:"-"`
// whether user can close it
CanClose bool `json:"-"`
// whether user can reopen it
CanReopen bool `json:"-"`
// whether user can use reserved it
CanUseReservedTag bool `json:"-"`
}
type CheckCanQuestionUpdate struct {
@@ -69,11 +99,15 @@ type QuestionUpdate struct {
EditSummary string `validate:"omitempty" json:"edit_summary"`
// user id
UserID string `json:"-"`
IsAdmin bool `json:"-"`
NoNeedReview bool `json:"-"`
QuestionPermission
}
func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) {
req.HTML = converter.Markdown2HTML(req.Content)
return nil, nil
}
type QuestionBaseInfo struct {
ID string `json:"id" `
Title string `json:"title" xorm:"title"` // title
@@ -88,8 +122,10 @@ type QuestionBaseInfo struct {
type QuestionInfo struct {
ID string `json:"id" `
Title string `json:"title" xorm:"title"` // title
UrlTitle string `json:"url_title" xorm:"url_title"` // title
Content string `json:"content" xorm:"content"` // content
HTML string `json:"html" xorm:"html"` // html
Description string `json:"description"` //description
Tags []*TagResp `json:"tags" ` // tags
ViewCount int `json:"view_count" xorm:"view_count"` // view_count
UniqueViewCount int `json:"unique_view_count" xorm:"unique_view_count"` // unique_view_count
@@ -209,3 +245,18 @@ type AdminSetQuestionStatusRequest struct {
StatusStr string `json:"status" form:"status"`
QuestionID string `json:"question_id" form:"question_id"`
}
type SiteMapList struct {
QuestionIDs []*SiteMapQuestionInfo `json:"question_ids"`
MaxPageNum []int `json:"max_page_num"`
}
type SiteMapPageList struct {
PageData []*SiteMapQuestionInfo `json:"page_data"`
}
type SiteMapQuestionInfo struct {
ID string `json:"id"`
Title string `json:"title"`
UpdateTime string `json:"time"`
}
+8
View File
@@ -0,0 +1,8 @@
package schema
// GetRoleResp get role response
type GetRoleResp struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
+98 -3
View File
@@ -1,10 +1,17 @@
package schema
import (
"context"
"fmt"
"net/url"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/translator"
)
const PermaLinkQuestionIDAndTitle = 1
const PermaLinkQuestionID = 2
// SiteGeneralReq site general request
type SiteGeneralReq struct {
Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"`
@@ -14,6 +21,11 @@ type SiteGeneralReq struct {
ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
}
type SiteSeoReq struct {
PermaLink int `validate:"required,lte=3,gte=0" form:"permalink" json:"permalink"`
Robots string `validate:"required" form:"robots" json:"robots"`
}
func (r *SiteGeneralReq) FormatSiteUrl() {
parsedUrl, err := url.Parse(r.SiteUrl)
if err != nil {
@@ -74,6 +86,26 @@ type GetSiteLegalInfoResp struct {
PrivacyPolicyParsedText string `json:"privacy_policy_parsed_text,omitempty"`
}
// SiteLoginReq site login request
type SiteLoginReq struct {
AllowNewRegistrations bool `json:"allow_new_registrations"`
LoginRequired bool `json:"login_required"`
}
// 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"`
}
// SiteThemeReq site theme config
type SiteThemeReq struct {
Theme string `validate:"required,gt=0,lte=255" json:"theme"`
ThemeConfig map[string]interface{} `validate:"omitempty" json:"theme_config"`
}
// SiteGeneralResp site general response
type SiteGeneralResp SiteGeneralReq
@@ -83,17 +115,67 @@ type SiteInterfaceResp SiteInterfaceReq
// SiteBrandingResp site branding response
type SiteBrandingResp SiteBrandingReq
// SiteLoginResp site login response
type SiteLoginResp SiteLoginReq
// SiteCustomCssHTMLResp site custom css html response
type SiteCustomCssHTMLResp SiteCustomCssHTMLReq
// SiteThemeResp site theme response
type SiteThemeResp struct {
ThemeOptions []*ThemeOption `json:"theme_options"`
Theme string `json:"theme"`
ThemeConfig map[string]interface{} `json:"theme_config"`
}
func (s *SiteThemeResp) TrTheme(ctx context.Context) {
la := handler.GetLangByCtx(ctx)
for _, option := range s.ThemeOptions {
tr := translator.GlobalTrans.Tr(la, option.Value)
// if tr is equal the option value means not found translation, so use the original label
if tr != option.Value {
option.Label = tr
}
}
}
// ThemeOption get label option
type ThemeOption struct {
Label string `json:"label"`
Value string `json:"value"`
}
// SiteWriteResp site write response
type SiteWriteResp SiteWriteReq
// SiteLegalResp site write response
type SiteLegalResp SiteLegalReq
// SiteSeoResp site write response
type SiteSeoResp SiteSeoReq
// SiteInfoResp get site info response
type SiteInfoResp struct {
General *SiteGeneralResp `json:"general"`
Interface *SiteInterfaceResp `json:"interface"`
Branding *SiteBrandingResp `json:"branding"`
General *SiteGeneralResp `json:"general"`
Interface *SiteInterfaceResp `json:"interface"`
Branding *SiteBrandingResp `json:"branding"`
Login *SiteLoginResp `json:"login"`
Theme *SiteThemeResp `json:"theme"`
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
SiteSeo *SiteSeoReq `json:"site_seo"`
}
type TemplateSiteInfoResp struct {
General *SiteGeneralResp `json:"general"`
Interface *SiteInterfaceResp `json:"interface"`
Branding *SiteBrandingResp `json:"branding"`
SiteSeo *SiteSeoReq `json:"site_seo"`
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
Title string
Year string
Canonical string
JsonLD string
Keywords string
Description string
}
// UpdateSMTPConfigReq get smtp config request
@@ -120,3 +202,16 @@ type GetSMTPConfigResp struct {
SMTPPassword string `json:"smtp_password"`
SMTPAuthentication bool `json:"smtp_authentication"`
}
// GetManifestJsonResp get manifest json response
type GetManifestJsonResp struct {
ManifestVersion int `json:"manifest_version"`
Version string `json:"version"`
ShortName string `json:"short_name"`
Name string `json:"name"`
Icons map[string]string `json:"icons"`
StartUrl string `json:"start_url"`
Display string `json:"display"`
ThemeColor string `json:"theme_color"`
BackgroundColor string `json:"background_color"`
}
+16 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/validator"
"github.com/answerdev/answer/pkg/converter"
"github.com/segmentfault/pacman/errors"
)
@@ -29,6 +30,17 @@ type GetTagInfoReq struct {
CanDelete bool `json:"-"`
}
type GetTamplateTagInfoReq struct {
// tag id
ID string `validate:"omitempty" form:"id"`
// tag slug name
Name string `validate:"omitempty" form:"name"`
// user id
UserID string `json:"-"`
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
}
func (r *GetTagInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
if len(r.ID) == 0 && len(r.Name) == 0 {
return nil, errors.BadRequest(reason.RequestFormatError)
@@ -55,6 +67,8 @@ type GetTagResp struct {
OriginalText string `json:"original_text"`
// parsed text
ParsedText string `json:"parsed_text"`
// description text
Description string `json:"description"`
// follower amount
FollowCount int `json:"follow_count"`
// question amount
@@ -162,8 +176,9 @@ type UpdateTagReq struct {
func (r *UpdateTagReq) Check() (errFields []*validator.FormErrorField, err error) {
if len(r.EditSummary) == 0 {
r.EditSummary = "tag.edit.summary" // to do i18n
r.EditSummary = "tag.edit.summary"
}
r.ParsedText = converter.Markdown2HTML(r.OriginalText)
return nil, nil
}
+54
View File
@@ -0,0 +1,54 @@
package schema
import "time"
type Paginator struct {
Pages []int
Totalpages int
Prevpage int
Nextpage int
Currpage int
}
type QAPageJsonLD struct {
Context string `json:"@context"`
Type string `json:"@type"`
MainEntity struct {
Type string `json:"@type"`
Name string `json:"name"`
Text string `json:"text"`
AnswerCount int `json:"answerCount"`
UpvoteCount int `json:"upvoteCount"`
DateCreated time.Time `json:"dateCreated"`
Author struct {
Type string `json:"@type"`
Name string `json:"name"`
} `json:"author"`
AcceptedAnswer *AcceptedAnswerItem `json:"acceptedAnswer,omitempty"`
SuggestedAnswer []*SuggestedAnswerItem `json:"suggestedAnswer"`
} `json:"mainEntity"`
}
type AcceptedAnswerItem struct {
Type string `json:"@type"`
Text string `json:"text"`
DateCreated time.Time `json:"dateCreated"`
UpvoteCount int `json:"upvoteCount"`
URL string `json:"url"`
Author struct {
Type string `json:"@type"`
Name string `json:"name"`
} `json:"author"`
}
type SuggestedAnswerItem struct {
Type string `json:"@type"`
Text string `json:"text"`
DateCreated time.Time `json:"dateCreated"`
UpvoteCount int `json:"upvoteCount"`
URL string `json:"url"`
Author struct {
Type string `json:"@type"`
Name string `json:"name"`
} `json:"author"`
}

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