feat(seo): seo update

This commit is contained in:
LinkinStar 2022-12-09 10:35:49 +08:00
commit c09d861b5d
33 changed files with 269 additions and 678 deletions

1
.gitignore vendored
View File

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

View File

@ -45,6 +45,7 @@ func runApp() {
if err != nil {
panic(err)
}
conf.GetPathIgnoreList()
app, cleanup, err := initApplication(
c.Debug, c.Server, c.Data.Database, c.Data.Cache, c.I18n, c.Swaggerui, c.ServiceConfig, log.GetLogger())
if err != nil {

View File

@ -4,3 +4,6 @@ import _ "embed"
//go:embed config.yaml
var Config []byte
//go:embed path_ignore.yaml
var PathIgnore []byte

11
configs/path_ignore.yaml Normal file
View File

@ -0,0 +1,11 @@
# url path reserves the keywords list
users:
- settings
- login
- register
- account-recovery
- change-email
- password-reset
- account-activation
- confirm-new-email
- account-suspended

View File

@ -822,77 +822,6 @@ const docTemplate = `{
}
}
},
"/answer/admin/api/siteinfo/login": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site info login config",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site info login config",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteLoginResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site login",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site login",
"parameters": [
{
"description": "login info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteLoginReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/seo": {
"get": {
"security": [
@ -1060,84 +989,6 @@ const docTemplate = `{
}
}
},
"/answer/admin/api/user": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "add user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "add user",
"parameters": [
{
"description": "user",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.AddUserReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/user/password": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update user password",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update user password",
"parameters": [
{
"description": "user",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UpdateUserPasswordReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/user/role": {
"put": {
"security": [
@ -3605,7 +3456,7 @@ const docTemplate = `{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteInfoResp"
"$ref": "#/definitions/schema.SiteGeneralResp"
}
}
}
@ -5208,29 +5059,6 @@ const docTemplate = `{
}
}
},
"schema.AddUserReq": {
"type": "object",
"required": [
"display_name",
"email",
"password"
],
"properties": {
"display_name": {
"type": "string",
"maxLength": 30
},
"email": {
"type": "string",
"maxLength": 500
},
"password": {
"type": "string",
"maxLength": 32,
"minLength": 8
}
}
},
"schema.AdminSetAnswerStatusRequest": {
"type": "object",
"properties": {
@ -5896,6 +5724,10 @@ const docTemplate = `{
"description": "created time",
"type": "integer"
},
"description": {
"description": "description text",
"type": "string"
},
"display_name": {
"description": "display name",
"type": "string"
@ -6710,23 +6542,6 @@ const docTemplate = `{
}
}
},
"schema.SiteInfoResp": {
"type": "object",
"properties": {
"branding": {
"$ref": "#/definitions/schema.SiteBrandingResp"
},
"general": {
"$ref": "#/definitions/schema.SiteGeneralResp"
},
"interface": {
"$ref": "#/definitions/schema.SiteInterfaceResp"
},
"login": {
"$ref": "#/definitions/schema.SiteLoginResp"
}
}
},
"schema.SiteInterfaceReq": {
"type": "object",
"required": [
@ -6805,28 +6620,6 @@ const docTemplate = `{
}
}
},
"schema.SiteLoginReq": {
"type": "object",
"properties": {
"allow_new_registrations": {
"type": "boolean"
},
"login_required": {
"type": "boolean"
}
}
},
"schema.SiteLoginResp": {
"type": "object",
"properties": {
"allow_new_registrations": {
"type": "boolean"
},
"login_required": {
"type": "boolean"
}
}
},
"schema.SiteSeoReq": {
"type": "object",
"required": [
@ -7161,23 +6954,6 @@ const docTemplate = `{
}
}
},
"schema.UpdateUserPasswordReq": {
"type": "object",
"required": [
"password",
"user_id"
],
"properties": {
"password": {
"type": "string",
"maxLength": 32,
"minLength": 8
},
"user_id": {
"type": "string"
}
}
},
"schema.UpdateUserRoleReq": {
"type": "object",
"required": [

View File

@ -810,77 +810,6 @@
}
}
},
"/answer/admin/api/siteinfo/login": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "get site info login config",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "get site info login config",
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.RespBody"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteLoginResp"
}
}
}
]
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update site login",
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update site login",
"parameters": [
{
"description": "login info",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.SiteLoginReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/siteinfo/seo": {
"get": {
"security": [
@ -1048,84 +977,6 @@
}
}
},
"/answer/admin/api/user": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "add user",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "add user",
"parameters": [
{
"description": "user",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.AddUserReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/user/password": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "update user password",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"admin"
],
"summary": "update user password",
"parameters": [
{
"description": "user",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.UpdateUserPasswordReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/admin/api/user/role": {
"put": {
"security": [
@ -3593,7 +3444,7 @@
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/schema.SiteInfoResp"
"$ref": "#/definitions/schema.SiteGeneralResp"
}
}
}
@ -5196,29 +5047,6 @@
}
}
},
"schema.AddUserReq": {
"type": "object",
"required": [
"display_name",
"email",
"password"
],
"properties": {
"display_name": {
"type": "string",
"maxLength": 30
},
"email": {
"type": "string",
"maxLength": 500
},
"password": {
"type": "string",
"maxLength": 32,
"minLength": 8
}
}
},
"schema.AdminSetAnswerStatusRequest": {
"type": "object",
"properties": {
@ -5884,6 +5712,10 @@
"description": "created time",
"type": "integer"
},
"description": {
"description": "description text",
"type": "string"
},
"display_name": {
"description": "display name",
"type": "string"
@ -6698,23 +6530,6 @@
}
}
},
"schema.SiteInfoResp": {
"type": "object",
"properties": {
"branding": {
"$ref": "#/definitions/schema.SiteBrandingResp"
},
"general": {
"$ref": "#/definitions/schema.SiteGeneralResp"
},
"interface": {
"$ref": "#/definitions/schema.SiteInterfaceResp"
},
"login": {
"$ref": "#/definitions/schema.SiteLoginResp"
}
}
},
"schema.SiteInterfaceReq": {
"type": "object",
"required": [
@ -6793,28 +6608,6 @@
}
}
},
"schema.SiteLoginReq": {
"type": "object",
"properties": {
"allow_new_registrations": {
"type": "boolean"
},
"login_required": {
"type": "boolean"
}
}
},
"schema.SiteLoginResp": {
"type": "object",
"properties": {
"allow_new_registrations": {
"type": "boolean"
},
"login_required": {
"type": "boolean"
}
}
},
"schema.SiteSeoReq": {
"type": "object",
"required": [
@ -7149,23 +6942,6 @@
}
}
},
"schema.UpdateUserPasswordReq": {
"type": "object",
"required": [
"password",
"user_id"
],
"properties": {
"password": {
"type": "string",
"maxLength": 32,
"minLength": 8
},
"user_id": {
"type": "string"
}
}
},
"schema.UpdateUserRoleReq": {
"type": "object",
"required": [

View File

@ -176,23 +176,6 @@ definitions:
- object_id
- report_type
type: object
schema.AddUserReq:
properties:
display_name:
maxLength: 30
type: string
email:
maxLength: 500
type: string
password:
maxLength: 32
minLength: 8
type: string
required:
- display_name
- email
- password
type: object
schema.AdminSetAnswerStatusRequest:
properties:
answer_id:
@ -667,6 +650,9 @@ definitions:
created_at:
description: created time
type: integer
description:
description: description text
type: string
display_name:
description: display name
type: string
@ -1257,17 +1243,6 @@ definitions:
- permalink
- site_url
type: object
schema.SiteInfoResp:
properties:
branding:
$ref: '#/definitions/schema.SiteBrandingResp'
general:
$ref: '#/definitions/schema.SiteGeneralResp'
interface:
$ref: '#/definitions/schema.SiteInterfaceResp'
login:
$ref: '#/definitions/schema.SiteLoginResp'
type: object
schema.SiteInterfaceReq:
properties:
language:
@ -1322,20 +1297,6 @@ definitions:
terms_of_service_parsed_text:
type: string
type: object
schema.SiteLoginReq:
properties:
allow_new_registrations:
type: boolean
login_required:
type: boolean
type: object
schema.SiteLoginResp:
properties:
allow_new_registrations:
type: boolean
login_required:
type: boolean
type: object
schema.SiteSeoReq:
properties:
robots:
@ -1572,18 +1533,6 @@ definitions:
required:
- language
type: object
schema.UpdateUserPasswordReq:
properties:
password:
maxLength: 32
minLength: 8
type: string
user_id:
type: string
required:
- password
- user_id
type: object
schema.UpdateUserRoleReq:
properties:
role_id:
@ -2276,47 +2225,6 @@ paths:
summary: update site legal info
tags:
- admin
/answer/admin/api/siteinfo/login:
get:
description: get site info login config
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteLoginResp'
type: object
security:
- ApiKeyAuth: []
summary: get site info login config
tags:
- admin
put:
description: update site login
parameters:
- description: login info
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.SiteLoginReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update site login
tags:
- admin
/answer/admin/api/siteinfo/seo:
get:
description: get site seo information
@ -2414,54 +2322,6 @@ paths:
summary: Get theme options
tags:
- admin
/answer/admin/api/user:
post:
consumes:
- application/json
description: add user
parameters:
- description: user
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.AddUserReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: add user
tags:
- admin
/answer/admin/api/user/password:
put:
consumes:
- application/json
description: update user password
parameters:
- description: user
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.UpdateUserPasswordReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: update user password
tags:
- admin
/answer/admin/api/user/role:
put:
consumes:
@ -3964,7 +3824,7 @@ paths:
- $ref: '#/definitions/handler.RespBody'
- properties:
data:
$ref: '#/definitions/schema.SiteInfoResp'
$ref: '#/definitions/schema.SiteGeneralResp'
type: object
summary: get site info
tags:

View File

@ -19,14 +19,14 @@ backend:
other: "User"
admin:
other: "Admin"
Moderator:
moderator:
other: "Moderator"
description:
user:
other: "Default with no special access."
admin:
other: "Have the full power to access the site."
Moderator:
moderator:
other: "Has access to all posts except admin settings."
email:
@ -109,6 +109,8 @@ backend:
other: "Should not contain synonym tags."
cannot_update:
other: "No permission to update."
cannot_set_synonym_as_itself:
other: "You cannot set the synonym of the current tag as itself."
theme:
not_found:
other: "Theme not found."

View File

@ -97,6 +97,8 @@ backend:
other: "不应包含同义词标签。"
cannot_update:
other: "没有更新标签权限。"
cannot_set_synonym_as_itself:
other: "你无法将当前标签的同义词设置为当前标签自己"
theme:
not_found:
other: "主题未找到"

View File

@ -4,6 +4,8 @@ import (
"bytes"
"path/filepath"
"github.com/answerdev/answer/configs"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/server"
"github.com/answerdev/answer/internal/base/translator"
@ -12,6 +14,7 @@ import (
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/pkg/writer"
"github.com/segmentfault/pacman/contrib/conf/viper"
"github.com/segmentfault/pacman/log"
"gopkg.in/yaml.v3"
)
@ -25,6 +28,10 @@ type AllConfig struct {
Swaggerui *router.SwaggerConfig `json:"swaggerui" mapstructure:"swaggerui" yaml:"swaggerui"`
}
type PathIgnore struct {
Users []string `yaml:"users"`
}
// Server server config
type Server struct {
HTTP *server.HTTP `json:"http" mapstructure:"http" yaml:"http"`
@ -62,3 +69,18 @@ func RewriteConfig(configFilePath string, allConfig *AllConfig) error {
}
return writer.ReplaceFile(configFilePath, buf.String())
}
func GetPathIgnoreList() map[string]bool {
list := make(map[string]bool, 0)
data := &PathIgnore{}
err := yaml.Unmarshal(configs.PathIgnore, data)
if err != nil {
log.Error(err)
return list
}
for _, item := range data.Users {
list[item] = true
}
constant.PathIgnoreMap = list
return list
}

View File

@ -32,6 +32,8 @@ const (
var (
Version string = ""
PathIgnoreMap map[string]bool
ObjectTypeStrMapping = map[string]int{
QuestionObjectType: 1,
AnswerObjectType: 2,
@ -62,3 +64,11 @@ const (
SiteTypeSeo = "seo"
SiteTypeLogin = "login"
)
func ExistInPathIgnore(name string) bool {
_, ok := PathIgnoreMap[name]
if ok {
return true
}
return false
}

View File

@ -58,5 +58,6 @@ const (
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration"
)

View File

@ -5,6 +5,7 @@ import (
"io/fs"
"math"
"os"
"regexp"
"strconv"
"strings"
"time"
@ -69,20 +70,41 @@ func NewHTTPServer(debug bool,
answerRouter.RegisterAnswerCmsAPIRouter(cmsauthV1)
funcMap := template.FuncMap{
"replaceHTMLTag": func(src string, tags ...string) string {
p := `(?U)<(\d+)>.+</(\d+)>`
re := regexp.MustCompile(p)
ms := re.FindAllStringSubmatch(src, -1)
for _, mi := range ms {
if mi[1] == mi[2] {
i, err := strconv.Atoi(mi[1])
if err != nil || len(tags) < i {
break
}
src = strings.ReplaceAll(src, mi[0], tags[i-1])
}
}
return src
},
"join": func(sep string, elems ...string) string {
return strings.Join(elems, sep)
},
"templateHTML": func(data string) template.HTML {
return template.HTML(data)
},
"translator": func(la i18n.Language, data string, params ...interface{}) string {
// todo
/*if len(params) > 0 && len(params)%2 == 0 {
trans := translator.GlobalTrans.Tr(la, data)
if len(params) > 0 && len(params)%2 == 0 {
for i := 0; i < len(params); i += 2 {
k := converter.InterfaceToString(params[i])
v := converter.InterfaceToString(params[i+1])
data = strings.ReplaceAll(data, "{{ "+k+" }}", v)
trans = strings.ReplaceAll(trans, "{{ "+k+" }}", v)
}
}*/
}
trans := translator.GlobalTrans.Tr(la, data)
return trans
},
"timeFormatISO": func(tz string, timestamp int64) string {
@ -120,7 +142,7 @@ func NewHTTPServer(debug bool,
}
if between >= 3600 && between < 3600*24 {
h := math.Floor(float64(between / 60))
h := math.Floor(float64(between / 3600))
trans = translator.GlobalTrans.Tr(la, "ui.dates.x_hours_ago")
return strings.ReplaceAll(trans, "{{count}}", strconv.FormatFloat(h, 'f', 0, 64))
}

View File

@ -296,6 +296,7 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
permission.QuestionEdit,
permission.QuestionDelete,
permission.QuestionEditWithoutReview,
permission.TagUseReservedTag,
}, req.ID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
@ -304,16 +305,18 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
req.CanEdit = canList[0]
req.CanDelete = canList[1]
req.NoNeedReview = canList[2]
req.CanClose = middleware.GetIsAdminFromContext(ctx)
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
req.CanUseReservedTag = canList[3]
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
_, err = qc.questionService.UpdateQuestion(ctx, req)
handler.HandleResponse(ctx, err, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
resp, err := qc.questionService.UpdateQuestion(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, resp)
return
}
handler.HandleResponse(ctx, nil, &schema.UpdateQuestionResp{WaitForReview: !req.NoNeedReview})
}
// CloseMsgList close question msg list

View File

@ -3,6 +3,7 @@ package controller
import (
"net/http"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
@ -83,13 +84,17 @@ func (sc *SiteinfoController) GetSiteLegalInfo(ctx *gin.Context) {
// GetManifestJson get manifest.json
func (sc *SiteinfoController) GetManifestJson(ctx *gin.Context) {
favicon := "favicon.ico"
resp := &schema.GetManifestJsonResp{
ShortName: "Answer",
Name: "Answer.dev",
ManifestVersion: 3,
Version: constant.Version,
ShortName: "Answer",
Name: "Answer.dev",
Icons: map[string]string{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon",
"16": favicon,
"32": favicon,
"48": favicon,
"128": favicon,
},
StartUrl: ".",
Display: "standalone",
@ -100,7 +105,10 @@ func (sc *SiteinfoController) GetManifestJson(ctx *gin.Context) {
if err != nil {
log.Error(err)
} else if len(branding.Favicon) > 0 {
resp.Icons["scr"] = branding.Favicon
resp.Icons["16"] = branding.Favicon
resp.Icons["32"] = branding.Favicon
resp.Icons["48"] = branding.Favicon
resp.Icons["128"] = branding.Favicon
}
ctx.JSON(http.StatusOK, resp)
}

View File

@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
templaterender "github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/schema"
@ -78,7 +79,9 @@ func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInf
// Index question list
func (tc *TemplateController) Index(ctx *gin.Context) {
req := &schema.QuestionSearch{}
req := &schema.QuestionSearch{
Order: "newest",
}
if handler.BindAndCheck(ctx, req) {
tc.Page404(ctx)
return
@ -101,7 +104,9 @@ func (tc *TemplateController) Index(ctx *gin.Context) {
}
func (tc *TemplateController) QuestionList(ctx *gin.Context) {
req := &schema.QuestionSearch{}
req := &schema.QuestionSearch{
Order: "newest",
}
if handler.BindAndCheck(ctx, req) {
tc.Page404(ctx)
return
@ -261,15 +266,23 @@ func (tc *TemplateController) TagInfo(ctx *gin.Context) {
// UserInfo user info
func (tc *TemplateController) UserInfo(ctx *gin.Context) {
// urlPath := ctx.Request.URL.Path
// filePath := ""
// switch urlPath {
// case "/users/login":
// filePath = "build/index.html"
// case "/users/register":
// filePath = "build/index.html"
// default:
username := ctx.Param("username")
if username == "" {
tc.Page404(ctx)
return
}
exist := constant.ExistInPathIgnore(username)
if exist {
file, err := ui.Build.ReadFile("build/index.html")
if err != nil {
log.Error(err)
tc.Page404(ctx)
return
}
ctx.Header("content-type", "text/html;charset=utf-8")
ctx.String(http.StatusOK, string(file))
return
}
req := &schema.GetOtherUserInfoByUsernameReq{}
req.Username = username
userinfo, err := tc.templateRenderController.UserInfo(ctx, req)
@ -288,16 +301,6 @@ func (tc *TemplateController) UserInfo(ctx *gin.Context) {
"userinfo": userinfo,
"bio": template.HTML(userinfo.Info.BioHTML),
})
// }
// file, err := ui.Build.ReadFile(filePath)
// if err != nil {
// log.Error(err)
// ctx.Status(http.StatusNotFound)
// return
// }
// ctx.Header("content-type", "text/html;charset=utf-8")
// ctx.String(http.StatusOK, string(file))
}

View File

@ -1,8 +1,11 @@
package migrations
import (
"fmt"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
@ -118,6 +121,7 @@ func addRoleFeatures(x *xorm.Engine) error {
{RoleID: 2, PowerType: permission.AnswerAudit},
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
@ -151,6 +155,7 @@ func addRoleFeatures(x *xorm.Engine) error {
{RoleID: 3, PowerType: permission.AnswerAudit},
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
}
// insert default powers
@ -183,5 +188,28 @@ func addRoleFeatures(x *xorm.Engine) error {
return err
}
}
defaultConfigTable := []*entity.Config{
{ID: 115, Key: "rank.question.close", Value: `-1`},
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
return nil
}

View File

@ -55,7 +55,7 @@ func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string)
}
// GetTagListByName get tag list all like name
func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error) {
func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, hasReserved bool) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
cond := &entity.Tag{}
session := tr.data.DB.Where("")
@ -65,10 +65,7 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, limi
cond.Recommend = true
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
// if limit == 0 {
// session.Asc("slug_name")
// }
session.Limit(limit).Asc("slug_name")
session.Asc("slug_name")
// if !hasReserved {
// cond.Reserved = false
// session.UseBool("recommend", "reserved")

View File

@ -41,6 +41,6 @@ func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
r.GET("/tags", a.templateController.TagList)
r.GET("/tags/:tag", a.templateController.TagInfo)
// r.GET("/users/:username", a.templateController.UserInfo)
r.GET("/users/:username", a.templateController.UserInfo)
r.GET("/404", a.templateController.Page404)
}

View File

@ -51,6 +51,8 @@ type QuestionPermission struct {
CanClose bool `json:"-"`
// whether user can reopen it
CanReopen bool `json:"-"`
// whether user can use reserved it
CanUseReservedTag bool `json:"-"`
}
type CheckCanQuestionUpdate struct {
@ -76,7 +78,6 @@ type QuestionUpdate struct {
EditSummary string `validate:"omitempty" json:"edit_summary"`
// user id
UserID string `json:"-"`
IsAdmin bool `json:"-"`
NoNeedReview bool `json:"-"`
QuestionPermission
}
@ -97,6 +98,7 @@ type QuestionInfo struct {
Title string `json:"title" xorm:"title"` // title
Content string `json:"content" xorm:"content"` // content
HTML string `json:"html" xorm:"html"` // html
Description string `json:"description"` //description
Tags []*TagResp `json:"tags" ` // tags
ViewCount int `json:"view_count" xorm:"view_count"` // view_count
UniqueViewCount int `json:"unique_view_count" xorm:"unique_view_count"` // unique_view_count

View File

@ -153,6 +153,8 @@ type GetSMTPConfigResp struct {
// GetManifestJsonResp get manifest json response
type GetManifestJsonResp struct {
ManifestVersion int `json:"manifest_version"`
Version string `json:"version"`
ShortName string `json:"short_name"`
Name string `json:"name"`
Icons map[string]string `json:"icons"`

View File

@ -66,6 +66,8 @@ type GetTagResp struct {
OriginalText string `json:"original_text"`
// parsed text
ParsedText string `json:"parsed_text"`
// description text
Description string `json:"description"`
// follower amount
FollowCount int `json:"follow_count"`
// question amount

View File

@ -34,4 +34,5 @@ const (
AnswerAudit = "answer.audit"
QuestionAudit = "question.audit"
TagAudit = "tag.audit"
TagUseReservedTag = "tag.use_reserved_tag"
)

View File

@ -23,6 +23,7 @@ import (
"github.com/answerdev/answer/internal/service/revision_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
@ -351,14 +352,23 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
return questionInfo, tagerr
}
// If it's not admin
if !req.IsAdmin {
//CheckChangeTag
CheckTag, CheckTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
if !CheckTag {
// if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added.
if !req.CanUseReservedTag {
CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
if !CheckOldTag {
errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`,
strings.Join(CheckTaglist, ","))
strings.Join(CheckOldTaglist, ","))
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: errMsg,
})
err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
return errorlist, err
}
if !CheckNewTag {
errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
strings.Join(CheckNewTaglist, ","))
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
@ -392,7 +402,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
Log: req.EditSummary,
}
if req.NoNeedReview || req.IsAdmin || dbinfo.UserID == req.UserID {
if req.NoNeedReview {
canUpdate = true
}
@ -454,6 +464,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
if question.Status == entity.QuestionStatusClosed {
per.CanClose = false
}
question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen)
return question, nil
@ -474,7 +485,7 @@ func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
}
func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) {
return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)
}

View File

@ -150,16 +150,10 @@ func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID
if !exist {
return can, nil
}
// TODO administrator have all permissions
//if userInfo.IsAdmin {
// return true, nil
//}
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
if err != nil {
return can, err
}
action := ""
switch objectInfo.ObjectType {
case constant.QuestionObjectType:
@ -181,6 +175,11 @@ func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID
action = permission.CommentVoteDown
}
}
powerMapping := rs.getUserPowerMapping(ctx, userID)
if powerMapping[action] {
return true, nil
}
meetRank := rs.checkUserRank(ctx, userInfo.ID, userInfo.Rank, PermissionPrefix+action)
return meetRank, nil
}

View File

@ -105,6 +105,7 @@ func (ts *TagService) GetTagInfo(ctx context.Context, req *schema.GetTagInfoReq)
resp.DisplayName = tagInfo.DisplayName
resp.OriginalText = tagInfo.OriginalText
resp.ParsedText = tagInfo.ParsedText
resp.Description = htmltext.FetchExcerpt(tagInfo.ParsedText, "...", 240)
resp.FollowCount = tagInfo.FollowCount
resp.QuestionCount = tagInfo.QuestionCount
resp.Recommend = tagInfo.Recommend
@ -215,6 +216,9 @@ func (ts *TagService) UpdateTagSynonym(ctx context.Context, req *schema.UpdateTa
// find all exist tag
for _, item := range req.SynonymTagList {
if item.SlugName == mainTagInfo.SlugName {
return errors.BadRequest(reason.TagCannotSetSynonymAsItself)
}
addSynonymTagList = append(addSynonymTagList, item.SlugName)
}
tagListInDB, err := ts.tagCommonService.GetTagListByNames(ctx, addSynonymTagList)

View File

@ -24,7 +24,7 @@ type TagCommonRepo interface {
AddTagList(ctx context.Context, tagList []*entity.Tag) (err error)
GetTagListByIDs(ctx context.Context, ids []string) (tagList []*entity.Tag, err error)
GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error)
GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error)
GetTagListByName(ctx context.Context, name string, hasReserved bool) (tagList []*entity.Tag, err error)
GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error)
GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (tag *entity.Tag, exist bool, err error)
GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error)
@ -79,7 +79,7 @@ func NewTagCommonService(
// SearchTagLike get tag list all
func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.SearchTagLikeReq) (resp []schema.SearchTagLikeResp, err error) {
tags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, 0, req.IsAdmin)
tags, err := ts.tagCommonRepo.GetTagListByName(ctx, req.Tag, req.IsAdmin)
if err != nil {
return
}
@ -443,9 +443,10 @@ func (ts *TagCommonService) CheckTagsIsChange(ctx context.Context, tagNameList,
return false
}
func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, []string) {
func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) {
reservedTagsMap := make(map[string]bool)
needTagsMap := make([]string, 0)
notNeedTagsMap := make([]string, 0)
for _, tag := range objectTagData {
if tag.Reserved {
reservedTagsMap[tag.SlugName] = true
@ -456,14 +457,27 @@ func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjec
_, ok := reservedTagsMap[tag.SlugName]
if !ok {
needTagsMap = append(needTagsMap, tag.SlugName)
} else {
reservedTagsMap[tag.SlugName] = false
}
}
}
if len(needTagsMap) > 0 {
return false, needTagsMap
for k, v := range reservedTagsMap {
if v {
notNeedTagsMap = append(notNeedTagsMap, k)
}
}
return true, []string{}
if len(needTagsMap) > 0 {
return false, true, needTagsMap, []string{}
}
if len(notNeedTagsMap) > 0 {
return true, false, []string{}, notNeedTagsMap
}
return true, true, []string{}, []string{}
}
// ObjectChangeTag change object tag list

View File

@ -2,6 +2,7 @@ package converter
import (
"fmt"
"github.com/segmentfault/pacman/log"
"strconv"
)
@ -25,6 +26,29 @@ func IntToString(data int64) string {
return fmt.Sprintf("%d", data)
}
// InterfaceToString converts data to string
// It will be used in template render
func InterfaceToString(data interface{}) string {
return fmt.Sprintf("%d", data)
switch t := data.(type) {
case int:
i := data.(int)
return strconv.Itoa(i)
case int8:
i := data.(int8)
return strconv.Itoa(int(i))
case int16:
i := data.(int16)
return strconv.Itoa(int(i))
case int32:
i := data.(int32)
return string(i)
case int64:
i := data.(int64)
return strconv.FormatInt(i, 10)
case string:
return data.(string)
default:
log.Warn("can't convert type:", t)
}
return ""
}

View File

@ -1 +1 @@
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Answer</title><script defer="defer" src="/static/js/main.2617abc6.js"></script><link href="/static/css/main.d4180d41.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#doc-spinner{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}#doc-spinner .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="doc-spinner"><div class="spinner"></div></div></div></body></html>
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><title>Answer</title><script defer="defer" src="/static/js/main.9de9552b.js"></script><link href="/static/css/main.d4180d41.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#doc-spinner{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}#doc-spinner .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="doc-spinner"><div class="spinner"></div></div></div></body></html>

View File

@ -1,12 +1,11 @@
{{define "footer"}}
</div>
<footer class="bg-light py-3">
<div class="container">
<p class="text-center mb-0 fs-14 text-secondary">
Built on <a href="https://answer.dev/" target="_blank"> Answer </a>-
the open-source software that power Q&amp;A communities.<br />Made
with love © {{.siteinfo.Year}} {{.siteinfo.General.Name}}.
{{$cc := (join " " .siteinfo.Year .siteinfo.General.Name) -}}
{{- $ft := translator $.language "ui.footer.build_on" "cc" $cc -}}
{{templateHTML (replaceHTMLTag $ft "<a href=\"https://answer.dev/\" target=\"_blank\"> Answer </a>")}}
</p>
</div>
</footer>

View File

@ -1,5 +1,4 @@
{{template "header" . }}
{{translator $.language "error.object.old_password_verification_failed"}}
<div class="pt-4 mt-2 mb-5 container">
<div class="justify-content-center row">
<div class="col-xxl-7 col-lg-8 col-sm-12">

View File

@ -21,10 +21,17 @@
class="fw-bold" title="Reputation">{{.UserInfo.Rank}}</span>
</div>
{{if eq .CreateTime .UpdateTime}}
<time class="text-secondary ms-1"
datetime="{{timeFormatISO $.timezone .CreateTime}}"
title="{{translatorTimeFormatLongDate $.language $.timezone .CreateTime}}">{{translator $.language "ui.question.asked"}} {{translatorTimeFormat $.language $.timezone .CreateTime}}
</time>
{{else if gt .UpdateTime 0}}
<time class="text-secondary ms-1"
datetime="{{timeFormatISO $.timezone .UpdateTime}}"
title="{{translatorTimeFormatLongDate $.language $.timezone .UpdateTime}}">{{translator $.language "ui.question.modified"}} {{translatorTimeFormat $.language $.timezone .UpdateTime}}
</time>
{{end}}
</div>
<div class="ms-0 ms-md-3 mt-2 mt-md-0">
<span><i class="br bi-hand-thumbs-up-fill"></i><em

View File

@ -12,8 +12,9 @@
</div>
<div>
<div class="mb-3 d-flex flex-wrap justify-content-between">
<h5 class="fs-5 text-nowrap mb-3 mb-md-0">{{ .questionCount }}
{{translator ($.language) ("ui.question.questions")}}</h5>
<h5 class="fs-5 text-nowrap mb-3 mb-md-0">
{{translator ($.language) "ui.question.x_questions" "count" .questionCount}}
</h5>
</div>
<div class="border-top border-bottom-0 list-group list-group-flush">
{{range .questionList}}