Merge remote-tracking branch 'origin/main' into github-main
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -4,3 +4,6 @@ import _ "embed"
|
||||
|
||||
//go:embed config.yaml
|
||||
var Config []byte
|
||||
|
||||
//go:embed path_ignore.yaml
|
||||
var PathIgnore []byte
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+935
-14
File diff suppressed because it is too large
Load Diff
+586
-10
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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: "Can’t 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><https://url.com><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 you’ve 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 host’s 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, we’ll be back soon."
|
||||
desc: "We are under maintenance, we’ll 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
@@ -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><https://url.com><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 标签)。
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package cron
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
// ProviderSetService is providers.
|
||||
var ProviderSetService = wire.NewSet(
|
||||
NewScheduledTaskManager,
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -187,5 +187,4 @@ func InitBaseInfo(ctx *gin.Context) {
|
||||
time.Sleep(1 * time.Second)
|
||||
os.Exit(0)
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
+231
-22
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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`")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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, "/")
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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:"-"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user