Merge branch 'release/0.3.0' into 'main'

Release/0.3.0

See merge request 
This commit is contained in:
linkinstar 2022-11-11 06:24:10 +00:00
commit 2b1ad94791
228 changed files with 7352 additions and 3689 deletions
.gitignoreDockerfileINSTALL.mdINSTALL_CN.md
cmd/answer
configs
docs
go.modgo.sum
i18n
internal
pkg
script
ui

2
.gitignore vendored
View File

@ -9,7 +9,7 @@
/.fleet /.fleet
/.vscode/*.log /.vscode/*.log
/cmd/answer/*.sh /cmd/answer/*.sh
/cmd/answer/upfiles/* /cmd/answer/uploads/*
/cmd/logs /cmd/logs
/configs/config-dev.yaml /configs/config-dev.yaml
/go.work* /go.work*

View File

@ -29,7 +29,7 @@ RUN apk --no-cache add build-base git \
&& make clean build \ && make clean build \
&& cp answer /usr/bin/answer && cp answer /usr/bin/answer
RUN mkdir -p /data/upfiles && chmod 777 /data/upfiles \ RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n && mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
# stage3 copy the binary and resource files into fresh container # stage3 copy the binary and resource files into fresh container

View File

@ -91,7 +91,7 @@ swaggerui:
service_config: service_config:
secret_key: "answer" #encryption key secret_key: "answer" #encryption key
web_host: "http://127.0.0.1" #Page access using domain name address web_host: "http://127.0.0.1" #Page access using domain name address
upload_path: "./upfiles" #upload directory upload_path: "./uploads" #upload directory
``` ```
## Compile the image ## Compile the image
@ -100,4 +100,4 @@ If you have modified the source files and want to repackage the image, you can u
docker build -t answer:v1.0.0 . docker build -t answer:v1.0.0 .
``` ```
## common problem ## common problem
1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`upfiles`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and upfiles directories. 1. The project cannot be started: the main program startup depends on proper configuraiton of the configuration file, `config.yaml`, as well as the internationalization translation directory (`i18n`), and the upload file storage directory (`uploads`). Ensure that the configuration file is loaded when the project starts, such as when using `answer run -c config.yaml` and that the `config.yaml` correctly specifies the i18n and uploads directories.

View File

@ -1,80 +1,54 @@
# Answer 安装指引 # Answer 安装指引
安装 Answer 之前,您需要先安装基本环境。 ## 使用 docker 安装
- 数据库 ### 步骤 1: 使用 docker 命令启动项目
- [MySQL](http://dev.mysql.com):版本 >= 5.7
然后,您可以通过以下几种方式来安装 Answer
- 采用 Docker 部署
- 二进制安装
- 源码安装
## 使用 Docker-compose 安装 Answer
```bash ```bash
$ mkdir answer && cd answer docker run -d -p 9080:80 -v answer-data:/data --name answer answerdev/answer:latest
$ wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml ```
$ docker-compose up ### 步骤 2: 访问安装路径进行项目安装
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
选择语言后点击下一步选择合适的数据库,如果当前只是想体验,建议直接选择 sqlite 作为数据库,如下图所示
![install-database](docs/img/install-database.png)
然后点击下一步会进行配置文件创建等操作,点击下一步输入网站基本信息和管理员信息,如下图所示
![install-site-info](docs/img/install-site-info.png)
点击下一步即可安装完成
### 步骤 3安装完成后访问项目路径开始使用
[http://127.0.0.1:9080/](http://127.0.0.1:9080/)
使用刚才创建的管理员用户名密码即可登录。
## 使用 docker-compose 安装
### 步骤 1: 使用 docker-compose 命令启动项目
```bash
mkdir answer && cd answer
wget https://raw.githubusercontent.com/answerdev/answer/main/docker-compose.yaml
docker-compose up
``` ```
启动完成后使用浏览器访问 [http://127.0.0.1:9080/](http://127.0.0.1:9080/). ### 步骤 2: 访问安装路径进行项目安装
[http://127.0.0.1:9080/install](http://127.0.0.1:9080/install)
你可以使用默认的用户名:( **`admin@admin.com`** ) 和密码:( **`admin`** ) 进行登录. 具体配置与 docker 使用时相同
## 使用Docker 安装 Answer ### 步骤 3安装完成后访问项目路径开始使用
可以从 Docker Hub 或者 GitHub Container registry 下载最新的 tags 镜像 [http://127.0.0.1:9080/](http://127.0.0.1:9080/)
### 用法 ## 使用 二进制 安装
将配置和存储目录挂在到镜像之外 volume (/var/data -> /data),你可以修改外部挂载地址 ### 步骤 1: 下载二进制文件
[https://github.com/answerdev/answer/releases](https://github.com/answerdev/answer/releases)
请下载您当下系统所需要的对应版本
``` ### 步骤 2: 使用命令行安装
# 将镜像从 docker hub 拉到本地 > 以下命令中 -C 指定的是 answer 所需的数据目录,您可以根据实际需要进行修改
$ docker pull answerdev/answer:latest
# 创建一个挂载目录
$ mkdir -p /var/data
# 先运行一遍镜像
$ docker run --name=answer -p 9080:80 -v /var/data:/data answerdev/answer
# 第一次启动后会在/var/data 目录下生成配置文件
# /var/data/conf/config.yaml
# 需要修改配置文件中的Mysql 数据库地址
vim /var/data/conf/config.yaml
# 修改数据库连接 connection: [username]:[password]@tcp([host]:[port])/[DbName]
...
# 配置好配置文件后可以再次启动镜像即可启动服务
$ docker start answer
```
## 使用二进制 安装 Answer
可以使用编译完成的各个平台的二进制文件运行 Answer 项目
### 用法
从 GitHub 最新版本的tag中下载对应平台的二进制文件压缩包
1. 解压压缩包
2. 使用命令 cd 进入到刚刚创建的目录
3. 执行命令 ./answer init
4. Answer 会在当前目录生成 ./data 目录
5. 进入 data 目录修改 config.yaml 文件
6. 将数据库连接地址修改为你的数据库连接地址
connection: [username]:[password]@tcp([host]:[port])/[DbName]
7. 退出 data 目录,执行 ./answer run -c ./data/conf/config.yaml
## 当前支持的命令
用法: answer [command]
- help: 帮助
- init: 初始化环境
- run: 启动
- check: 环境依赖检查
- dump: 备份数据
## 配置文件 config.yaml 参数说明
```bash
./answer init -C ./answer-data/
``` ```
server: server:
http: http:

View File

@ -4,14 +4,14 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/install"
"github.com/answerdev/answer/internal/migrations" "github.com/answerdev/answer/internal/migrations"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var (
// configFilePath is the config file path
configFilePath string
// dataDirPath save all answer application data in this directory. like config file, upload file... // dataDirPath save all answer application data in this directory. like config file, upload file...
dataDirPath string dataDirPath string
// dumpDataPath dump data path // dumpDataPath dump data path
@ -21,9 +21,7 @@ var (
func init() { func init() {
rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time) rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time)
initCmd.Flags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/") rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
rootCmd.PersistentFlags().StringVarP(&configFilePath, "config", "c", "", "config path, eg: -c config.yaml")
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/") dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
@ -49,6 +47,9 @@ To run answer, use:
Short: "Run the application", Short: "Run the application",
Long: `Run the application`, Long: `Run the application`,
Run: func(_ *cobra.Command, _ []string) { Run: func(_ *cobra.Command, _ []string) {
cli.FormatAllPath(dataDirPath)
fmt.Println("config file path: ", cli.GetConfigFilePath())
fmt.Println("Answer is string..........................")
runApp() runApp()
}, },
} }
@ -59,18 +60,27 @@ To run answer, use:
Short: "init answer application", Short: "init answer application",
Long: `init answer application`, Long: `init answer application`,
Run: func(_ *cobra.Command, _ []string) { Run: func(_ *cobra.Command, _ []string) {
// check config file and database. if config file exists and database is already created, init done
cli.InstallAllInitialEnvironment(dataDirPath) cli.InstallAllInitialEnvironment(dataDirPath)
c, err := readConfig()
if err != nil { configFileExist := cli.CheckConfigFile(cli.GetConfigFilePath())
fmt.Println("read config failed: ", err.Error()) if configFileExist {
return fmt.Println("config file exists, try to read the config...")
c, err := conf.ReadConfig(cli.GetConfigFilePath())
if err != nil {
fmt.Println("read config failed: ", err.Error())
return
}
fmt.Println("config file read successfully, try to connect database...")
if cli.CheckDBTableExist(c.Data.Database) {
fmt.Println("connect to database successfully and table already exists, do nothing.")
return
}
} }
fmt.Println("read config successfully")
if err := migrations.InitDB(c.Data.Database); err != nil { // start installation server to install
fmt.Println("init database error: ", err.Error()) install.Run(cli.GetConfigFilePath())
return
}
fmt.Println("init database successfully")
}, },
} }
@ -80,7 +90,8 @@ To run answer, use:
Short: "upgrade Answer version", Short: "upgrade Answer version",
Long: `upgrade Answer version`, Long: `upgrade Answer version`,
Run: func(_ *cobra.Command, _ []string) { Run: func(_ *cobra.Command, _ []string) {
c, err := readConfig() cli.FormatAllPath(dataDirPath)
c, err := conf.ReadConfig(cli.GetConfigFilePath())
if err != nil { if err != nil {
fmt.Println("read config failed: ", err.Error()) fmt.Println("read config failed: ", err.Error())
return return
@ -100,7 +111,8 @@ To run answer, use:
Long: `back up data`, Long: `back up data`,
Run: func(_ *cobra.Command, _ []string) { Run: func(_ *cobra.Command, _ []string) {
fmt.Println("Answer is backing up data") fmt.Println("Answer is backing up data")
c, err := readConfig() cli.FormatAllPath(dataDirPath)
c, err := conf.ReadConfig(cli.GetConfigFilePath())
if err != nil { if err != nil {
fmt.Println("read config failed: ", err.Error()) fmt.Println("read config failed: ", err.Error())
return return
@ -120,8 +132,9 @@ To run answer, use:
Short: "checking the required environment", Short: "checking the required environment",
Long: `Check if the current environment meets the startup requirements`, Long: `Check if the current environment meets the startup requirements`,
Run: func(_ *cobra.Command, _ []string) { Run: func(_ *cobra.Command, _ []string) {
cli.FormatAllPath(dataDirPath)
fmt.Println("Start checking the required environment...") fmt.Println("Start checking the required environment...")
if cli.CheckConfigFile(configFilePath) { if cli.CheckConfigFile(cli.GetConfigFilePath()) {
fmt.Println("config file exists [✔]") fmt.Println("config file exists [✔]")
} else { } else {
fmt.Println("config file not exists [x]") fmt.Println("config file not exists [x]")
@ -133,13 +146,13 @@ To run answer, use:
fmt.Println("upload directory not exists [x]") fmt.Println("upload directory not exists [x]")
} }
c, err := readConfig() c, err := conf.ReadConfig(cli.GetConfigFilePath())
if err != nil { if err != nil {
fmt.Println("read config failed: ", err.Error()) fmt.Println("read config failed: ", err.Error())
return return
} }
if cli.CheckDB(c.Data.Database) { if cli.CheckDBConnection(c.Data.Database) {
fmt.Println("db connection successfully [✔]") fmt.Println("db connection successfully [✔]")
} else { } else {
fmt.Println("db connection failed [x]") fmt.Println("db connection failed [x]")

View File

@ -2,13 +2,14 @@ package main
import ( import (
"os" "os"
"path/filepath" "time"
"github.com/answerdev/answer/internal/base/conf" "github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/cli" "github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/segmentfault/pacman" "github.com/segmentfault/pacman"
"github.com/segmentfault/pacman/contrib/conf/viper"
"github.com/segmentfault/pacman/contrib/log/zap" "github.com/segmentfault/pacman/contrib/log/zap"
"github.com/segmentfault/pacman/contrib/server/http" "github.com/segmentfault/pacman/contrib/server/http"
"github.com/segmentfault/pacman/log" "github.com/segmentfault/pacman/log"
@ -40,8 +41,7 @@ func main() {
func runApp() { func runApp() {
log.SetLogger(zap.NewLogger( log.SetLogger(zap.NewLogger(
log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath())) log.ParseLevel(logLevel), zap.WithName("answer"), zap.WithPath(logPath), zap.WithCallerFullPath()))
c, err := conf.ReadConfig(cli.GetConfigFilePath())
c, err := readConfig()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -50,27 +50,15 @@ func runApp() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
constant.Version = Version
schema.AppStartTime = time.Now()
defer cleanup() defer cleanup()
if err := app.Run(); err != nil { if err := app.Run(); err != nil {
panic(err) panic(err)
} }
} }
func readConfig() (c *conf.AllConfig, err error) {
if len(configFilePath) == 0 {
configFilePath = filepath.Join(cli.ConfigFilePath, cli.DefaultConfigFileName)
}
c = &conf.AllConfig{}
config, err := viper.NewWithPath(configFilePath)
if err != nil {
return nil, err
}
if err = config.Parse(&c); err != nil {
return nil, err
}
return c, nil
}
func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application { func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application {
return pacman.NewApp( return pacman.NewApp(
pacman.WithName(Name), pacman.WithName(Name),

View File

@ -44,6 +44,7 @@ import (
auth2 "github.com/answerdev/answer/internal/service/auth" auth2 "github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/collection_common" "github.com/answerdev/answer/internal/service/collection_common"
comment2 "github.com/answerdev/answer/internal/service/comment" comment2 "github.com/answerdev/answer/internal/service/comment"
"github.com/answerdev/answer/internal/service/dashboard"
export2 "github.com/answerdev/answer/internal/service/export" export2 "github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/follow" "github.com/answerdev/answer/internal/service/follow"
meta2 "github.com/answerdev/answer/internal/service/meta" meta2 "github.com/answerdev/answer/internal/service/meta"
@ -58,6 +59,8 @@ import (
"github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
tag2 "github.com/answerdev/answer/internal/service/tag" tag2 "github.com/answerdev/answer/internal/service/tag"
"github.com/answerdev/answer/internal/service/tag_common" "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/uploader" "github.com/answerdev/answer/internal/service/uploader"
@ -76,7 +79,6 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
langController := controller.NewLangController(i18nTranslator)
engine, err := data.NewDB(debug, dbConf) engine, err := data.NewDB(debug, dbConf)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -90,6 +92,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
cleanup() cleanup()
return nil, nil, err return nil, nil, err
} }
siteInfoRepo := site_info.NewSiteInfo(dataData)
siteInfoCommonService := siteinfo_common.NewSiteInfoCommonService(siteInfoRepo)
langController := controller.NewLangController(i18nTranslator, siteInfoCommonService)
authRepo := auth.NewAuthRepo(dataData) authRepo := auth.NewAuthRepo(dataData)
authService := auth2.NewAuthService(authRepo) authService := auth2.NewAuthService(authRepo)
configRepo := config.NewConfigRepo(dataData) configRepo := config.NewConfigRepo(dataData)
@ -99,12 +104,11 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
userRankRepo := rank.NewUserRankRepo(dataData, configRepo) userRankRepo := rank.NewUserRankRepo(dataData, configRepo)
userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo) userActiveActivityRepo := activity.NewUserActiveActivityRepo(dataData, activityRepo, userRankRepo, configRepo)
emailRepo := export.NewEmailRepo(dataData) emailRepo := export.NewEmailRepo(dataData)
siteInfoRepo := site_info.NewSiteInfo(dataData)
emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo) emailService := export2.NewEmailService(configRepo, emailRepo, siteInfoRepo)
userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf) userService := service.NewUserService(userRepo, userActiveActivityRepo, emailService, authService, serviceConf, siteInfoCommonService)
captchaRepo := captcha.NewCaptchaRepo(dataData) captchaRepo := captcha.NewCaptchaRepo(dataData)
captchaService := action.NewCaptchaService(captchaRepo) captchaService := action.NewCaptchaService(captchaRepo)
uploaderService := uploader.NewUploaderService(serviceConf) uploaderService := uploader.NewUploaderService(serviceConf, siteInfoCommonService)
userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService) userController := controller.NewUserController(authService, userService, captchaService, emailService, uploaderService)
commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo) commentRepo := comment.NewCommentRepo(dataData, uniqueIDRepo)
commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo) commentCommonRepo := comment.NewCommentCommonRepo(dataData, uniqueIDRepo)
@ -148,7 +152,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService) questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService)
questionController := controller.NewQuestionController(questionService, rankService) questionController := controller.NewQuestionController(questionService, rankService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo) answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo)
answerController := controller.NewAnswerController(answerService, rankService) dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon) searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon)
searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo) searchService := service.NewSearchService(searchRepo, tagRepo, userCommon, followRepo)
searchController := controller.NewSearchController(searchService) searchController := controller.NewSearchController(searchService)
@ -166,14 +171,15 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
reasonService := reason2.NewReasonService(reasonRepo) reasonService := reason2.NewReasonService(reasonRepo)
reasonController := controller.NewReasonController(reasonService) reasonController := controller.NewReasonController(reasonService)
themeController := controller_backyard.NewThemeController() themeController := controller_backyard.NewThemeController()
siteInfoService := service.NewSiteInfoService(siteInfoRepo, emailService) siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, emailService)
siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService) siteInfoController := controller_backyard.NewSiteInfoController(siteInfoService)
siteinfoController := controller.NewSiteinfoController(siteInfoService) siteinfoController := controller.NewSiteinfoController(siteInfoCommonService)
notificationRepo := notification.NewNotificationRepo(dataData) notificationRepo := notification.NewNotificationRepo(dataData)
notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService) notificationCommon := notificationcommon.NewNotificationCommon(dataData, notificationRepo, userCommon, activityRepo, followRepo, objService)
notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon) notificationService := notification2.NewNotificationService(dataData, notificationRepo, notificationCommon)
notificationController := controller.NewNotificationController(notificationService) notificationController := controller.NewNotificationController(notificationService)
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 := controller.NewDashboardController(dashboardService)
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)
swaggerRouter := router.NewSwaggerRouter(swaggerConf) swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter() uiRouter := router.NewUIRouter()
authUserMiddleware := middleware.NewAuthUserMiddleware(authService) authUserMiddleware := middleware.NewAuthUserMiddleware(authService)

View File

@ -17,4 +17,4 @@ swaggerui:
service_config: service_config:
secret_key: "answer" secret_key: "answer"
web_host: "http://127.0.0.1:9080" web_host: "http://127.0.0.1:9080"
upload_path: "/data/upfiles" upload_path: "/data/uploads"

View File

@ -62,12 +62,6 @@ const docTemplate = `{
"description": "answer id or question title", "description": "answer id or question title",
"name": "query", "name": "query",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "question id",
"name": "question_id",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -119,6 +113,34 @@ const docTemplate = `{
} }
} }
}, },
"/answer/admin/api/dashboard": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "DashboardInfo",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "DashboardInfo",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/language/options": { "/answer/admin/api/language/options": {
"get": { "get": {
"security": [ "security": [
@ -487,14 +509,14 @@ const docTemplate = `{
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Get siteinfo general", "description": "get site general information",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"admin" "admin"
], ],
"summary": "Get siteinfo general", "summary": "get site general information",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -522,14 +544,14 @@ const docTemplate = `{
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Get siteinfo interface", "description": "update site general information",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"admin" "admin"
], ],
"summary": "Get siteinfo interface", "summary": "update site general information",
"parameters": [ "parameters": [
{ {
"description": "general", "description": "general",
@ -558,25 +580,14 @@ const docTemplate = `{
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Get siteinfo interface", "description": "get site interface",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"admin" "admin"
], ],
"summary": "Get siteinfo interface", "summary": "get site interface",
"parameters": [
{
"description": "general",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.AddCommentReq"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -604,14 +615,14 @@ const docTemplate = `{
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Get siteinfo interface", "description": "update site info interface",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"admin" "admin"
], ],
"summary": "Get siteinfo interface", "summary": "update site info interface",
"parameters": [ "parameters": [
{ {
"description": "general", "description": "general",
@ -2710,14 +2721,14 @@ const docTemplate = `{
}, },
"/answer/api/v1/siteinfo": { "/answer/api/v1/siteinfo": {
"get": { "get": {
"description": "Get siteinfo", "description": "get site info",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"site" "site"
], ],
"summary": "Get siteinfo", "summary": "get site info",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -5281,11 +5292,17 @@ const docTemplate = `{
"schema.SiteGeneralReq": { "schema.SiteGeneralReq": {
"type": "object", "type": "object",
"required": [ "required": [
"contact_email",
"description", "description",
"name", "name",
"short_description" "short_description",
"site_url"
], ],
"properties": { "properties": {
"contact_email": {
"type": "string",
"maxLength": 512
},
"description": { "description": {
"type": "string", "type": "string",
"maxLength": 2000 "maxLength": 2000
@ -5297,17 +5314,27 @@ const docTemplate = `{
"short_description": { "short_description": {
"type": "string", "type": "string",
"maxLength": 255 "maxLength": 255
},
"site_url": {
"type": "string",
"maxLength": 512
} }
} }
}, },
"schema.SiteGeneralResp": { "schema.SiteGeneralResp": {
"type": "object", "type": "object",
"required": [ "required": [
"contact_email",
"description", "description",
"name", "name",
"short_description" "short_description",
"site_url"
], ],
"properties": { "properties": {
"contact_email": {
"type": "string",
"maxLength": 512
},
"description": { "description": {
"type": "string", "type": "string",
"maxLength": 2000 "maxLength": 2000
@ -5319,6 +5346,10 @@ const docTemplate = `{
"short_description": { "short_description": {
"type": "string", "type": "string",
"maxLength": 255 "maxLength": 255
},
"site_url": {
"type": "string",
"maxLength": 512
} }
} }
}, },
@ -5326,7 +5357,8 @@ const docTemplate = `{
"type": "object", "type": "object",
"required": [ "required": [
"language", "language",
"theme" "theme",
"time_zone"
], ],
"properties": { "properties": {
"language": { "language": {
@ -5340,6 +5372,10 @@ const docTemplate = `{
"theme": { "theme": {
"type": "string", "type": "string",
"maxLength": 128 "maxLength": 128
},
"time_zone": {
"type": "string",
"maxLength": 128
} }
} }
}, },
@ -5347,7 +5383,8 @@ const docTemplate = `{
"type": "object", "type": "object",
"required": [ "required": [
"language", "language",
"theme" "theme",
"time_zone"
], ],
"properties": { "properties": {
"language": { "language": {
@ -5361,6 +5398,10 @@ const docTemplate = `{
"theme": { "theme": {
"type": "string", "type": "string",
"maxLength": 128 "maxLength": 128
},
"time_zone": {
"type": "string",
"maxLength": 128
} }
} }
}, },

Binary file not shown.

After

(image error) Size: 13 KiB

Binary file not shown.

After

(image error) Size: 42 KiB

View File

@ -50,12 +50,6 @@
"description": "answer id or question title", "description": "answer id or question title",
"name": "query", "name": "query",
"in": "query" "in": "query"
},
{
"type": "string",
"description": "question id",
"name": "question_id",
"in": "query"
} }
], ],
"responses": { "responses": {
@ -107,6 +101,34 @@
} }
} }
}, },
"/answer/admin/api/dashboard": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "DashboardInfo",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "DashboardInfo",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/language/options": { "/answer/admin/api/language/options": {
"get": { "get": {
"security": [ "security": [
@ -475,14 +497,14 @@
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Get siteinfo general", "description": "get site general information",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"admin" "admin"
], ],
"summary": "Get siteinfo general", "summary": "get site general information",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -510,14 +532,14 @@
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Get siteinfo interface", "description": "update site general information",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"admin" "admin"
], ],
"summary": "Get siteinfo interface", "summary": "update site general information",
"parameters": [ "parameters": [
{ {
"description": "general", "description": "general",
@ -546,25 +568,14 @@
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Get siteinfo interface", "description": "get site interface",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"admin" "admin"
], ],
"summary": "Get siteinfo interface", "summary": "get site interface",
"parameters": [
{
"description": "general",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.AddCommentReq"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -592,14 +603,14 @@
"ApiKeyAuth": [] "ApiKeyAuth": []
} }
], ],
"description": "Get siteinfo interface", "description": "update site info interface",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"admin" "admin"
], ],
"summary": "Get siteinfo interface", "summary": "update site info interface",
"parameters": [ "parameters": [
{ {
"description": "general", "description": "general",
@ -2698,14 +2709,14 @@
}, },
"/answer/api/v1/siteinfo": { "/answer/api/v1/siteinfo": {
"get": { "get": {
"description": "Get siteinfo", "description": "get site info",
"produces": [ "produces": [
"application/json" "application/json"
], ],
"tags": [ "tags": [
"site" "site"
], ],
"summary": "Get siteinfo", "summary": "get site info",
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
@ -5269,11 +5280,17 @@
"schema.SiteGeneralReq": { "schema.SiteGeneralReq": {
"type": "object", "type": "object",
"required": [ "required": [
"contact_email",
"description", "description",
"name", "name",
"short_description" "short_description",
"site_url"
], ],
"properties": { "properties": {
"contact_email": {
"type": "string",
"maxLength": 512
},
"description": { "description": {
"type": "string", "type": "string",
"maxLength": 2000 "maxLength": 2000
@ -5285,17 +5302,27 @@
"short_description": { "short_description": {
"type": "string", "type": "string",
"maxLength": 255 "maxLength": 255
},
"site_url": {
"type": "string",
"maxLength": 512
} }
} }
}, },
"schema.SiteGeneralResp": { "schema.SiteGeneralResp": {
"type": "object", "type": "object",
"required": [ "required": [
"contact_email",
"description", "description",
"name", "name",
"short_description" "short_description",
"site_url"
], ],
"properties": { "properties": {
"contact_email": {
"type": "string",
"maxLength": 512
},
"description": { "description": {
"type": "string", "type": "string",
"maxLength": 2000 "maxLength": 2000
@ -5307,6 +5334,10 @@
"short_description": { "short_description": {
"type": "string", "type": "string",
"maxLength": 255 "maxLength": 255
},
"site_url": {
"type": "string",
"maxLength": 512
} }
} }
}, },
@ -5314,7 +5345,8 @@
"type": "object", "type": "object",
"required": [ "required": [
"language", "language",
"theme" "theme",
"time_zone"
], ],
"properties": { "properties": {
"language": { "language": {
@ -5328,6 +5360,10 @@
"theme": { "theme": {
"type": "string", "type": "string",
"maxLength": 128 "maxLength": 128
},
"time_zone": {
"type": "string",
"maxLength": 128
} }
} }
}, },
@ -5335,7 +5371,8 @@
"type": "object", "type": "object",
"required": [ "required": [
"language", "language",
"theme" "theme",
"time_zone"
], ],
"properties": { "properties": {
"language": { "language": {
@ -5349,6 +5386,10 @@
"theme": { "theme": {
"type": "string", "type": "string",
"maxLength": 128 "maxLength": 128
},
"time_zone": {
"type": "string",
"maxLength": 128
} }
} }
}, },

View File

@ -983,6 +983,9 @@ definitions:
type: object type: object
schema.SiteGeneralReq: schema.SiteGeneralReq:
properties: properties:
contact_email:
maxLength: 512
type: string
description: description:
maxLength: 2000 maxLength: 2000
type: string type: string
@ -992,13 +995,21 @@ definitions:
short_description: short_description:
maxLength: 255 maxLength: 255
type: string type: string
site_url:
maxLength: 512
type: string
required: required:
- contact_email
- description - description
- name - name
- short_description - short_description
- site_url
type: object type: object
schema.SiteGeneralResp: schema.SiteGeneralResp:
properties: properties:
contact_email:
maxLength: 512
type: string
description: description:
maxLength: 2000 maxLength: 2000
type: string type: string
@ -1008,10 +1019,15 @@ definitions:
short_description: short_description:
maxLength: 255 maxLength: 255
type: string type: string
site_url:
maxLength: 512
type: string
required: required:
- contact_email
- description - description
- name - name
- short_description - short_description
- site_url
type: object type: object
schema.SiteInterfaceReq: schema.SiteInterfaceReq:
properties: properties:
@ -1024,9 +1040,13 @@ definitions:
theme: theme:
maxLength: 128 maxLength: 128
type: string type: string
time_zone:
maxLength: 128
type: string
required: required:
- language - language
- theme - theme
- time_zone
type: object type: object
schema.SiteInterfaceResp: schema.SiteInterfaceResp:
properties: properties:
@ -1039,9 +1059,13 @@ definitions:
theme: theme:
maxLength: 128 maxLength: 128
type: string type: string
time_zone:
maxLength: 128
type: string
required: required:
- language - language
- theme - theme
- time_zone
type: object type: object
schema.TagItem: schema.TagItem:
properties: properties:
@ -1394,10 +1418,6 @@ paths:
in: query in: query
name: query name: query
type: string type: string
- description: question id
in: query
name: question_id
type: string
produces: produces:
- application/json - application/json
responses: responses:
@ -1434,6 +1454,23 @@ paths:
summary: AdminSetAnswerStatus summary: AdminSetAnswerStatus
tags: tags:
- admin - admin
/answer/admin/api/dashboard:
get:
consumes:
- application/json
description: DashboardInfo
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: DashboardInfo
tags:
- admin
/answer/admin/api/language/options: /answer/admin/api/language/options:
get: get:
description: Get language options description: Get language options
@ -1662,7 +1699,7 @@ paths:
- admin - admin
/answer/admin/api/siteinfo/general: /answer/admin/api/siteinfo/general:
get: get:
description: Get siteinfo general description: get site general information
produces: produces:
- application/json - application/json
responses: responses:
@ -1677,11 +1714,11 @@ paths:
type: object type: object
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Get siteinfo general summary: get site general information
tags: tags:
- admin - admin
put: put:
description: Get siteinfo interface description: update site general information
parameters: parameters:
- description: general - description: general
in: body in: body
@ -1698,19 +1735,12 @@ paths:
$ref: '#/definitions/handler.RespBody' $ref: '#/definitions/handler.RespBody'
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Get siteinfo interface summary: update site general information
tags: tags:
- admin - admin
/answer/admin/api/siteinfo/interface: /answer/admin/api/siteinfo/interface:
get: get:
description: Get siteinfo interface description: get site interface
parameters:
- description: general
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.AddCommentReq'
produces: produces:
- application/json - application/json
responses: responses:
@ -1725,11 +1755,11 @@ paths:
type: object type: object
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Get siteinfo interface summary: get site interface
tags: tags:
- admin - admin
put: put:
description: Get siteinfo interface description: update site info interface
parameters: parameters:
- description: general - description: general
in: body in: body
@ -1746,7 +1776,7 @@ paths:
$ref: '#/definitions/handler.RespBody' $ref: '#/definitions/handler.RespBody'
security: security:
- ApiKeyAuth: [] - ApiKeyAuth: []
summary: Get siteinfo interface summary: update site info interface
tags: tags:
- admin - admin
/answer/admin/api/theme/options: /answer/admin/api/theme/options:
@ -3014,7 +3044,7 @@ paths:
- Search - Search
/answer/api/v1/siteinfo: /answer/api/v1/siteinfo:
get: get:
description: Get siteinfo description: get site info
produces: produces:
- application/json - application/json
responses: responses:
@ -3027,7 +3057,7 @@ paths:
data: data:
$ref: '#/definitions/schema.SiteGeneralResp' $ref: '#/definitions/schema.SiteGeneralResp'
type: object type: object
summary: Get siteinfo summary: get site info
tags: tags:
- site - site
/answer/api/v1/tag: /answer/api/v1/tag:

5
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/goccy/go-json v0.9.11 github.com/goccy/go-json v0.9.11
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0 github.com/google/wire v0.5.0
github.com/grokify/html-strip-tags-go v0.0.1
github.com/jinzhu/copier v0.3.5 github.com/jinzhu/copier v0.3.5
github.com/jinzhu/now v1.1.5 github.com/jinzhu/now v1.1.5
github.com/lib/pq v1.10.7 github.com/lib/pq v1.10.7
@ -24,7 +25,7 @@ require (
github.com/segmentfault/pacman v1.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/cache/memory v0.0.0-20221018072427-a15dd1434e05
github.com/segmentfault/pacman/contrib/conf/viper 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-20221018072427-a15dd1434e05 github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 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/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05
github.com/spf13/cobra v1.6.1 github.com/spf13/cobra v1.6.1
@ -35,6 +36,7 @@ require (
golang.org/x/crypto v0.1.0 golang.org/x/crypto v0.1.0
golang.org/x/net v0.1.0 golang.org/x/net v0.1.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
xorm.io/builder v0.3.12 xorm.io/builder v0.3.12
xorm.io/core v0.7.3 xorm.io/core v0.7.3
xorm.io/xorm v1.3.2 xorm.io/xorm v1.3.2
@ -110,6 +112,5 @@ require (
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect
) )

6
go.sum
View File

@ -299,6 +299,8 @@ 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.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/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/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
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= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
@ -594,8 +596,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/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 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/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05 h1:gFCY9KUxhYg+/MXNcDYl4ILK+R1SG78FtaSR3JqZNYY= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221018072427-a15dd1434e05/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8= github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
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 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc=
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05/go.mod h1:L4GqtXLoR73obTYqUQIzfkm8NG8pvZafxFb6KZFSSHk= 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= github.com/segmentfault/pacman/contrib/server/http v0.0.0-20221018072427-a15dd1434e05 h1:91is1nKNbfTOl8CvMYiFgg4c5Vmol+5mVmMV/jDXD+A=

File diff suppressed because it is too large Load Diff

6
i18n/i18n.yaml Normal file
View File

@ -0,0 +1,6 @@
# all support language
language_options:
- label: "简体中文(CN)"
value: "zh_CN"
- label: "English(US)"
value: "en_US"

View File

@ -1,170 +1,172 @@
base: # The following fields are used for back-end
success: backend:
other: "Successo" base:
unknown: success:
other: "Errore sconosciuto" other: "Successo"
request_format_error: unknown:
other: "Il formato della richiesta non è valido" other: "Errore sconosciuto"
unauthorized_error: request_format_error:
other: "Non autorizzato" other: "Il formato della richiesta non è valido"
database_error: unauthorized_error:
other: "Errore server dati" other: "Non autorizzato"
database_error:
other: "Errore server dati"
email:
other: "email"
password:
other: "password"
email_or_password_wrong_error: &email_or_password_wrong
other: "Email o password errati"
error:
admin:
email_or_password_wrong: *email_or_password_wrong
answer:
not_found:
other: "Risposta non trovata"
comment:
edit_without_permission:
other: "Non si hanno di privilegi sufficienti per modificare il commento"
not_found:
other: "Commento non trovato"
email: email:
duplicate: other: "email"
other: "email già esistente" password:
need_to_be_verified: other: "password"
other: "email deve essere verificata"
verify_url_expired: email_or_password_wrong_error: &email_or_password_wrong
other: "l'url di verifica email è scaduto, si prega di reinviare la email" other: "Email o password errati"
lang:
not_found: error:
other: "lingua non trovata" admin:
object: email_or_password_wrong: *email_or_password_wrong
captcha_verification_failed: answer:
other: "captcha errato" not_found:
disallow_follow: other: "Risposta non trovata"
other: "Non sei autorizzato a seguire" comment:
disallow_vote: edit_without_permission:
other: "non sei autorizzato a votare" other: "Non si hanno di privilegi sufficienti per modificare il commento"
disallow_vote_your_self: not_found:
other: "Non puoi votare un tuo post!" other: "Commento non trovato"
not_found: email:
other: "oggetto non trovato" duplicate:
verification_failed: other: "email già esistente"
other: "verifica fallita" need_to_be_verified:
email_or_password_incorrect: other: "email deve essere verificata"
other: "email o password incorretti" verify_url_expired:
old_password_verification_failed: other: "l'url di verifica email è scaduto, si prega di reinviare la email"
other: "la verifica della vecchia password è fallita" lang:
new_password_same_as_previous_setting: not_found:
other: "La nuova password è identica alla precedente" other: "lingua non trovata"
question: object:
not_found: captcha_verification_failed:
other: "domanda non trovata" other: "captcha errato"
rank: disallow_follow:
fail_to_meet_the_condition: other: "Non sei autorizzato a seguire"
other: "Condizioni non valide per il grado" disallow_vote:
other: "non sei autorizzato a votare"
disallow_vote_your_self:
other: "Non puoi votare un tuo post!"
not_found:
other: "oggetto non trovato"
verification_failed:
other: "verifica fallita"
email_or_password_incorrect:
other: "email o password incorretti"
old_password_verification_failed:
other: "la verifica della vecchia password è fallita"
new_password_same_as_previous_setting:
other: "La nuova password è identica alla precedente"
question:
not_found:
other: "domanda non trovata"
rank:
fail_to_meet_the_condition:
other: "Condizioni non valide per il grado"
report:
handle_failed:
other: "Gestione del report fallita"
not_found:
other: "Report non trovato"
tag:
not_found:
other: "Etichetta non trovata"
theme:
not_found:
other: "tema non trovato"
user:
email_or_password_wrong:
other: *email_or_password_wrong
not_found:
other: "utente non trovato"
suspended:
other: "utente sospeso"
username_invalid:
other: "utente non valido"
username_duplicate:
other: "utente già in uso"
report: report:
handle_failed: spam:
other: "Gestione del report fallita"
not_found:
other: "Report non trovato"
tag:
not_found:
other: "Etichetta non trovata"
theme:
not_found:
other: "tema non trovato"
user:
email_or_password_wrong:
other: *email_or_password_wrong
not_found:
other: "utente non trovato"
suspended:
other: "utente sospeso"
username_invalid:
other: "utente non valido"
username_duplicate:
other: "utente già in uso"
report:
spam:
name:
other: "spam"
description:
other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente"
rude:
name:
other: "scortese o violento"
description:
other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso"
duplicate:
name:
other: "duplicato"
description:
other: "Questa domanda è già stata posta e ha già una risposta."
not_answer:
name:
other: "non è una risposta"
description:
other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto."
not_need:
name:
other: "non più necessario"
description:
other: "Questo commento è datato, conversazionale o non rilevante a questo articolo."
other:
name:
other: "altro"
description:
other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
question:
close:
duplicate:
name: name:
other: "spam" other: "spam"
description: description:
other: "Questa domanda è già stata chiesta o ha già una risposta." other: "Questo articolo è una pubblicità o vandalismo. Non è utile o rilevante all'argomento corrente"
guideline: rude:
name: name:
other: "motivo legato alla community" other: "scortese o violento"
description: description:
other: "Questa domanda non soddisfa le linee guida della comunità." other: "Una persona ragionevole trova questo contenuto inappropriato a un discorso rispettoso"
multiple: duplicate:
name: name:
other: "richiede maggiori dettagli o chiarezza" other: "duplicato"
description: description:
other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema." other: "Questa domanda è già stata posta e ha già una risposta."
not_answer:
name:
other: "non è una risposta"
description:
other: "Questo è stato postato come una risposta, ma non sta cercando di rispondere alla domanda. Dovrebbe essere una modifica, un commento, un'altra domanda o cancellato del tutto."
not_need:
name:
other: "non più necessario"
description:
other: "Questo commento è datato, conversazionale o non rilevante a questo articolo."
other: other:
name: name:
other: "altro" other: "altro"
description: description:
other: "Questo articolo richiede un'altro motivo non listato sopra." other: "Questo articolo richiede una supervisione dello staff per altre ragioni non listate sopra."
notification: question:
action: close:
update_question: duplicate:
other: "domanda aggiornata" name:
answer_the_question: other: "spam"
other: "domanda risposta" description:
update_answer: other: "Questa domanda è già stata chiesta o ha già una risposta."
other: "risposta aggiornata" guideline:
adopt_answer: name:
other: "risposta accettata" other: "motivo legato alla community"
comment_question: description:
other: "domanda commentata" other: "Questa domanda non soddisfa le linee guida della comunità."
comment_answer: multiple:
other: "risposta commentata" name:
reply_to_you: other: "richiede maggiori dettagli o chiarezza"
other: "hai ricevuto risposta" description:
mention_you: other: "Questa domanda attualmente contiene più domande. Deve concentrarsi solamente su un unico problema."
other: "sei stato menzionato" other:
your_question_is_closed: name:
other: "la tua domanda è stata chiusa" other: "altro"
your_question_was_deleted: description:
other: "la tua domanda è stata rimossa" other: "Questo articolo richiede un'altro motivo non listato sopra."
your_answer_was_deleted:
other: "la tua risposta è stata rimossa" notification:
your_comment_was_deleted: action:
other: "il tuo commento è stato rimosso" update_question:
other: "domanda aggiornata"
answer_the_question:
other: "domanda risposta"
update_answer:
other: "risposta aggiornata"
adopt_answer:
other: "risposta accettata"
comment_question:
other: "domanda commentata"
comment_answer:
other: "risposta commentata"
reply_to_you:
other: "hai ricevuto risposta"
mention_you:
other: "sei stato menzionato"
your_question_is_closed:
other: "la tua domanda è stata chiusa"
your_question_was_deleted:
other: "la tua domanda è stata rimossa"
your_answer_was_deleted:
other: "la tua risposta è stata rimossa"
your_comment_was_deleted:
other: "il tuo commento è stato rimosso"

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,64 @@
package conf package conf
import ( import (
"bytes"
"path/filepath"
"github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/server" "github.com/answerdev/answer/internal/base/server"
"github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/router" "github.com/answerdev/answer/internal/router"
"github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/pkg/writer"
"github.com/segmentfault/pacman/contrib/conf/viper"
"gopkg.in/yaml.v3"
) )
// AllConfig all config // AllConfig all config
type AllConfig struct { type AllConfig struct {
Debug bool `json:"debug" mapstructure:"debug"` Debug bool `json:"debug" mapstructure:"debug" yaml:"debug"`
Data *Data `json:"data" mapstructure:"data"` Server *Server `json:"server" mapstructure:"server" yaml:"server"`
Server *Server `json:"server" mapstructure:"server"` Data *Data `json:"data" mapstructure:"data" yaml:"data"`
I18n *translator.I18n `json:"i18n" mapstructure:"i18n"` I18n *translator.I18n `json:"i18n" mapstructure:"i18n" yaml:"i18n"`
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui"` ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config" yaml:"service_config"`
ServiceConfig *service_config.ServiceConfig `json:"service_config" mapstructure:"service_config"` Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"`
} }
// Server server config // Server server config
type Server struct { type Server struct {
HTTP *server.HTTP `json:"http" mapstructure:"http"` HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"`
} }
// Data data config // Data data config
type Data struct { type Data struct {
Database *data.Database `json:"database" mapstructure:"database"` Database *data.Database `json:"database" mapstructure:"database" yaml:"database"`
Cache *data.CacheConf `json:"cache" mapstructure:"cache"` Cache *data.CacheConf `json:"cache" mapstructure:"cache" yaml:"cache"`
}
// ReadConfig read config
func ReadConfig(configFilePath string) (c *AllConfig, err error) {
if len(configFilePath) == 0 {
configFilePath = filepath.Join(cli.ConfigFileDir, cli.DefaultConfigFileName)
}
c = &AllConfig{}
config, err := viper.NewWithPath(configFilePath)
if err != nil {
return nil, err
}
if err = config.Parse(&c); err != nil {
return nil, err
}
return c, nil
}
// RewriteConfig rewrite config file path
func RewriteConfig(configFilePath string, allConfig *AllConfig) error {
buf := bytes.Buffer{}
enc := yaml.NewEncoder(&buf)
enc.SetIndent(2)
if err := enc.Encode(allConfig); err != nil {
return err
}
return writer.ReplaceFile(configFilePath, buf.String())
} }

View File

@ -27,6 +27,8 @@ const (
// object TagID AnswerList // object TagID AnswerList
// key equal database's table name // key equal database's table name
var ( var (
Version string = ""
ObjectTypeStrMapping = map[string]int{ ObjectTypeStrMapping = map[string]int{
QuestionObjectType: 1, QuestionObjectType: 1,
AnswerObjectType: 2, AnswerObjectType: 2,
@ -47,3 +49,8 @@ var (
8: ReportObjectType, 8: ReportObjectType,
} }
) )
const (
SiteTypeGeneral = "general"
SiteTypeInterface = "interface"
)

View File

@ -2,14 +2,14 @@ package data
// Database database config // Database database config
type Database struct { type Database struct {
Driver string `json:"driver" mapstructure:"driver"` Driver string `json:"driver" mapstructure:"driver" yaml:"driver"`
Connection string `json:"connection" mapstructure:"connection"` Connection string `json:"connection" mapstructure:"connection" yaml:"connection"`
ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time"` ConnMaxLifeTime int `json:"conn_max_life_time" mapstructure:"conn_max_life_time" yaml:"conn_max_life_time,omitempty"`
MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn"` MaxOpenConn int `json:"max_open_conn" mapstructure:"max_open_conn" yaml:"max_open_conn,omitempty"`
MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn"` MaxIdleConn int `json:"max_idle_conn" mapstructure:"max_idle_conn" yaml:"max_idle_conn,omitempty"`
} }
// CacheConf cache // CacheConf cache
type CacheConf struct { type CacheConf struct {
FilePath string `json:"file_path" mapstructure:"file_path"` FilePath string `json:"file_path" mapstructure:"file_path" yaml:"file_path"`
} }

View File

@ -38,4 +38,9 @@ const (
LangNotFound = "error.lang.not_found" LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed" ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found" ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
) )

View File

@ -2,5 +2,5 @@ package translator
// I18n i18n config // I18n i18n config
type I18n struct { type I18n struct {
BundleDir string `json:"bundle_dir" mapstructure:"bundle_dir"` BundleDir string `json:"bundle_dir" mapstructure:"bundle_dir" yaml:"bundle_dir"`
} }

View File

@ -1,17 +1,100 @@
package translator package translator
import ( import (
"fmt"
"os"
"path/filepath"
"github.com/google/wire" "github.com/google/wire"
myTran "github.com/segmentfault/pacman/contrib/i18n" myTran "github.com/segmentfault/pacman/contrib/i18n"
"github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/i18n"
"gopkg.in/yaml.v3"
) )
// ProviderSet is providers. // ProviderSet is providers.
var ProviderSet = wire.NewSet(NewTranslator) var ProviderSet = wire.NewSet(NewTranslator)
var GlobalTrans i18n.Translator var GlobalTrans i18n.Translator
// LangOption language option
type LangOption struct {
Label string `json:"label"`
Value string `json:"value"`
}
// DefaultLangOption default language option. If user config the language is default, the language option is admin choose.
const DefaultLangOption = "Default"
var (
// LanguageOptions language
LanguageOptions []*LangOption
)
// NewTranslator new a translator // NewTranslator new a translator
func NewTranslator(c *I18n) (tr i18n.Translator, err error) { func NewTranslator(c *I18n) (tr i18n.Translator, err error) {
GlobalTrans, err = myTran.NewTranslator(c.BundleDir) entries, err := os.ReadDir(c.BundleDir)
if err != nil {
return nil, err
}
// read the Bundle resources file from entries
for _, file := range entries {
// ignore directory
if file.IsDir() {
continue
}
// ignore non-YAML file
if filepath.Ext(file.Name()) != ".yaml" && file.Name() != "i18n.yaml" {
continue
}
buf, err := os.ReadFile(filepath.Join(c.BundleDir, file.Name()))
if err != nil {
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"`
}{}
if err = yaml.Unmarshal(buf, &translation); err != nil {
return nil, err
}
content, err := yaml.Marshal(translation.Content)
if err != nil {
return nil, fmt.Errorf("marshal translation content failed: %s %s", file.Name(), err)
}
// add translator use backend translation
if err = myTran.AddTranslator(content, file.Name()); err != nil {
return nil, fmt.Errorf("add translator failed: %s %s", file.Name(), err)
}
}
GlobalTrans = myTran.GlobalTrans
i18nFile, err := os.ReadFile(filepath.Join(c.BundleDir, "i18n.yaml"))
if err != nil {
return nil, fmt.Errorf("read i18n file failed: %s", err)
}
s := struct {
LangOption []*LangOption `yaml:"language_options"`
}{}
err = yaml.Unmarshal(i18nFile, &s)
if err != nil {
return nil, fmt.Errorf("i18n file parsing failed: %s", err)
}
LanguageOptions = s.LangOption
return GlobalTrans, err return GlobalTrans, err
} }
// CheckLanguageIsValid check user input language is valid
func CheckLanguageIsValid(lang string) bool {
if lang == DefaultLangOption {
return true
}
for _, option := range LanguageOptions {
if option.Value == lang {
return true
}
}
return false
}

View File

@ -3,6 +3,7 @@ package validator
import ( import (
"errors" "errors"
"reflect" "reflect"
"strings"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator" "github.com/answerdev/answer/internal/base/translator"
@ -97,9 +98,19 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
for _, fieldError := range valErrors { for _, fieldError := range valErrors {
errField = &ErrorField{ errField = &ErrorField{
Key: translator.GlobalTrans.Tr(m.Lang, fieldError.Field()), Key: fieldError.Field(),
Value: fieldError.Translate(m.Tran), Value: fieldError.Translate(m.Tran),
} }
// get original tag name from value for set err field key.
structNamespace := fieldError.StructNamespace()
_, fieldName, found := strings.Cut(structNamespace, ".")
if found {
originalTag := getObjectTagByFieldName(value, fieldName)
if len(originalTag) > 0 {
errField.Key = originalTag
}
}
return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(fieldError.Translate(m.Tran)) return errField, myErrors.BadRequest(reason.RequestFormatError).WithMsg(fieldError.Translate(m.Tran))
} }
} }
@ -117,3 +128,24 @@ func (m *MyValidator) Check(value interface{}) (errField *ErrorField, err error)
type Checker interface { type Checker interface {
Check() (errField *ErrorField, err error) Check() (errField *ErrorField, err error)
} }
func getObjectTagByFieldName(obj interface{}, fieldName string) (tag string) {
defer func() {
if err := recover(); err != nil {
log.Error(err)
}
}()
objT := reflect.TypeOf(obj)
objT = objT.Elem()
structField, exists := objT.FieldByName(fieldName)
if !exists {
return ""
}
tag = structField.Tag.Get("json")
if len(tag) == 0 {
return structField.Tag.Get("form")
}
return tag
}

View File

@ -1,59 +1,71 @@
package cli package cli
import ( import (
"bufio"
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"github.com/answerdev/answer/configs" "github.com/answerdev/answer/configs"
"github.com/answerdev/answer/i18n" "github.com/answerdev/answer/i18n"
"github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/dir"
"github.com/answerdev/answer/pkg/writer"
) )
const ( const (
DefaultConfigFileName = "config.yaml" DefaultConfigFileName = "config.yaml"
DefaultCacheFileName = "cache.db"
) )
var ( var (
ConfigFilePath = "/conf/" ConfigFileDir = "/conf/"
UploadFilePath = "/upfiles/" UploadFilePath = "/uploads/"
I18nPath = "/i18n/" I18nPath = "/i18n/"
CacheDir = "/cache/"
) )
// GetConfigFilePath get config file path
func GetConfigFilePath() string {
return filepath.Join(ConfigFileDir, DefaultConfigFileName)
}
func FormatAllPath(dataDirPath string) {
ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir)
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
I18nPath = filepath.Join(dataDirPath, I18nPath)
CacheDir = filepath.Join(dataDirPath, CacheDir)
}
// InstallAllInitialEnvironment install all initial environment // InstallAllInitialEnvironment install all initial environment
func InstallAllInitialEnvironment(dataDirPath string) { func InstallAllInitialEnvironment(dataDirPath string) {
ConfigFilePath = filepath.Join(dataDirPath, ConfigFilePath) FormatAllPath(dataDirPath)
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
I18nPath = filepath.Join(dataDirPath, I18nPath)
installConfigFile()
installUploadDir() installUploadDir()
installI18nBundle() installI18nBundle()
fmt.Println("install all initial environment done") fmt.Println("install all initial environment done")
} }
func installConfigFile() { func InstallConfigFile(configFilePath string) error {
fmt.Println("[config-file] try to install...") if len(configFilePath) == 0 {
defaultConfigFile := filepath.Join(ConfigFilePath, DefaultConfigFileName) configFilePath = filepath.Join(ConfigFileDir, DefaultConfigFileName)
}
fmt.Println("[config-file] try to create at ", configFilePath)
// if config file already exists do nothing. // if config file already exists do nothing.
if CheckConfigFile(defaultConfigFile) { if CheckConfigFile(configFilePath) {
fmt.Printf("[config-file] %s already exists\n", defaultConfigFile) fmt.Printf("[config-file] %s already exists\n", configFilePath)
return return nil
} }
if err := dir.CreateDirIfNotExist(ConfigFilePath); err != nil { if err := dir.CreateDirIfNotExist(ConfigFileDir); err != nil {
fmt.Printf("[config-file] create directory fail %s\n", err.Error()) fmt.Printf("[config-file] create directory fail %s\n", err.Error())
return return fmt.Errorf("create directory fail %s", err.Error())
} }
fmt.Printf("[config-file] create directory success, config file is %s\n", defaultConfigFile) fmt.Printf("[config-file] create directory success, config file is %s\n", configFilePath)
if err := writerFile(defaultConfigFile, string(configs.Config)); err != nil { if err := writer.WriteFile(configFilePath, string(configs.Config)); err != nil {
fmt.Printf("[config-file] install fail %s\n", err.Error()) fmt.Printf("[config-file] install fail %s\n", err.Error())
return return fmt.Errorf("write file failed %s", err)
} }
fmt.Printf("[config-file] install success\n") fmt.Printf("[config-file] install success\n")
return nil
} }
func installUploadDir() { func installUploadDir() {
@ -85,7 +97,7 @@ func installI18nBundle() {
continue continue
} }
fmt.Printf("[i18n] install %s bundle...\n", item.Name()) fmt.Printf("[i18n] install %s bundle...\n", item.Name())
err = writerFile(path, string(content)) err = writer.WriteFile(path, string(content))
if err != nil { if err != nil {
fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error()) fmt.Printf("[i18n] install %s bundle fail: %s\n", item.Name(), err.Error())
} else { } else {
@ -93,21 +105,3 @@ func installI18nBundle() {
} }
} }
} }
func writerFile(filePath, content string) error {
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return err
}
defer func() {
_ = file.Close()
}()
writer := bufio.NewWriter(file)
if _, err := writer.WriteString(content); err != nil {
return err
}
if err := writer.Flush(); err != nil {
return err
}
return nil
}

View File

@ -1,7 +1,10 @@
package cli package cli
import ( import (
"fmt"
"github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/dir"
) )
@ -13,12 +16,40 @@ func CheckUploadDir() bool {
return dir.CheckDirExist(UploadFilePath) return dir.CheckDirExist(UploadFilePath)
} }
func CheckDB(dataConf *data.Database) bool { // CheckDBConnection check database whether the connection is normal
func CheckDBConnection(dataConf *data.Database) bool {
db, err := data.NewDB(false, dataConf) db, err := data.NewDB(false, dataConf)
if err != nil { if err != nil {
fmt.Printf("connection database failed: %s\n", err)
return false return false
} }
if err = db.Ping(); err != nil { if err = db.Ping(); err != nil {
fmt.Printf("connection ping database failed: %s\n", err)
return false
}
return true
}
// CheckDBTableExist check database whether the table is already exists
func CheckDBTableExist(dataConf *data.Database) bool {
db, err := data.NewDB(false, dataConf)
if err != nil {
fmt.Printf("connection database failed: %s\n", err)
return false
}
if err = db.Ping(); err != nil {
fmt.Printf("connection ping database failed: %s\n", err)
return false
}
exist, err := db.IsTableExist(&entity.Version{})
if err != nil {
fmt.Printf("check table exist failed: %s\n", err)
return false
}
if !exist {
fmt.Printf("check table not exist\n")
return false return false
} }
return true return true

View File

@ -9,6 +9,7 @@ import (
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service" "github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/dashboard"
"github.com/answerdev/answer/internal/service/rank" "github.com/answerdev/answer/internal/service/rank"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/errors"
@ -16,13 +17,21 @@ import (
// AnswerController answer controller // AnswerController answer controller
type AnswerController struct { type AnswerController struct {
answerService *service.AnswerService answerService *service.AnswerService
rankService *rank.RankService rankService *rank.RankService
dashboardService *dashboard.DashboardService
} }
// NewAnswerController new controller // NewAnswerController new controller
func NewAnswerController(answerService *service.AnswerService, rankService *rank.RankService) *AnswerController { func NewAnswerController(answerService *service.AnswerService,
return &AnswerController{answerService: answerService, rankService: rankService} rankService *rank.RankService,
dashboardService *dashboard.DashboardService,
) *AnswerController {
return &AnswerController{
answerService: answerService,
rankService: rankService,
dashboardService: dashboardService,
}
} }
// RemoveAnswer delete answer // RemoveAnswer delete answer

View File

@ -20,4 +20,5 @@ var ProviderSetController = wire.NewSet(
NewReasonController, NewReasonController,
NewNotificationController, NewNotificationController,
NewSiteinfoController, NewSiteinfoController,
NewDashboardController,
) )

View File

@ -0,0 +1 @@
package controller

View File

@ -0,0 +1,36 @@
package controller
import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/service/dashboard"
"github.com/gin-gonic/gin"
)
type DashboardController struct {
dashboardService *dashboard.DashboardService
}
// NewDashboardController new controller
func NewDashboardController(
dashboardService *dashboard.DashboardService,
) *DashboardController {
return &DashboardController{
dashboardService: dashboardService,
}
}
// DashboardInfo godoc
// @Summary DashboardInfo
// @Description DashboardInfo
// @Tags admin
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Router /answer/admin/api/dashboard [get]
// @Success 200 {object} handler.RespBody
func (ac *DashboardController) DashboardInfo(ctx *gin.Context) {
info, err := ac.dashboardService.StatisticalByCache(ctx)
handler.HandleResponse(ctx, err, gin.H{
"info": info,
})
}

View File

@ -4,18 +4,20 @@ import (
"encoding/json" "encoding/json"
"github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/i18n" "github.com/segmentfault/pacman/i18n"
) )
type LangController struct { type LangController struct {
translator i18n.Translator translator i18n.Translator
siteInfoService *siteinfo_common.SiteInfoCommonService
} }
// NewLangController new language controller. // NewLangController new language controller.
func NewLangController(tr i18n.Translator) *LangController { func NewLangController(tr i18n.Translator, siteInfoService *siteinfo_common.SiteInfoCommonService) *LangController {
return &LangController{translator: tr} return &LangController{translator: tr, siteInfoService: siteInfoService}
} }
// GetLangMapping get language config mapping // GetLangMapping get language config mapping
@ -33,15 +35,38 @@ func (u *LangController) GetLangMapping(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, resp) handler.HandleResponse(ctx, nil, resp)
} }
// GetLangOptions Get language options // GetAdminLangOptions Get language options
// @Summary Get language options // @Summary Get language options
// @Description Get language options // @Description Get language options
// @Security ApiKeyAuth
// @Tags Lang // @Tags Lang
// @Produce json // @Produce json
// @Success 200 {object} handler.RespBody{} // @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/language/options [get] // @Router /answer/api/v1/language/options [get]
// @Router /answer/admin/api/language/options [get] // @Router /answer/admin/api/language/options [get]
func (u *LangController) GetLangOptions(ctx *gin.Context) { func (u *LangController) GetAdminLangOptions(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, schema.GetLangOptions) handler.HandleResponse(ctx, nil, translator.LanguageOptions)
}
// GetUserLangOptions Get language options
// @Summary Get language options
// @Description Get language options
// @Tags Lang
// @Produce json
// @Success 200 {object} handler.RespBody{}
// @Router /answer/api/v1/language/options [get]
func (u *LangController) GetUserLangOptions(ctx *gin.Context) {
siteInterfaceResp, err := u.siteInfoService.GetSiteInterface(ctx)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
options := translator.LanguageOptions
if len(siteInterfaceResp.Language) > 0 {
defaultOption := []*translator.LangOption{
{Label: translator.DefaultLangOption, Value: translator.DefaultLangOption},
}
options = append(defaultOption, options...)
}
handler.HandleResponse(ctx, nil, options)
} }

View File

@ -3,45 +3,36 @@ package controller
import ( import (
"github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service" "github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type SiteinfoController struct { type SiteinfoController struct {
siteInfoService *service.SiteInfoService siteInfoService *siteinfo_common.SiteInfoCommonService
} }
// NewSiteinfoController new siteinfo controller. // NewSiteinfoController new siteinfo controller.
func NewSiteinfoController(siteInfoService *service.SiteInfoService) *SiteinfoController { func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonService) *SiteinfoController {
return &SiteinfoController{ return &SiteinfoController{
siteInfoService: siteInfoService, siteInfoService: siteInfoService,
} }
} }
// GetInfo godoc // GetSiteInfo get site info
// @Summary Get siteinfo // @Summary get site info
// @Description Get siteinfo // @Description get site info
// @Tags site // @Tags site
// @Produce json // @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp} // @Success 200 {object} handler.RespBody{data=schema.SiteGeneralResp}
// @Router /answer/api/v1/siteinfo [get] // @Router /answer/api/v1/siteinfo [get]
func (sc *SiteinfoController) GetInfo(ctx *gin.Context) { func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
var ( var err error
resp = &schema.SiteInfoResp{} resp := &schema.SiteInfoResp{}
general schema.SiteGeneralResp resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
face schema.SiteInterfaceResp
err error
)
general, err = sc.siteInfoService.GetSiteGeneral(ctx)
resp.General = &general
if err != nil { if err != nil {
handler.HandleResponse(ctx, err, resp) handler.HandleResponse(ctx, err, resp)
return return
} }
resp.Face, err = sc.siteInfoService.GetSiteInterface(ctx)
face, err = sc.siteInfoService.GetSiteInterface(ctx)
resp.Face = &face
handler.HandleResponse(ctx, err, resp) handler.HandleResponse(ctx, err, resp)
} }

View File

@ -89,22 +89,6 @@ func (uc *UserController) GetOtherUserInfoByUsername(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp) handler.HandleResponse(ctx, err, resp)
} }
// GetUserStatus get user status info
// @Summary get user status info
// @Description get user status info
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} handler.RespBody{data=schema.GetUserResp}
// @Router /answer/api/v1/user/status [get]
func (uc *UserController) GetUserStatus(ctx *gin.Context) {
userID := middleware.GetLoginUserIDFromContext(ctx)
token := middleware.ExtractToken(ctx)
resp, err := uc.userService.GetUserStatus(ctx, userID, token)
handler.HandleResponse(ctx, err, resp)
}
// UserEmailLogin godoc // UserEmailLogin godoc
// @Summary UserEmailLogin // @Summary UserEmailLogin
// @Description UserEmailLogin // @Description UserEmailLogin
@ -373,6 +357,27 @@ func (uc *UserController) UserUpdateInfo(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil) handler.HandleResponse(ctx, err, nil)
} }
// UserUpdateInterface update user interface config
// @Summary UserUpdateInterface update user interface config
// @Description UserUpdateInterface update user interface config
// @Tags User
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param Authorization header string true "access-token"
// @Param data body schema.UpdateUserInterfaceRequest true "UpdateInfoRequest"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/user/interface [put]
func (uc *UserController) UserUpdateInterface(ctx *gin.Context) {
req := &schema.UpdateUserInterfaceRequest{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserId = middleware.GetLoginUserIDFromContext(ctx)
err := uc.userService.UserUpdateInterface(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// UploadUserAvatar godoc // UploadUserAvatar godoc
// @Summary UserUpdateInfo // @Summary UserUpdateInfo
// @Description UserUpdateInfo // @Description UserUpdateInfo
@ -490,6 +495,10 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
req.UserID = middleware.GetLoginUserIDFromContext(ctx) req.UserID = middleware.GetLoginUserIDFromContext(ctx)
// If the user is not logged in, the api cannot be used. // If the user is not logged in, the api cannot be used.
// If the user email is not verified, that also can use this api to modify the email. // If the user email is not verified, that also can use this api to modify the email.
if len(req.UserID) == 0 {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
return
}
captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode) captchaPass := uc.actionService.ActionRecordVerifyCaptcha(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP(), req.CaptchaID, req.CaptchaCode)
if !captchaPass { if !captchaPass {
@ -501,13 +510,15 @@ func (uc *UserController) UserChangeEmailSendCode(ctx *gin.Context) {
handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp) handler.HandleResponse(ctx, errors.BadRequest(reason.CaptchaVerificationFailed), resp)
return return
} }
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
if len(req.UserID) == 0 { resp, err := uc.userService.UserChangeEmailSendCode(ctx, req)
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil) if err != nil {
if resp != nil {
resp.Value = translator.GlobalTrans.Tr(handler.GetLang(ctx), resp.Value)
}
handler.HandleResponse(ctx, err, resp)
return return
} }
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
err := uc.userService.UserChangeEmailSendCode(ctx, req)
handler.HandleResponse(ctx, err, nil) handler.HandleResponse(ctx, err, nil)
} }

View File

@ -3,24 +3,24 @@ package controller_backyard
import ( import (
"github.com/answerdev/answer/internal/base/handler" "github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service" "github.com/answerdev/answer/internal/service/siteinfo"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type SiteInfoController struct { type SiteInfoController struct {
siteInfoService *service.SiteInfoService siteInfoService *siteinfo.SiteInfoService
} }
// NewSiteInfoController new siteinfo controller. // NewSiteInfoController new siteinfo controller.
func NewSiteInfoController(siteInfoService *service.SiteInfoService) *SiteInfoController { func NewSiteInfoController(siteInfoService *siteinfo.SiteInfoService) *SiteInfoController {
return &SiteInfoController{ return &SiteInfoController{
siteInfoService: siteInfoService, siteInfoService: siteInfoService,
} }
} }
// GetGeneral godoc // GetGeneral get site general information
// @Summary Get siteinfo general // @Summary get site general information
// @Description Get siteinfo general // @Description get site general information
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Tags admin // @Tags admin
// @Produce json // @Produce json
@ -31,23 +31,22 @@ func (sc *SiteInfoController) GetGeneral(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp) handler.HandleResponse(ctx, err, resp)
} }
// GetInterface godoc // GetInterface get site interface
// @Summary Get siteinfo interface // @Summary get site interface
// @Description Get siteinfo interface // @Description get site interface
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Tags admin // @Tags admin
// @Produce json // @Produce json
// @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp} // @Success 200 {object} handler.RespBody{data=schema.SiteInterfaceResp}
// @Router /answer/admin/api/siteinfo/interface [get] // @Router /answer/admin/api/siteinfo/interface [get]
// @Param data body schema.AddCommentReq true "general"
func (sc *SiteInfoController) GetInterface(ctx *gin.Context) { func (sc *SiteInfoController) GetInterface(ctx *gin.Context) {
resp, err := sc.siteInfoService.GetSiteInterface(ctx) resp, err := sc.siteInfoService.GetSiteInterface(ctx)
handler.HandleResponse(ctx, err, resp) handler.HandleResponse(ctx, err, resp)
} }
// UpdateGeneral godoc // UpdateGeneral update site general information
// @Summary Get siteinfo interface // @Summary update site general information
// @Description Get siteinfo interface // @Description update site general information
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Tags admin // @Tags admin
// @Produce json // @Produce json
@ -63,9 +62,9 @@ func (sc *SiteInfoController) UpdateGeneral(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil) handler.HandleResponse(ctx, err, nil)
} }
// UpdateInterface godoc // UpdateInterface update site interface
// @Summary Get siteinfo interface // @Summary update site info interface
// @Description Get siteinfo interface // @Description update site info interface
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Tags admin // @Tags admin
// @Produce json // @Produce json

View File

@ -45,6 +45,7 @@ type User struct {
Location string `xorm:"not null default '' VARCHAR(100) location"` Location string `xorm:"not null default '' VARCHAR(100) location"`
IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"` IPInfo string `xorm:"not null default '' VARCHAR(255) ip_info"`
IsAdmin bool `xorm:"not null default false BOOL is_admin"` IsAdmin bool `xorm:"not null default false BOOL is_admin"`
Language string `xorm:"not null default '' VARCHAR(100) language"`
} }
// TableName user table name // TableName user table name

View File

@ -0,0 +1,191 @@
package install
import (
"os"
"path/filepath"
"time"
"github.com/answerdev/answer/configs"
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/migrations"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// LangOptions get installation language options
// @Summary get installation language options
// @Description get installation language options
// @Tags Lang
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]*translator.LangOption}
// @Router /installation/language/options [get]
func LangOptions(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
}
// CheckConfigFile check config file if exist when installation
// @Summary check config file if exist when installation
// @Description check config file if exist when installation
// @Tags installation
// @Accept json
// @Produce json
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
// @Router /installation/config-file/check [post]
func CheckConfigFile(ctx *gin.Context) {
resp := &CheckConfigFileResp{}
resp.ConfigFileExist = cli.CheckConfigFile(confPath)
if !resp.ConfigFileExist {
handler.HandleResponse(ctx, nil, resp)
return
}
allConfig, err := conf.ReadConfig(confPath)
if err != nil {
log.Error(err)
err = errors.BadRequest(reason.ReadConfigFailed)
handler.HandleResponse(ctx, err, nil)
return
}
resp.DBConnectionSuccess = cli.CheckDBConnection(allConfig.Data.Database)
if resp.DBConnectionSuccess {
resp.DbTableExist = cli.CheckDBTableExist(allConfig.Data.Database)
}
handler.HandleResponse(ctx, nil, resp)
}
// CheckDatabase check database if exist when installation
// @Summary check database if exist when installation
// @Description check database if exist when installation
// @Tags installation
// @Accept json
// @Produce json
// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq"
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
// @Router /installation/db/check [post]
func CheckDatabase(ctx *gin.Context) {
req := &CheckDatabaseReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp := &CheckDatabaseResp{}
dataConf := &data.Database{
Driver: req.DbType,
Connection: req.GetConnection(),
}
resp.ConnectionSuccess = cli.CheckDBConnection(dataConf)
if !resp.ConnectionSuccess {
handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert)
return
}
handler.HandleResponse(ctx, nil, resp)
}
// InitEnvironment init environment
// @Summary init environment
// @Description init environment
// @Tags installation
// @Accept json
// @Produce json
// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq"
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
// @Router /installation/init [post]
func InitEnvironment(ctx *gin.Context) {
req := &CheckDatabaseReq{}
if handler.BindAndCheck(ctx, req) {
return
}
// check config file if exist
if cli.CheckConfigFile(confPath) {
log.Debug("config file already exists")
handler.HandleResponse(ctx, nil, nil)
return
}
if err := cli.InstallConfigFile(confPath); err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{
Success: false,
CreateConfigFailed: true,
DefaultConfig: string(configs.Config),
ErrType: schema.ErrTypeAlert.ErrType,
})
return
}
c, err := conf.ReadConfig(confPath)
if err != nil {
log.Errorf("read config failed %s", err)
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
return
}
c.Data.Database.Driver = req.DbType
c.Data.Database.Connection = req.GetConnection()
c.Data.Cache.FilePath = filepath.Join(cli.CacheDir, cli.DefaultCacheFileName)
c.I18n.BundleDir = cli.I18nPath
c.ServiceConfig.UploadPath = cli.UploadFilePath
if err := conf.RewriteConfig(confPath, c); err != nil {
log.Errorf("rewrite config failed %s", err)
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
return
}
handler.HandleResponse(ctx, nil, nil)
}
// InitBaseInfo init base info
// @Summary init base info
// @Description init base info
// @Tags installation
// @Accept json
// @Produce json
// @Param data body install.InitBaseInfoReq true "InitBaseInfoReq"
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
// @Router /installation/base-info [post]
func InitBaseInfo(ctx *gin.Context) {
req := &InitBaseInfoReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.FormatSiteUrl()
c, err := conf.ReadConfig(confPath)
if err != nil {
log.Errorf("read config failed %s", err)
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
return
}
if cli.CheckDBTableExist(c.Data.Database) {
log.Warnf("database is already initialized")
handler.HandleResponse(ctx, nil, nil)
return
}
if err := migrations.InitDB(c.Data.Database); err != nil {
log.Error("init database error: ", err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), schema.ErrTypeAlert)
return
}
err = migrations.UpdateInstallInfo(c.Data.Database, req.Language, req.SiteName, req.SiteURL, req.ContactEmail,
req.AdminName, req.AdminPassword, req.AdminEmail)
if err != nil {
log.Error(err)
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), nil)
return
}
handler.HandleResponse(ctx, nil, nil)
go func() {
time.Sleep(1 * time.Second)
os.Exit(0)
}()
return
}

View File

@ -0,0 +1,32 @@
package install
import (
"fmt"
"os"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/cli"
)
var (
port = os.Getenv("INSTALL_PORT")
confPath = ""
)
func Run(configPath string) {
confPath = configPath
// initialize translator for return internationalization error when installing.
_, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath})
if err != nil {
panic(err)
}
installServer := NewInstallHTTPServer()
if len(port) == 0 {
port = "80"
}
fmt.Printf("[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \n", port)
if err = installServer.Run(":" + port); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,91 @@
package install
import (
"fmt"
"net/url"
"strings"
"xorm.io/xorm/schemas"
)
// CheckConfigFileResp check config file if exist or not response
type CheckConfigFileResp struct {
ConfigFileExist bool `json:"config_file_exist"`
DBConnectionSuccess bool `json:"db_connection_success"`
DbTableExist bool `json:"db_table_exist"`
}
// CheckDatabaseReq check database
type CheckDatabaseReq struct {
DbType string `validate:"required,oneof=postgres sqlite3 mysql" json:"db_type"`
DbUsername string `json:"db_username"`
DbPassword string `json:"db_password"`
DbHost string `json:"db_host"`
DbName string `json:"db_name"`
DbFile string `json:"db_file"`
}
// GetConnection get connection string
func (r *CheckDatabaseReq) GetConnection() string {
if r.DbType == string(schemas.SQLITE) {
return r.DbFile
}
if r.DbType == string(schemas.MYSQL) {
return fmt.Sprintf("%s:%s@tcp(%s)/%s",
r.DbUsername, r.DbPassword, r.DbHost, r.DbName)
}
if r.DbType == string(schemas.POSTGRES) {
host, port := parsePgSQLHostPort(r.DbHost)
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, r.DbUsername, r.DbPassword, r.DbName)
}
return ""
}
func parsePgSQLHostPort(dbHost string) (host string, port string) {
if strings.Contains(dbHost, ":") {
idx := strings.LastIndex(dbHost, ":")
host, port = dbHost[:idx], dbHost[idx+1:]
} else if len(dbHost) > 0 {
host = dbHost
}
if host == "" {
host = "127.0.0.1"
}
if port == "" {
port = "5432"
}
return host, port
}
// CheckDatabaseResp check database response
type CheckDatabaseResp struct {
ConnectionSuccess bool `json:"connection_success"`
}
// InitEnvironmentResp init environment response
type InitEnvironmentResp struct {
Success bool `json:"success"`
CreateConfigFailed bool `json:"create_config_failed"`
DefaultConfig string `json:"default_config"`
ErrType string `json:"err_type"`
}
// InitBaseInfoReq init base info request
type InitBaseInfoReq struct {
Language string `validate:"required,gt=0,lte=30" json:"lang"`
SiteName string `validate:"required,gt=0,lte=30" json:"site_name"`
SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"`
ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"`
AdminName string `validate:"required,gt=4,lte=30" json:"name"`
AdminPassword string `validate:"required,gte=8,lte=32" json:"password"`
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
}
func (r *InitBaseInfoReq) FormatSiteUrl() {
parsedUrl, err := url.Parse(r.SiteURL)
if err != nil {
return
}
r.SiteURL = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host)
}

View File

@ -0,0 +1,64 @@
package install
import (
"embed"
"fmt"
"io/fs"
"net/http"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
)
const UIStaticPath = "build/static"
type _resource struct {
fs embed.FS
}
// Open to implement the interface by http.FS required
func (r *_resource) Open(name string) (fs.File, error) {
name = fmt.Sprintf(UIStaticPath+"/%s", name)
log.Debugf("open static path %s", name)
return r.fs.Open(name)
}
// NewInstallHTTPServer new install http server.
func NewInstallHTTPServer() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
r.StaticFS("/static", http.FS(&_resource{
fs: ui.Build,
}))
installApi := r.Group("")
installApi.GET("/install", WebPage)
installApi.GET("/50x", WebPage)
installApi.GET("/installation/language/options", LangOptions)
installApi.POST("/installation/db/check", CheckDatabase)
installApi.POST("/installation/config-file/check", CheckConfigFile)
installApi.POST("/installation/init", InitEnvironment)
installApi.POST("/installation/base-info", InitBaseInfo)
r.NoRoute(func(ctx *gin.Context) {
ctx.Redirect(http.StatusFound, "/50x")
})
return r
}
func WebPage(c *gin.Context) {
filePath := ""
var file []byte
var err error
filePath = "build/index.html"
c.Header("content-type", "text/html;charset=utf-8")
file, err = ui.Build.ReadFile(filePath)
if err != nil {
log.Error(err)
c.Status(http.StatusNotFound)
return
}
c.String(http.StatusOK, string(file))
}

View File

@ -1,10 +1,12 @@
package migrations package migrations
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"golang.org/x/crypto/bcrypt"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -55,11 +57,6 @@ func InitDB(dataConf *data.Database) (err error) {
return fmt.Errorf("init admin user failed: %s", err) return fmt.Errorf("init admin user failed: %s", err)
} }
err = initSiteInfo(engine)
if err != nil {
return fmt.Errorf("init site info failed: %s", err)
}
err = initConfigTable(engine) err = initConfigTable(engine)
if err != nil { if err != nil {
return fmt.Errorf("init config table: %s", err) return fmt.Errorf("init config table: %s", err)
@ -82,12 +79,79 @@ func initAdminUser(engine *xorm.Engine) error {
return err return err
} }
func initSiteInfo(engine *xorm.Engine) error { func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail string) error {
interfaceData := map[string]string{
"logo": "",
"theme": "black",
"language": language,
}
interfaceDataBytes, _ := json.Marshal(interfaceData)
_, err := engine.InsertOne(&entity.SiteInfo{ _, err := engine.InsertOne(&entity.SiteInfo{
Type: "interface", Type: "interface",
Content: `{"logo":"","theme":"black","language":"en_US"}`, Content: string(interfaceDataBytes),
Status: 1, Status: 1,
}) })
if err != nil {
return err
}
generalData := map[string]string{
"name": siteName,
"site_url": siteURL,
"contact_email": contactEmail,
}
generalDataBytes, _ := json.Marshal(generalData)
_, err = engine.InsertOne(&entity.SiteInfo{
Type: "general",
Content: string(generalDataBytes),
Status: 1,
})
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("")
}
adminPassword = string(generateFromPassword)
// update admin info
_, err = engine.ID("1").Update(&entity.User{
Username: adminName,
Pass: adminPassword,
EMail: adminEmail,
DisplayName: adminName,
})
if err != nil {
return fmt.Errorf("update admin user info failed: %s", err)
}
return nil
}
// UpdateInstallInfo update some init data about the admin interface and admin password
func UpdateInstallInfo(dataConf *data.Database, language string,
siteName string,
siteURL string,
contactEmail string,
adminName string,
adminPassword string,
adminEmail string) error {
engine, err := data.NewDB(false, dataConf)
if err != nil {
return fmt.Errorf("database connection error: %s", err)
}
err = updateAdminInfo(engine, adminName, adminPassword, adminEmail)
if err != nil {
return fmt.Errorf("update admin info failed: %s", err)
}
err = initSiteInfo(engine, language, siteName, siteURL, contactEmail)
if err != nil {
return fmt.Errorf("init site info failed: %s", err)
}
return err return err
} }
@ -125,7 +189,7 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 30, Key: "answer.vote_up", Value: `0`}, {ID: 30, Key: "answer.vote_up", Value: `0`},
{ID: 31, Key: "answer.vote_up_cancel", Value: `0`}, {ID: 31, Key: "answer.vote_up_cancel", Value: `0`},
{ID: 32, Key: "question.follow", Value: `0`}, {ID: 32, Key: "question.follow", Value: `0`},
{ID: 33, Key: "email.config", Value: `{"from_name":"answer","from_email":"answer@answer.com","smtp_host":"smtp.answer.org","smtp_port":465,"smtp_password":"answer","smtp_username":"answer@answer.com","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: 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: 35, Key: "tag.follow", Value: `0`},
{ID: 36, Key: "rank.question.add", Value: `0`}, {ID: 36, Key: "rank.question.add", Value: `0`},
{ID: 37, Key: "rank.question.edit", Value: `0`}, {ID: 37, Key: "rank.question.edit", Value: `0`},

View File

@ -5,7 +5,6 @@ import (
"github.com/answerdev/answer/internal/base/data" "github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm" "xorm.io/xorm"
) )
@ -43,6 +42,7 @@ var noopMigration = func(_ *xorm.Engine) error { return nil }
var migrations = []Migration{ var migrations = []Migration{
// 0->1 // 0->1
NewMigration("this is first version, no operation", noopMigration), NewMigration("this is first version, no operation", noopMigration),
NewMigration("add user language", addUserLanguage),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version
@ -86,17 +86,17 @@ func Migrate(dataConf *data.Database) error {
expectedVersion := ExpectedVersion() expectedVersion := ExpectedVersion()
for currentDBVersion < expectedVersion { for currentDBVersion < expectedVersion {
log.Infof("[migrate] current db version is %d, try to migrate version %d, latest version is %d", fmt.Printf("[migrate] current db version is %d, try to migrate version %d, latest version is %d\n",
currentDBVersion, currentDBVersion+1, expectedVersion) currentDBVersion, currentDBVersion+1, expectedVersion)
migrationFunc := migrations[currentDBVersion] migrationFunc := migrations[currentDBVersion]
log.Infof("[migrate] try to migrate db version %d, description: %s", currentDBVersion+1, migrationFunc.Description()) fmt.Printf("[migrate] try to migrate db version %d, description: %s\n", currentDBVersion+1, migrationFunc.Description())
if err := migrationFunc.Migrate(engine); err != nil { if err := migrationFunc.Migrate(engine); err != nil {
log.Errorf("[migrate] migrate to db version %d failed: ", currentDBVersion+1, err.Error()) fmt.Printf("[migrate] migrate to db version %d failed: %s\n", currentDBVersion+1, err.Error())
return err return err
} }
log.Infof("[migrate] migrate to db version %d success", currentDBVersion+1) fmt.Printf("[migrate] migrate to db version %d success\n", currentDBVersion+1)
if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil { if _, err := engine.Update(&entity.Version{ID: 1, VersionNumber: currentDBVersion + 1}); err != nil {
log.Errorf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error()) fmt.Printf("[migrate] migrate to db version %d, update failed: %s", currentDBVersion+1, err.Error())
return err return err
} }
currentDBVersion++ currentDBVersion++

12
internal/migrations/v1.go Normal file
View File

@ -0,0 +1,12 @@
package migrations
import (
"xorm.io/xorm"
)
func addUserLanguage(x *xorm.Engine) error {
type User struct {
Language string `xorm:"not null default '' VARCHAR(100) language"`
}
return x.Sync(new(User))
}

View File

@ -4,8 +4,10 @@ import (
"context" "context"
"github.com/answerdev/answer/internal/base/data" "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/entity"
"github.com/answerdev/answer/internal/service/activity_common" "github.com/answerdev/answer/internal/service/activity_common"
"github.com/segmentfault/pacman/errors"
) )
// VoteRepo activity repository // VoteRepo activity repository
@ -39,3 +41,12 @@ func (vr *VoteRepo) GetVoteStatus(ctx context.Context, objectID, userID string)
} }
return "" return ""
} }
func (vr *VoteRepo) GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error) {
list := make([]*entity.Activity, 0)
count, err = vr.data.DB.Where("cancelled =0").In("activity_type", activityTypes).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"time" "time"
"unicode" "unicode"
"xorm.io/builder" "xorm.io/builder"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
@ -102,6 +103,16 @@ func (ar *answerRepo) GetAnswer(ctx context.Context, id string) (
return return
} }
// GetQuestionCount
func (ar *answerRepo) GetAnswerCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.Answer, 0)
count, err = ar.data.DB.Where("status = ?", entity.AnswerStatusAvailable).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetAnswerList get answer list all // GetAnswerList get answer list all
func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) { func (ar *answerRepo) GetAnswerList(ctx context.Context, answer *entity.Answer) (answerList []*entity.Answer, err error) {
answerList = make([]*entity.Answer, 0) answerList = make([]*entity.Answer, 0)

View File

@ -79,6 +79,15 @@ func (cr *commentRepo) GetComment(ctx context.Context, commentID string) (
return return
} }
func (cr *commentRepo) GetCommentCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.Comment, 0)
count, err = cr.data.DB.Where("status = ?", entity.CommentStatusAvailable).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetCommentPage get comment page // GetCommentPage get comment page
func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) ( func (cr *commentRepo) GetCommentPage(ctx context.Context, commentQuery *comment.CommentQuery) (
commentList []*entity.Comment, total int64, err error, commentList []*entity.Comment, total int64, err error,

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"time" "time"
"unicode" "unicode"
"xorm.io/builder" "xorm.io/builder"
"github.com/answerdev/answer/internal/base/constant" "github.com/answerdev/answer/internal/base/constant"
@ -162,6 +163,16 @@ func (qr *questionRepo) GetQuestionList(ctx context.Context, question *entity.Qu
return return
} }
func (qr *questionRepo) GetQuestionCount(ctx context.Context) (count int64, err error) {
questionList := make([]*entity.Question, 0)
count, err = qr.data.DB.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusclosed}).FindAndCount(&questionList)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// GetQuestionPage get question page // GetQuestionPage get question page
func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int, question *entity.Question) (questionList []*entity.Question, total int64, err error) { 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) questionList = make([]*entity.Question, 0)

View File

@ -94,3 +94,12 @@ func (ar *reportRepo) UpdateByID(
} }
return return
} }
func (vr *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)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}

View File

@ -3,6 +3,7 @@ package search_common
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/answerdev/answer/pkg/htmltext"
"strings" "strings"
"time" "time"
@ -25,7 +26,7 @@ var (
"`question`.`id`", "`question`.`id`",
"`question`.`id` as `question_id`", "`question`.`id` as `question_id`",
"`title`", "`title`",
"`original_text`", "`parsed_text`",
"`question`.`created_at`", "`question`.`created_at`",
"`user_id`", "`user_id`",
"`vote_count`", "`vote_count`",
@ -38,7 +39,7 @@ var (
"`answer`.`id` as `id`", "`answer`.`id` as `id`",
"`question_id`", "`question_id`",
"`question`.`title` as `title`", "`question`.`title` as `title`",
"`answer`.`original_text` as `original_text`", "`answer`.`parsed_text` as `parsed_text`",
"`answer`.`created_at`", "`answer`.`created_at`",
"`answer`.`user_id` as `user_id`", "`answer`.`user_id` as `user_id`",
"`answer`.`vote_count` as `vote_count`", "`answer`.`vote_count` as `vote_count`",
@ -142,13 +143,22 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagID,
argsA = append(argsA, votes) argsA = append(argsA, votes)
} }
b = b.Union("all", ub) //b = b.Union("all", ub)
ubSQL, _, err := ub.ToSQL()
querySQL, _, err := builder.MySQL().Select("*").From(b, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil { if err != nil {
return return
} }
countSQL, _, err := builder.MySQL().Select("count(*) total").From(b, "c").ToSQL() bSQL, _, err := b.ToSQL()
if err != nil {
return
}
sql := fmt.Sprintf("(%s UNION ALL %s)", ubSQL, bSQL)
querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
if err != nil {
return
}
countSQL, _, err := builder.MySQL().Select("count(*) total").From(sql, "c").ToSQL()
if err != nil { if err != nil {
return return
} }
@ -412,7 +422,7 @@ func (sr *searchRepo) parseResult(ctx context.Context, res []map[string][]byte)
object = schema.SearchObject{ object = schema.SearchObject{
ID: string(r["id"]), ID: string(r["id"]),
Title: string(r["title"]), Title: string(r["title"]),
Excerpt: cutOutParsedText(string(r["original_text"])), Excerpt: htmltext.FetchExcerpt(string(r["parsed_text"]), "...", 240),
CreatedAtParsed: tp.Unix(), CreatedAtParsed: tp.Unix(),
UserInfo: userInfo, UserInfo: userInfo,
Tags: tags, Tags: tags,
@ -443,15 +453,6 @@ func (sr *searchRepo) userBasicInfoFormat(ctx context.Context, dbinfo *entity.Us
} }
} }
func cutOutParsedText(parsedText string) string {
parsedText = strings.TrimSpace(parsedText)
idx := strings.Index(parsedText, "\n")
if idx >= 0 {
parsedText = parsedText[0:idx]
}
return parsedText
}
func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) { func addRelevanceField(searchFields, words, fields []string) (res []string, args []interface{}) {
relevanceRes := []string{} relevanceRes := []string{}
args = []interface{}{} args = []interface{}{}

View File

@ -101,6 +101,14 @@ func (ur *userRepo) UpdateEmail(ctx context.Context, userID, email string) (err
return return
} }
func (ur *userRepo) UpdateLanguage(ctx context.Context, userID, language string) (err error) {
_, err = ur.data.DB.Where("id = ?", userID).Update(&entity.User{Language: language})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}
// UpdateInfo update user info // UpdateInfo update user info
func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) { func (ur *userRepo) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) {
_, err = ur.data.DB.Where("id = ?", userInfo.ID). _, err = ur.data.DB.Where("id = ?", userInfo.ID).
@ -149,3 +157,12 @@ func (ur *userRepo) GetByEmail(ctx context.Context, email string) (userInfo *ent
} }
return return
} }
func (vr *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
list := make([]*entity.User, 0)
count, err = vr.data.DB.Where("mail_status =?", entity.EmailStatusAvailable).And("status =?", entity.UserStatusAvailable).FindAndCount(&list)
if err != nil {
return count, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return
}

View File

@ -27,6 +27,7 @@ type AnswerAPIRouter struct {
siteInfoController *controller_backyard.SiteInfoController siteInfoController *controller_backyard.SiteInfoController
siteinfoController *controller.SiteinfoController siteinfoController *controller.SiteinfoController
notificationController *controller.NotificationController notificationController *controller.NotificationController
dashboardController *controller.DashboardController
} }
func NewAnswerAPIRouter( func NewAnswerAPIRouter(
@ -50,6 +51,7 @@ func NewAnswerAPIRouter(
siteInfoController *controller_backyard.SiteInfoController, siteInfoController *controller_backyard.SiteInfoController,
siteinfoController *controller.SiteinfoController, siteinfoController *controller.SiteinfoController,
notificationController *controller.NotificationController, notificationController *controller.NotificationController,
dashboardController *controller.DashboardController,
) *AnswerAPIRouter { ) *AnswerAPIRouter {
return &AnswerAPIRouter{ return &AnswerAPIRouter{
@ -73,13 +75,14 @@ func NewAnswerAPIRouter(
siteInfoController: siteInfoController, siteInfoController: siteInfoController,
notificationController: notificationController, notificationController: notificationController,
siteinfoController: siteinfoController, siteinfoController: siteinfoController,
dashboardController: dashboardController,
} }
} }
func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// i18n // i18n
r.GET("/language/config", a.langController.GetLangMapping) r.GET("/language/config", a.langController.GetLangMapping)
r.GET("/language/options", a.langController.GetLangOptions) r.GET("/language/options", a.langController.GetUserLangOptions)
// comment // comment
r.GET("/comment/page", a.commentController.GetCommentWithPage) r.GET("/comment/page", a.commentController.GetCommentWithPage)
@ -88,7 +91,6 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
// user // user
r.GET("/user/info", a.userController.GetUserInfoByUserID) r.GET("/user/info", a.userController.GetUserInfoByUserID)
r.GET("/user/status", a.userController.GetUserStatus)
r.GET("/user/action/record", a.userController.ActionRecord) r.GET("/user/action/record", a.userController.ActionRecord)
r.POST("/user/login/email", a.userController.UserEmailLogin) r.POST("/user/login/email", a.userController.UserEmailLogin)
r.POST("/user/register/email", a.userController.UserRegisterByEmail) r.POST("/user/register/email", a.userController.UserRegisterByEmail)
@ -131,7 +133,7 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage)
//siteinfo //siteinfo
r.GET("/siteinfo", a.siteinfoController.GetInfo) r.GET("/siteinfo", a.siteinfoController.GetSiteInfo)
} }
func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) { func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
@ -177,6 +179,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// user // user
r.PUT("/user/password", a.userController.UserModifyPassWord) r.PUT("/user/password", a.userController.UserModifyPassWord)
r.PUT("/user/info", a.userController.UserUpdateInfo) r.PUT("/user/info", a.userController.UserUpdateInfo)
r.PUT("/user/interface", a.userController.UserUpdateInterface)
r.POST("/user/avatar/upload", a.userController.UploadUserAvatar) r.POST("/user/avatar/upload", a.userController.UploadUserAvatar)
r.POST("/user/post/file", a.userController.UploadUserPostFile) r.POST("/user/post/file", a.userController.UploadUserPostFile)
r.POST("/user/notice/set", a.userController.UserNoticeSet) r.POST("/user/notice/set", a.userController.UserNoticeSet)
@ -213,7 +216,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
r.GET("/reasons", a.reasonController.Reasons) r.GET("/reasons", a.reasonController.Reasons)
// language // language
r.GET("/language/options", a.langController.GetLangOptions) r.GET("/language/options", a.langController.GetAdminLangOptions)
// theme // theme
r.GET("/theme/options", a.themeController.GetThemeOptions) r.GET("/theme/options", a.themeController.GetThemeOptions)
@ -225,4 +228,7 @@ func (a *AnswerAPIRouter) RegisterAnswerCmsAPIRouter(r *gin.RouterGroup) {
r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface) r.PUT("/siteinfo/interface", a.siteInfoController.UpdateInterface)
r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig) r.GET("/setting/smtp", a.siteInfoController.GetSMTPConfig)
r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig) r.PUT("/setting/smtp", a.siteInfoController.UpdateSMTPConfig)
//dashboard
r.GET("/dashboard", a.dashboardController.DashboardInfo)
} }

View File

@ -2,8 +2,8 @@ package router
// SwaggerConfig struct describes configure for the Swagger API endpoint // SwaggerConfig struct describes configure for the Swagger API endpoint
type SwaggerConfig struct { type SwaggerConfig struct {
Show bool `json:"show"` Show bool `json:"show" mapstructure:"show" yaml:"show"`
Protocol string `json:"protocol"` Protocol string `json:"protocol" mapstructure:"protocol" yaml:"protocol"`
Host string `json:"host"` Host string `json:"host" mapstructure:"host" yaml:"host"`
Address string `json:"address"` Address string `json:"address" mapstructure:"address" yaml:"address"`
} }

View File

@ -78,6 +78,9 @@ func (a *UIRouter) Register(r *gin.Engine) {
filePath = UIRootFilePath + name filePath = UIRootFilePath + name
case "/manifest.json": case "/manifest.json":
filePath = UIRootFilePath + name filePath = UIRootFilePath + name
case "/install":
c.Redirect(http.StatusFound, "/")
return
default: default:
filePath = UIIndexFilePath filePath = UIIndexFilePath
c.Header("content-type", "text/html;charset=utf-8") c.Header("content-type", "text/html;charset=utf-8")

View File

@ -0,0 +1,38 @@
package schema
import "time"
var AppStartTime time.Time
const (
DashBoardCachekey = "answer@dashboard"
DashBoardCacheTime = 60 * time.Minute
)
type DashboardInfo struct {
QuestionCount int64 `json:"question_count"`
AnswerCount int64 `json:"answer_count"`
CommentCount int64 `json:"comment_count"`
VoteCount int64 `json:"vote_count"`
UserCount int64 `json:"user_count"`
ReportCount int64 `json:"report_count"`
UploadingFiles bool `json:"uploading_files"`
SMTP bool `json:"smtp"`
HTTPS bool `json:"https"`
TimeZone string `json:"time_zone"`
OccupyingStorageSpace string `json:"occupying_storage_space"`
AppStartTime string `json:"app_start_time"`
VersionInfo DashboardInfoVersion `json:"version_info"`
}
type DashboardInfoVersion struct {
Version string `json:"version"`
RemoteVersion string `json:"remote_version"`
}
type RemoteVersion struct {
Release struct {
Version string `json:"version"`
URL string `json:"url"`
} `json:"release"`
}

View File

@ -7,3 +7,5 @@ type ErrTypeData struct {
var ErrTypeModal = ErrTypeData{ErrType: "modal"} var ErrTypeModal = ErrTypeData{ErrType: "modal"}
var ErrTypeToast = ErrTypeData{ErrType: "toast"} var ErrTypeToast = ErrTypeData{ErrType: "toast"}
var ErrTypeAlert = ErrTypeData{ErrType: "alert"}

View File

@ -1,18 +0,0 @@
package schema
// GetLangOption get label option
type GetLangOption struct {
Label string `json:"label"`
Value string `json:"value"`
}
var GetLangOptions = []*GetLangOption{
{
Label: "English(US)",
Value: "en_US",
},
{
Label: "中文(CN)",
Value: "zh_CN",
},
}

View File

@ -1,17 +1,33 @@
package schema package schema
import (
"fmt"
"net/url"
)
// SiteGeneralReq site general request // SiteGeneralReq site general request
type SiteGeneralReq struct { type SiteGeneralReq struct {
Name string `validate:"required,gt=1,lte=128" comment:"site name" form:"name" json:"name"` Name string `validate:"required,gt=1,lte=128" form:"name" json:"name"`
ShortDescription string `validate:"required,gt=3,lte=255" comment:"short site description" form:"short_description" json:"short_description"` ShortDescription string `validate:"required,gt=3,lte=255" form:"short_description" json:"short_description"`
Description string `validate:"required,gt=3,lte=2000" comment:"site description" form:"description" json:"description"` Description string `validate:"required,gt=3,lte=2000" form:"description" json:"description"`
SiteUrl string `validate:"required,gt=1,lte=512,url" form:"site_url" json:"site_url"`
ContactEmail string `validate:"required,gt=1,lte=512,email" form:"contact_email" json:"contact_email"`
}
func (r *SiteGeneralReq) FormatSiteUrl() {
parsedUrl, err := url.Parse(r.SiteUrl)
if err != nil {
return
}
r.SiteUrl = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host)
} }
// SiteInterfaceReq site interface request // SiteInterfaceReq site interface request
type SiteInterfaceReq struct { type SiteInterfaceReq struct {
Logo string `validate:"omitempty,gt=0,lte=256" comment:"logo" form:"logo" json:"logo"` Logo string `validate:"omitempty,gt=0,lte=256" form:"logo" json:"logo"`
Theme string `validate:"required,gt=1,lte=128" comment:"theme" form:"theme" json:"theme"` Theme string `validate:"required,gt=1,lte=128" form:"theme" json:"theme"`
Language string `validate:"required,gt=1,lte=128" comment:"interface language" form:"language" json:"language"` Language string `validate:"required,gt=1,lte=128" form:"language" json:"language"`
TimeZone string `validate:"required,gt=1,lte=128" form:"time_zone" json:"time_zone"`
} }
// SiteGeneralResp site general response // SiteGeneralResp site general response

View File

@ -62,6 +62,8 @@ type GetUserResp struct {
Location string `json:"location"` Location string `json:"location"`
// ip info // ip info
IPInfo string `json:"ip_info"` IPInfo string `json:"ip_info"`
// language
Language string `json:"language"`
// access token // access token
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
// is admin // is admin
@ -305,6 +307,14 @@ func (u *UpdateInfoRequest) Check() (errField *validator.ErrorField, err error)
return nil, nil return nil, nil
} }
// UpdateUserInterfaceRequest update user interface request
type UpdateUserInterfaceRequest struct {
// language
Language string `validate:"required,gt=1,lte=100" json:"language"`
// user id
UserId string `json:"-" `
}
type UserRetrievePassWordRequest struct { type UserRetrievePassWordRequest struct {
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" ` // e_mail
CaptchaID string `json:"captcha_id" ` // captcha_id CaptchaID string `json:"captcha_id" ` // captcha_id

View File

@ -7,4 +7,5 @@ import (
// VoteRepo activity repository // VoteRepo activity repository
type VoteRepo interface { type VoteRepo interface {
GetVoteStatus(ctx context.Context, objectId, userId string) (status string) GetVoteStatus(ctx context.Context, objectId, userId string) (status string)
GetVoteCount(ctx context.Context, activityTypes []int) (count int64, err error)
} }

View File

@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/pkg/htmltext"
) )
type AnswerRepo interface { type AnswerRepo interface {
@ -20,6 +21,7 @@ type AnswerRepo interface {
SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error) SearchList(ctx context.Context, search *entity.AnswerSearch) ([]*entity.Answer, int64, error)
CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error) CmsSearchList(ctx context.Context, search *entity.CmsAnswerSearch) ([]*entity.Answer, int64, error)
UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error) UpdateAnswerStatus(ctx context.Context, answer *entity.Answer) (err error)
GetAnswerCount(ctx context.Context) (count int64, err error)
} }
// AnswerCommon user service // AnswerCommon user service
@ -74,11 +76,11 @@ func (as *AnswerCommon) AdminShowFormat(ctx context.Context, data *entity.Answer
info := schema.AdminAnswerInfo{} info := schema.AdminAnswerInfo{}
info.ID = data.ID info.ID = data.ID
info.QuestionID = data.QuestionID info.QuestionID = data.QuestionID
info.Description = data.ParsedText
info.Adopted = data.Adopted info.Adopted = data.Adopted
info.VoteCount = data.VoteCount info.VoteCount = data.VoteCount
info.CreateTime = data.CreatedAt.Unix() info.CreateTime = data.CreatedAt.Unix()
info.UpdateTime = data.UpdatedAt.Unix() info.UpdateTime = data.UpdatedAt.Unix()
info.UserID = data.UserID info.UserID = data.UserID
info.Description = htmltext.FetchExcerpt(data.ParsedText, "...", 240)
return &info return &info
} }

View File

@ -12,6 +12,7 @@ import (
// CommentCommonRepo comment repository // CommentCommonRepo comment repository
type CommentCommonRepo interface { type CommentCommonRepo interface {
GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error) GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
GetCommentCount(ctx context.Context) (count int64, err error)
} }
// CommentCommonService user service // CommentCommonService user service

View File

@ -0,0 +1,237 @@
package dashboard
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"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/schema"
"github.com/answerdev/answer/internal/service/activity_common"
answercommon "github.com/answerdev/answer/internal/service/answer_common"
"github.com/answerdev/answer/internal/service/comment_common"
"github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/internal/service/export"
questioncommon "github.com/answerdev/answer/internal/service/question_common"
"github.com/answerdev/answer/internal/service/report_common"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/dir"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type DashboardService struct {
questionRepo questioncommon.QuestionRepo
answerRepo answercommon.AnswerRepo
commentRepo comment_common.CommentCommonRepo
voteRepo activity_common.VoteRepo
userRepo usercommon.UserRepo
reportRepo report_common.ReportRepo
configRepo config.ConfigRepo
siteInfoService *siteinfo_common.SiteInfoCommonService
serviceConfig *service_config.ServiceConfig
data *data.Data
}
func NewDashboardService(
questionRepo questioncommon.QuestionRepo,
answerRepo answercommon.AnswerRepo,
commentRepo comment_common.CommentCommonRepo,
voteRepo activity_common.VoteRepo,
userRepo usercommon.UserRepo,
reportRepo report_common.ReportRepo,
configRepo config.ConfigRepo,
siteInfoService *siteinfo_common.SiteInfoCommonService,
serviceConfig *service_config.ServiceConfig,
data *data.Data,
) *DashboardService {
return &DashboardService{
questionRepo: questionRepo,
answerRepo: answerRepo,
commentRepo: commentRepo,
voteRepo: voteRepo,
userRepo: userRepo,
reportRepo: reportRepo,
configRepo: configRepo,
siteInfoService: siteInfoService,
serviceConfig: serviceConfig,
data: data,
}
}
func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.DashboardInfo, error) {
dashboardInfo := &schema.DashboardInfo{}
infoStr, err := ds.data.Cache.GetString(ctx, schema.DashBoardCachekey)
if err != nil {
info, statisticalErr := ds.Statistical(ctx)
if statisticalErr != nil {
return dashboardInfo, err
}
setCacheErr := ds.SetCache(ctx, info)
if setCacheErr != nil {
log.Error("ds.SetCache", setCacheErr)
}
return info, err
}
err = json.Unmarshal([]byte(infoStr), dashboardInfo)
if err != nil {
return dashboardInfo, err
}
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
return dashboardInfo, nil
}
func (ds *DashboardService) SetCache(ctx context.Context, info *schema.DashboardInfo) error {
infoStr, err := json.Marshal(info)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
err = ds.data.Cache.SetString(ctx, schema.DashBoardCachekey, string(infoStr), schema.DashBoardCacheTime)
if err != nil {
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
return nil
}
// Statistical
func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardInfo, error) {
dashboardInfo := &schema.DashboardInfo{}
questionCount, err := ds.questionRepo.GetQuestionCount(ctx)
if err != nil {
return dashboardInfo, err
}
answerCount, err := ds.answerRepo.GetAnswerCount(ctx)
if err != nil {
return dashboardInfo, err
}
commentCount, err := ds.commentRepo.GetCommentCount(ctx)
if err != nil {
return dashboardInfo, err
}
typeKeys := []string{
"question.vote_up",
"question.vote_down",
"answer.vote_up",
"answer.vote_down",
}
var activityTypes []int
for _, typeKey := range typeKeys {
var t int
t, err = ds.configRepo.GetConfigType(typeKey)
if err != nil {
continue
}
activityTypes = append(activityTypes, t)
}
voteCount, err := ds.voteRepo.GetVoteCount(ctx, activityTypes)
if err != nil {
return dashboardInfo, err
}
userCount, err := ds.userRepo.GetUserCount(ctx)
if err != nil {
return dashboardInfo, err
}
reportCount, err := ds.reportRepo.GetReportCount(ctx)
if err != nil {
return dashboardInfo, err
}
siteInfoInterface, err := ds.siteInfoService.GetSiteInterface(ctx)
if err != nil {
return dashboardInfo, err
}
dashboardInfo.QuestionCount = questionCount
dashboardInfo.AnswerCount = answerCount
dashboardInfo.CommentCount = commentCount
dashboardInfo.VoteCount = voteCount
dashboardInfo.UserCount = userCount
dashboardInfo.ReportCount = reportCount
dashboardInfo.UploadingFiles = true
emailconfig, err := ds.GetEmailConfig()
if err != nil {
return dashboardInfo, err
}
if emailconfig.SMTPHost != "" {
dashboardInfo.SMTP = true
}
siteGeneral, err := ds.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
return dashboardInfo, err
}
siteUrl, err := url.Parse(siteGeneral.SiteUrl)
if err != nil {
return dashboardInfo, err
}
if siteUrl.Scheme == "https" {
dashboardInfo.HTTPS = true
}
dirSize, err := dir.DirSize(ds.serviceConfig.UploadPath)
if err != nil {
return dashboardInfo, err
}
size := dir.FormatFileSize(dirSize)
dashboardInfo.OccupyingStorageSpace = size
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
dashboardInfo.TimeZone = siteInfoInterface.TimeZone
dashboardInfo.VersionInfo.Version = constant.Version
dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx)
return dashboardInfo, nil
}
func (ds *DashboardService) RemoteVersion(ctx context.Context) string {
url := "https://answer.dev/getlatest"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("User-Agent", "Answer/"+constant.Version)
resp, err := (&http.Client{}).Do(req)
if err != nil {
log.Error("http.Client error", err)
return ""
}
defer resp.Body.Close()
respByte, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error("http.Client error", err)
return ""
}
remoteVersion := &schema.RemoteVersion{}
err = json.Unmarshal(respByte, remoteVersion)
if err != nil {
log.Error("json.Unmarshal error", err)
return ""
}
return remoteVersion.Release.Version
}
func (ds *DashboardService) GetEmailConfig() (ec *export.EmailConfig, err error) {
emailConf, err := ds.configRepo.GetString("email.config")
if err != nil {
return nil, err
}
ec = &export.EmailConfig{}
err = json.Unmarshal([]byte(emailConf), ec)
if err != nil {
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return ec, nil
}

View File

@ -0,0 +1 @@
package dashboard

View File

@ -8,6 +8,7 @@ import (
collectioncommon "github.com/answerdev/answer/internal/service/collection_common" collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
"github.com/answerdev/answer/internal/service/comment" "github.com/answerdev/answer/internal/service/comment"
"github.com/answerdev/answer/internal/service/comment_common" "github.com/answerdev/answer/internal/service/comment_common"
"github.com/answerdev/answer/internal/service/dashboard"
"github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/follow" "github.com/answerdev/answer/internal/service/follow"
"github.com/answerdev/answer/internal/service/meta" "github.com/answerdev/answer/internal/service/meta"
@ -21,6 +22,8 @@ import (
"github.com/answerdev/answer/internal/service/report_backyard" "github.com/answerdev/answer/internal/service/report_backyard"
"github.com/answerdev/answer/internal/service/report_handle_backyard" "github.com/answerdev/answer/internal/service/report_handle_backyard"
"github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/internal/service/siteinfo"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/internal/service/tag" "github.com/answerdev/answer/internal/service/tag"
tagcommon "github.com/answerdev/answer/internal/service/tag_common" tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/internal/service/uploader" "github.com/answerdev/answer/internal/service/uploader"
@ -61,8 +64,10 @@ var ProviderSetService = wire.NewSet(
report_backyard.NewReportBackyardService, report_backyard.NewReportBackyardService,
user_backyard.NewUserBackyardService, user_backyard.NewUserBackyardService,
reason.NewReasonService, reason.NewReasonService,
NewSiteInfoService, siteinfo_common.NewSiteInfoCommonService,
siteinfo.NewSiteInfoService,
notficationcommon.NewNotificationCommon, notficationcommon.NewNotificationCommon,
notification.NewNotificationService, notification.NewNotificationService,
activity.NewAnswerActivityService, activity.NewAnswerActivityService,
dashboard.NewDashboardService,
) )

View File

@ -38,6 +38,7 @@ type QuestionRepo interface {
UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error) UpdateLastAnswer(ctx context.Context, question *entity.Question) (err error)
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error) FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error) CmsSearchList(ctx context.Context, search *schema.CmsQuestionSearch) ([]*entity.Question, int64, error)
GetQuestionCount(ctx context.Context) (count int64, err error)
} }
// QuestionCommon user service // QuestionCommon user service

View File

@ -2,9 +2,8 @@ package report_backyard
import ( import (
"context" "context"
"strings"
"github.com/answerdev/answer/internal/service/config" "github.com/answerdev/answer/internal/service/config"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
@ -180,20 +179,20 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
case "question": case "question":
r.QuestionID = questionId r.QuestionID = questionId
r.Title = question.Title r.Title = question.Title
r.Excerpt = rs.cutOutTagParsedText(question.OriginalText) r.Excerpt = htmltext.FetchExcerpt(question.ParsedText, "...", 240)
case "answer": case "answer":
r.QuestionID = questionId r.QuestionID = questionId
r.AnswerID = answerId r.AnswerID = answerId
r.Title = question.Title r.Title = question.Title
r.Excerpt = rs.cutOutTagParsedText(answer.OriginalText) r.Excerpt = htmltext.FetchExcerpt(answer.ParsedText, "...", 240)
case "comment": case "comment":
r.QuestionID = questionId r.QuestionID = questionId
r.AnswerID = answerId r.AnswerID = answerId
r.CommentID = commentId r.CommentID = commentId
r.Title = question.Title r.Title = question.Title
r.Excerpt = rs.cutOutTagParsedText(cmt.OriginalText) r.Excerpt = htmltext.FetchExcerpt(cmt.ParsedText, "...", 240)
} }
// parse reason // parse reason
@ -214,12 +213,3 @@ func (rs *ReportBackyardService) parseObject(ctx context.Context, resp *[]*schem
} }
resp = &res resp = &res
} }
func (rs *ReportBackyardService) cutOutTagParsedText(parsedText string) string {
parsedText = strings.TrimSpace(parsedText)
idx := strings.Index(parsedText, "\n")
if idx >= 0 {
parsedText = parsedText[0:idx]
}
return parsedText
}

View File

@ -2,6 +2,7 @@ package report_common
import ( import (
"context" "context"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
) )
@ -12,4 +13,5 @@ type ReportRepo interface {
GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error) GetReportListPage(ctx context.Context, query schema.GetReportListPageDTO) (reports []entity.Report, total int64, err error)
GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error) GetByID(ctx context.Context, id string) (report entity.Report, exist bool, err error)
UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error) UpdateByID(ctx context.Context, id string, handleData entity.Report) (err error)
GetReportCount(ctx context.Context) (count int64, err error)
} }

View File

@ -1,7 +1,6 @@
package service_config package service_config
type ServiceConfig struct { type ServiceConfig struct {
SecretKey string `json:"secret_key" mapstructure:"secret_key"` SecretKey string `json:"secret_key" mapstructure:"secret_key" yaml:"secret_key"`
WebHost string `json:"web_host" mapstructure:"web_host"` UploadPath string `json:"upload_path" mapstructure:"upload_path" yaml:"upload_path"`
UploadPath string `json:"upload_path" mapstructure:"upload_path"`
} }

View File

@ -1,10 +1,12 @@
package service package siteinfo
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/export"
@ -25,41 +27,37 @@ func NewSiteInfoService(siteInfoRepo siteinfo_common.SiteInfoRepo, emailService
} }
} }
func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp schema.SiteGeneralResp, err error) { // GetSiteGeneral get site info general
var ( func (s *SiteInfoService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
siteType = "general" siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
siteInfo *entity.SiteInfo if err != nil {
exist bool return nil, err
) }
resp = schema.SiteGeneralResp{}
siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType)
if !exist { if !exist {
return return nil, errors.BadRequest(reason.SiteInfoNotFound)
} }
_ = json.Unmarshal([]byte(siteInfo.Content), &resp) resp = &schema.SiteGeneralResp{}
return _ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
} }
func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp schema.SiteInterfaceResp, err error) { // GetSiteInterface get site info interface
var ( func (s *SiteInfoService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
siteType = "interface" siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
siteInfo *entity.SiteInfo if err != nil {
exist bool return nil, err
)
resp = schema.SiteInterfaceResp{}
siteInfo, exist, err = s.siteInfoRepo.GetByType(ctx, siteType)
if !exist {
return
} }
if !exist {
_ = json.Unmarshal([]byte(siteInfo.Content), &resp) return nil, errors.BadRequest(reason.SiteInfoNotFound)
return }
resp = &schema.SiteInterfaceResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
} }
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) { func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
req.FormatSiteUrl()
var ( var (
siteType = "general" siteType = "general"
content []byte content []byte
@ -77,10 +75,9 @@ func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGe
func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) { func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.SiteInterfaceReq) (err error) {
var ( var (
siteType = "interface" siteType = "interface"
themeExist, themeExist bool
langExist bool content []byte
content []byte
) )
// check theme // check theme
@ -96,13 +93,7 @@ func (s *SiteInfoService) SaveSiteInterface(ctx context.Context, req schema.Site
} }
// check language // check language
for _, lang := range schema.GetLangOptions { if !translator.CheckLanguageIsValid(req.Language) {
if lang.Value == req.Language {
langExist = true
break
}
}
if !langExist {
err = errors.BadRequest(reason.LangNotFound) err = errors.BadRequest(reason.LangNotFound)
return return
} }

View File

@ -2,6 +2,7 @@ package siteinfo_common
import ( import (
"context" "context"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
) )

View File

@ -0,0 +1,50 @@
package siteinfo_common
import (
"context"
"encoding/json"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/schema"
"github.com/segmentfault/pacman/errors"
)
type SiteInfoCommonService struct {
siteInfoRepo SiteInfoRepo
}
func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) *SiteInfoCommonService {
return &SiteInfoCommonService{
siteInfoRepo: siteInfoRepo,
}
}
// GetSiteGeneral get site info general
func (s *SiteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) {
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeGeneral)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
}
resp = &schema.SiteGeneralResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}
// GetSiteInterface get site info interface
func (s *SiteInfoCommonService) GetSiteInterface(ctx context.Context) (resp *schema.SiteInterfaceResp, err error) {
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeInterface)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.SiteInfoNotFound)
}
resp = &schema.SiteInterfaceResp{}
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
return resp, nil
}

View File

@ -3,9 +3,8 @@ package tag
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"strings"
"github.com/answerdev/answer/internal/service/revision_common" "github.com/answerdev/answer/internal/service/revision_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/internal/base/pager" "github.com/answerdev/answer/internal/base/pager"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
@ -344,12 +343,13 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
resp := make([]*schema.GetTagPageResp, 0) resp := make([]*schema.GetTagPageResp, 0)
for _, tag := range tags { for _, tag := range tags {
excerpt := htmltext.FetchExcerpt(tag.ParsedText, "...", 240)
resp = append(resp, &schema.GetTagPageResp{ resp = append(resp, &schema.GetTagPageResp{
TagID: tag.ID, TagID: tag.ID,
SlugName: tag.SlugName, SlugName: tag.SlugName,
DisplayName: tag.DisplayName, DisplayName: tag.DisplayName,
OriginalText: cutOutTagParsedText(tag.OriginalText), OriginalText: excerpt,
ParsedText: cutOutTagParsedText(tag.ParsedText), ParsedText: excerpt,
FollowCount: tag.FollowCount, FollowCount: tag.FollowCount,
QuestionCount: tag.QuestionCount, QuestionCount: tag.QuestionCount,
IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID), IsFollower: ts.checkTagIsFollow(ctx, req.UserID, tag.ID),
@ -371,12 +371,3 @@ func (ts *TagService) checkTagIsFollow(ctx context.Context, userID, tagID string
} }
return followed return followed
} }
func cutOutTagParsedText(parsedText string) string {
parsedText = strings.TrimSpace(parsedText)
idx := strings.Index(parsedText, "\n")
if idx >= 0 {
parsedText = parsedText[0:idx]
}
return parsedText
}

View File

@ -12,6 +12,7 @@ import (
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/dir" "github.com/answerdev/answer/pkg/dir"
"github.com/answerdev/answer/pkg/uid" "github.com/answerdev/answer/pkg/uid"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
@ -27,11 +28,13 @@ const (
// UploaderService user service // UploaderService user service
type UploaderService struct { type UploaderService struct {
serviceConfig *service_config.ServiceConfig serviceConfig *service_config.ServiceConfig
siteInfoService *siteinfo_common.SiteInfoCommonService
} }
// NewUploaderService new upload service // NewUploaderService new upload service
func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderService { func NewUploaderService(serviceConfig *service_config.ServiceConfig,
siteInfoService *siteinfo_common.SiteInfoCommonService) *UploaderService {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath)) err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, avatarSubPath))
if err != nil { if err != nil {
panic(err) panic(err)
@ -41,7 +44,8 @@ func NewUploaderService(serviceConfig *service_config.ServiceConfig) *UploaderSe
panic(err) panic(err)
} }
return &UploaderService{ return &UploaderService{
serviceConfig: serviceConfig, serviceConfig: serviceConfig,
siteInfoService: siteInfoService,
} }
} }
@ -122,10 +126,14 @@ func (us *UploaderService) UploadPostFile(ctx *gin.Context, file *multipart.File
func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) ( func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
url string, err error) { url string, err error) {
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
return "", err
}
filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath) filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)
if err := ctx.SaveUploadedFile(file, filePath); err != nil { if err := ctx.SaveUploadedFile(file, filePath); err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack() return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
} }
url = fmt.Sprintf("%s/uploads/%s", us.serviceConfig.WebHost, fileSubPath) url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath)
return url, nil return url, nil
} }

View File

@ -15,12 +15,14 @@ type UserRepo interface {
UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error UpdateEmailStatus(ctx context.Context, userID string, emailStatus int) error
UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error UpdateNoticeStatus(ctx context.Context, userID string, noticeStatus int) error
UpdateEmail(ctx context.Context, userID, email string) error UpdateEmail(ctx context.Context, userID, email string) error
UpdateLanguage(ctx context.Context, userID, language string) error
UpdatePass(ctx context.Context, userID, pass string) error UpdatePass(ctx context.Context, userID, pass string) error
UpdateInfo(ctx context.Context, userInfo *entity.User) (err error) UpdateInfo(ctx context.Context, userInfo *entity.User) (err error)
GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error) GetByUserID(ctx context.Context, userID string) (userInfo *entity.User, exist bool, err error)
BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error) BatchGetByID(ctx context.Context, ids []string) ([]*entity.User, error)
GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error) GetByUsername(ctx context.Context, username string) (userInfo *entity.User, exist bool, err error)
GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error) GetByEmail(ctx context.Context, email string) (userInfo *entity.User, exist bool, err error)
GetUserCount(ctx context.Context) (count int64, err error)
} }
// UserCommon user service // UserCommon user service

View File

@ -11,12 +11,14 @@ import (
"github.com/Chain-Zhang/pinyin" "github.com/Chain-Zhang/pinyin"
"github.com/answerdev/answer/internal/base/reason" "github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/entity" "github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema" "github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/activity" "github.com/answerdev/answer/internal/service/activity"
"github.com/answerdev/answer/internal/service/auth" "github.com/answerdev/answer/internal/service/auth"
"github.com/answerdev/answer/internal/service/export" "github.com/answerdev/answer/internal/service/export"
"github.com/answerdev/answer/internal/service/service_config" "github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
usercommon "github.com/answerdev/answer/internal/service/user_common" usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/checker" "github.com/answerdev/answer/pkg/checker"
"github.com/google/uuid" "github.com/google/uuid"
@ -29,11 +31,12 @@ import (
// UserService user service // UserService user service
type UserService struct { type UserService struct {
userRepo usercommon.UserRepo userRepo usercommon.UserRepo
userActivity activity.UserActiveActivityRepo userActivity activity.UserActiveActivityRepo
serviceConfig *service_config.ServiceConfig serviceConfig *service_config.ServiceConfig
emailService *export.EmailService emailService *export.EmailService
authService *auth.AuthService authService *auth.AuthService
siteInfoService *siteinfo_common.SiteInfoCommonService
} }
func NewUserService(userRepo usercommon.UserRepo, func NewUserService(userRepo usercommon.UserRepo,
@ -41,13 +44,15 @@ func NewUserService(userRepo usercommon.UserRepo,
emailService *export.EmailService, emailService *export.EmailService,
authService *auth.AuthService, authService *auth.AuthService,
serviceConfig *service_config.ServiceConfig, serviceConfig *service_config.ServiceConfig,
siteInfoService *siteinfo_common.SiteInfoCommonService,
) *UserService { ) *UserService {
return &UserService{ return &UserService{
userRepo: userRepo, userRepo: userRepo,
userActivity: userActivity, userActivity: userActivity,
emailService: emailService, emailService: emailService,
serviceConfig: serviceConfig, serviceConfig: serviceConfig,
authService: authService, authService: authService,
siteInfoService: siteInfoService,
} }
} }
@ -66,35 +71,6 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
return resp, nil return resp, nil
} }
// GetUserStatus get user info by user id
func (us *UserService) GetUserStatus(ctx context.Context, userID, token string) (resp *schema.GetUserStatusResp, err error) {
resp = &schema.GetUserStatusResp{}
if len(userID) == 0 {
return resp, nil
}
userInfo, exist, err := us.userRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
if !exist {
return nil, errors.BadRequest(reason.UserNotFound)
}
userCacheInfo := &entity.UserCacheInfo{
UserID: userID,
UserStatus: userInfo.Status,
EmailStatus: userInfo.MailStatus,
}
err = us.authService.UpdateUserCacheInfo(ctx, token, userCacheInfo)
if err != nil {
return nil, err
}
resp = &schema.GetUserStatusResp{
Status: schema.UserStatusShow[userInfo.Status],
}
return resp, nil
}
func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) ( func (us *UserService) GetOtherUserInfoByUsername(ctx context.Context, username string) (
resp *schema.GetOtherUserInfoResp, err error, resp *schema.GetOtherUserInfoResp, err error,
) { ) {
@ -168,7 +144,7 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
UserID: userInfo.ID, UserID: userInfo.ID,
} }
code := uuid.NewString() code := uuid.NewString()
verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.serviceConfig.WebHost, code) verifyEmailURL := fmt.Sprintf("%s/users/password-reset?code=%s", us.getSiteUrl(ctx), code)
title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL) title, body, err := us.emailService.PassResetTemplate(ctx, verifyEmailURL)
if err != nil { if err != nil {
return "", err return "", err
@ -283,6 +259,18 @@ func (us *UserService) UserEmailHas(ctx context.Context, email string) (bool, er
return has, nil return has, nil
} }
// UserUpdateInterface update user interface
func (us *UserService) UserUpdateInterface(ctx context.Context, req *schema.UpdateUserInterfaceRequest) (err error) {
if !translator.CheckLanguageIsValid(req.Language) {
return errors.BadRequest(reason.LangNotFound)
}
err = us.userRepo.UpdateLanguage(ctx, req.UserId, req.Language)
if err != nil {
return
}
return nil
}
// UserRegisterByEmail user register // UserRegisterByEmail user register
func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) ( func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo *schema.UserRegisterReq) (
resp *schema.GetUserResp, err error, resp *schema.GetUserResp, err error,
@ -320,7 +308,7 @@ func (us *UserService) UserRegisterByEmail(ctx context.Context, registerUserInfo
UserID: userInfo.ID, UserID: userInfo.ID,
} }
code := uuid.NewString() code := uuid.NewString()
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code)
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
if err != nil { if err != nil {
return nil, err return nil, err
@ -363,7 +351,7 @@ func (us *UserService) UserVerifyEmailSend(ctx context.Context, userID string) e
UserID: userInfo.ID, UserID: userInfo.ID,
} }
code := uuid.NewString() code := uuid.NewString()
verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.serviceConfig.WebHost, code) verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", us.getSiteUrl(ctx), code)
title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL)
if err != nil { if err != nil {
return err return err
@ -489,21 +477,26 @@ func (us *UserService) encryptPassword(ctx context.Context, Pass string) (string
} }
// UserChangeEmailSendCode user change email verification // UserChangeEmailSendCode user change email verification
func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) error { func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.UserChangeEmailSendCodeReq) (
resp *schema.UserVerifyEmailErrorResponse, err error) {
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID) userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
if err != nil { if err != nil {
return err return nil, err
} }
if !exist { if !exist {
return errors.BadRequest(reason.UserNotFound) return nil, errors.BadRequest(reason.UserNotFound)
} }
_, exist, err = us.userRepo.GetByEmail(ctx, req.Email) _, exist, err = us.userRepo.GetByEmail(ctx, req.Email)
if err != nil { if err != nil {
return err return nil, err
} }
if exist { if exist {
return errors.BadRequest(reason.EmailDuplicate) resp = &schema.UserVerifyEmailErrorResponse{
Key: "e_mail",
Value: reason.EmailDuplicate,
}
return resp, errors.BadRequest(reason.EmailDuplicate)
} }
data := &schema.EmailCodeContent{ data := &schema.EmailCodeContent{
@ -512,19 +505,19 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
} }
code := uuid.NewString() code := uuid.NewString()
var title, body string var title, body string
verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.serviceConfig.WebHost, code) verifyEmailURL := fmt.Sprintf("%s/users/confirm-new-email?code=%s", us.getSiteUrl(ctx), code)
if userInfo.MailStatus == entity.EmailStatusToBeVerified { if userInfo.MailStatus == entity.EmailStatusToBeVerified {
title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL) title, body, err = us.emailService.RegisterTemplate(ctx, verifyEmailURL)
} else { } else {
title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL) title, body, err = us.emailService.ChangeEmailTemplate(ctx, verifyEmailURL)
} }
if err != nil { if err != nil {
return err return nil, err
} }
log.Infof("send email confirmation %s", verifyEmailURL) log.Infof("send email confirmation %s", verifyEmailURL)
go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString()) go us.emailService.Send(context.Background(), req.Email, title, body, code, data.ToJSONString())
return nil return nil, nil
} }
// UserChangeEmailVerify user change email verify code // UserChangeEmailVerify user change email verify code
@ -560,3 +553,13 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string
} }
return nil return nil
} }
// getSiteUrl get site url
func (us *UserService) getSiteUrl(ctx context.Context) string {
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Errorf("get site general failed: %s", err)
return ""
}
return siteGeneral.SiteUrl
}

View File

@ -1,6 +1,10 @@
package dir package dir
import "os" import (
"fmt"
"os"
"path/filepath"
)
func CreateDirIfNotExist(path string) error { func CreateDirIfNotExist(path string) error {
return os.MkdirAll(path, os.ModePerm) return os.MkdirAll(path, os.ModePerm)
@ -15,3 +19,32 @@ func CheckFileExist(path string) bool {
f, err := os.Stat(path) f, err := os.Stat(path)
return err == nil && !f.IsDir() return err == nil && !f.IsDir()
} }
func DirSize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if !info.IsDir() {
size += info.Size()
}
return err
})
return size, err
}
func FormatFileSize(fileSize int64) (size string) {
if fileSize < 1024 {
//return strconv.FormatInt(fileSize, 10) + "B"
return fmt.Sprintf("%.2f B", float64(fileSize)/float64(1))
} else if fileSize < (1024 * 1024) {
return fmt.Sprintf("%.2f KB", float64(fileSize)/float64(1024))
} else if fileSize < (1024 * 1024 * 1024) {
return fmt.Sprintf("%.2f MB", float64(fileSize)/float64(1024*1024))
} else if fileSize < (1024 * 1024 * 1024 * 1024) {
return fmt.Sprintf("%.2f GB", float64(fileSize)/float64(1024*1024*1024))
} else if fileSize < (1024 * 1024 * 1024 * 1024 * 1024) {
return fmt.Sprintf("%.2f TB", float64(fileSize)/float64(1024*1024*1024*1024))
} else { //if fileSize < (1024 * 1024 * 1024 * 1024 * 1024 * 1024)
return fmt.Sprintf("%.2f EB", float64(fileSize)/float64(1024*1024*1024*1024*1024))
}
}

60
pkg/htmltext/htmltext.go Normal file
View File

@ -0,0 +1,60 @@
package htmltext
import (
"github.com/grokify/html-strip-tags-go"
"regexp"
"strings"
)
// ClearText clear HTML, get the clear text
func ClearText(html string) (text string) {
if len(html) == 0 {
text = html
return
}
var (
re *regexp.Regexp
codeReg = `(?ism)<(pre)>.*<\/pre>`
codeRepl = "{code...}"
linkReg = `(?ism)<a.*?[^<]>(.*)?<\/a>`
linkRepl = " [$1] "
spaceReg = ` +`
spaceRepl = " "
)
re = regexp.MustCompile(codeReg)
html = re.ReplaceAllString(html, codeRepl)
re = regexp.MustCompile(linkReg)
html = re.ReplaceAllString(html, linkRepl)
text = strings.NewReplacer(
"\n", " ",
"\r", " ",
"\t", " ",
).Replace(strip.StripTags(html))
// replace multiple spaces to one space
re = regexp.MustCompile(spaceReg)
text = strings.TrimSpace(re.ReplaceAllString(text, spaceRepl))
return
}
// FetchExcerpt return the excerpt from the HTML string
func FetchExcerpt(html, trimMarker string, limit int) (text string) {
if len(html) == 0 {
text = html
return
}
text = ClearText(html)
runeText := []rune(text)
if len(runeText) <= limit {
text = string(runeText)
} else {
text = string(runeText[0:limit])
}
text += trimMarker
return
}

View File

@ -0,0 +1,51 @@
package htmltext
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestClearText(t *testing.T) {
var (
expected,
clearedText string
)
// test code clear text
expected = "hello{code...}"
clearedText = ClearText("<p>hello<pre>var a = \"good\"</pre></p>")
assert.Equal(t, expected, clearedText)
// test link clear text
expected = "hello [example.com]"
clearedText = ClearText("<p>hello <a href=\"http://example.com/\">example.com</a></p>")
assert.Equal(t, expected, clearedText)
clearedText = ClearText("<p>hello<a href=\"https://example.com/\">example.com</a></p>")
assert.Equal(t, expected, clearedText)
expected = "hello world"
clearedText = ClearText("<div> hello</div>\n<div>world</div>")
assert.Equal(t, expected, clearedText)
}
func TestFetchExcerpt(t *testing.T) {
var (
expected,
text string
)
// test english string
expected = "hello..."
text = FetchExcerpt("<p>hello world</p>", "...", 5)
assert.Equal(t, expected, text)
// test mixed string
expected = "hello你好..."
text = FetchExcerpt("<p>hello你好world</p>", "...", 7)
assert.Equal(t, expected, text)
// test mixed string with emoticon
expected = "hello你好😂..."
text = FetchExcerpt("<p>hello你好😂world</p>", "...", 8)
assert.Equal(t, expected, text)
}

31
pkg/writer/writer.go Normal file
View File

@ -0,0 +1,31 @@
package writer
import (
"bufio"
"os"
)
// ReplaceFile remove old file and write new file
func ReplaceFile(filePath, content string) error {
_ = os.Remove(filePath)
return WriteFile(filePath, content)
}
// WriteFile write file to path
func WriteFile(filePath, content string) error {
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o666)
if err != nil {
return err
}
defer func() {
_ = file.Close()
}()
writer := bufio.NewWriter(file)
if _, err := writer.WriteString(content); err != nil {
return err
}
if err := writer.Flush(); err != nil {
return err
}
return nil
}

View File

@ -1,3 +1,4 @@
#!/bin/bash #!/bin/bash
/usr/bin/answer init /usr/bin/answer init
/usr/bin/answer run -c /data/conf/config.yaml /usr/bin/answer upgrade
/usr/bin/answer run -C /data/

View File

@ -1,4 +1,5 @@
module.exports = { module.exports = {
root: true,
env: { env: {
browser: true, browser: true,
es2021: true, es2021: true,
@ -19,7 +20,8 @@ module.exports = {
}, },
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
project: './tsconfig.json', tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
}, },
plugins: ['react', '@typescript-eslint'], plugins: ['react', '@typescript-eslint'],
rules: { rules: {
@ -64,7 +66,7 @@ module.exports = {
position: 'before', position: 'before',
}, },
{ {
pattern: '@answer/**', pattern: '@/**',
group: 'internal', group: 'internal',
}, },
{ {

View File

@ -1,3 +1,3 @@
module.exports = { module.exports = {
extends: ['@commitlint/config-conventional'], extends: ['@commitlint/routes-conventional'],
}; };

View File

@ -1,36 +1,51 @@
const path = require('path'); const {
addWebpackModuleRule,
addWebpackAlias
} = require("customize-cra");
const path = require("path");
const i18nPath = path.resolve(__dirname, "../i18n");
module.exports = { module.exports = {
webpack: function (config, env) { webpack: function(config, env) {
if (env === 'production') { if (env === "production") {
config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH; config.output.publicPath = process.env.REACT_APP_PUBLIC_PATH;
} }
config.resolve.alias = {
...config.resolve.alias, addWebpackAlias({
'@': path.resolve(__dirname, 'src'), ["@"]: path.resolve(__dirname, "src"),
'@answer/pages': path.resolve(__dirname, 'src/pages'), "@i18n": i18nPath
'@answer/components': path.resolve(__dirname, 'src/components'), })(config);
'@answer/stores': path.resolve(__dirname, 'src/stores'),
'@answer/hooks': path.resolve(__dirname, 'src/hooks'), addWebpackModuleRule({
'@answer/utils': path.resolve(__dirname, 'src/utils'), test: /\.ya?ml$/,
'@answer/common': path.resolve(__dirname, 'src/common'), use: "yaml-loader"
'@answer/api': path.resolve(__dirname, 'src/services/api'), })(config);
};
// add i18n dir to ModuleScopePlugin allowedPaths
const moduleScopePlugin = config.resolve.plugins.find(_ => _.constructor.name === "ModuleScopePlugin");
if (moduleScopePlugin) {
moduleScopePlugin.allowedPaths.push(i18nPath);
}
return config; return config;
}, },
devServer: function(configFunction) {
devServer: function (configFunction) { return function(proxy, allowedHost) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost); const config = configFunction(proxy, allowedHost);
config.proxy = { config.proxy = {
'/answer': { "/answer": {
target: 'http://10.0.10.98:2060', target: "http://10.0.10.98:2060",
changeOrigin: true, changeOrigin: true,
secure: false, secure: false
}, },
"/installation": {
target: "http://10.0.10.98:2060",
changeOrigin: true,
secure: false
}
}; };
return config; return config;
}; };
}, }
}; };

View File

@ -10,12 +10,12 @@
"build:prod": "env-cmd -f .env.production react-app-rewired build", "build:prod": "env-cmd -f .env.production react-app-rewired build",
"build": "env-cmd -f .env react-app-rewired build", "build": "env-cmd -f .env react-app-rewired build",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-scripts eject",
"lint": "eslint . --cache --fix --ext .ts,.tsx", "lint": "eslint . --cache --fix --ext .ts,.tsx",
"prepare": "cd .. && husky install", "prepare": "cd .. && husky install",
"cz": "cz", "cz": "cz",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
"preinstall": "node ./scripts/preinstall.js"
}, },
"config": { "config": {
"commitizen": { "commitizen": {
@ -101,7 +101,8 @@
"sass": "^1.54.4", "sass": "^1.54.4",
"tsconfig-paths-webpack-plugin": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "*", "typescript": "*",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4",
"yaml-loader": "^0.8.0"
}, },
"packageManager": "pnpm@7.9.5", "packageManager": "pnpm@7.9.5",
"engines": { "engines": {

View File

@ -77,6 +77,7 @@ specifiers:
tsconfig-paths-webpack-plugin: ^4.0.0 tsconfig-paths-webpack-plugin: ^4.0.0
typescript: '*' typescript: '*'
web-vitals: ^2.1.4 web-vitals: ^2.1.4
yaml-loader: ^0.8.0
zustand: ^4.1.1 zustand: ^4.1.1
dependencies: dependencies:
@ -159,6 +160,7 @@ devDependencies:
tsconfig-paths-webpack-plugin: 4.0.0 tsconfig-paths-webpack-plugin: 4.0.0
typescript: 4.8.3 typescript: 4.8.3
web-vitals: 2.1.4 web-vitals: 2.1.4
yaml-loader: 0.8.0
packages: packages:
@ -7040,6 +7042,10 @@ packages:
filelist: 1.0.4 filelist: 1.0.4
minimatch: 3.1.2 minimatch: 3.1.2
/javascript-stringify/2.1.0:
resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==}
dev: true
/jest-changed-files/27.5.1: /jest-changed-files/27.5.1:
resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@ -11682,6 +11688,15 @@ packages:
/yallist/4.0.0: /yallist/4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
/yaml-loader/0.8.0:
resolution: {integrity: sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg==}
engines: {node: '>= 12.13'}
dependencies:
javascript-stringify: 2.1.0
loader-utils: 2.0.2
yaml: 2.1.1
dev: true
/yaml/1.10.2: /yaml/1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'} engines: {node: '>= 6'}

5
ui/scripts/preinstall.js Normal file
View File

@ -0,0 +1,5 @@
// There is a bug when using npm to install: the execution of preinstall is after install, so when this prompt is displayed, the dependent packages have already been installed.
if (!/pnpm/.test(process.env.npm_execpath)) {
console.warn(`\u001b[33mThis repository requires using pnpm as the package manager for scripts to work properly.\u001b[39m\n`)
process.exit(1)
}

View File

@ -1,8 +1,10 @@
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import router from '@/router'; import './i18n/init';
import { routes, createBrowserRouter } from '@/router';
function App() { function App() {
const router = createBrowserRouter(routes);
return <RouterProvider router={router} />; return <RouterProvider router={router} />;
} }

View File

@ -1,9 +1,10 @@
export const LOGIN_NEED_BACK = [ export const DEFAULT_LANG = 'en_US';
'/users/login', export const CURRENT_LANG_STORAGE_KEY = '_a_lang_';
'/users/register', export const LANG_RESOURCE_STORAGE_KEY = '_a_lang_r_';
'/users/account-recovery', export const LOGGED_USER_STORAGE_KEY = '_a_lui_';
'/users/password-reset', export const LOGGED_TOKEN_STORAGE_KEY = '_a_ltk_';
]; export const REDIRECT_PATH_STORAGE_KEY = '_a_rp_';
export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
export const ADMIN_LIST_STATUS = { export const ADMIN_LIST_STATUS = {
// normal; // normal;
@ -56,3 +57,494 @@ export const ADMIN_NAV_MENUS = [
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
}, },
]; ];
export const TIMEZONES = [
{
label: 'Africa',
options: [
{ value: 'Africa/Abidjan', label: 'Abidjan' },
{ value: 'Africa/Accra', label: 'Accra' },
{ value: 'Africa/Addis_Ababa', label: 'Addis Ababa' },
{ value: 'Africa/Algiers', label: 'Algiers' },
{ value: 'Africa/Asmara', label: 'Asmara' },
{ value: 'Africa/Bamako', label: 'Bamako' },
{ value: 'Africa/Bangui', label: 'Bangui' },
{ value: 'Africa/Banjul', label: 'Banjul' },
{ value: 'Africa/Bissau', label: 'Bissau' },
{ value: 'Africa/Blantyre', label: 'Blantyre' },
{ value: 'Africa/Brazzaville', label: 'Brazzaville' },
{ value: 'Africa/Bujumbura', label: 'Bujumbura' },
{ value: 'Africa/Cairo', label: 'Cairo' },
{ value: 'Africa/Casablanca', label: 'Casablanca' },
{ value: 'Africa/Ceuta', label: 'Ceuta' },
{ value: 'Africa/Conakry', label: 'Conakry' },
{ value: 'Africa/Dakar', label: 'Dakar' },
{ value: 'Africa/Dar_es_Salaam', label: 'Dar es Salaam' },
{ value: 'Africa/Djibouti', label: 'Djibouti' },
{ value: 'Africa/Douala', label: 'Douala' },
{ value: 'Africa/El_Aaiun', label: 'El Aaiun' },
{ value: 'Africa/Freetown', label: 'Freetown' },
{ value: 'Africa/Gaborone', label: 'Gaborone' },
{ value: 'Africa/Harare', label: 'Harare' },
{ value: 'Africa/Johannesburg', label: 'Johannesburg' },
{ value: 'Africa/Juba', label: 'Juba' },
{ value: 'Africa/Kampala', label: 'Kampala' },
{ value: 'Africa/Khartoum', label: 'Khartoum' },
{ value: 'Africa/Kigali', label: 'Kigali' },
{ value: 'Africa/Kinshasa', label: 'Kinshasa' },
{ value: 'Africa/Lagos', label: 'Lagos' },
{ value: 'Africa/Libreville', label: 'Libreville' },
{ value: 'Africa/Lome', label: 'Lome' },
{ value: 'Africa/Luanda', label: 'Luanda' },
{ value: 'Africa/Lubumbashi', label: 'Lubumbashi' },
{ value: 'Africa/Lusaka', label: 'Lusaka' },
{ value: 'Africa/Malabo', label: 'Malabo' },
{ value: 'Africa/Maputo', label: 'Maputo' },
{ value: 'Africa/Maseru', label: 'Maseru' },
{ value: 'Africa/Mbabane', label: 'Mbabane' },
{ value: 'Africa/Mogadishu', label: 'Mogadishu' },
{ value: 'Africa/Monrovia', label: 'Monrovia' },
{ value: 'Africa/Nairobi', label: 'Nairobi' },
{ value: 'Africa/Ndjamena', label: 'Ndjamena' },
{ value: 'Africa/Niamey', label: 'Niamey' },
{ value: 'Africa/Nouakchott', label: 'Nouakchott' },
{ value: 'Africa/Ouagadougou', label: 'Ouagadougou' },
{ value: 'Africa/Porto-Novo', label: 'Porto-Novo' },
{ value: 'Africa/Sao_Tome', label: 'Sao Tome' },
{ value: 'Africa/Tripoli', label: 'Tripoli' },
{ value: 'Africa/Tunis', label: 'Tunis' },
{ value: 'Africa/Windhoek', label: 'Windhoek' },
],
},
{
label: 'America',
options: [
{ value: 'America/Adak', label: 'Adak' },
{ value: 'America/Anchorage', label: 'Anchorage' },
{ value: 'America/Anguilla', label: 'Anguilla' },
{ value: 'America/Antigua', label: 'Antigua' },
{ value: 'America/Araguaina', label: 'Araguaina' },
{
value: 'America/Argentina/Buenos_Aires',
label: 'Argentina - Buenos Aires',
},
{ value: 'America/Argentina/Catamarca', label: 'Argentina - Catamarca' },
{ value: 'America/Argentina/Cordoba', label: 'Argentina - Cordoba' },
{ value: 'America/Argentina/Jujuy', label: 'Argentina - Jujuy' },
{ value: 'America/Argentina/La_Rioja', label: 'Argentina - La Rioja' },
{ value: 'America/Argentina/Mendoza', label: 'Argentina - Mendoza' },
{
value: 'America/Argentina/Rio_Gallegos',
label: 'Argentina - Rio Gallegos',
},
{ value: 'America/Argentina/Salta', label: 'Argentina - Salta' },
{ value: 'America/Argentina/San_Juan', label: 'Argentina - San Juan' },
{ value: 'America/Argentina/San_Luis', label: 'Argentina - San Luis' },
{ value: 'America/Argentina/Tucuman', label: 'Argentina - Tucuman' },
{ value: 'America/Argentina/Ushuaia', label: 'Argentina - Ushuaia' },
{ value: 'America/Aruba', label: 'Aruba' },
{ value: 'America/Asuncion', label: 'Asuncion' },
{ value: 'America/Atikokan', label: 'Atikokan' },
{ value: 'America/Bahia', label: 'Bahia' },
{ value: 'America/Bahia_Banderas', label: 'Bahia Banderas' },
{ value: 'America/Barbados', label: 'Barbados' },
{ value: 'America/Belem', label: 'Belem' },
{ value: 'America/Belize', label: 'Belize' },
{ value: 'America/Blanc-Sablon', label: 'Blanc-Sablon' },
{ value: 'America/Boa_Vista', label: 'Boa Vista' },
{ value: 'America/Bogota', label: 'Bogota' },
{ value: 'America/Boise', label: 'Boise' },
{ value: 'America/Cambridge_Bay', label: 'Cambridge Bay' },
{ value: 'America/Campo_Grande', label: 'Campo Grande' },
{ value: 'America/Cancun', label: 'Cancun' },
{ value: 'America/Caracas', label: 'Caracas' },
{ value: 'America/Cayenne', label: 'Cayenne' },
{ value: 'America/Cayman', label: 'Cayman' },
{ value: 'America/Chicago', label: 'Chicago' },
{ value: 'America/Chihuahua', label: 'Chihuahua' },
{ value: 'America/Costa_Rica', label: 'Costa Rica' },
{ value: 'America/Creston', label: 'Creston' },
{ value: 'America/Cuiaba', label: 'Cuiaba' },
{ value: 'America/Curacao', label: 'Curacao' },
{ value: 'America/Danmarkshavn', label: 'Danmarkshavn' },
{ value: 'America/Dawson', label: 'Dawson' },
{ value: 'America/Dawson_Creek', label: 'Dawson Creek' },
{ value: 'America/Denver', label: 'Denver' },
{ value: 'America/Detroit', label: 'Detroit' },
{ value: 'America/Dominica', label: 'Dominica' },
{ value: 'America/Edmonton', label: 'Edmonton' },
{ value: 'America/Eirunepe', label: 'Eirunepe' },
{ value: 'America/El_Salvador', label: 'El Salvador' },
{ value: 'America/Fort_Nelson', label: 'Fort Nelson' },
{ value: 'America/Fortaleza', label: 'Fortaleza' },
{ value: 'America/Glace_Bay', label: 'Glace Bay' },
{ value: 'America/Godthab', label: 'Godthab' },
{ value: 'America/Goose_Bay', label: 'Goose Bay' },
{ value: 'America/Grand_Turk', label: 'Grand Turk' },
{ value: 'America/Grenada', label: 'Grenada' },
{ value: 'America/Guadeloupe', label: 'Guadeloupe' },
{ value: 'America/Guatemala', label: 'Guatemala' },
{ value: 'America/Guayaquil', label: 'Guayaquil' },
{ value: 'America/Guyana', label: 'Guyana' },
{ value: 'America/Halifax', label: 'Halifax' },
{ value: 'America/Havana', label: 'Havana' },
{ value: 'America/Hermosillo', label: 'Hermosillo' },
{
value: 'America/Indiana/Indianapolis',
label: 'Indiana - Indianapolis',
},
{ value: 'America/Indiana/Knox', label: 'Indiana - Knox' },
{ value: 'America/Indiana/Marengo', label: 'Indiana - Marengo' },
{ value: 'America/Indiana/Petersburg', label: 'Indiana - Petersburg' },
{ value: 'America/Indiana/Tell_City', label: 'Indiana - Tell City' },
{ value: 'America/Indiana/Vevay', label: 'Indiana - Vevay' },
{ value: 'America/Indiana/Vincennes', label: 'Indiana - Vincennes' },
{ value: 'America/Indiana/Winamac', label: 'Indiana - Winamac' },
{ value: 'America/Inuvik', label: 'Inuvik' },
{ value: 'America/Iqaluit', label: 'Iqaluit' },
{ value: 'America/Jamaica', label: 'Jamaica' },
{ value: 'America/Juneau', label: 'Juneau' },
{ value: 'America/Kentucky/Louisville', label: 'Kentucky - Louisville' },
{ value: 'America/Kentucky/Monticello', label: 'Kentucky - Monticello' },
{ value: 'America/Kralendijk', label: 'Kralendijk' },
{ value: 'America/La_Paz', label: 'La Paz' },
{ value: 'America/Lima', label: 'Lima' },
{ value: 'America/Los_Angeles', label: 'Los Angeles' },
{ value: 'America/Lower_Princes', label: 'Lower Princes' },
{ value: 'America/Maceio', label: 'Maceio' },
{ value: 'America/Managua', label: 'Managua' },
{ value: 'America/Manaus', label: 'Manaus' },
{ value: 'America/Marigot', label: 'Marigot' },
{ value: 'America/Martinique', label: 'Martinique' },
{ value: 'America/Matamoros', label: 'Matamoros' },
{ value: 'America/Mazatlan', label: 'Mazatlan' },
{ value: 'America/Miquelon', label: 'Miquelon' },
{ value: 'America/Moncton', label: 'Moncton' },
{ value: 'America/Monterrey', label: 'Monterrey' },
{ value: 'America/Montevideo', label: 'Montevideo' },
{ value: 'America/Montserrat', label: 'Montserrat' },
{ value: 'America/Nassau', label: 'Nassau' },
{ value: 'America/New_York', label: 'New York' },
{ value: 'America/Nipigon', label: 'Nipigon' },
{ value: 'America/Nome', label: 'Nome' },
{ value: 'America/Noronha', label: 'Noronha' },
{ value: 'America/North_Dakota/Beulah', label: 'North Dakota - Beulah' },
{ value: 'America/North_Dakota/Center', label: 'North Dakota - Center' },
{
value: 'America/North_Dakota/New_Salem',
label: 'North Dakota - New Salem',
},
{ value: 'America/Ojinaga', label: 'Ojinaga' },
{ value: 'America/Panama', label: 'Panama' },
{ value: 'America/Pangnirtung', label: 'Pangnirtung' },
{ value: 'America/Paramaribo', label: 'Paramaribo' },
{ value: 'America/Phoenix', label: 'Phoenix' },
{ value: 'America/Port-au-Prince', label: 'Port-au-Prince' },
{ value: 'America/Port_of_Spain', label: 'Port of Spain' },
{ value: 'America/Porto_Velho', label: 'Porto Velho' },
{ value: 'America/Puerto_Rico', label: 'Puerto Rico' },
{ value: 'America/Punta_Arenas', label: 'Punta Arenas' },
{ value: 'America/Rainy_River', label: 'Rainy River' },
{ value: 'America/Rankin_Inlet', label: 'Rankin Inlet' },
{ value: 'America/Recife', label: 'Recife' },
{ value: 'America/Regina', label: 'Regina' },
{ value: 'America/Resolute', label: 'Resolute' },
{ value: 'America/Rio_Branco', label: 'Rio Branco' },
{ value: 'America/Santarem', label: 'Santarem' },
{ value: 'America/Santiago', label: 'Santiago' },
{ value: 'America/Santo_Domingo', label: 'Santo Domingo' },
{ value: 'America/Sao_Paulo', label: 'Sao Paulo' },
{ value: 'America/Scoresbysund', label: 'Scoresbysund' },
{ value: 'America/Sitka', label: 'Sitka' },
{ value: 'America/St_Barthelemy', label: 'St Barthelemy' },
{ value: 'America/St_Johns', label: 'St Johns' },
{ value: 'America/St_Kitts', label: 'St Kitts' },
{ value: 'America/St_Lucia', label: 'St Lucia' },
{ value: 'America/St_Thomas', label: 'St Thomas' },
{ value: 'America/St_Vincent', label: 'St Vincent' },
{ value: 'America/Swift_Current', label: 'Swift Current' },
{ value: 'America/Tegucigalpa', label: 'Tegucigalpa' },
{ value: 'America/Thule', label: 'Thule' },
{ value: 'America/Thunder_Bay', label: 'Thunder Bay' },
{ value: 'America/Tijuana', label: 'Tijuana' },
{ value: 'America/Toronto', label: 'Toronto' },
{ value: 'America/Tortola', label: 'Tortola' },
{ value: 'America/Vancouver', label: 'Vancouver' },
{ value: 'America/Whitehorse', label: 'Whitehorse' },
{ value: 'America/Winnipeg', label: 'Winnipeg' },
{ value: 'America/Yakutat', label: 'Yakutat' },
{ value: 'America/Yellowknife', label: 'Yellowknife' },
],
},
{
label: 'Antarctica',
options: [
{ value: 'Antarctica/Casey', label: 'Casey' },
{ value: 'Antarctica/Davis', label: 'Davis' },
{ value: 'Antarctica/DumontDUrville', label: 'DumontDUrville' },
{ value: 'Antarctica/Macquarie', label: 'Macquarie' },
{ value: 'Antarctica/Mawson', label: 'Mawson' },
{ value: 'Antarctica/McMurdo', label: 'McMurdo' },
{ value: 'Antarctica/Palmer', label: 'Palmer' },
{ value: 'Antarctica/Rothera', label: 'Rothera' },
{ value: 'Antarctica/Syowa', label: 'Syowa' },
{ value: 'Antarctica/Troll', label: 'Troll' },
{ value: 'Antarctica/Vostok', label: 'Vostok' },
],
},
{
label: 'Arctic',
options: [{ value: 'Arctic/Longyearbyen', label: 'Longyearbyen' }],
},
{
label: 'Asia',
options: [
{ value: 'Asia/Aden', label: 'Aden' },
{ value: 'Asia/Almaty', label: 'Almaty' },
{ value: 'Asia/Amman', label: 'Amman' },
{ value: 'Asia/Anadyr', label: 'Anadyr' },
{ value: 'Asia/Aqtau', label: 'Aqtau' },
{ value: 'Asia/Aqtobe', label: 'Aqtobe' },
{ value: 'Asia/Ashgabat', label: 'Ashgabat' },
{ value: 'Asia/Atyrau', label: 'Atyrau' },
{ value: 'Asia/Baghdad', label: 'Baghdad' },
{ value: 'Asia/Bahrain', label: 'Bahrain' },
{ value: 'Asia/Baku', label: 'Baku' },
{ value: 'Asia/Bangkok', label: 'Bangkok' },
{ value: 'Asia/Barnaul', label: 'Barnaul' },
{ value: 'Asia/Beirut', label: 'Beirut' },
{ value: 'Asia/Bishkek', label: 'Bishkek' },
{ value: 'Asia/Brunei', label: 'Brunei' },
{ value: 'Asia/Chita', label: 'Chita' },
{ value: 'Asia/Choibalsan', label: 'Choibalsan' },
{ value: 'Asia/Colombo', label: 'Colombo' },
{ value: 'Asia/Damascus', label: 'Damascus' },
{ value: 'Asia/Dhaka', label: 'Dhaka' },
{ value: 'Asia/Dili', label: 'Dili' },
{ value: 'Asia/Dubai', label: 'Dubai' },
{ value: 'Asia/Dushanbe', label: 'Dushanbe' },
{ value: 'Asia/Famagusta', label: 'Famagusta' },
{ value: 'Asia/Gaza', label: 'Gaza' },
{ value: 'Asia/Hebron', label: 'Hebron' },
{ value: 'Asia/Ho_Chi_Minh', label: 'Ho Chi Minh' },
{ value: 'Asia/Hong_Kong', label: 'Hong Kong' },
{ value: 'Asia/Hovd', label: 'Hovd' },
{ value: 'Asia/Irkutsk', label: 'Irkutsk' },
{ value: 'Asia/Jakarta', label: 'Jakarta' },
{ value: 'Asia/Jayapura', label: 'Jayapura' },
{ value: 'Asia/Jerusalem', label: 'Jerusalem' },
{ value: 'Asia/Kabul', label: 'Kabul' },
{ value: 'Asia/Kamchatka', label: 'Kamchatka' },
{ value: 'Asia/Karachi', label: 'Karachi' },
{ value: 'Asia/Kathmandu', label: 'Kathmandu' },
{ value: 'Asia/Khandyga', label: 'Khandyga' },
{ value: 'Asia/Kolkata', label: 'Kolkata' },
{ value: 'Asia/Krasnoyarsk', label: 'Krasnoyarsk' },
{ value: 'Asia/Kuala_Lumpur', label: 'Kuala Lumpur' },
{ value: 'Asia/Kuching', label: 'Kuching' },
{ value: 'Asia/Kuwait', label: 'Kuwait' },
{ value: 'Asia/Macau', label: 'Macau' },
{ value: 'Asia/Magadan', label: 'Magadan' },
{ value: 'Asia/Makassar', label: 'Makassar' },
{ value: 'Asia/Manila', label: 'Manila' },
{ value: 'Asia/Muscat', label: 'Muscat' },
{ value: 'Asia/Nicosia', label: 'Nicosia' },
{ value: 'Asia/Novokuznetsk', label: 'Novokuznetsk' },
{ value: 'Asia/Novosibirsk', label: 'Novosibirsk' },
{ value: 'Asia/Omsk', label: 'Omsk' },
{ value: 'Asia/Oral', label: 'Oral' },
{ value: 'Asia/Phnom_Penh', label: 'Phnom Penh' },
{ value: 'Asia/Pontianak', label: 'Pontianak' },
{ value: 'Asia/Pyongyang', label: 'Pyongyang' },
{ value: 'Asia/Qatar', label: 'Qatar' },
{ value: 'Asia/Qostanay', label: 'Qostanay' },
{ value: 'Asia/Qyzylorda', label: 'Qyzylorda' },
{ value: 'Asia/Riyadh', label: 'Riyadh' },
{ value: 'Asia/Sakhalin', label: 'Sakhalin' },
{ value: 'Asia/Samarkand', label: 'Samarkand' },
{ value: 'Asia/Seoul', label: 'Seoul' },
{ value: 'Asia/Shanghai', label: 'Shanghai' },
{ value: 'Asia/Singapore', label: 'Singapore' },
{ value: 'Asia/Srednekolymsk', label: 'Srednekolymsk' },
{ value: 'Asia/Taipei', label: 'Taipei' },
{ value: 'Asia/Tashkent', label: 'Tashkent' },
{ value: 'Asia/Tbilisi', label: 'Tbilisi' },
{ value: 'Asia/Tehran', label: 'Tehran' },
{ value: 'Asia/Thimphu', label: 'Thimphu' },
{ value: 'Asia/Tokyo', label: 'Tokyo' },
{ value: 'Asia/Tomsk', label: 'Tomsk' },
{ value: 'Asia/Ulaanbaatar', label: 'Ulaanbaatar' },
{ value: 'Asia/Urumqi', label: 'Urumqi' },
{ value: 'Asia/Ust-Nera', label: 'Ust-Nera' },
{ value: 'Asia/Vientiane', label: 'Vientiane' },
{ value: 'Asia/Vladivostok', label: 'Vladivostok' },
{ value: 'Asia/Yakutsk', label: 'Yakutsk' },
{ value: 'Asia/Yangon', label: 'Yangon' },
{ value: 'Asia/Yekaterinburg', label: 'Yekaterinburg' },
{ value: 'Asia/Yerevan', label: 'Yerevan' },
],
},
{
label: 'Atlantic',
options: [
{ value: 'Atlantic/Azores', label: 'Azores' },
{ value: 'Atlantic/Bermuda', label: 'Bermuda' },
{ value: 'Atlantic/Canary', label: 'Canary' },
{ value: 'Atlantic/Cape_Verde', label: 'Cape Verde' },
{ value: 'Atlantic/Faroe', label: 'Faroe' },
{ value: 'Atlantic/Madeira', label: 'Madeira' },
{ value: 'Atlantic/Reykjavik', label: 'Reykjavik' },
{ value: 'Atlantic/South_Georgia', label: 'South Georgia' },
{ value: 'Atlantic/Stanley', label: 'Stanley' },
{ value: 'Atlantic/St_Helena', label: 'St Helena' },
],
},
{
label: 'Australia',
options: [
{ value: 'Australia/Adelaide', label: 'Adelaide' },
{ value: 'Australia/Brisbane', label: 'Brisbane' },
{ value: 'Australia/Broken_Hill', label: 'Broken Hill' },
{ value: 'Australia/Currie', label: 'Currie' },
{ value: 'Australia/Darwin', label: 'Darwin' },
{ value: 'Australia/Eucla', label: 'Eucla' },
{ value: 'Australia/Hobart', label: 'Hobart' },
{ value: 'Australia/Lindeman', label: 'Lindeman' },
{ value: 'Australia/Lord_Howe', label: 'Lord Howe' },
{ value: 'Australia/Melbourne', label: 'Melbourne' },
{ value: 'Australia/Perth', label: 'Perth' },
{ value: 'Australia/Sydney', label: 'Sydney' },
],
},
{
label: 'Europe',
options: [
{ value: 'Europe/Amsterdam', label: 'Amsterdam' },
{ value: 'Europe/Andorra', label: 'Andorra' },
{ value: 'Europe/Astrakhan', label: 'Astrakhan' },
{ value: 'Europe/Athens', label: 'Athens' },
{ value: 'Europe/Belgrade', label: 'Belgrade' },
{ value: 'Europe/Berlin', label: 'Berlin' },
{ value: 'Europe/Bratislava', label: 'Bratislava' },
{ value: 'Europe/Brussels', label: 'Brussels' },
{ value: 'Europe/Bucharest', label: 'Bucharest' },
{ value: 'Europe/Budapest', label: 'Budapest' },
{ value: 'Europe/Busingen', label: 'Busingen' },
{ value: 'Europe/Chisinau', label: 'Chisinau' },
{ value: 'Europe/Copenhagen', label: 'Copenhagen' },
{ value: 'Europe/Dublin', label: 'Dublin' },
{ value: 'Europe/Gibraltar', label: 'Gibraltar' },
{ value: 'Europe/Guernsey', label: 'Guernsey' },
{ value: 'Europe/Helsinki', label: 'Helsinki' },
{ value: 'Europe/Isle_of_Man', label: 'Isle of Man' },
{ value: 'Europe/Istanbul', label: 'Istanbul' },
{ value: 'Europe/Jersey', label: 'Jersey' },
{ value: 'Europe/Kaliningrad', label: 'Kaliningrad' },
{ value: 'Europe/Kiev', label: 'Kiev' },
{ value: 'Europe/Kirov', label: 'Kirov' },
{ value: 'Europe/Lisbon', label: 'Lisbon' },
{ value: 'Europe/Ljubljana', label: 'Ljubljana' },
{ value: 'Europe/London', label: 'London' },
{ value: 'Europe/Luxembourg', label: 'Luxembourg' },
{ value: 'Europe/Madrid', label: 'Madrid' },
{ value: 'Europe/Malta', label: 'Malta' },
{ value: 'Europe/Mariehamn', label: 'Mariehamn' },
{ value: 'Europe/Minsk', label: 'Minsk' },
{ value: 'Europe/Monaco', label: 'Monaco' },
{ value: 'Europe/Moscow', label: 'Moscow' },
{ value: 'Europe/Oslo', label: 'Oslo' },
{ value: 'Europe/Paris', label: 'Paris' },
{ value: 'Europe/Podgorica', label: 'Podgorica' },
{ value: 'Europe/Prague', label: 'Prague' },
{ value: 'Europe/Riga', label: 'Riga' },
{ value: 'Europe/Rome', label: 'Rome' },
{ value: 'Europe/Samara', label: 'Samara' },
{ value: 'Europe/San_Marino', label: 'San Marino' },
{ value: 'Europe/Sarajevo', label: 'Sarajevo' },
{ value: 'Europe/Saratov', label: 'Saratov' },
{ value: 'Europe/Simferopol', label: 'Simferopol' },
{ value: 'Europe/Skopje', label: 'Skopje' },
{ value: 'Europe/Sofia', label: 'Sofia' },
{ value: 'Europe/Stockholm', label: 'Stockholm' },
{ value: 'Europe/Tallinn', label: 'Tallinn' },
{ value: 'Europe/Tirane', label: 'Tirane' },
{ value: 'Europe/Ulyanovsk', label: 'Ulyanovsk' },
{ value: 'Europe/Uzhgorod', label: 'Uzhgorod' },
{ value: 'Europe/Vaduz', label: 'Vaduz' },
{ value: 'Europe/Vatican', label: 'Vatican' },
{ value: 'Europe/Vienna', label: 'Vienna' },
{ value: 'Europe/Vilnius', label: 'Vilnius' },
{ value: 'Europe/Volgograd', label: 'Volgograd' },
{ value: 'Europe/Warsaw', label: 'Warsaw' },
{ value: 'Europe/Zagreb', label: 'Zagreb' },
{ value: 'Europe/Zaporozhye', label: 'Zaporozhye' },
{ value: 'Europe/Zurich', label: 'Zurich' },
],
},
{
label: 'Indian',
options: [
{ value: 'Indian/Antananarivo', label: 'Antananarivo' },
{ value: 'Indian/Chagos', label: 'Chagos' },
{ value: 'Indian/Christmas', label: 'Christmas' },
{ value: 'Indian/Cocos', label: 'Cocos' },
{ value: 'Indian/Comoro', label: 'Comoro' },
{ value: 'Indian/Kerguelen', label: 'Kerguelen' },
{ value: 'Indian/Mahe', label: 'Mahe' },
{ value: 'Indian/Maldives', label: 'Maldives' },
{ value: 'Indian/Mauritius', label: 'Mauritius' },
{ value: 'Indian/Mayotte', label: 'Mayotte' },
{ value: 'Indian/Reunion', label: 'Reunion' },
],
},
{
label: 'Pacific',
options: [
{ value: 'Pacific/Apia', label: 'Apia' },
{ value: 'Pacific/Auckland', label: 'Auckland' },
{ value: 'Pacific/Bougainville', label: 'Bougainville' },
{ value: 'Pacific/Chatham', label: 'Chatham' },
{ value: 'Pacific/Chuuk', label: 'Chuuk' },
{ value: 'Pacific/Easter', label: 'Easter' },
{ value: 'Pacific/Efate', label: 'Efate' },
{ value: 'Pacific/Enderbury', label: 'Enderbury' },
{ value: 'Pacific/Fakaofo', label: 'Fakaofo' },
{ value: 'Pacific/Fiji', label: 'Fiji' },
{ value: 'Pacific/Funafuti', label: 'Funafuti' },
{ value: 'Pacific/Galapagos', label: 'Galapagos' },
{ value: 'Pacific/Gambier', label: 'Gambier' },
{ value: 'Pacific/Guadalcanal', label: 'Guadalcanal' },
{ value: 'Pacific/Guam', label: 'Guam' },
{ value: 'Pacific/Honolulu', label: 'Honolulu' },
{ value: 'Pacific/Kiritimati', label: 'Kiritimati' },
{ value: 'Pacific/Kosrae', label: 'Kosrae' },
{ value: 'Pacific/Kwajalein', label: 'Kwajalein' },
{ value: 'Pacific/Majuro', label: 'Majuro' },
{ value: 'Pacific/Marquesas', label: 'Marquesas' },
{ value: 'Pacific/Midway', label: 'Midway' },
{ value: 'Pacific/Nauru', label: 'Nauru' },
{ value: 'Pacific/Niue', label: 'Niue' },
{ value: 'Pacific/Norfolk', label: 'Norfolk' },
{ value: 'Pacific/Noumea', label: 'Noumea' },
{ value: 'Pacific/Pago_Pago', label: 'Pago Pago' },
{ value: 'Pacific/Palau', label: 'Palau' },
{ value: 'Pacific/Pitcairn', label: 'Pitcairn' },
{ value: 'Pacific/Pohnpei', label: 'Pohnpei' },
{ value: 'Pacific/Port_Moresby', label: 'Port Moresby' },
{ value: 'Pacific/Rarotonga', label: 'Rarotonga' },
{ value: 'Pacific/Saipan', label: 'Saipan' },
{ value: 'Pacific/Tahiti', label: 'Tahiti' },
{ value: 'Pacific/Tarawa', label: 'Tarawa' },
{ value: 'Pacific/Tongatapu', label: 'Tongatapu' },
{ value: 'Pacific/Wake', label: 'Wake' },
{ value: 'Pacific/Wallis', label: 'Wallis' },
],
},
{
label: 'UTC',
options: [{ value: 'UTC', label: 'UTC' }],
},
];
export const DEFAULT_TIMEZONE = 'UTC+0';

View File

@ -109,16 +109,19 @@ export interface UserInfoBase {
*/ */
status?: string; status?: string;
/** roles */ /** roles */
is_admin?: true; is_admin?: boolean;
} }
export interface UserInfoRes extends UserInfoBase { export interface UserInfoRes extends UserInfoBase {
bio: string; bio: string;
bio_html: string; bio_html: string;
create_time?: string; create_time?: string;
/** value = 1 active; value = 2 inactivated /**
* value = 1 active;
* value = 2 inactivated
*/ */
mail_status: number; mail_status: number;
language: string;
e_mail?: string; e_mail?: string;
[prop: string]: any; [prop: string]: any;
} }
@ -228,6 +231,7 @@ export type AdminContentsFilterBy = 'normal' | 'closed' | 'deleted';
export interface AdminContentsReq extends Paging { export interface AdminContentsReq extends Paging {
status: AdminContentsFilterBy; status: AdminContentsFilterBy;
query?: string;
} }
/** /**
@ -257,12 +261,15 @@ export interface AdminSettingsGeneral {
name: string; name: string;
short_description: string; short_description: string;
description: string; description: string;
site_url: string;
contact_email: string;
} }
export interface AdminSettingsInterface { export interface AdminSettingsInterface {
logo: string; logo: string;
language: string; language: string;
theme: string; theme: string;
time_zone?: string;
} }
export interface AdminSettingsSmtp { export interface AdminSettingsSmtp {
@ -321,3 +328,24 @@ export interface SearchResItem {
export interface SearchRes extends ListResult<SearchResItem> { export interface SearchRes extends ListResult<SearchResItem> {
extra: any; extra: any;
} }
export interface AdminDashboard {
info: {
question_count: number;
answer_count: number;
comment_count: number;
vote_count: number;
user_count: number;
report_count: number;
uploading_files: boolean;
smtp: boolean;
time_zone: string;
occupying_storage_space: string;
app_start_time: number;
https: boolean;
version_info: {
remote_version: string;
version: string;
};
};
}

View File

@ -5,7 +5,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
import { useAccordionButton } from 'react-bootstrap/AccordionButton'; import { useAccordionButton } from 'react-bootstrap/AccordionButton';
import { Icon } from '@answer/components'; import { Icon } from '@/components';
function MenuNode({ menu, callback, activeKey, isLeaf = false }) { function MenuNode({ menu, callback, activeKey, isLeaf = false }) {
const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' }); const { t } = useTranslation('translation', { keyPrefix: 'admin.nav_menus' });

View File

@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon } from '@answer/components'; import { Icon } from '@/components';
import { bookmark, postVote } from '@answer/api'; import { loggedUserInfoStore } from '@/stores';
import { isLogin } from '@answer/utils'; import { useToast } from '@/hooks';
import { userInfoStore } from '@answer/stores'; import { tryNormalLogged } from '@/utils/guard';
import { useToast } from '@answer/hooks'; import { bookmark, postVote } from '@/services';
interface Props { interface Props {
className?: string; className?: string;
@ -32,7 +32,7 @@ const Index: FC<Props> = ({ className, data }) => {
state: data?.collected, state: data?.collected,
count: data?.collectCount, count: data?.collectCount,
}); });
const { username = '' } = userInfoStore((state) => state.user); const { username = '' } = loggedUserInfoStore((state) => state.user);
const toast = useToast(); const toast = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
@ -48,7 +48,7 @@ const Index: FC<Props> = ({ className, data }) => {
}, []); }, []);
const handleVote = (type: 'up' | 'down') => { const handleVote = (type: 'up' | 'down') => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
@ -84,7 +84,7 @@ const Index: FC<Props> = ({ className, data }) => {
}; };
const handleBookmark = () => { const handleBookmark = () => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
bookmark({ bookmark({

View File

@ -1,8 +1,7 @@
import { memo, FC } from 'react'; import { memo, FC } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Avatar } from '@answer/components'; import { Avatar } from '@/components';
import { formatCount } from '@/utils'; import { formatCount } from '@/utils';
interface Props { interface Props {

View File

@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon, FormatTime } from '@answer/components'; import { Icon, FormatTime } from '@/components';
const ActionBar = ({ const ActionBar = ({
nickName, nickName,

View File

@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next';
import classNames from 'classnames'; import classNames from 'classnames';
import { TextArea, Mentions } from '@answer/components'; import { TextArea, Mentions } from '@/components';
import { usePageUsers } from '@answer/hooks'; import { usePageUsers } from '@/hooks';
const Form = ({ const Form = ({
className = '', className = '',

View File

@ -2,8 +2,8 @@ import { useState, memo } from 'react';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TextArea, Mentions } from '@answer/components'; import { TextArea, Mentions } from '@/components';
import { usePageUsers } from '@answer/hooks'; import { usePageUsers } from '@/hooks';
const Form = ({ userName, onSendReply, onCancel, mode }) => { const Form = ({ userName, onSendReply, onCancel, mode }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');

View File

@ -7,17 +7,18 @@ import classNames from 'classnames';
import { unionBy } from 'lodash'; import { unionBy } from 'lodash';
import { marked } from 'marked'; import { marked } from 'marked';
import * as Types from '@answer/common/interface'; import * as Types from '@/common/interface';
import { Modal } from '@/components';
import { usePageUsers, useReportModal } from '@/hooks';
import { matchedUsers, parseUserInfo } from '@/utils';
import { tryNormalLogged } from '@/utils/guard';
import { import {
useQueryComments, useQueryComments,
addComment, addComment,
deleteComment, deleteComment,
updateComment, updateComment,
postVote, postVote,
} from '@answer/api'; } from '@/services';
import { Modal } from '@answer/components';
import { usePageUsers, useReportModal } from '@answer/hooks';
import { matchedUsers, parseUserInfo, isLogin } from '@answer/utils';
import { Form, ActionBar, Reply } from './components'; import { Form, ActionBar, Reply } from './components';
@ -163,7 +164,7 @@ const Comment = ({ objectId, mode }) => {
}; };
const handleVote = (id, is_cancel) => { const handleVote = (id, is_cancel) => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
@ -189,7 +190,7 @@ const Comment = ({ objectId, mode }) => {
}; };
const handleAction = ({ action }, item) => { const handleAction = ({ action }, item) => {
if (!isLogin(true)) { if (!tryNormalLogged(true)) {
return; return;
} }
if (action === 'report') { if (action === 'report') {

View File

@ -2,10 +2,10 @@ import { FC, useEffect, useState, memo } from 'react';
import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap'; import { Button, Form, Modal, Tab, Tabs } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Modal as AnswerModal } from '@answer/components'; import { Modal as AnswerModal } from '@/components';
import { uploadImage } from '@answer/api';
import ToolItem from '../toolItem'; import ToolItem from '../toolItem';
import { IEditorContext } from '../types'; import { IEditorContext } from '../types';
import { uploadImage } from '@/services';
const Image: FC<IEditorContext> = ({ editor }) => { const Image: FC<IEditorContext> = ({ editor }) => {
const { t } = useTranslation('translation', { keyPrefix: 'editor' }); const { t } = useTranslation('translation', { keyPrefix: 'editor' });

View File

@ -3,9 +3,9 @@ import { Card, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { TagSelector, Tag } from '@answer/components'; import { TagSelector, Tag } from '@/components';
import { isLogin } from '@answer/utils'; import { tryLoggedAndActicevated } from '@/utils/guard';
import { useFollowingTags, followTags } from '@answer/api'; import { useFollowingTags, followTags } from '@/services';
const Index: FC = () => { const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'question' }); const { t } = useTranslation('translation', { keyPrefix: 'question' });
@ -32,7 +32,7 @@ const Index: FC = () => {
}); });
}; };
if (!isLogin()) { if (!tryLoggedAndActicevated().ok) {
return null; return null;
} }

View File

@ -37,10 +37,10 @@ const Index: FC<Props> = ({ time, preFix, className }) => {
between < 3600 * 24 * 366 && between < 3600 * 24 * 366 &&
dayjs.unix(from).format('YYYY') === dayjs.unix(now).format('YYYY') dayjs.unix(from).format('YYYY') === dayjs.unix(now).format('YYYY')
) { ) {
return dayjs.unix(from).format(t('dates.long_date')); return dayjs.unix(from).tz().format(t('dates.long_date'));
} }
return dayjs.unix(from).format(t('dates.long_date_with_year')); return dayjs.unix(from).tz().format(t('dates.long_date_with_year'));
}; };
if (!time) { if (!time) {
@ -50,8 +50,8 @@ const Index: FC<Props> = ({ time, preFix, className }) => {
return ( return (
<time <time
className={classNames('', className)} className={classNames('', className)}
dateTime={dayjs.unix(time).toISOString()} dateTime={dayjs.unix(time).tz().toISOString()}
title={dayjs.unix(time).format(t('dates.long_date_with_time'))}> title={dayjs.unix(time).tz().format(t('dates.long_date_with_time'))}>
{preFix ? `${preFix} ` : ''} {preFix ? `${preFix} ` : ''}
{formatTime(time)} {formatTime(time)}
</time> </time>

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