mirror of https://gitee.com/answerdev/answer.git
Merge remote-tracking branch 'origin/beta.2/1.1.0' into feat/plugin/search
This commit is contained in:
commit
5b7b49f87d
|
@ -1,9 +1,10 @@
|
|||
FROM amd64/node AS node-builder
|
||||
FROM amd64/node:18 AS node-builder
|
||||
|
||||
LABEL maintainer="mingcheng<mc@sf.com>"
|
||||
|
||||
COPY . /answer
|
||||
WORKDIR /answer
|
||||
RUN node -v
|
||||
RUN make install-ui-packages ui && mv ui/build /tmp
|
||||
|
||||
# stage2 build the main binary within static resource
|
||||
|
@ -11,12 +12,14 @@ FROM golang:1.19-alpine AS golang-builder
|
|||
LABEL maintainer="aichy@sf.com"
|
||||
|
||||
ARG GOPROXY
|
||||
# ENV GOPROXY ${GOPROXY:-direct}
|
||||
ENV GOPROXY=https://goproxy.io,direct
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV GOROOT /usr/local/go
|
||||
ENV PACKAGE github.com/answerdev/answer
|
||||
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
|
||||
ENV ANSWER_MODULE ${BUILD_DIR}
|
||||
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
ENV TAGS "bindata timetzdata $TAGS"
|
||||
|
@ -25,9 +28,11 @@ ARG CGO_EXTRA_CFLAGS
|
|||
COPY . ${BUILD_DIR}
|
||||
WORKDIR ${BUILD_DIR}
|
||||
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
|
||||
RUN apk --no-cache add build-base git \
|
||||
&& make clean build \
|
||||
&& cp answer /usr/bin/answer
|
||||
RUN apk --no-cache add build-base git bash \
|
||||
&& make clean build
|
||||
RUN chmod 755 answer
|
||||
RUN ["/bin/bash","-c","script/build_plugin.sh"]
|
||||
RUN cp answer /usr/bin/answer
|
||||
|
||||
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
|
||||
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
name: Manual Build DockerHub Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag_name:
|
||||
type: string
|
||||
required: true
|
||||
description: 'DockerHub img tag name'
|
||||
logLevel:
|
||||
description: 'Log level'
|
||||
required: true
|
||||
default: 'warning'
|
||||
type: choice
|
||||
options:
|
||||
- info
|
||||
- warning
|
||||
- debug
|
||||
tags:
|
||||
description: 'Test scenario tags'
|
||||
required: false
|
||||
type: boolean
|
||||
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: answerdev/answer
|
||||
tags: |
|
||||
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: ./.github/Dockerfile
|
||||
tags: answerdev/answer:${{ inputs.tag_name }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
FROM amd64/node AS node-builder
|
||||
# FROM amd64/node AS node-builder
|
||||
FROM amd64/node:18 AS node-builder
|
||||
|
||||
LABEL maintainer="mingcheng<mc@sf.com>"
|
||||
|
||||
COPY . /answer
|
||||
WORKDIR /answer
|
||||
RUN node -v
|
||||
RUN make install-ui-packages ui && mv ui/build /tmp
|
||||
|
||||
# stage2 build the main binary within static resource
|
||||
|
@ -11,12 +13,14 @@ FROM golang:1.19-alpine AS golang-builder
|
|||
LABEL maintainer="aichy@sf.com"
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY ${GOPROXY:-direct}
|
||||
# ENV GOPROXY ${GOPROXY:-direct}
|
||||
ENV GOPROXY=https://goproxy.io,direct
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV GOROOT /usr/local/go
|
||||
ENV PACKAGE github.com/answerdev/answer
|
||||
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
|
||||
ENV ANSWER_MODULE ${BUILD_DIR}
|
||||
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
ENV TAGS "bindata timetzdata $TAGS"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package answercmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
@ -56,6 +57,7 @@ func runApp() {
|
|||
constant.Version = Version
|
||||
constant.Revision = Revision
|
||||
schema.AppStartTime = time.Now()
|
||||
fmt.Println("answer Version:", constant.Version, " Revision:", constant.Revision)
|
||||
|
||||
defer cleanup()
|
||||
if err := app.Run(); err != nil {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# url path reserves the keywords list
|
||||
users:
|
||||
- unsubscribe
|
||||
- settings
|
||||
- login
|
||||
- register
|
||||
|
@ -9,5 +10,7 @@ users:
|
|||
- account-activation
|
||||
- confirm-new-email
|
||||
- account-suspended
|
||||
- confirm-email
|
||||
- auth-landing
|
||||
questions:
|
||||
- ask
|
||||
|
|
91
docs/docs.go
91
docs/docs.go
|
@ -3003,7 +3003,7 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "UserAnswerList",
|
||||
"description": "list personal answers",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3011,9 +3011,9 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-answer"
|
||||
"Personal"
|
||||
],
|
||||
"summary": "UserAnswerList",
|
||||
"summary": "list personal answers",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -3045,8 +3045,8 @@ const docTemplate = `{
|
|||
{
|
||||
"type": "string",
|
||||
"default": "20",
|
||||
"description": "pagesize",
|
||||
"name": "pagesize",
|
||||
"description": "page_size",
|
||||
"name": "page_size",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
|
@ -3068,7 +3068,7 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "UserCollectionList",
|
||||
"description": "list personal collections",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3078,7 +3078,7 @@ const docTemplate = `{
|
|||
"tags": [
|
||||
"Collection"
|
||||
],
|
||||
"summary": "UserCollectionList",
|
||||
"summary": "list personal collections",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -3091,8 +3091,8 @@ const docTemplate = `{
|
|||
{
|
||||
"type": "string",
|
||||
"default": "20",
|
||||
"description": "pagesize",
|
||||
"name": "pagesize",
|
||||
"description": "page_size",
|
||||
"name": "page_size",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
|
@ -5289,12 +5289,12 @@ const docTemplate = `{
|
|||
"summary": "UserModifyPassWord",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "UserModifyPassWordRequest",
|
||||
"description": "UserModifyPasswordReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.UserModifyPassWordRequest"
|
||||
"$ref": "#/definitions/schema.UserModifyPasswordReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -5810,7 +5810,7 @@ const docTemplate = `{
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "UserList",
|
||||
"description": "list personal questions",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -5818,9 +5818,9 @@ const docTemplate = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Question"
|
||||
"Personal"
|
||||
],
|
||||
"summary": "UserList",
|
||||
"summary": "list personal questions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -5852,8 +5852,8 @@ const docTemplate = `{
|
|||
{
|
||||
"type": "string",
|
||||
"default": "20",
|
||||
"description": "pagesize",
|
||||
"name": "pagesize",
|
||||
"description": "page_size",
|
||||
"name": "page_size",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
|
@ -6385,19 +6385,11 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.ConfigFieldUIOptionAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ConfigFieldUIOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/schema.ConfigFieldUIOptionAction"
|
||||
"$ref": "#/definitions/schema.UIOptionAction"
|
||||
},
|
||||
"input_type": {
|
||||
"type": "string"
|
||||
|
@ -7484,6 +7476,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.LoadingAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.NotificationClearIDRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7501,6 +7504,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.OnCompleteAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refresh_form_config": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"toast_return_message": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.OperationQuestionReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -8489,6 +8503,23 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.UIOptionAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loading": {
|
||||
"$ref": "#/definitions/schema.LoadingAction"
|
||||
},
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"on_complete": {
|
||||
"$ref": "#/definitions/schema.OnCompleteAction"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.UnreviewedRevisionInfoInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8545,9 +8576,6 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.UpdateInfoRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"display_name"
|
||||
],
|
||||
"properties": {
|
||||
"avatar": {
|
||||
"description": "avatar",
|
||||
|
@ -8843,6 +8871,11 @@ const docTemplate = `{
|
|||
"e_mail": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"pass": {
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
"minLength": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8886,7 +8919,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.UserModifyPassWordRequest": {
|
||||
"schema.UserModifyPasswordReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pass"
|
||||
|
|
|
@ -2991,7 +2991,7 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "UserAnswerList",
|
||||
"description": "list personal answers",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -2999,9 +2999,9 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"api-answer"
|
||||
"Personal"
|
||||
],
|
||||
"summary": "UserAnswerList",
|
||||
"summary": "list personal answers",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -3033,8 +3033,8 @@
|
|||
{
|
||||
"type": "string",
|
||||
"default": "20",
|
||||
"description": "pagesize",
|
||||
"name": "pagesize",
|
||||
"description": "page_size",
|
||||
"name": "page_size",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
|
@ -3056,7 +3056,7 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "UserCollectionList",
|
||||
"description": "list personal collections",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -3066,7 +3066,7 @@
|
|||
"tags": [
|
||||
"Collection"
|
||||
],
|
||||
"summary": "UserCollectionList",
|
||||
"summary": "list personal collections",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -3079,8 +3079,8 @@
|
|||
{
|
||||
"type": "string",
|
||||
"default": "20",
|
||||
"description": "pagesize",
|
||||
"name": "pagesize",
|
||||
"description": "page_size",
|
||||
"name": "page_size",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
|
@ -5277,12 +5277,12 @@
|
|||
"summary": "UserModifyPassWord",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "UserModifyPassWordRequest",
|
||||
"description": "UserModifyPasswordReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.UserModifyPassWordRequest"
|
||||
"$ref": "#/definitions/schema.UserModifyPasswordReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -5798,7 +5798,7 @@
|
|||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "UserList",
|
||||
"description": "list personal questions",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -5806,9 +5806,9 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Question"
|
||||
"Personal"
|
||||
],
|
||||
"summary": "UserList",
|
||||
"summary": "list personal questions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
|
@ -5840,8 +5840,8 @@
|
|||
{
|
||||
"type": "string",
|
||||
"default": "20",
|
||||
"description": "pagesize",
|
||||
"name": "pagesize",
|
||||
"description": "page_size",
|
||||
"name": "page_size",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
|
@ -6373,19 +6373,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.ConfigFieldUIOptionAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ConfigFieldUIOptions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"$ref": "#/definitions/schema.ConfigFieldUIOptionAction"
|
||||
"$ref": "#/definitions/schema.UIOptionAction"
|
||||
},
|
||||
"input_type": {
|
||||
"type": "string"
|
||||
|
@ -7472,6 +7464,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.LoadingAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string"
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.NotificationClearIDRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7489,6 +7492,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.OnCompleteAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"refresh_form_config": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"toast_return_message": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.OperationQuestionReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -8477,6 +8491,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.UIOptionAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loading": {
|
||||
"$ref": "#/definitions/schema.LoadingAction"
|
||||
},
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"on_complete": {
|
||||
"$ref": "#/definitions/schema.OnCompleteAction"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.UnreviewedRevisionInfoInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8533,9 +8564,6 @@
|
|||
},
|
||||
"schema.UpdateInfoRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"display_name"
|
||||
],
|
||||
"properties": {
|
||||
"avatar": {
|
||||
"description": "avatar",
|
||||
|
@ -8831,6 +8859,11 @@
|
|||
"e_mail": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"pass": {
|
||||
"type": "string",
|
||||
"maxLength": 32,
|
||||
"minLength": 8
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -8874,7 +8907,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.UserModifyPassWordRequest": {
|
||||
"schema.UserModifyPasswordReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"pass"
|
||||
|
|
|
@ -341,15 +341,10 @@ definitions:
|
|||
value:
|
||||
type: string
|
||||
type: object
|
||||
schema.ConfigFieldUIOptionAction:
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
schema.ConfigFieldUIOptions:
|
||||
properties:
|
||||
action:
|
||||
$ref: '#/definitions/schema.ConfigFieldUIOptionAction'
|
||||
$ref: '#/definitions/schema.UIOptionAction'
|
||||
input_type:
|
||||
type: string
|
||||
label:
|
||||
|
@ -1130,6 +1125,13 @@ definitions:
|
|||
description: vote type
|
||||
type: string
|
||||
type: object
|
||||
schema.LoadingAction:
|
||||
properties:
|
||||
state:
|
||||
type: string
|
||||
text:
|
||||
type: string
|
||||
type: object
|
||||
schema.NotificationClearIDRequest:
|
||||
properties:
|
||||
id:
|
||||
|
@ -1141,6 +1143,13 @@ definitions:
|
|||
description: inbox achievement
|
||||
type: string
|
||||
type: object
|
||||
schema.OnCompleteAction:
|
||||
properties:
|
||||
refresh_form_config:
|
||||
type: boolean
|
||||
toast_return_message:
|
||||
type: boolean
|
||||
type: object
|
||||
schema.OperationQuestionReq:
|
||||
properties:
|
||||
id:
|
||||
|
@ -1827,6 +1836,17 @@ definitions:
|
|||
value:
|
||||
type: string
|
||||
type: object
|
||||
schema.UIOptionAction:
|
||||
properties:
|
||||
loading:
|
||||
$ref: '#/definitions/schema.LoadingAction'
|
||||
method:
|
||||
type: string
|
||||
on_complete:
|
||||
$ref: '#/definitions/schema.OnCompleteAction'
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
schema.UnreviewedRevisionInfoInfo:
|
||||
properties:
|
||||
content:
|
||||
|
@ -1889,8 +1909,6 @@ definitions:
|
|||
description: website
|
||||
maxLength: 500
|
||||
type: string
|
||||
required:
|
||||
- display_name
|
||||
type: object
|
||||
schema.UpdatePluginConfigReq:
|
||||
properties:
|
||||
|
@ -2078,6 +2096,10 @@ definitions:
|
|||
e_mail:
|
||||
maxLength: 500
|
||||
type: string
|
||||
pass:
|
||||
maxLength: 32
|
||||
minLength: 8
|
||||
type: string
|
||||
required:
|
||||
- e_mail
|
||||
type: object
|
||||
|
@ -2110,7 +2132,7 @@ definitions:
|
|||
- e_mail
|
||||
- pass
|
||||
type: object
|
||||
schema.UserModifyPassWordRequest:
|
||||
schema.UserModifyPasswordReq:
|
||||
properties:
|
||||
old_pass:
|
||||
maxLength: 32
|
||||
|
@ -4050,7 +4072,7 @@ paths:
|
|||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: UserAnswerList
|
||||
description: list personal answers
|
||||
parameters:
|
||||
- default: string
|
||||
description: username
|
||||
|
@ -4073,9 +4095,9 @@ paths:
|
|||
required: true
|
||||
type: string
|
||||
- default: "20"
|
||||
description: pagesize
|
||||
description: page_size
|
||||
in: query
|
||||
name: pagesize
|
||||
name: page_size
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
|
@ -4087,14 +4109,14 @@ paths:
|
|||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserAnswerList
|
||||
summary: list personal answers
|
||||
tags:
|
||||
- api-answer
|
||||
- Personal
|
||||
/answer/api/v1/personal/collection/page:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: UserCollectionList
|
||||
description: list personal collections
|
||||
parameters:
|
||||
- default: "0"
|
||||
description: page
|
||||
|
@ -4103,9 +4125,9 @@ paths:
|
|||
required: true
|
||||
type: string
|
||||
- default: "20"
|
||||
description: pagesize
|
||||
description: page_size
|
||||
in: query
|
||||
name: pagesize
|
||||
name: page_size
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
|
@ -4117,7 +4139,7 @@ paths:
|
|||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserCollectionList
|
||||
summary: list personal collections
|
||||
tags:
|
||||
- Collection
|
||||
/answer/api/v1/personal/comment/page:
|
||||
|
@ -5434,12 +5456,12 @@ paths:
|
|||
- application/json
|
||||
description: UserModifyPassWord
|
||||
parameters:
|
||||
- description: UserModifyPassWordRequest
|
||||
- description: UserModifyPasswordReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.UserModifyPassWordRequest'
|
||||
$ref: '#/definitions/schema.UserModifyPasswordReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
@ -5751,7 +5773,7 @@ paths:
|
|||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: UserList
|
||||
description: list personal questions
|
||||
parameters:
|
||||
- default: string
|
||||
description: username
|
||||
|
@ -5774,9 +5796,9 @@ paths:
|
|||
required: true
|
||||
type: string
|
||||
- default: "20"
|
||||
description: pagesize
|
||||
description: page_size
|
||||
in: query
|
||||
name: pagesize
|
||||
name: page_size
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
|
@ -5788,9 +5810,9 @@ paths:
|
|||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: UserList
|
||||
summary: list personal questions
|
||||
tags:
|
||||
- Question
|
||||
- Personal
|
||||
/robots.txt:
|
||||
get:
|
||||
description: get site robots information
|
||||
|
|
3
go.mod
3
go.mod
|
@ -42,7 +42,7 @@ require (
|
|||
github.com/swaggo/swag v1.8.10
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/yuin/goldmark v1.4.13
|
||||
golang.org/x/crypto v0.4.0
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/net v0.7.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
@ -102,7 +102,6 @@ require (
|
|||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/gomega v1.20.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.2 // indirect
|
||||
|
|
7
go.sum
7
go.sum
|
@ -538,9 +538,8 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v
|
|||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
|
||||
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
|
@ -782,8 +781,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
|
|
@ -12,6 +12,8 @@ backend:
|
|||
other: Unauthorized.
|
||||
database_error:
|
||||
other: Data server error.
|
||||
forbidden_error:
|
||||
other: Forbidden.
|
||||
action:
|
||||
report:
|
||||
other: Flag
|
||||
|
@ -339,6 +341,16 @@ backend:
|
|||
other: Your answer has been deleted
|
||||
your_comment_was_deleted:
|
||||
other: Your comment has been deleted
|
||||
up_voted_question:
|
||||
other: upvoted question
|
||||
down_voted_question:
|
||||
other: downvoted question
|
||||
up_voted_answer:
|
||||
other: upvoted answer
|
||||
down_voted_answer:
|
||||
other: downvoted answer
|
||||
up_voted_comment:
|
||||
other: upvoted comment
|
||||
|
||||
# The following fields are used for interface presentation(Front-end)
|
||||
ui:
|
||||
|
@ -394,6 +406,7 @@ ui:
|
|||
achievement: Achievements
|
||||
all_read: Mark all as read
|
||||
show_more: Show more
|
||||
someone: Someone
|
||||
suspended:
|
||||
title: Your Account has been Suspended
|
||||
until_time: "Your account was suspended until {{ time }}."
|
||||
|
@ -611,7 +624,7 @@ ui:
|
|||
tip_answer: >-
|
||||
Use comments to reply to other users or notify them of changes. If you are
|
||||
adding new information, edit your post instead of commenting.
|
||||
tip_vote: It adds something useful to the post
|
||||
tip_vote: It adds something useful to the post
|
||||
edit_answer:
|
||||
title: Edit Answer
|
||||
default_reason: Edit answer
|
||||
|
@ -843,8 +856,11 @@ ui:
|
|||
We've sent an email to that address. Please follow the confirmation
|
||||
instructions.
|
||||
email:
|
||||
label: Email
|
||||
msg: Email cannot be empty.
|
||||
label: New Email
|
||||
msg: New Email cannot be empty.
|
||||
pass:
|
||||
label: Current Password
|
||||
msg: Password cannot be empty.
|
||||
password_title: Password
|
||||
current_pass:
|
||||
label: Current Password
|
||||
|
@ -891,8 +907,8 @@ ui:
|
|||
closed_in: Closed in
|
||||
show_exist: Show existing question.
|
||||
useful: Useful
|
||||
question_useful: It is useful and clear
|
||||
question_un_useful: It is unclear or not useful
|
||||
question_useful: It is useful and clear
|
||||
question_un_useful: It is unclear or not useful
|
||||
answer_useful: It is useful
|
||||
answer_un_useful: It is not useful
|
||||
answers:
|
||||
|
@ -955,6 +971,10 @@ ui:
|
|||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
pinned: Pinned
|
||||
all: All
|
||||
question: Question
|
||||
answer: Answer
|
||||
comment: Comment
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -1233,6 +1253,7 @@ ui:
|
|||
pending: Pending
|
||||
completed: Completed
|
||||
flagged: Flagged
|
||||
flagged_type: Flagged {{ type }}
|
||||
created: Created
|
||||
action: Action
|
||||
review: Review
|
||||
|
|
|
@ -329,6 +329,16 @@ backend:
|
|||
other: 你的答案已被删除
|
||||
your_comment_was_deleted:
|
||||
other: 你的评论已被删除
|
||||
up_voted_question:
|
||||
other: 赞了问题
|
||||
down_voted_question:
|
||||
other: 踩了问题
|
||||
up_voted_answer:
|
||||
other: 赞了答案
|
||||
down_voted_answer:
|
||||
other: 踩了答案
|
||||
up_voted_comment:
|
||||
other: 赞了评论
|
||||
#The following fields are used for interface presentation(Front-end)
|
||||
ui:
|
||||
how_to_format:
|
||||
|
@ -374,6 +384,7 @@ ui:
|
|||
achievement: 成就
|
||||
all_read: 全部标记为已读
|
||||
show_more: 显示更多
|
||||
someone: 有人
|
||||
suspended:
|
||||
title: 账号已封禁
|
||||
until_time: "你的账号被封禁至{{ time }}。"
|
||||
|
@ -895,6 +906,10 @@ ui:
|
|||
skip: 略过
|
||||
discard_draft: 丢弃草稿
|
||||
pinned: 已置顶
|
||||
all: 所有
|
||||
question: 问题
|
||||
answer: 回答
|
||||
comment: 评论
|
||||
search:
|
||||
title: 搜索结果
|
||||
keywords: 关键词
|
||||
|
@ -1164,6 +1179,7 @@ ui:
|
|||
pending: 等待处理
|
||||
completed: 已完成
|
||||
flagged: 被举报内容
|
||||
flagged_type: 被举报的{{ type }}
|
||||
created: 创建于
|
||||
action: 操作
|
||||
review: 审查
|
||||
|
@ -1451,6 +1467,30 @@ ui:
|
|||
deactivate: 停用
|
||||
activate: 启用
|
||||
settings: 设置
|
||||
settings_users:
|
||||
title: 用户
|
||||
avatar:
|
||||
label: 默认头像
|
||||
text: 未设置自定义头像的用户所展示的头像。
|
||||
profile_editable:
|
||||
title: 可编辑的个人资料
|
||||
allow_update_display_name:
|
||||
label: 允许用户更改显示名称
|
||||
allow_update_username:
|
||||
label: 允许用户更改用户名
|
||||
allow_update_avatar:
|
||||
label: 允许用户更改头像
|
||||
allow_update_bio:
|
||||
label: 允许用户更改自我介绍
|
||||
allow_update_website:
|
||||
label: 允许用户更改个人网站
|
||||
allow_update_location:
|
||||
label: 允许用户更改所在地
|
||||
privilege:
|
||||
title: 声望权限
|
||||
level:
|
||||
label: 所需声望等级
|
||||
text: 选择所需的声望等级以获取权限
|
||||
form:
|
||||
optional: (选填)
|
||||
empty: 不能为空
|
||||
|
|
|
@ -1,28 +1,38 @@
|
|||
package constant
|
||||
|
||||
const (
|
||||
// UpdateQuestion update question
|
||||
UpdateQuestion = "notification.action.update_question"
|
||||
// AnswerTheQuestion answer the question
|
||||
AnswerTheQuestion = "notification.action.answer_the_question"
|
||||
// UpdateAnswer update answer
|
||||
UpdateAnswer = "notification.action.update_answer"
|
||||
// AcceptAnswer accept answer
|
||||
AcceptAnswer = "notification.action.accept_answer"
|
||||
// CommentQuestion comment question
|
||||
CommentQuestion = "notification.action.comment_question"
|
||||
// CommentAnswer comment answer
|
||||
CommentAnswer = "notification.action.comment_answer"
|
||||
// ReplyToYou reply to you
|
||||
ReplyToYou = "notification.action.reply_to_you"
|
||||
// MentionYou mention you
|
||||
MentionYou = "notification.action.mention_you"
|
||||
// YourQuestionIsClosed your question is closed
|
||||
YourQuestionIsClosed = "notification.action.your_question_is_closed"
|
||||
// YourQuestionWasDeleted your question was deleted
|
||||
YourQuestionWasDeleted = "notification.action.your_question_was_deleted"
|
||||
// YourAnswerWasDeleted your answer was deleted
|
||||
YourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
|
||||
// YourCommentWasDeleted your comment was deleted
|
||||
YourCommentWasDeleted = "notification.action.your_comment_was_deleted"
|
||||
// NotificationUpdateQuestion update question
|
||||
NotificationUpdateQuestion = "notification.action.update_question"
|
||||
// NotificationAnswerTheQuestion answer the question
|
||||
NotificationAnswerTheQuestion = "notification.action.answer_the_question"
|
||||
// NotificationUpVotedTheQuestion up voted the question
|
||||
NotificationUpVotedTheQuestion = "notification.action.up_voted_question"
|
||||
// NotificationDownVotedTheQuestion down voted the question
|
||||
NotificationDownVotedTheQuestion = "notification.action.down_voted_question"
|
||||
// NotificationUpdateAnswer update answer
|
||||
NotificationUpdateAnswer = "notification.action.update_answer"
|
||||
// NotificationAcceptAnswer accept answer
|
||||
NotificationAcceptAnswer = "notification.action.accept_answer"
|
||||
// NotificationUpVotedTheAnswer up voted the answer
|
||||
NotificationUpVotedTheAnswer = "notification.action.up_voted_answer"
|
||||
// NotificationDownVotedTheAnswer down voted the answer
|
||||
NotificationDownVotedTheAnswer = "notification.action.down_voted_answer"
|
||||
// NotificationCommentQuestion comment question
|
||||
NotificationCommentQuestion = "notification.action.comment_question"
|
||||
// NotificationCommentAnswer comment answer
|
||||
NotificationCommentAnswer = "notification.action.comment_answer"
|
||||
// NotificationUpVotedTheComment up voted the comment
|
||||
NotificationUpVotedTheComment = "notification.action.up_voted_comment"
|
||||
// NotificationReplyToYou reply to you
|
||||
NotificationReplyToYou = "notification.action.reply_to_you"
|
||||
// NotificationMentionYou mention you
|
||||
NotificationMentionYou = "notification.action.mention_you"
|
||||
// NotificationYourQuestionIsClosed your question is closed
|
||||
NotificationYourQuestionIsClosed = "notification.action.your_question_is_closed"
|
||||
// NotificationYourQuestionWasDeleted your question was deleted
|
||||
NotificationYourQuestionWasDeleted = "notification.action.your_question_was_deleted"
|
||||
// NotificationYourAnswerWasDeleted your answer was deleted
|
||||
NotificationYourAnswerWasDeleted = "notification.action.your_answer_was_deleted"
|
||||
// NotificationYourCommentWasDeleted your comment was deleted
|
||||
NotificationYourCommentWasDeleted = "notification.action.your_comment_was_deleted"
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package constant
|
||||
|
||||
var (
|
||||
DefaultAvatar = "system"
|
||||
DefaultAvatar = "system"
|
||||
DefaultSiteURL = ""
|
||||
)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func HeadersByRequestURI() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if strings.HasPrefix(c.Request.RequestURI, "/static/") {
|
||||
c.Header("cache-control", "public, max-age=31536000")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ func NewHTTPServer(debug bool,
|
|||
html, _ := fs.Sub(ui.Template, "template")
|
||||
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
|
||||
r.SetHTMLTemplate(htmlTemplate)
|
||||
|
||||
r.Use(middleware.HeadersByRequestURI())
|
||||
viewRouter.Register(r)
|
||||
|
||||
rootGroup := r.Group("")
|
||||
|
@ -71,7 +71,7 @@ func NewHTTPServer(debug bool,
|
|||
pluginAPIRouter.RegisterAuthAdminConnectorRouter(adminauthV1)
|
||||
|
||||
_ = plugin.CallAgent(func(agent plugin.Agent) error {
|
||||
agent.RegisterUnAuthRouter(unAuthV1)
|
||||
agent.RegisterUnAuthRouter(mustUnAuthV1)
|
||||
agent.RegisterAuthUserRouter(authV1)
|
||||
agent.RegisterAuthAdminRouter(adminauthV1)
|
||||
return nil
|
||||
|
|
|
@ -11,7 +11,6 @@ import (
|
|||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
"github.com/answerdev/answer/internal/service/rank"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/copier"
|
||||
|
@ -552,84 +551,75 @@ func (qc *QuestionController) UserTop(ctx *gin.Context) {
|
|||
})
|
||||
}
|
||||
|
||||
// UserList godoc
|
||||
// @Summary UserList
|
||||
// @Description UserList
|
||||
// @Tags Question
|
||||
// PersonalQuestionPage list personal questions
|
||||
// @Summary list personal questions
|
||||
// @Description list personal questions
|
||||
// @Tags Personal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username" default(string)
|
||||
// @Param order query string true "order" Enums(newest,score)
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param pagesize query string true "pagesize" default(20)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /personal/question/page [get]
|
||||
func (qc *QuestionController) UserList(ctx *gin.Context) {
|
||||
userName := ctx.Query("username")
|
||||
order := ctx.Query("order")
|
||||
pageStr := ctx.Query("page")
|
||||
pageSizeStr := ctx.Query("pagesize")
|
||||
page := converter.StringToInt(pageStr)
|
||||
pageSize := converter.StringToInt(pageSizeStr)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, count, err := qc.questionService.SearchUserList(ctx, userName, order, page, pageSize, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"list": questionList,
|
||||
"count": count,
|
||||
})
|
||||
func (qc *QuestionController) PersonalQuestionPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalQuestionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := qc.questionService.PersonalQuestionPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserAnswerList godoc
|
||||
// @Summary UserAnswerList
|
||||
// @Description UserAnswerList
|
||||
// @Tags api-answer
|
||||
// PersonalAnswerPage list personal answers
|
||||
// @Summary list personal answers
|
||||
// @Description list personal answers
|
||||
// @Tags Personal
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "username" default(string)
|
||||
// @Param order query string true "order" Enums(newest,score)
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param pagesize query string true "pagesize" default(20)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/personal/answer/page [get]
|
||||
func (qc *QuestionController) UserAnswerList(ctx *gin.Context) {
|
||||
userName := ctx.Query("username")
|
||||
order := ctx.Query("order")
|
||||
pageStr := ctx.Query("page")
|
||||
pageSizeStr := ctx.Query("pagesize")
|
||||
page := converter.StringToInt(pageStr)
|
||||
pageSize := converter.StringToInt(pageSizeStr)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, count, err := qc.questionService.SearchUserAnswerList(ctx, userName, order, page, pageSize, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"list": questionList,
|
||||
"count": count,
|
||||
})
|
||||
func (qc *QuestionController) PersonalAnswerPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalAnswerPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.LoginUserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
resp, err := qc.questionService.PersonalAnswerPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// UserCollectionList godoc
|
||||
// @Summary UserCollectionList
|
||||
// @Description UserCollectionList
|
||||
// PersonalCollectionPage list personal collections
|
||||
// @Summary list personal collections
|
||||
// @Description list personal collections
|
||||
// @Tags Collection
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param page query string true "page" default(0)
|
||||
// @Param pagesize query string true "pagesize" default(20)
|
||||
// @Param page_size query string true "page_size" default(20)
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/personal/collection/page [get]
|
||||
func (qc *QuestionController) UserCollectionList(ctx *gin.Context) {
|
||||
pageStr := ctx.Query("page")
|
||||
pageSizeStr := ctx.Query("pagesize")
|
||||
page := converter.StringToInt(pageStr)
|
||||
pageSize := converter.StringToInt(pageSizeStr)
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
questionList, count, err := qc.questionService.SearchUserCollectionList(ctx, page, pageSize, userID)
|
||||
handler.HandleResponse(ctx, err, gin.H{
|
||||
"list": questionList,
|
||||
"count": count,
|
||||
})
|
||||
func (qc *QuestionController) PersonalCollectionPage(ctx *gin.Context) {
|
||||
req := &schema.PersonalCollectionPageReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := qc.questionService.PersonalCollectionPage(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
// AdminSearchList godoc
|
||||
|
|
|
@ -184,9 +184,9 @@ func (uc *UserController) UseRePassWord(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
resp, err := uc.userService.UseRePassword(ctx, req)
|
||||
err := uc.userService.UpdatePasswordWhenForgot(ctx, req)
|
||||
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UserLogout user logout
|
||||
|
@ -339,15 +339,16 @@ func (uc *UserController) UserVerifyEmailSend(ctx *gin.Context) {
|
|||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param data body schema.UserModifyPassWordRequest true "UserModifyPassWordRequest"
|
||||
// @Param data body schema.UserModifyPasswordReq true "UserModifyPasswordReq"
|
||||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/api/v1/user/password [put]
|
||||
func (uc *UserController) UserModifyPassWord(ctx *gin.Context) {
|
||||
req := &schema.UserModifyPassWordRequest{}
|
||||
req := &schema.UserModifyPasswordReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.AccessToken = middleware.ExtractToken(ctx)
|
||||
|
||||
oldPassVerification, err := uc.userService.UserModifyPassWordVerification(ctx, req)
|
||||
if err != nil {
|
||||
|
|
|
@ -80,10 +80,6 @@ func (uc *UserAdminController) UpdateUserRole(ctx *gin.Context) {
|
|||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/user [post]
|
||||
func (uc *UserAdminController) AddUser(ctx *gin.Context) {
|
||||
if plugin.UserCenterEnabled() {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
|
||||
return
|
||||
}
|
||||
req := &schema.AddUserReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
|
@ -106,10 +102,6 @@ func (uc *UserAdminController) AddUser(ctx *gin.Context) {
|
|||
// @Success 200 {object} handler.RespBody
|
||||
// @Router /answer/admin/api/user/password [put]
|
||||
func (uc *UserAdminController) UpdateUserPassword(ctx *gin.Context) {
|
||||
if plugin.UserCenterEnabled() {
|
||||
handler.HandleResponse(ctx, errors.Forbidden(reason.ForbiddenError), nil)
|
||||
return
|
||||
}
|
||||
req := &schema.UpdateUserPasswordReq{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
|
|
|
@ -366,10 +366,14 @@ func initConfigTable(engine *xorm.Engine) error {
|
|||
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
|
||||
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
|
||||
{ID: 118, Key: "plugin.status", Value: `{}`},
|
||||
{ID: 119, Key: "question.pin", Value: `-1`},
|
||||
{ID: 120, Key: "question.unpin", Value: `-1`},
|
||||
{ID: 121, Key: "question.show", Value: `-1`},
|
||||
{ID: 122, Key: "question.hide", Value: `-1`},
|
||||
{ID: 119, Key: "question.pin", Value: `0`},
|
||||
{ID: 120, Key: "question.unpin", Value: `0`},
|
||||
{ID: 121, Key: "question.show", Value: `0`},
|
||||
{ID: 122, Key: "question.hide", Value: `0`},
|
||||
{ID: 123, Key: "rank.question.pin", Value: `-1`},
|
||||
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
|
||||
{ID: 125, Key: "rank.question.show", Value: `-1`},
|
||||
{ID: 126, Key: "rank.question.hide", Value: `-1`},
|
||||
}
|
||||
_, err := engine.Insert(defaultConfigTable)
|
||||
return err
|
||||
|
|
|
@ -59,6 +59,8 @@ var migrations = []Migration{
|
|||
NewMigration("add user pin hide features", addRolePinAndHideFeatures, true),
|
||||
NewMigration("update accept answer rank", updateAcceptAnswerRank, true),
|
||||
NewMigration("add plugin", addPlugin, false),
|
||||
NewMigration("update user pin hide features", updateRolePinAndHideFeatures, true),
|
||||
NewMigration("update question post time", updateQuestionPostTime, true),
|
||||
NewMigration("add login limitations", addLoginLimitations, true),
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func updateRolePinAndHideFeatures(x *xorm.Engine) error {
|
||||
|
||||
defaultConfigTable := []*entity.Config{
|
||||
{ID: 119, Key: "question.pin", Value: `0`},
|
||||
{ID: 120, Key: "question.unpin", Value: `0`},
|
||||
{ID: 121, Key: "question.show", Value: `0`},
|
||||
{ID: 122, Key: "question.hide", Value: `0`},
|
||||
{ID: 123, Key: "rank.question.pin", Value: `-1`},
|
||||
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
|
||||
{ID: 125, Key: "rank.question.show", Value: `-1`},
|
||||
{ID: 126, Key: "rank.question.hide", Value: `-1`},
|
||||
}
|
||||
for _, c := range defaultConfigTable {
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("update config failed: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
|
||||
log.Errorf("insert %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("add config failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func updateQuestionPostTime(x *xorm.Engine) error {
|
||||
questionList := make([]entity.Question, 0)
|
||||
err := x.Find(&questionList, &entity.Question{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get questions failed: %w", err)
|
||||
}
|
||||
for _, item := range questionList {
|
||||
if item.PostUpdateTime.IsZero() {
|
||||
if !item.UpdatedAt.IsZero() {
|
||||
item.PostUpdateTime = item.UpdatedAt
|
||||
} else if !item.CreatedAt.IsZero() {
|
||||
item.PostUpdateTime = item.CreatedAt
|
||||
}
|
||||
if _, err = x.Update(item, &entity.Question{ID: item.ID}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", item, err)
|
||||
return fmt.Errorf("update question failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -18,10 +18,6 @@ func addPlugin(x *xorm.Engine) error {
|
|||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("update config failed: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
|
||||
|
|
|
@ -63,18 +63,22 @@ func addRolePinAndHideFeatures(x *xorm.Engine) error {
|
|||
}
|
||||
|
||||
defaultConfigTable := []*entity.Config{
|
||||
{ID: 119, Key: "question.pin", Value: `-1`},
|
||||
{ID: 120, Key: "question.unpin", Value: `-1`},
|
||||
{ID: 121, Key: "question.show", Value: `-1`},
|
||||
{ID: 122, Key: "question.hide", Value: `-1`},
|
||||
{ID: 119, Key: "question.pin", Value: `0`},
|
||||
{ID: 120, Key: "question.unpin", Value: `0`},
|
||||
{ID: 121, Key: "question.show", Value: `0`},
|
||||
{ID: 122, Key: "question.hide", Value: `0`},
|
||||
{ID: 123, Key: "rank.question.pin", Value: `-1`},
|
||||
{ID: 124, Key: "rank.question.unpin", Value: `-1`},
|
||||
{ID: 125, Key: "rank.question.show", Value: `-1`},
|
||||
{ID: 126, Key: "rank.question.hide", Value: `-1`},
|
||||
}
|
||||
for _, c := range defaultConfigTable {
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID})
|
||||
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 {
|
||||
if _, err = x.Update(c, &entity.Config{ID: c.ID}); err != nil {
|
||||
log.Errorf("update %+v config failed: %s", c, err)
|
||||
return fmt.Errorf("update config failed: %w", err)
|
||||
}
|
||||
|
|
|
@ -202,7 +202,9 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
|
|||
msg.TriggerUserID = questionUserID
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
}
|
||||
notice_queue.AddNotification(msg)
|
||||
if msg.TriggerUserID != msg.ReceiverUserID {
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, act := range addActivityList {
|
||||
|
@ -214,7 +216,7 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
|
|||
if act.UserID != questionUserID {
|
||||
msg.TriggerUserID = questionUserID
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.AcceptAnswer
|
||||
msg.NotificationAction = constant.NotificationAcceptAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
|
@ -70,7 +71,9 @@ var LimitDownActions = map[string][]string{
|
|||
|
||||
func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUserID string, actions []string) (resp *schema.VoteResp, err error) {
|
||||
resp = &schema.VoteResp{}
|
||||
notificationUserIDs := make([]string, 0)
|
||||
achievementNotificationUserIDs := make([]string, 0)
|
||||
sendInboxNotification := false
|
||||
upVote := false
|
||||
_, err = vr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
|
||||
result = nil
|
||||
for _, action := range actions {
|
||||
|
@ -127,7 +130,7 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
|
|||
if isReachStandard {
|
||||
insertActivity.Rank = 0
|
||||
}
|
||||
notificationUserIDs = append(notificationUserIDs, activityUserID)
|
||||
achievementNotificationUserIDs = append(achievementNotificationUserIDs, activityUserID)
|
||||
}
|
||||
|
||||
if has {
|
||||
|
@ -142,13 +145,17 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sendInboxNotification = true
|
||||
}
|
||||
|
||||
// update votes
|
||||
if action == "vote_down" || action == "vote_up" {
|
||||
if action == constant.ActVoteDown || action == constant.ActVoteUp {
|
||||
votes := 1
|
||||
if action == "vote_down" {
|
||||
if action == constant.ActVoteDown {
|
||||
upVote = false
|
||||
votes = -1
|
||||
} else {
|
||||
upVote = true
|
||||
}
|
||||
err = vr.updateVotes(ctx, session, objectID, votes)
|
||||
if err != nil {
|
||||
|
@ -165,9 +172,12 @@ func (vr *VoteRepo) vote(ctx context.Context, objectID string, userID, objectUse
|
|||
resp, err = vr.GetVoteResultByObjectId(ctx, objectID)
|
||||
resp.VoteStatus = vr.voteCommon.GetVoteStatus(ctx, objectID, userID)
|
||||
|
||||
for _, activityUserID := range notificationUserIDs {
|
||||
for _, activityUserID := range achievementNotificationUserIDs {
|
||||
vr.sendNotification(ctx, activityUserID, objectUserID, objectID)
|
||||
}
|
||||
if sendInboxNotification {
|
||||
vr.sendVoteInboxNotification(userID, objectUserID, objectID, upVote)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -441,3 +451,40 @@ func (vr *VoteRepo) sendNotification(ctx context.Context, activityUserID, object
|
|||
}
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
func (vr *VoteRepo) sendVoteInboxNotification(triggerUserID, receiverUserID, objectID string, upvote bool) {
|
||||
if triggerUserID == receiverUserID {
|
||||
return
|
||||
}
|
||||
objectType, _ := obj.GetObjectTypeStrByObjectID(objectID)
|
||||
|
||||
msg := &schema.NotificationMsg{
|
||||
TriggerUserID: triggerUserID,
|
||||
ReceiverUserID: receiverUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: objectID,
|
||||
ObjectType: objectType,
|
||||
}
|
||||
if objectType == constant.QuestionObjectType {
|
||||
if upvote {
|
||||
msg.NotificationAction = constant.NotificationUpVotedTheQuestion
|
||||
} else {
|
||||
msg.NotificationAction = constant.NotificationDownVotedTheQuestion
|
||||
}
|
||||
}
|
||||
if objectType == constant.AnswerObjectType {
|
||||
if upvote {
|
||||
msg.NotificationAction = constant.NotificationUpVotedTheAnswer
|
||||
} else {
|
||||
msg.NotificationAction = constant.NotificationDownVotedTheAnswer
|
||||
}
|
||||
}
|
||||
if objectType == constant.CommentObjectType {
|
||||
if upvote {
|
||||
msg.NotificationAction = constant.NotificationUpVotedTheComment
|
||||
}
|
||||
}
|
||||
if len(msg.NotificationAction) > 0 {
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken
|
|||
}
|
||||
|
||||
// RemoveUserTokens Log out all users under this user id
|
||||
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
|
||||
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string, remainToken string) {
|
||||
key := constant.UserTokenMappingCacheKey + userID
|
||||
resp, _ := ar.data.Cache.GetString(ctx, key)
|
||||
mapping := make(map[string]bool, 0)
|
||||
|
@ -158,6 +158,9 @@ func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
|
|||
}
|
||||
|
||||
for token := range mapping {
|
||||
if token == remainToken {
|
||||
continue
|
||||
}
|
||||
if err := ar.RemoveUserCacheInfo(ctx, token); err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
|
|
|
@ -104,26 +104,21 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
|
|||
argsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow)
|
||||
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
|
||||
|
||||
for i, word := range words {
|
||||
if i == 0 {
|
||||
b.Where(builder.Like{"title", word}).
|
||||
Or(builder.Like{"original_text", word})
|
||||
argsQ = append(argsQ, "%"+word+"%")
|
||||
argsQ = append(argsQ, "%"+word+"%")
|
||||
likeConQ := builder.NewCond()
|
||||
likeConA := builder.NewCond()
|
||||
for _, word := range words {
|
||||
likeConQ = likeConQ.Or(builder.Like{"title", word}).
|
||||
Or(builder.Like{"original_text", word})
|
||||
argsQ = append(argsQ, "%"+word+"%")
|
||||
argsQ = append(argsQ, "%"+word+"%")
|
||||
|
||||
ub.Where(builder.Like{"`answer`.original_text", word})
|
||||
argsA = append(argsA, "%"+word+"%")
|
||||
} else {
|
||||
b.Or(builder.Like{"title", word}).
|
||||
Or(builder.Like{"original_text", word})
|
||||
argsQ = append(argsQ, "%"+word+"%")
|
||||
argsQ = append(argsQ, "%"+word+"%")
|
||||
|
||||
ub.Or(builder.Like{"`answer`.original_text", word})
|
||||
argsA = append(argsA, "%"+word+"%")
|
||||
}
|
||||
likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word})
|
||||
argsA = append(argsA, "%"+word+"%")
|
||||
}
|
||||
|
||||
b.Where(likeConQ)
|
||||
ub.Where(likeConA)
|
||||
|
||||
// check tag
|
||||
if len(tagIDs) > 0 {
|
||||
for ti, tagID := range tagIDs {
|
||||
|
@ -234,17 +229,14 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagID
|
|||
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
|
||||
args = append(args, entity.QuestionStatusDeleted, entity.QuestionShow)
|
||||
|
||||
for i, word := range words {
|
||||
if i == 0 {
|
||||
b.Where(builder.Like{"title", word}).
|
||||
Or(builder.Like{"original_text", word})
|
||||
args = append(args, "%"+word+"%")
|
||||
args = append(args, "%"+word+"%")
|
||||
} else {
|
||||
b.Or(builder.Like{"original_text", word})
|
||||
args = append(args, "%"+word+"%")
|
||||
}
|
||||
likeConQ := builder.NewCond()
|
||||
for _, word := range words {
|
||||
likeConQ = likeConQ.Or(builder.Like{"title", word}).
|
||||
Or(builder.Like{"original_text", word})
|
||||
args = append(args, "%"+word+"%")
|
||||
args = append(args, "%"+word+"%")
|
||||
}
|
||||
b.Where(likeConQ)
|
||||
|
||||
// check tag
|
||||
if len(tagIDs) > 0 {
|
||||
|
@ -349,16 +341,14 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs
|
|||
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
|
||||
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
|
||||
|
||||
for i, word := range words {
|
||||
if i == 0 {
|
||||
b.Where(builder.Like{"`answer`.original_text", word})
|
||||
args = append(args, "%"+word+"%")
|
||||
} else {
|
||||
b.Or(builder.Like{"`answer`.original_text", word})
|
||||
args = append(args, "%"+word+"%")
|
||||
}
|
||||
likeConA := builder.NewCond()
|
||||
for _, word := range words {
|
||||
likeConA = likeConA.Or(builder.Like{"`answer`.original_text", word})
|
||||
args = append(args, "%"+word+"%")
|
||||
}
|
||||
|
||||
b.Where(likeConA)
|
||||
|
||||
// check tag
|
||||
if len(tagIDs) > 0 {
|
||||
for ti, tagID := range tagIDs {
|
||||
|
|
|
@ -46,7 +46,7 @@ func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tag
|
|||
// GetTagBySlugName get tag by slug name
|
||||
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
|
||||
tagInfo = &entity.Tag{}
|
||||
session := tr.data.DB.Where("slug_name = LOWER(?)", slugName)
|
||||
session := tr.data.DB.Where("LOWER(slug_name) = ?", slugName)
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
exist, err = session.Get(tagInfo)
|
||||
if err != nil {
|
||||
|
|
|
@ -86,6 +86,9 @@ func (ur *userAdminRepo) GetUserInfo(ctx context.Context, userID string) (user *
|
|||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
|
@ -102,6 +105,9 @@ func (ur *userAdminRepo) GetUserInfoByEmail(ctx context.Context, email string) (
|
|||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
err = tryToDecorateUserInfoFromUserCenter(ctx, ur.data, user)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
|
|
|
@ -196,6 +196,9 @@ func (ur *userRepo) GetUserCount(ctx context.Context) (count int64, err error) {
|
|||
}
|
||||
|
||||
func tryToDecorateUserInfoFromUserCenter(ctx context.Context, data *data.Data, original *entity.User) (err error) {
|
||||
if original == nil {
|
||||
return nil
|
||||
}
|
||||
uc, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return nil
|
||||
|
|
|
@ -125,14 +125,14 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
//answer
|
||||
r.GET("/answer/info", a.answerController.Get)
|
||||
r.GET("/answer/page", a.answerController.AnswerList)
|
||||
r.GET("/personal/answer/page", a.questionController.UserAnswerList)
|
||||
r.GET("/personal/answer/page", a.questionController.PersonalAnswerPage)
|
||||
|
||||
//question
|
||||
r.GET("/question/info", a.questionController.GetQuestion)
|
||||
r.GET("/question/page", a.questionController.QuestionPage)
|
||||
r.GET("/question/similar/tag", a.questionController.SimilarQuestion)
|
||||
r.GET("/personal/qa/top", a.questionController.UserTop)
|
||||
r.GET("/personal/question/page", a.questionController.UserList)
|
||||
r.GET("/personal/question/page", a.questionController.PersonalQuestionPage)
|
||||
|
||||
// comment
|
||||
r.GET("/comment/page", a.commentController.GetCommentWithPage)
|
||||
|
@ -187,7 +187,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
|||
|
||||
// collection
|
||||
r.POST("/collection/switch", a.collectionController.CollectionSwitch)
|
||||
r.GET("/personal/collection/page", a.questionController.UserCollectionList)
|
||||
r.GET("/personal/collection/page", a.questionController.PersonalCollectionPage)
|
||||
|
||||
// question
|
||||
r.POST("/question", a.questionController.AddQuestion)
|
||||
|
@ -248,8 +248,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
|
|||
r.GET("/users/page", a.adminUserController.GetUserPage)
|
||||
r.PUT("/user/status", a.adminUserController.UpdateUserStatus)
|
||||
r.PUT("/user/role", a.adminUserController.UpdateUserRole)
|
||||
r.POST("/user", middleware.BanAPIForUserCenter, a.adminUserController.AddUser)
|
||||
r.PUT("/user/password", middleware.BanAPIForUserCenter, a.adminUserController.UpdateUserPassword)
|
||||
r.POST("/user", a.adminUserController.AddUser)
|
||||
r.PUT("/user/password", a.adminUserController.UpdateUserPassword)
|
||||
|
||||
// reason
|
||||
r.GET("/reasons", a.reasonController.Reasons)
|
||||
|
|
|
@ -63,9 +63,23 @@ func (g *GetPluginConfigResp) SetConfigFields(ctx *gin.Context, fields []plugin.
|
|||
configField.UIOptions.Label = field.UIOptions.Label.Translate(ctx)
|
||||
configField.UIOptions.Text = field.UIOptions.Text.Translate(ctx)
|
||||
if field.UIOptions.Action != nil {
|
||||
configField.UIOptions.Action = &ConfigFieldUIOptionAction{
|
||||
Url: field.UIOptions.Action.Url,
|
||||
uiOptionAction := &UIOptionAction{
|
||||
Url: field.UIOptions.Action.Url,
|
||||
Method: field.UIOptions.Action.Method,
|
||||
}
|
||||
if field.UIOptions.Action.Loading != nil {
|
||||
uiOptionAction.Loading = &LoadingAction{
|
||||
Text: field.UIOptions.Action.Loading.Text.Translate(ctx),
|
||||
State: string(field.UIOptions.Action.Loading.State),
|
||||
}
|
||||
}
|
||||
if field.UIOptions.Action.OnComplete != nil {
|
||||
uiOptionAction.OnCompleteAction = &OnCompleteAction{
|
||||
ToastReturnMessage: field.UIOptions.Action.OnComplete.ToastReturnMessage,
|
||||
RefreshFormConfig: field.UIOptions.Action.OnComplete.RefreshFormConfig,
|
||||
}
|
||||
}
|
||||
configField.UIOptions.Action = uiOptionAction
|
||||
}
|
||||
|
||||
for _, option := range field.Options {
|
||||
|
@ -90,13 +104,13 @@ type ConfigField struct {
|
|||
}
|
||||
|
||||
type ConfigFieldUIOptions struct {
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Rows string `json:"rows,omitempty"`
|
||||
InputType string `json:"input_type,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Action *ConfigFieldUIOptionAction `json:"action,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
Rows string `json:"rows,omitempty"`
|
||||
InputType string `json:"input_type,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Action *UIOptionAction `json:"action,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigFieldOption struct {
|
||||
|
@ -104,8 +118,21 @@ type ConfigFieldOption struct {
|
|||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type ConfigFieldUIOptionAction struct {
|
||||
Url string `json:"url"`
|
||||
type UIOptionAction struct {
|
||||
Url string `json:"url"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Loading *LoadingAction `json:"loading,omitempty"`
|
||||
OnCompleteAction *OnCompleteAction `json:"on_complete,omitempty"`
|
||||
}
|
||||
|
||||
type LoadingAction struct {
|
||||
Text string `json:"text"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
type OnCompleteAction struct {
|
||||
ToastReturnMessage bool `json:"toast_return_message"`
|
||||
RefreshFormConfig bool `json:"refresh_form_config"`
|
||||
}
|
||||
|
||||
type UpdatePluginConfigReq struct {
|
||||
|
|
|
@ -375,3 +375,25 @@ type SiteMapQuestionInfo struct {
|
|||
Title string `json:"title"`
|
||||
UpdateTime string `json:"time"`
|
||||
}
|
||||
|
||||
type PersonalQuestionPageReq struct {
|
||||
Page int `validate:"omitempty,min=1" form:"page"`
|
||||
PageSize int `validate:"omitempty,min=1" form:"page_size"`
|
||||
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
|
||||
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
|
||||
LoginUserID string `json:"-"`
|
||||
}
|
||||
|
||||
type PersonalAnswerPageReq struct {
|
||||
Page int `validate:"omitempty,min=1" form:"page"`
|
||||
PageSize int `validate:"omitempty,min=1" form:"page_size"`
|
||||
OrderCond string `validate:"omitempty,oneof=newest active frequent score unanswered" form:"order"`
|
||||
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
|
||||
LoginUserID string `json:"-"`
|
||||
}
|
||||
|
||||
type PersonalCollectionPageReq struct {
|
||||
Page int `validate:"omitempty,min=1" form:"page"`
|
||||
PageSize int `validate:"omitempty,min=1" form:"page_size"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
|
|
@ -69,8 +69,10 @@ type UserCenterUserSettingsResp struct {
|
|||
}
|
||||
|
||||
type UserCenterAdminFunctionAgentResp struct {
|
||||
UserStatusAgentEnabled bool `json:"user_status_agent_enabled"`
|
||||
UserPasswordAgentEnabled bool `json:"user_password_agent_enabled"`
|
||||
AllowCreateUser bool `json:"allow_create_user"`
|
||||
AllowUpdateUserStatus bool `json:"allow_update_user_status"`
|
||||
AllowUpdateUserPassword bool `json:"allow_update_user_password"`
|
||||
AllowUpdateUserRole bool `json:"allow_update_user_role"`
|
||||
}
|
||||
|
||||
type UserSettingAgent struct {
|
||||
|
|
|
@ -4,14 +4,12 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/gravatar"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
||||
// UserVerifyEmailReq user verify email request
|
||||
|
@ -277,14 +275,14 @@ func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err er
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// UserModifyPassWordRequest
|
||||
type UserModifyPassWordRequest struct {
|
||||
type UserModifyPasswordReq struct {
|
||||
OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
|
||||
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
|
||||
UserID string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
AccessToken string `json:"-"`
|
||||
}
|
||||
|
||||
func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
// TODO i18n
|
||||
err = checker.CheckPassword(8, 32, 0, u.Pass)
|
||||
if err != nil {
|
||||
|
@ -300,7 +298,7 @@ func (u *UserModifyPassWordRequest) Check() (errFields []*validator.FormErrorFie
|
|||
|
||||
type UpdateInfoRequest struct {
|
||||
// display_name
|
||||
DisplayName string `validate:"required,gt=0,lte=30" json:"display_name"`
|
||||
DisplayName string `validate:"omitempty,gt=0,lte=30" json:"display_name"`
|
||||
// username
|
||||
Username string `validate:"omitempty,gt=3,lte=30" json:"username"`
|
||||
// avatar
|
||||
|
@ -329,16 +327,6 @@ func (a *AvatarInfo) ToJsonString() string {
|
|||
}
|
||||
|
||||
func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(req.Username) > 0 {
|
||||
if checker.IsInvalidUsername(req.Username) {
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
req.BioHTML = converter.Markdown2BasicHTML(req.Bio)
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -421,6 +409,7 @@ type GetOtherUserInfoResp struct {
|
|||
type UserChangeEmailSendCodeReq struct {
|
||||
UserVerifyEmailSendReq
|
||||
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"`
|
||||
Pass string `validate:"omitempty,gte=8,lte=32" json:"pass"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
|
|
|
@ -476,7 +476,7 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.A
|
|||
msg.ReceiverUserID = answerInfo.UserID
|
||||
msg.TriggerUserID = answerInfo.UserID
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.YourAnswerWasDeleted
|
||||
msg.NotificationAction = constant.NotificationYourAnswerWasDeleted
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
return nil
|
||||
|
@ -566,7 +566,7 @@ func (as *AnswerService) notificationUpdateAnswer(ctx context.Context, questionU
|
|||
ObjectID: answerID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.UpdateAnswer
|
||||
msg.NotificationAction = constant.NotificationUpdateAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
|
@ -583,7 +583,7 @@ func (as *AnswerService) notificationAnswerTheQuestion(ctx context.Context,
|
|||
ObjectID: answerID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.AnswerTheQuestion
|
||||
msg.NotificationAction = constant.NotificationAnswerTheQuestion
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
userInfo, exist, err := as.userRepo.GetByUserID(ctx, questionUserID)
|
||||
|
|
|
@ -21,7 +21,7 @@ type AuthRepo interface {
|
|||
SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error
|
||||
RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error)
|
||||
AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error)
|
||||
RemoveUserTokens(ctx context.Context, userID string)
|
||||
RemoveUserTokens(ctx context.Context, userID string, remainToken string)
|
||||
}
|
||||
|
||||
// AuthService kit service
|
||||
|
@ -43,7 +43,6 @@ func (as *AuthService) GetUserCacheInfo(ctx context.Context, accessToken string)
|
|||
}
|
||||
cacheInfo, _ := as.authRepo.GetUserStatus(ctx, userCacheInfo.UserID)
|
||||
if cacheInfo != nil {
|
||||
log.Debugf("user status updated: %+v", cacheInfo)
|
||||
userCacheInfo.UserStatus = cacheInfo.UserStatus
|
||||
userCacheInfo.EmailStatus = cacheInfo.EmailStatus
|
||||
userCacheInfo.RoleID = cacheInfo.RoleID
|
||||
|
@ -94,9 +93,14 @@ func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessTo
|
|||
return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken)
|
||||
}
|
||||
|
||||
// RemoveUserTokens Log out all users under this user id
|
||||
func (as *AuthService) RemoveUserTokens(ctx context.Context, userID string) {
|
||||
as.authRepo.RemoveUserTokens(ctx, userID)
|
||||
// RemoveUserAllTokens Log out all users under this user id
|
||||
func (as *AuthService) RemoveUserAllTokens(ctx context.Context, userID string) {
|
||||
as.authRepo.RemoveUserTokens(ctx, userID, "")
|
||||
}
|
||||
|
||||
// RemoveTokensExceptCurrentUser remove all tokens except the current user
|
||||
func (as *AuthService) RemoveTokensExceptCurrentUser(ctx context.Context, userID string, accessToken string) {
|
||||
as.authRepo.RemoveUserTokens(ctx, userID, accessToken)
|
||||
}
|
||||
|
||||
//Admin
|
||||
|
|
|
@ -471,7 +471,7 @@ func (cs *CommentService) notificationQuestionComment(ctx context.Context, quest
|
|||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.CommentQuestion
|
||||
msg.NotificationAction = constant.NotificationCommentQuestion
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
|
||||
|
@ -526,7 +526,7 @@ func (cs *CommentService) notificationAnswerComment(ctx context.Context,
|
|||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.CommentAnswer
|
||||
msg.NotificationAction = constant.NotificationCommentAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
|
||||
|
@ -578,7 +578,7 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
|
|||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.ReplyToYou
|
||||
msg.NotificationAction = constant.NotificationReplyToYou
|
||||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
|
@ -599,7 +599,7 @@ func (cs *CommentService) notificationMention(
|
|||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.MentionYou
|
||||
msg.NotificationAction = constant.NotificationMentionYou
|
||||
notice_queue.AddNotification(msg)
|
||||
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
|
||||
}
|
||||
|
|
|
@ -7,14 +7,15 @@ import (
|
|||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
notficationcommon "github.com/answerdev/answer/internal/service/notification_common"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/i18n"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
|
@ -127,35 +128,47 @@ func (ns *NotificationService) GetNotificationPage(ctx context.Context, searchCo
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err = ns.formatNotificationPage(ctx, notifications)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pager.NewPageModel(total, resp), nil
|
||||
}
|
||||
|
||||
func (ns *NotificationService) formatNotificationPage(ctx context.Context, notifications []*entity.Notification) (
|
||||
resp []*schema.NotificationContent, err error) {
|
||||
lang := handler.GetLangByCtx(ctx)
|
||||
for _, notificationInfo := range notifications {
|
||||
item := &schema.NotificationContent{}
|
||||
err := json.Unmarshal([]byte(notificationInfo.Content), item)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal([]byte(notificationInfo.Content), item); err != nil {
|
||||
log.Error("NotificationContent Unmarshal Error", err.Error())
|
||||
continue
|
||||
}
|
||||
lang, _ := ctx.Value(constant.AcceptLanguageFlag).(i18n.Language)
|
||||
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
|
||||
item.ID = notificationInfo.ID
|
||||
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
|
||||
if notificationInfo.IsRead == schema.NotificationRead {
|
||||
item.IsRead = true
|
||||
// If notification is downvote, the user info is not needed.
|
||||
if item.NotificationAction == constant.NotificationDownVotedTheQuestion ||
|
||||
item.NotificationAction == constant.NotificationDownVotedTheAnswer {
|
||||
item.UserInfo = nil
|
||||
}
|
||||
answerID, ok := item.ObjectInfo.ObjectMap["answer"]
|
||||
if ok {
|
||||
|
||||
item.ID = notificationInfo.ID
|
||||
item.NotificationAction = translator.Tr(lang, item.NotificationAction)
|
||||
item.UpdateTime = notificationInfo.UpdatedAt.Unix()
|
||||
item.IsRead = notificationInfo.IsRead == schema.NotificationRead
|
||||
|
||||
if answerID, ok := item.ObjectInfo.ObjectMap["answer"]; ok {
|
||||
if item.ObjectInfo.ObjectID == answerID {
|
||||
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
|
||||
}
|
||||
item.ObjectInfo.ObjectMap["answer"] = uid.EnShortID(item.ObjectInfo.ObjectMap["answer"])
|
||||
}
|
||||
questionID, ok := item.ObjectInfo.ObjectMap["question"]
|
||||
if ok {
|
||||
if questionID, ok := item.ObjectInfo.ObjectMap["question"]; ok {
|
||||
if item.ObjectInfo.ObjectID == questionID {
|
||||
item.ObjectInfo.ObjectID = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
|
||||
}
|
||||
item.ObjectInfo.ObjectMap["question"] = uid.EnShortID(item.ObjectInfo.ObjectMap["question"])
|
||||
}
|
||||
|
||||
resp = append(resp, item)
|
||||
}
|
||||
return pager.NewPageModel(total, resp), nil
|
||||
return resp, nil
|
||||
}
|
||||
|
|
|
@ -193,10 +193,10 @@ func (ns *NotificationCommon) SendNotificationToAllFollower(ctx context.Context,
|
|||
if msg.NoNeedPushAllFollow {
|
||||
return
|
||||
}
|
||||
if msg.NotificationAction != constant.UpdateQuestion &&
|
||||
msg.NotificationAction != constant.AnswerTheQuestion &&
|
||||
msg.NotificationAction != constant.UpdateAnswer &&
|
||||
msg.NotificationAction != constant.AcceptAnswer {
|
||||
if msg.NotificationAction != constant.NotificationUpdateQuestion &&
|
||||
msg.NotificationAction != constant.NotificationAnswerTheQuestion &&
|
||||
msg.NotificationAction != constant.NotificationUpdateAnswer &&
|
||||
msg.NotificationAction != constant.NotificationAcceptAnswer {
|
||||
return
|
||||
}
|
||||
condObjectID := msg.ObjectID
|
||||
|
|
|
@ -10,10 +10,10 @@ const (
|
|||
QuestionReopen = "question.reopen"
|
||||
QuestionVoteUp = "question.vote_up"
|
||||
QuestionVoteDown = "question.vote_down"
|
||||
QuestionPin = "question.pin" //Top the question
|
||||
QuestionUnPin = "question.unpin" //untop the question
|
||||
QuestionHide = "question.hide" //hide the question
|
||||
QuestionShow = "question.show" //show the question
|
||||
QuestionPin = "question.pin"
|
||||
QuestionUnPin = "question.unpin"
|
||||
QuestionHide = "question.hide"
|
||||
QuestionShow = "question.show"
|
||||
AnswerAdd = "answer.add"
|
||||
AnswerEdit = "answer.edit"
|
||||
AnswerEditWithoutReview = "answer.edit_without_review"
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/pager"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/translator"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
|
@ -270,6 +271,7 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
|||
question.Status = entity.QuestionStatusAvailable
|
||||
question.RevisionID = "0"
|
||||
question.CreatedAt = now
|
||||
question.PostUpdateTime = now
|
||||
question.Pin = entity.QuestionUnPin
|
||||
question.Show = entity.QuestionShow
|
||||
//question.UpdatedAt = nil
|
||||
|
@ -733,70 +735,74 @@ func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobject
|
|||
return qs.tagCommon.CheckChangeReservedTag(ctx, oldobjectTagData, objectTagData)
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserQuestionInfo, int64, error) {
|
||||
userlist := make([]*schema.UserQuestionInfo, 0)
|
||||
// PersonalQuestionPage get question list by user
|
||||
func (qs *QuestionService) PersonalQuestionPage(ctx context.Context, req *schema.PersonalQuestionPageReq) (
|
||||
pageModel *pager.PageModel, err error) {
|
||||
|
||||
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
|
||||
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
|
||||
if err != nil {
|
||||
return userlist, 0, err
|
||||
return nil, err
|
||||
}
|
||||
if !Exist {
|
||||
return userlist, 0, nil
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
search := &schema.QuestionPageReq{}
|
||||
search.OrderCond = order
|
||||
search.Page = page
|
||||
search.PageSize = pageSize
|
||||
search.OrderCond = req.OrderCond
|
||||
search.Page = req.Page
|
||||
search.PageSize = req.PageSize
|
||||
search.UserIDBeSearched = userinfo.ID
|
||||
search.LoginUserID = loginUserID
|
||||
questionlist, count, err := qs.GetQuestionPage(ctx, search)
|
||||
search.LoginUserID = req.LoginUserID
|
||||
questionList, total, err := qs.GetQuestionPage(ctx, search)
|
||||
if err != nil {
|
||||
return userlist, 0, err
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range questionlist {
|
||||
userQuestionInfoList := make([]*schema.UserQuestionInfo, 0)
|
||||
for _, item := range questionList {
|
||||
info := &schema.UserQuestionInfo{}
|
||||
_ = copier.Copy(info, item)
|
||||
status, ok := entity.AdminQuestionSearchStatusIntToString[item.Status]
|
||||
if ok {
|
||||
info.Status = status
|
||||
}
|
||||
userlist = append(userlist, info)
|
||||
userQuestionInfoList = append(userQuestionInfoList, info)
|
||||
}
|
||||
return userlist, count, nil
|
||||
return pager.NewPageModel(total, userQuestionInfoList), nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, order string, page, pageSize int, loginUserID string) ([]*schema.UserAnswerInfo, int64, error) {
|
||||
answerlist := make([]*schema.AnswerInfo, 0)
|
||||
userAnswerlist := make([]*schema.UserAnswerInfo, 0)
|
||||
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, userName)
|
||||
func (qs *QuestionService) PersonalAnswerPage(ctx context.Context, req *schema.PersonalAnswerPageReq) (
|
||||
pageModel *pager.PageModel, err error) {
|
||||
userinfo, exist, err := qs.userCommon.GetUserBasicInfoByUserName(ctx, req.Username)
|
||||
if err != nil {
|
||||
return userAnswerlist, 0, err
|
||||
return nil, err
|
||||
}
|
||||
if !Exist {
|
||||
return userAnswerlist, 0, nil
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
answersearch := &entity.AnswerSearch{}
|
||||
answersearch.UserID = userinfo.ID
|
||||
answersearch.PageSize = pageSize
|
||||
answersearch.Page = page
|
||||
if order == "newest" {
|
||||
answersearch.PageSize = req.PageSize
|
||||
answersearch.Page = req.Page
|
||||
if req.OrderCond == "newest" {
|
||||
answersearch.Order = entity.AnswerSearchOrderByTime
|
||||
} else {
|
||||
answersearch.Order = entity.AnswerSearchOrderByDefault
|
||||
}
|
||||
questionIDs := make([]string, 0)
|
||||
answerList, count, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
|
||||
answerList, total, err := qs.questioncommon.AnswerCommon.Search(ctx, answersearch)
|
||||
if err != nil {
|
||||
return userAnswerlist, count, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
answerlist := make([]*schema.AnswerInfo, 0)
|
||||
userAnswerlist := make([]*schema.UserAnswerInfo, 0)
|
||||
for _, item := range answerList {
|
||||
answerinfo := qs.questioncommon.AnswerCommon.ShowFormat(ctx, item)
|
||||
answerlist = append(answerlist, answerinfo)
|
||||
questionIDs = append(questionIDs, uid.DeShortID(item.QuestionID))
|
||||
}
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.LoginUserID)
|
||||
if err != nil {
|
||||
return userAnswerlist, count, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range answerlist {
|
||||
|
@ -813,34 +819,29 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
|
|||
}
|
||||
}
|
||||
|
||||
return userAnswerlist, count, nil
|
||||
return pager.NewPageModel(total, userAnswerlist), nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, pageSize int, loginUserID string) ([]*schema.QuestionInfo, int64, error) {
|
||||
// PersonalCollectionPage get collection list by user
|
||||
func (qs *QuestionService) PersonalCollectionPage(ctx context.Context, req *schema.PersonalCollectionPageReq) (
|
||||
pageModel *pager.PageModel, err error) {
|
||||
list := make([]*schema.QuestionInfo, 0)
|
||||
userinfo, Exist, err := qs.userCommon.GetUserBasicInfoByID(ctx, loginUserID)
|
||||
if err != nil {
|
||||
return list, 0, err
|
||||
}
|
||||
if !Exist {
|
||||
return list, 0, nil
|
||||
}
|
||||
collectionSearch := &entity.CollectionSearch{}
|
||||
collectionSearch.UserID = userinfo.ID
|
||||
collectionSearch.Page = page
|
||||
collectionSearch.PageSize = pageSize
|
||||
collectionlist, count, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
|
||||
collectionSearch.UserID = req.UserID
|
||||
collectionSearch.Page = req.Page
|
||||
collectionSearch.PageSize = req.PageSize
|
||||
collectionList, total, err := qs.collectionCommon.SearchList(ctx, collectionSearch)
|
||||
if err != nil {
|
||||
return list, 0, err
|
||||
return nil, err
|
||||
}
|
||||
questionIDs := make([]string, 0)
|
||||
for _, item := range collectionlist {
|
||||
for _, item := range collectionList {
|
||||
questionIDs = append(questionIDs, item.ObjectID)
|
||||
}
|
||||
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, loginUserID)
|
||||
questionMaps, err := qs.questioncommon.FindInfoByID(ctx, questionIDs, req.UserID)
|
||||
if err != nil {
|
||||
return list, count, err
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range questionIDs {
|
||||
_, ok := questionMaps[uid.EnShortID(id)]
|
||||
|
@ -853,7 +854,7 @@ func (qs *QuestionService) SearchUserCollectionList(ctx context.Context, page, p
|
|||
}
|
||||
}
|
||||
|
||||
return list, count, nil
|
||||
return pager.NewPageModel(total, list), nil
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SearchUserTopList(ctx context.Context, userName string, loginUserID string) ([]*schema.UserQuestionInfo, []*schema.UserAnswerInfo, error) {
|
||||
|
@ -1063,7 +1064,7 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
|
|||
msg.ReceiverUserID = questionInfo.UserID
|
||||
msg.TriggerUserID = questionInfo.UserID
|
||||
msg.ObjectType = constant.QuestionObjectType
|
||||
msg.NotificationAction = constant.YourQuestionWasDeleted
|
||||
msg.NotificationAction = constant.NotificationYourQuestionWasDeleted
|
||||
notice_queue.AddNotification(msg)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ func (rh *ReportHandle) HandleObject(ctx context.Context, reported *entity.Repor
|
|||
switch req.FlaggedType {
|
||||
case reasonDelete:
|
||||
err = rh.commentRepo.RemoveComment(ctx, objectID)
|
||||
rh.sendNotification(ctx, reportedUserID, objectID, constant.YourCommentWasDeleted)
|
||||
rh.sendNotification(ctx, reportedUserID, objectID, constant.NotificationYourCommentWasDeleted)
|
||||
}
|
||||
}
|
||||
return
|
||||
|
|
|
@ -209,7 +209,7 @@ func (rs *RevisionService) revisionAuditAnswer(ctx context.Context, revisionitem
|
|||
ObjectID: answerinfo.ID,
|
||||
}
|
||||
msg.ObjectType = constant.AnswerObjectType
|
||||
msg.NotificationAction = constant.UpdateAnswer
|
||||
msg.NotificationAction = constant.NotificationUpdateAnswer
|
||||
notice_queue.AddNotification(msg)
|
||||
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
|
|
|
@ -36,12 +36,15 @@ func NewSiteInfoService(
|
|||
tagCommonService *tagcommon.TagCommonService,
|
||||
configRepo config.ConfigRepo,
|
||||
) *SiteInfoService {
|
||||
resp, err := siteInfoCommonService.GetSiteUsers(context.Background())
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
constant.DefaultAvatar = resp.DefaultAvatar
|
||||
usersSiteInfo, _ := siteInfoCommonService.GetSiteUsers(context.Background())
|
||||
if usersSiteInfo != nil {
|
||||
constant.DefaultAvatar = usersSiteInfo.DefaultAvatar
|
||||
}
|
||||
generalSiteInfo, _ := siteInfoCommonService.GetSiteGeneral(context.Background())
|
||||
if generalSiteInfo != nil {
|
||||
constant.DefaultSiteURL = generalSiteInfo.SiteUrl
|
||||
}
|
||||
|
||||
return &SiteInfoService{
|
||||
siteInfoRepo: siteInfoRepo,
|
||||
siteInfoCommonService: siteInfoCommonService,
|
||||
|
@ -116,18 +119,16 @@ func (s *SiteInfoService) GetSiteTheme(ctx context.Context) (resp *schema.SiteTh
|
|||
|
||||
func (s *SiteInfoService) SaveSiteGeneral(ctx context.Context, req schema.SiteGeneralReq) (err error) {
|
||||
req.FormatSiteUrl()
|
||||
var (
|
||||
siteType = "general"
|
||||
content []byte
|
||||
)
|
||||
content, _ = json.Marshal(req)
|
||||
|
||||
data := entity.SiteInfo{
|
||||
Type: siteType,
|
||||
content, _ := json.Marshal(req)
|
||||
data := &entity.SiteInfo{
|
||||
Type: constant.SiteTypeGeneral,
|
||||
Content: string(content),
|
||||
Status: 1,
|
||||
}
|
||||
err = s.siteInfoRepo.SaveByType(ctx, constant.SiteTypeGeneral, data)
|
||||
if err == nil {
|
||||
constant.DefaultSiteURL = req.SiteUrl
|
||||
}
|
||||
|
||||
err = s.siteInfoRepo.SaveByType(ctx, siteType, &data)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -267,8 +268,11 @@ func (s *SiteInfoService) UpdateSMTPConfig(ctx context.Context, req *schema.Upda
|
|||
return
|
||||
}
|
||||
|
||||
func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp, err error) {
|
||||
resp = &schema.SiteSeoResp{}
|
||||
func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoReq, err error) {
|
||||
resp = &schema.SiteSeoReq{}
|
||||
if err = s.siteInfoCommonService.GetSiteInfoByType(ctx, constant.SiteTypeSeo, resp); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
loginConfig, err := s.GetSiteLogin(ctx)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -279,17 +283,6 @@ func (s *SiteInfoService) GetSeo(ctx context.Context) (resp *schema.SiteSeoResp,
|
|||
resp.Robots = "User-agent: *\nDisallow: /"
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
resp = &schema.SiteSeoResp{}
|
||||
siteInfo, exist, err := s.siteInfoRepo.GetByType(ctx, constant.SiteTypeSeo)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return resp, nil
|
||||
}
|
||||
if !exist {
|
||||
return resp, nil
|
||||
}
|
||||
_ = json.Unmarshal([]byte(siteInfo.Content), resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.Upda
|
|||
return err
|
||||
}
|
||||
|
||||
us.authService.RemoveUserTokens(ctx, req.UserID)
|
||||
us.authService.RemoveUserAllTokens(ctx, req.UserID)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -179,7 +179,7 @@ func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.
|
|||
return err
|
||||
}
|
||||
// logout this user
|
||||
us.authService.RemoveUserTokens(ctx, req.UserID)
|
||||
us.authService.RemoveUserAllTokens(ctx, req.UserID)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -204,19 +204,28 @@ func (us *UserCenterLoginService) UserCenterUserSettings(ctx context.Context, us
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// UserCenterAdminFunctionAgent Check in the backend administration interface if the user-related functions
|
||||
// are turned off due to turning on the User Center plugin.
|
||||
func (us *UserCenterLoginService) UserCenterAdminFunctionAgent(ctx context.Context) (
|
||||
resp *schema.UserCenterAdminFunctionAgentResp, err error) {
|
||||
resp = &schema.UserCenterAdminFunctionAgentResp{}
|
||||
resp = &schema.UserCenterAdminFunctionAgentResp{
|
||||
AllowCreateUser: true,
|
||||
AllowUpdateUserStatus: true,
|
||||
AllowUpdateUserPassword: true,
|
||||
AllowUpdateUserRole: true,
|
||||
}
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
desc := userCenter.Description()
|
||||
// If user status agent is enabled, admin can not update user status in answer.
|
||||
resp.UserStatusAgentEnabled = desc.UserStatusAgentEnabled
|
||||
// If original user system is enabled, admin can update user password in answer.
|
||||
// So user password agent is disabled.
|
||||
resp.UserPasswordAgentEnabled = !desc.EnabledOriginalUserSystem
|
||||
resp.AllowUpdateUserStatus = !desc.UserStatusAgentEnabled
|
||||
|
||||
// If original user system is enabled, admin can update user password and role in answer.
|
||||
resp.AllowUpdateUserPassword = desc.EnabledOriginalUserSystem
|
||||
resp.AllowUpdateUserRole = desc.EnabledOriginalUserSystem
|
||||
resp.AllowCreateUser = desc.EnabledOriginalUserSystem
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/random"
|
||||
"github.com/answerdev/answer/pkg/token"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -318,3 +319,33 @@ func (us *UserExternalLoginService) ExternalLoginUnbinding(
|
|||
|
||||
return nil, us.userExternalLoginRepo.DeleteUserExternalLogin(ctx, req.UserID, req.ExternalID)
|
||||
}
|
||||
|
||||
// CheckUserStatusInUserCenter check user status in user center
|
||||
func (us *UserExternalLoginService) CheckUserStatusInUserCenter(ctx context.Context, userID string) (
|
||||
valid bool, externalID string, err error) {
|
||||
// If enable user center plugin, user status should be checked by user center
|
||||
userCenter, ok := plugin.GetUserCenter()
|
||||
if !ok {
|
||||
return true, "", nil
|
||||
}
|
||||
userInfoList, err := us.GetExternalLoginUserInfoList(ctx, userID)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
var thisUcUserInfo *entity.UserExternalLogin
|
||||
for _, t := range userInfoList {
|
||||
if t.Provider == userCenter.Info().SlugName {
|
||||
thisUcUserInfo = t
|
||||
break
|
||||
}
|
||||
}
|
||||
// If this user not login by user center, no need to check user status
|
||||
if thisUcUserInfo == nil {
|
||||
return true, "", nil
|
||||
}
|
||||
userStatus := userCenter.UserStatus(thisUcUserInfo.ExternalID)
|
||||
if userStatus == plugin.UserStatusDeleted {
|
||||
return false, thisUcUserInfo.ExternalID, nil
|
||||
}
|
||||
return true, thisUcUserInfo.ExternalID, nil
|
||||
}
|
||||
|
|
|
@ -80,6 +80,9 @@ func (us *UserService) GetUserInfoByUserID(ctx context.Context, token, userID st
|
|||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
if userInfo.Status == entity.UserStatusDeleted {
|
||||
return nil, errors.Unauthorized(reason.UnauthorizedError)
|
||||
}
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
|
@ -119,10 +122,17 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
|
|||
if !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {
|
||||
return nil, errors.BadRequest(reason.EmailOrPasswordWrong)
|
||||
}
|
||||
ok, externalID, err := us.userExternalLoginService.CheckUserStatusInUserCenter(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.BadRequest(reason.EmailOrPasswordWrong)
|
||||
}
|
||||
|
||||
err = us.userRepo.UpdateLastLoginDate(ctx, userInfo.ID)
|
||||
if err != nil {
|
||||
log.Error("UpdateLastLoginDate", err.Error())
|
||||
log.Errorf("update last login data failed, err: %v", err)
|
||||
}
|
||||
|
||||
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
|
||||
|
@ -137,6 +147,7 @@ func (us *UserService) EmailLogin(ctx context.Context, req *schema.UserEmailLogi
|
|||
EmailStatus: userInfo.MailStatus,
|
||||
UserStatus: userInfo.Status,
|
||||
RoleID: roleID,
|
||||
ExternalID: externalID,
|
||||
}
|
||||
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
|
||||
if err != nil {
|
||||
|
@ -178,42 +189,43 @@ func (us *UserService) RetrievePassWord(ctx context.Context, req *schema.UserRet
|
|||
return nil
|
||||
}
|
||||
|
||||
// UseRePassword
|
||||
func (us *UserService) UseRePassword(ctx context.Context, req *schema.UserRePassWordRequest) (resp *schema.GetUserResp, err error) {
|
||||
// UpdatePasswordWhenForgot update user password when user forgot password
|
||||
func (us *UserService) UpdatePasswordWhenForgot(ctx context.Context, req *schema.UserRePassWordRequest) (err error) {
|
||||
data := &schema.EmailCodeContent{}
|
||||
err = data.FromJSONString(req.Content)
|
||||
if err != nil {
|
||||
return nil, errors.BadRequest(reason.EmailVerifyURLExpired)
|
||||
return errors.BadRequest(reason.EmailVerifyURLExpired)
|
||||
}
|
||||
|
||||
userInfo, exist, err := us.userRepo.GetByEmail(ctx, data.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
return errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
enpass, err := us.encryptPassword(ctx, req.Pass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
resp = &schema.GetUserResp{}
|
||||
return resp, nil
|
||||
// When the user changes the password, all the current user's tokens are invalid.
|
||||
us.authService.RemoveUserAllTokens(ctx, userInfo.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, request *schema.UserModifyPassWordRequest) (bool, error) {
|
||||
userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
|
||||
func (us *UserService) UserModifyPassWordVerification(ctx context.Context, req *schema.UserModifyPasswordReq) (bool, error) {
|
||||
userInfo, has, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !has {
|
||||
return false, fmt.Errorf("user does not exist")
|
||||
return false, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
|
||||
isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
|
||||
if !isPass {
|
||||
return false, nil
|
||||
}
|
||||
|
@ -222,26 +234,29 @@ func (us *UserService) UserModifyPassWordVerification(ctx context.Context, reque
|
|||
}
|
||||
|
||||
// UserModifyPassword user modify password
|
||||
func (us *UserService) UserModifyPassword(ctx context.Context, request *schema.UserModifyPassWordRequest) error {
|
||||
enpass, err := us.encryptPassword(ctx, request.Pass)
|
||||
func (us *UserService) UserModifyPassword(ctx context.Context, req *schema.UserModifyPasswordReq) error {
|
||||
enpass, err := us.encryptPassword(ctx, req.Pass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userInfo, has, err := us.userRepo.GetByUserID(ctx, request.UserID)
|
||||
userInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return fmt.Errorf("user does not exist")
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
isPass := us.verifyPassword(ctx, request.OldPass, userInfo.Pass)
|
||||
|
||||
isPass := us.verifyPassword(ctx, req.OldPass, userInfo.Pass)
|
||||
if !isPass {
|
||||
return fmt.Errorf("the old password verification failed")
|
||||
return errors.BadRequest(reason.OldPasswordVerificationFailed)
|
||||
}
|
||||
err = us.userRepo.UpdatePass(ctx, userInfo.ID, enpass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
us.authService.RemoveTokensExceptCurrentUser(ctx, userInfo.ID, req.AccessToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -252,15 +267,23 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
|
||||
if len(req.Username) > 0 {
|
||||
if siteUsers.AllowUpdateUsername && len(req.Username) > 0 {
|
||||
if checker.IsInvalidUsername(req.Username) {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
if checker.IsReservedUsername(req.Username) {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
|
||||
userInfo, exist, err := us.userRepo.GetByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -272,13 +295,14 @@ func (us *UserService) UpdateInfo(ctx context.Context, req *schema.UpdateInfoReq
|
|||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameDuplicate)
|
||||
}
|
||||
if checker.IsReservedUsername(req.Username) {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
})
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
oldUserInfo, exist, err := us.userRepo.GetByUserID(ctx, req.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
|
||||
cond := us.formatUserInfoForUpdateInfo(oldUserInfo, req, siteUsers)
|
||||
|
@ -300,13 +324,13 @@ func (us *UserService) formatUserInfoForUpdateInfo(
|
|||
userInfo.Location = oldUserInfo.Location
|
||||
userInfo.ID = req.UserID
|
||||
|
||||
if siteUsersConf.AllowUpdateDisplayName {
|
||||
if len(req.DisplayName) > 0 && siteUsersConf.AllowUpdateDisplayName {
|
||||
userInfo.DisplayName = req.DisplayName
|
||||
}
|
||||
if siteUsersConf.AllowUpdateUsername {
|
||||
if len(req.Username) > 0 && siteUsersConf.AllowUpdateUsername {
|
||||
userInfo.Username = req.Username
|
||||
}
|
||||
if siteUsersConf.AllowUpdateAvatar {
|
||||
if len(avatar) > 0 && siteUsersConf.AllowUpdateAvatar {
|
||||
userInfo.Avatar = string(avatar)
|
||||
}
|
||||
if siteUsersConf.AllowUpdateBio {
|
||||
|
@ -543,6 +567,15 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
|
|||
return nil, errors.BadRequest(reason.UserNotFound)
|
||||
}
|
||||
|
||||
// If user's email already verified, then must verify password first.
|
||||
if userInfo.MailStatus == entity.EmailStatusAvailable && !us.verifyPassword(ctx, req.Pass, userInfo.Pass) {
|
||||
resp = append(resp, &validator.FormErrorField{
|
||||
ErrorField: "pass",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.OldPasswordVerificationFailed),
|
||||
})
|
||||
return resp, errors.BadRequest(reason.OldPasswordVerificationFailed)
|
||||
}
|
||||
|
||||
_, exist, err = us.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -63,12 +63,12 @@ func NewVoteService(
|
|||
}
|
||||
|
||||
// VoteUp vote up
|
||||
func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
|
||||
func (vs *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
|
||||
voteResp = &schema.VoteResp{}
|
||||
|
||||
var objectUserID string
|
||||
|
||||
objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID)
|
||||
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -80,19 +80,19 @@ func (as *VoteService) VoteUp(ctx context.Context, dto *schema.VoteDTO) (voteRes
|
|||
}
|
||||
|
||||
if dto.IsCancel {
|
||||
return as.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
return vs.voteRepo.VoteUpCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
} else {
|
||||
return as.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
return vs.voteRepo.VoteUp(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
}
|
||||
}
|
||||
|
||||
// VoteDown vote down
|
||||
func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
|
||||
func (vs *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteResp *schema.VoteResp, err error) {
|
||||
voteResp = &schema.VoteResp{}
|
||||
|
||||
var objectUserID string
|
||||
|
||||
objectUserID, err = as.GetObjectUserID(ctx, dto.ObjectID)
|
||||
objectUserID, err = vs.GetObjectUserID(ctx, dto.ObjectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -104,9 +104,9 @@ func (as *VoteService) VoteDown(ctx context.Context, dto *schema.VoteDTO) (voteR
|
|||
}
|
||||
|
||||
if dto.IsCancel {
|
||||
return as.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
return vs.voteRepo.VoteDownCancel(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
} else {
|
||||
return as.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
return vs.voteRepo.VoteDown(ctx, dto.ObjectID, dto.UserID, objectUserID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ func Markdown2HTML(source string) string {
|
|||
filter.RequireNoFollowOnLinks(false)
|
||||
filter.RequireParseableURLs(false)
|
||||
filter.RequireNoFollowOnFullyQualifiedLinks(false)
|
||||
filter.AllowElements("kbd")
|
||||
html = filter.Sanitize(html)
|
||||
return html
|
||||
}
|
||||
|
@ -46,7 +47,7 @@ func Markdown2HTML(source string) string {
|
|||
func Markdown2BasicHTML(source string) string {
|
||||
content := Markdown2HTML(source)
|
||||
filter := bluemonday.NewPolicy()
|
||||
filter.AllowElements("p", "b", "br")
|
||||
filter.AllowElements("p", "b", "br", "strong", "em")
|
||||
filter.AllowAttrs("src").OnElements("img")
|
||||
filter.AddSpaceWhenStrippingTag(true)
|
||||
content = filter.Sanitize(content)
|
||||
|
@ -87,7 +88,11 @@ func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, n
|
|||
l := n.Segments.Len()
|
||||
for i := 0; i < l; i++ {
|
||||
segment := n.Segments.At(i)
|
||||
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
|
||||
if string(source[segment.Start:segment.Stop]) == "<kbd>" || string(source[segment.Start:segment.Stop]) == "</kbd>" {
|
||||
_, _ = w.Write(segment.Value(source))
|
||||
} else {
|
||||
_, _ = w.Write(r.Filter.SanitizeBytes(segment.Value(source)))
|
||||
}
|
||||
}
|
||||
return ast.WalkSkipChildren, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
@ -15,3 +16,9 @@ var (
|
|||
CallAgent,
|
||||
registerAgent = MakePlugin[Agent](true)
|
||||
)
|
||||
|
||||
// SiteURL The site url is the domain address of the current site. e.g. http://localhost:8080
|
||||
// When some Agent plugins want to redirect to the origin site, it can use this function to get the site url.
|
||||
func SiteURL() string {
|
||||
return constant.DefaultSiteURL
|
||||
}
|
||||
|
|
|
@ -44,13 +44,13 @@ type ConfigField struct {
|
|||
}
|
||||
|
||||
type ConfigFieldUIOptions struct {
|
||||
Placeholder Translator `json:"placeholder,omitempty"`
|
||||
Rows string `json:"rows,omitempty"`
|
||||
InputType InputType `json:"input_type,omitempty"`
|
||||
Label Translator `json:"label,omitempty"`
|
||||
Action *ConfigFieldUIOptionAction `json:"action,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Text Translator `json:"text,omitempty"`
|
||||
Placeholder Translator `json:"placeholder,omitempty"`
|
||||
Rows string `json:"rows,omitempty"`
|
||||
InputType InputType `json:"input_type,omitempty"`
|
||||
Label Translator `json:"label,omitempty"`
|
||||
Action *UIOptionAction `json:"action,omitempty"`
|
||||
Variant string `json:"variant,omitempty"`
|
||||
Text Translator `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type ConfigFieldOption struct {
|
||||
|
@ -58,8 +58,29 @@ type ConfigFieldOption struct {
|
|||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type ConfigFieldUIOptionAction struct {
|
||||
Url string `json:"url"`
|
||||
type UIOptionAction struct {
|
||||
Url string `json:"url"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Loading *LoadingAction `json:"loading,omitempty"`
|
||||
OnComplete *OnCompleteAction `json:"on_complete,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
LoadingActionStateNone LoadingActionType = "none"
|
||||
LoadingActionStatePending LoadingActionType = "pending"
|
||||
LoadingActionStateComplete LoadingActionType = "completed"
|
||||
)
|
||||
|
||||
type LoadingActionType string
|
||||
|
||||
type LoadingAction struct {
|
||||
Text Translator `json:"text"`
|
||||
State LoadingActionType `json:"state"`
|
||||
}
|
||||
|
||||
type OnCompleteAction struct {
|
||||
ToastReturnMessage bool `json:"toast_return_message"`
|
||||
RefreshFormConfig bool `json:"refresh_form_config"`
|
||||
}
|
||||
|
||||
type Config interface {
|
||||
|
|
|
@ -140,7 +140,7 @@ type TranslateFn func(ctx *GinContext) string
|
|||
|
||||
// Translator contains a function that translates the key to the current language of the context
|
||||
type Translator struct {
|
||||
fn TranslateFn
|
||||
Fn TranslateFn
|
||||
}
|
||||
|
||||
// MakeTranslator generates a translator from the key
|
||||
|
@ -148,13 +148,13 @@ func MakeTranslator(key string) Translator {
|
|||
t := func(ctx *GinContext) string {
|
||||
return Translate(ctx, key)
|
||||
}
|
||||
return Translator{fn: t}
|
||||
return Translator{Fn: t}
|
||||
}
|
||||
|
||||
// Translate translates the key to the current language of the context
|
||||
func (t Translator) Translate(ctx *GinContext) string {
|
||||
if &t == nil || t.fn == nil {
|
||||
if &t == nil || t.Fn == nil {
|
||||
return ""
|
||||
}
|
||||
return t.fn(ctx)
|
||||
return t.Fn(ctx)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="theme-color" content="#0033FF" />
|
||||
<meta name="generator" content="Answer - https://github.com/answerdev/answer">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
</head>
|
||||
|
|
|
@ -15,18 +15,6 @@ export const USER_AGENT_NAMES = {
|
|||
DingTalk: 'DingTalk',
|
||||
};
|
||||
|
||||
export const IGNORE_PATH_LIST = [
|
||||
'/users/login',
|
||||
'/users/register',
|
||||
'/users/account-recovery',
|
||||
'/users/change-email',
|
||||
'/users/password-reset',
|
||||
'/users/account-activation',
|
||||
'/users/account-activation/success',
|
||||
'/users/account-activation/failed',
|
||||
'/users/confirm-new-email',
|
||||
];
|
||||
|
||||
export const ADMIN_LIST_STATUS = {
|
||||
// normal;
|
||||
1: {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
export interface FormValue<T = any> {
|
||||
value: T;
|
||||
isInvalid: boolean;
|
||||
|
@ -549,6 +547,11 @@ export interface User {
|
|||
avatar: string;
|
||||
}
|
||||
|
||||
export interface QuestionOperationReq {
|
||||
id: string;
|
||||
operation: 'pin' | 'unpin' | 'hide' | 'show';
|
||||
}
|
||||
|
||||
export interface OauthBindEmailReq {
|
||||
binding_key: string;
|
||||
email: string;
|
||||
|
@ -565,27 +568,6 @@ export interface UserOauthConnectorItem extends OauthConnectorItem {
|
|||
binding: boolean;
|
||||
external_id: string;
|
||||
}
|
||||
export interface PluginOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
name: string;
|
||||
type: UIWidget;
|
||||
title: string;
|
||||
description: string;
|
||||
ui_options?: UIOptions;
|
||||
options?: PluginOption[];
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
slug_name: string;
|
||||
config_fields: PluginItem[];
|
||||
}
|
||||
|
||||
export interface QuestionOperationReq {
|
||||
id: string;
|
||||
|
|
|
@ -11,6 +11,9 @@ const Index: FC = () => {
|
|||
let primaryColor;
|
||||
if (theme_config?.[theme]?.primary_color) {
|
||||
primaryColor = Color(theme_config[theme].primary_color);
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute('content', primaryColor.hex());
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -55,9 +58,12 @@ const Index: FC = () => {
|
|||
--bs-pagination-active-border-color: ${primaryColor.hex()};
|
||||
}
|
||||
.form-select:focus,
|
||||
.form-control:focus {
|
||||
box-shadow: 0 0 0 0.25rem ${primaryColor.fade(0.75).string()};
|
||||
border-color: ${tintColor(primaryColor, 0.5)};
|
||||
.form-control:focus,
|
||||
.form-control.focus{
|
||||
box-shadow: 0 0 0 0.25rem ${primaryColor
|
||||
.fade(0.75)
|
||||
.string()} !important;
|
||||
border-color: ${tintColor(primaryColor, 0.5)} !important;
|
||||
}
|
||||
.form-check-input:checked {
|
||||
background-color: ${primaryColor.hex()};
|
||||
|
@ -80,7 +86,7 @@ const Index: FC = () => {
|
|||
color: ${primaryColor.hex()}!important;
|
||||
}
|
||||
.link-primary:hover, .link-primary:focus {
|
||||
color: ${shadeColor(primaryColor, 0.8).hex()}!important
|
||||
color: ${shadeColor(primaryColor, 0.8).hex()}!important;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
|
|
@ -101,9 +101,12 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
variant="success"
|
||||
id="dropdown-uca"
|
||||
as="span"
|
||||
className="no-toggle pointer p-0">
|
||||
className="no-toggle">
|
||||
<Nav>
|
||||
<Icon name="grid-3x3-gap-fill" className="nav-link fs-4 ms-3" />
|
||||
<Icon
|
||||
name="grid-3x3-gap-fill"
|
||||
className="nav-link pointer p-0 fs-4 ms-3"
|
||||
/>
|
||||
</Nav>
|
||||
</Dropdown.Toggle>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
background: linear-gradient(180deg, rgb(var(--bs-primary-rgb)) 0%, rgba(var(--bs-primary-rgb), 0.95) 100%);
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15), 0 0.125rem 0.25rem rgb(0 0 0 / 8%);
|
||||
.logo {
|
||||
max-height: 1.75rem;
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { floppyNavigation, userCenter } from '@/utils';
|
||||
import { userCenter } from '@/utils';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
siteInfoStore,
|
||||
|
@ -29,7 +29,6 @@ import {
|
|||
themeSettingStore,
|
||||
} from '@/stores';
|
||||
import { logout, useQueryNotificationStatus } from '@/services';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
|
||||
import NavItems from './components/NavItems';
|
||||
|
||||
|
@ -73,19 +72,6 @@ const Header: FC = () => {
|
|||
clearUserStore();
|
||||
window.location.replace(window.location.href);
|
||||
};
|
||||
const onLoginClick = (evt) => {
|
||||
if (location.pathname === RouteAlias.login) {
|
||||
evt.preventDefault();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
if (floppyNavigation.shouldProcessLinkClick(evt)) {
|
||||
evt.preventDefault();
|
||||
floppyNavigation.navigateToLogin({
|
||||
handler: navigate,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (q) {
|
||||
|
@ -123,17 +109,17 @@ const Header: FC = () => {
|
|||
/>
|
||||
|
||||
<div className="d-flex justify-content-between align-items-center nav-grow flex-nowrap">
|
||||
<Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-3">
|
||||
<Navbar.Brand to="/" as={Link} className="lh-1 me-0 me-sm-3 p-0">
|
||||
{brandingInfo.logo ? (
|
||||
<>
|
||||
<img
|
||||
className="d-none d-lg-block logo rounded-1 me-0"
|
||||
className="d-none d-lg-block logo me-0"
|
||||
src={brandingInfo.logo}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<img
|
||||
className="lg-none logo rounded-1 me-0"
|
||||
className="lg-none logo me-0"
|
||||
src={brandingInfo.mobile_logo || brandingInfo.logo}
|
||||
alt=""
|
||||
/>
|
||||
|
@ -155,7 +141,6 @@ const Header: FC = () => {
|
|||
'link-light': navbarStyle === 'theme-colored',
|
||||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href={userCenter.getLoginUrl()}>
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
|
@ -243,7 +228,6 @@ const Header: FC = () => {
|
|||
'link-light': navbarStyle === 'theme-colored',
|
||||
'link-primary': navbarStyle !== 'theme-colored',
|
||||
})}
|
||||
onClick={onLoginClick}
|
||||
href={userCenter.getLoginUrl()}>
|
||||
{t('btns.login')}
|
||||
</Button>
|
||||
|
|
|
@ -10,7 +10,9 @@ import { useHotQuestions } from '@/services';
|
|||
const HotQuestions: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'question' });
|
||||
const { data: questionRes } = useHotQuestions();
|
||||
|
||||
if (!questionRes?.list?.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header className="text-nowrap text-capitalize">
|
||||
|
|
|
@ -66,8 +66,13 @@ const Index: FC<{
|
|||
contentClassName="bg-transparent"
|
||||
onHide={onClose}>
|
||||
<Modal.Body onClick={onClose} className="img-viewer p-0 d-flex">
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<img
|
||||
className="cursor-zoom-out img-fluid m-auto"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
src={imgSrc}
|
||||
alt={imgSrc}
|
||||
/>
|
||||
|
|
|
@ -51,7 +51,7 @@ const Index: React.FC<IProps> = ({
|
|||
type="text"
|
||||
autoComplete="off"
|
||||
placeholder={t('placeholder')}
|
||||
isInvalid={captcha.isInvalid}
|
||||
isInvalid={captcha?.isInvalid}
|
||||
onChange={(e) => {
|
||||
Storage.set(CAPTCHA_CODE_STORAGE_KEY, e.target.value);
|
||||
handleCaptcha({
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import React, { FC, useState } from 'react';
|
||||
import { Button, ButtonProps } from 'react-bootstrap';
|
||||
import React, { FC, useLayoutEffect, useState } from 'react';
|
||||
import { Button, ButtonProps, Spinner } from 'react-bootstrap';
|
||||
|
||||
import { request } from '@/utils';
|
||||
import type * as Type from '@/common/interface';
|
||||
import type { UIAction } from '../index.d';
|
||||
import type { UIAction, FormKit } from '../types';
|
||||
import { useToast } from '@/hooks';
|
||||
|
||||
interface Props {
|
||||
fieldName: string;
|
||||
text: string;
|
||||
action: UIAction | undefined;
|
||||
formData: Type.FormDataType;
|
||||
formKit: FormKit;
|
||||
readOnly: boolean;
|
||||
variant?: ButtonProps['variant'];
|
||||
size?: ButtonProps['size'];
|
||||
|
@ -17,24 +17,65 @@ interface Props {
|
|||
const Index: FC<Props> = ({
|
||||
fieldName,
|
||||
action,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
formData,
|
||||
formKit,
|
||||
text = '',
|
||||
readOnly = false,
|
||||
variant = 'primary',
|
||||
size,
|
||||
}) => {
|
||||
const Toast = useToast();
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const handleAction = async () => {
|
||||
const handleToast = (msg, type: 'success' | 'danger' = 'success') => {
|
||||
const tm = action?.on_complete?.toast_return_message;
|
||||
if (tm === false || !msg) {
|
||||
return;
|
||||
}
|
||||
Toast.onShow({
|
||||
msg,
|
||||
variant: type,
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const handleCallback = (resp) => {
|
||||
const callback = action?.on_complete;
|
||||
if (callback?.refresh_form_config) {
|
||||
formKit.refreshConfig();
|
||||
}
|
||||
};
|
||||
const handleAction = () => {
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const method = action.method || 'get';
|
||||
await request[method](action.url);
|
||||
setLoading(false);
|
||||
request
|
||||
.request({
|
||||
method: action.method,
|
||||
url: action.url,
|
||||
timeout: 0,
|
||||
})
|
||||
.then((resp) => {
|
||||
if ('message' in resp) {
|
||||
handleToast(resp.message, 'success');
|
||||
}
|
||||
handleCallback(resp);
|
||||
})
|
||||
.catch((ex) => {
|
||||
if (ex && 'msg' in ex) {
|
||||
handleToast(ex.msg, 'danger');
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
useLayoutEffect(() => {
|
||||
if (action?.loading?.state === 'pending') {
|
||||
setLoading(true);
|
||||
}
|
||||
}, []);
|
||||
const loadingText = action?.loading?.text || text;
|
||||
const disabled = isLoading || readOnly;
|
||||
|
||||
return (
|
||||
<div className="d-flex">
|
||||
<Button
|
||||
|
@ -43,8 +84,19 @@ const Index: FC<Props> = ({
|
|||
disabled={disabled}
|
||||
size={size}
|
||||
variant={variant}>
|
||||
{text || fieldName}
|
||||
{isLoading ? '...' : ''}
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Spinner
|
||||
className="align-middle me-2"
|
||||
animation="border"
|
||||
size="sm"
|
||||
variant={variant}
|
||||
/>
|
||||
{loadingText}
|
||||
</>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
export interface UIAction {
|
||||
url: string;
|
||||
method?: 'get' | 'post' | 'put' | 'delete';
|
||||
event?: 'click' | 'change';
|
||||
handler?: ({evt, formData, request}) => Promise<void>
|
||||
}
|
|
@ -4,14 +4,15 @@ import React, {
|
|||
useImperativeHandle,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { Form, Button, ButtonProps } from 'react-bootstrap';
|
||||
import { Form, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
import type { UIAction } from './index.d';
|
||||
import type { JSONSchema, UISchema, BaseUIOptions, FormKit } from './types';
|
||||
import {
|
||||
Legend,
|
||||
Select,
|
||||
|
@ -21,122 +22,16 @@ import {
|
|||
Upload,
|
||||
Textarea,
|
||||
Input,
|
||||
Button as CtrlButton,
|
||||
Button as SfButton,
|
||||
} from './components';
|
||||
|
||||
export interface JSONSchema {
|
||||
title: string;
|
||||
description?: string;
|
||||
required?: string[];
|
||||
properties: {
|
||||
[key: string]: {
|
||||
type: 'string' | 'boolean' | 'number';
|
||||
title: string;
|
||||
description?: string;
|
||||
enum?: Array<string | boolean | number>;
|
||||
enumNames?: string[];
|
||||
default?: string | boolean | number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface BaseUIOptions {
|
||||
empty?: string;
|
||||
// Will be appended to the className of the form component itself
|
||||
className?: classnames.Argument;
|
||||
// The className that will be attached to a form field container
|
||||
fieldClassName?: classnames.Argument;
|
||||
// Make a form component render into simplified mode
|
||||
readOnly?: boolean;
|
||||
simplify?: boolean;
|
||||
validator?: (
|
||||
value,
|
||||
formData?,
|
||||
) => Promise<string | true | void> | true | string;
|
||||
}
|
||||
|
||||
export interface InputOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
inputType?:
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
| 'email'
|
||||
| 'month'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'range'
|
||||
| 'search'
|
||||
| 'tel'
|
||||
| 'text'
|
||||
| 'time'
|
||||
| 'url'
|
||||
| 'week';
|
||||
}
|
||||
export interface SelectOptions extends BaseUIOptions {}
|
||||
export interface UploadOptions extends BaseUIOptions {
|
||||
acceptType?: string;
|
||||
imageType?: Type.UploadType;
|
||||
}
|
||||
|
||||
export interface SwitchOptions extends BaseUIOptions {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface TimezoneOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface CheckboxOptions extends BaseUIOptions {}
|
||||
|
||||
export interface RadioOptions extends BaseUIOptions {}
|
||||
|
||||
export interface TextareaOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
export interface ButtonOptions extends BaseUIOptions {
|
||||
text: string;
|
||||
icon?: string;
|
||||
action?: UIAction;
|
||||
variant?: ButtonProps['variant'];
|
||||
size?: ButtonProps['size'];
|
||||
}
|
||||
|
||||
export type UIOptions =
|
||||
| InputOptions
|
||||
| SelectOptions
|
||||
| UploadOptions
|
||||
| SwitchOptions
|
||||
| TimezoneOptions
|
||||
| CheckboxOptions
|
||||
| RadioOptions
|
||||
| TextareaOptions
|
||||
| ButtonOptions;
|
||||
|
||||
export type UIWidget =
|
||||
| 'textarea'
|
||||
| 'input'
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'select'
|
||||
| 'upload'
|
||||
| 'timezone'
|
||||
| 'switch'
|
||||
| 'legend'
|
||||
| 'button';
|
||||
export interface UISchema {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?: UIOptions;
|
||||
};
|
||||
}
|
||||
export * from './types';
|
||||
|
||||
interface IProps {
|
||||
schema: JSONSchema;
|
||||
schema: JSONSchema | null;
|
||||
formData: Type.FormDataType | null;
|
||||
uiSchema?: UISchema;
|
||||
formData?: Type.FormDataType;
|
||||
refreshConfig?: FormKit['refreshConfig'];
|
||||
hiddenSubmit?: boolean;
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
|
@ -148,6 +43,7 @@ interface IRef {
|
|||
|
||||
/**
|
||||
* TODO:
|
||||
* - [!] Standardised `Admin/Plugins/Config/index.tsx` method for generating dynamic form configurations.
|
||||
* - Normalize and document `formData[key].hidden && 'd-none'`
|
||||
* - Normalize and document `hiddenSubmit`
|
||||
* - Improving field hints for `formData`
|
||||
|
@ -168,7 +64,8 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
{
|
||||
schema,
|
||||
uiSchema = {},
|
||||
formData = {},
|
||||
refreshConfig,
|
||||
formData,
|
||||
onChange,
|
||||
onSubmit,
|
||||
hiddenSubmit = false,
|
||||
|
@ -181,11 +78,10 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
const { required = [], properties = {} } = schema || {};
|
||||
// check required field
|
||||
const excludes = required.filter((key) => !properties[key]);
|
||||
|
||||
if (excludes.length > 0) {
|
||||
console.error(t('not_found_props', { key: excludes.join(', ') }));
|
||||
}
|
||||
|
||||
formData ||= {};
|
||||
const keys = Object.keys(properties);
|
||||
/**
|
||||
* Prevent components such as `select` from having default values,
|
||||
|
@ -193,14 +89,14 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
*/
|
||||
const setDefaultValueAsDomBehaviour = () => {
|
||||
keys.forEach((k) => {
|
||||
const fieldVal = formData[k]?.value;
|
||||
const fieldVal = formData![k]?.value;
|
||||
const metaProp = properties[k];
|
||||
const uiCtrl = uiSchema[k]?.['ui:widget'];
|
||||
if (!metaProp || !uiCtrl || fieldVal !== undefined) {
|
||||
return;
|
||||
}
|
||||
if (uiCtrl === 'select' && metaProp.enum?.[0] !== undefined) {
|
||||
formData[k] = {
|
||||
formData![k] = {
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
value: metaProp.enum?.[0],
|
||||
|
@ -212,10 +108,22 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
setDefaultValueAsDomBehaviour();
|
||||
}, [formData]);
|
||||
|
||||
const formKitWithContext: FormKit = {
|
||||
refreshConfig() {
|
||||
if (typeof refreshConfig === 'function') {
|
||||
refreshConfig();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Form validation
|
||||
* - Currently only dynamic forms are in use, the business form validation has been handed over to the server
|
||||
*/
|
||||
const requiredValidator = () => {
|
||||
const errors: string[] = [];
|
||||
required.forEach((key) => {
|
||||
if (!formData[key] || !formData[key].value) {
|
||||
if (!formData![key] || !formData![key].value) {
|
||||
errors.push(key);
|
||||
}
|
||||
});
|
||||
|
@ -231,7 +139,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
keys.forEach((key) => {
|
||||
const { validator } = uiSchema[key]?.['ui:options'] || {};
|
||||
if (validator instanceof Function) {
|
||||
const value = formData[key]?.value;
|
||||
const value = formData![key]?.value;
|
||||
promises.push({
|
||||
key,
|
||||
promise: validator(value, formData),
|
||||
|
@ -269,14 +177,14 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
if (errors.length > 0) {
|
||||
formData = errors.reduce((acc, cur) => {
|
||||
acc[cur] = {
|
||||
...formData[cur],
|
||||
...formData![cur],
|
||||
isInvalid: true,
|
||||
errorMsg:
|
||||
uiSchema[cur]?.['ui:options']?.empty ||
|
||||
`${schema.properties[cur]?.title} ${t('empty')}`,
|
||||
`${properties[cur]?.title} ${t('empty')}`,
|
||||
};
|
||||
return acc;
|
||||
}, formData);
|
||||
}, formData || {});
|
||||
if (onChange instanceof Function) {
|
||||
onChange({ ...formData });
|
||||
}
|
||||
|
@ -286,13 +194,12 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
if (syncErrors.length > 0) {
|
||||
formData = syncErrors.reduce((acc, cur) => {
|
||||
acc[cur.key] = {
|
||||
...formData[cur.key],
|
||||
...formData![cur.key],
|
||||
isInvalid: true,
|
||||
errorMsg:
|
||||
cur.msg || `${schema.properties[cur.key].title} ${t('invalid')}`,
|
||||
errorMsg: cur.msg || `${properties[cur.key].title} ${t('invalid')}`,
|
||||
};
|
||||
return acc;
|
||||
}, formData);
|
||||
}, formData || {});
|
||||
if (onChange instanceof Function) {
|
||||
onChange({ ...formData });
|
||||
}
|
||||
|
@ -308,12 +215,12 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
return;
|
||||
}
|
||||
|
||||
Object.keys(formData).forEach((key) => {
|
||||
formData[key].isInvalid = false;
|
||||
formData[key].errorMsg = '';
|
||||
Object.keys(formData!).forEach((key) => {
|
||||
formData![key].isInvalid = false;
|
||||
formData![key].errorMsg = '';
|
||||
});
|
||||
if (onChange instanceof Function) {
|
||||
onChange(formData);
|
||||
onChange(formData!);
|
||||
}
|
||||
if (onSubmit instanceof Function) {
|
||||
onSubmit(e);
|
||||
|
@ -323,9 +230,10 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
useImperativeHandle(ref, () => ({
|
||||
validator,
|
||||
}));
|
||||
if (!formData || !schema || !schema.properties) {
|
||||
if (!formData || !schema || isEmpty(schema.properties)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
{keys.map((key) => {
|
||||
|
@ -336,7 +244,8 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
enumNames = [],
|
||||
} = properties[key];
|
||||
const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
|
||||
uiSchema[key] || {};
|
||||
uiSchema?.[key] || {};
|
||||
formData ||= {};
|
||||
const fieldState = formData[key];
|
||||
const uiSimplify = widget === 'legend' || uiOpt?.simplify;
|
||||
let groupClassName: BaseUIOptions['fieldClassName'] = uiOpt?.simplify
|
||||
|
@ -441,11 +350,11 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
/>
|
||||
) : null}
|
||||
{widget === 'button' ? (
|
||||
<CtrlButton
|
||||
<SfButton
|
||||
fieldName={key}
|
||||
text={uiOpt && 'text' in uiOpt ? uiOpt.text : ''}
|
||||
action={uiOpt && 'action' in uiOpt ? uiOpt.action : undefined}
|
||||
formData={formData}
|
||||
formKit={formKitWithContext}
|
||||
readOnly={readOnly}
|
||||
variant={
|
||||
uiOpt && 'variant' in uiOpt ? uiOpt.variant : undefined
|
||||
|
@ -473,8 +382,9 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
|||
};
|
||||
export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
||||
const formData: Type.FormDataType = {};
|
||||
Object.keys(schema.properties).forEach((key) => {
|
||||
const prop = schema.properties[key];
|
||||
const props: JSONSchema['properties'] = schema?.properties || {};
|
||||
Object.keys(props).forEach((key) => {
|
||||
const prop = props[key];
|
||||
const defaultVal = prop?.default;
|
||||
|
||||
formData[key] = {
|
||||
|
@ -486,4 +396,27 @@ export const initFormData = (schema: JSONSchema): Type.FormDataType => {
|
|||
return formData;
|
||||
};
|
||||
|
||||
export const mergeFormData = (
|
||||
target: Type.FormDataType | null,
|
||||
origin: Type.FormDataType | null,
|
||||
) => {
|
||||
if (!target) {
|
||||
return origin;
|
||||
}
|
||||
if (!origin) {
|
||||
return target;
|
||||
}
|
||||
Object.keys(target).forEach((k) => {
|
||||
const oi = origin[k];
|
||||
if (oi && oi.value !== undefined) {
|
||||
target[k] = {
|
||||
value: oi.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
return target;
|
||||
};
|
||||
|
||||
export default forwardRef(SchemaForm);
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
import { ButtonProps } from 'react-bootstrap';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
export interface JSONSchema {
|
||||
title: string;
|
||||
description?: string;
|
||||
required?: string[];
|
||||
properties: {
|
||||
[key: string]: {
|
||||
type: 'string' | 'boolean' | 'number';
|
||||
title: string;
|
||||
description?: string;
|
||||
enum?: Array<string | boolean | number>;
|
||||
enumNames?: string[];
|
||||
default?: string | boolean | number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface BaseUIOptions {
|
||||
empty?: string;
|
||||
// Will be appended to the className of the form component itself
|
||||
className?: classnames.Argument;
|
||||
// The className that will be attached to a form field container
|
||||
fieldClassName?: classnames.Argument;
|
||||
// Make a form component render into simplified mode
|
||||
readOnly?: boolean;
|
||||
simplify?: boolean;
|
||||
validator?: (
|
||||
value,
|
||||
formData?,
|
||||
) => Promise<string | true | void> | true | string;
|
||||
}
|
||||
|
||||
export interface InputOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
inputType?:
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
| 'email'
|
||||
| 'month'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'range'
|
||||
| 'search'
|
||||
| 'tel'
|
||||
| 'text'
|
||||
| 'time'
|
||||
| 'url'
|
||||
| 'week';
|
||||
}
|
||||
export interface SelectOptions extends BaseUIOptions {}
|
||||
export interface UploadOptions extends BaseUIOptions {
|
||||
acceptType?: string;
|
||||
imageType?: Type.UploadType;
|
||||
}
|
||||
|
||||
export interface SwitchOptions extends BaseUIOptions {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface TimezoneOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export interface CheckboxOptions extends BaseUIOptions {}
|
||||
|
||||
export interface RadioOptions extends BaseUIOptions {}
|
||||
|
||||
export interface TextareaOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
}
|
||||
|
||||
export interface ButtonOptions extends BaseUIOptions {
|
||||
text: string;
|
||||
icon?: string;
|
||||
action?: UIAction;
|
||||
variant?: ButtonProps['variant'];
|
||||
size?: ButtonProps['size'];
|
||||
}
|
||||
|
||||
export type UIOptions =
|
||||
| InputOptions
|
||||
| SelectOptions
|
||||
| UploadOptions
|
||||
| SwitchOptions
|
||||
| TimezoneOptions
|
||||
| CheckboxOptions
|
||||
| RadioOptions
|
||||
| TextareaOptions
|
||||
| ButtonOptions;
|
||||
|
||||
export type UIWidget =
|
||||
| 'textarea'
|
||||
| 'input'
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'select'
|
||||
| 'upload'
|
||||
| 'timezone'
|
||||
| 'switch'
|
||||
| 'legend'
|
||||
| 'button';
|
||||
export interface UISchema {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?: UIOptions;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A few notes on button control:
|
||||
* - Mainly used to send a request and notify the result of the request, and to update the data as required
|
||||
* - A scenario where a message notification is displayed directly after a click without sending a request, implementing a dedicated control
|
||||
* - Scenarios where the page jumps directly after a click without sending a request, implementing a dedicated control
|
||||
*
|
||||
* @field url : Target address for sending requests
|
||||
* @field method : Method for sending requests, default `get`
|
||||
* @field callback: Button event handler function that will fully take over the button events when this field is configured
|
||||
* *** Incomplete, DO NOT USE ***
|
||||
* @field loading: Set button loading information
|
||||
* @field on_complete: What needs to be done when the `Action` completes
|
||||
* @field on_complete.toast_return_message: Does toast show the returned message
|
||||
* @field on_complete.refresh_form_config: Whether to refresh the form configuration (configuration only, no data included)
|
||||
*/
|
||||
export interface UIAction {
|
||||
url: string;
|
||||
method?: 'get' | 'post' | 'put' | 'delete';
|
||||
loading?: {
|
||||
text: string;
|
||||
state?: 'none' | 'pending' | 'completed';
|
||||
};
|
||||
on_complete?: {
|
||||
toast_return_message?: boolean;
|
||||
refresh_form_config?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Form tools
|
||||
* - Used to get or set the configuration of forms and form items, the value of a form item
|
||||
* * @method refreshConfig(): void
|
||||
*/
|
||||
|
||||
export interface FormKit {
|
||||
refreshConfig(): void;
|
||||
}
|
|
@ -73,7 +73,7 @@ const Flags: FC = () => {
|
|||
{flagTypeKeys.map((li) => {
|
||||
return (
|
||||
<option value={li} key={li}>
|
||||
{li}
|
||||
{t(li, { keyPrefix: 'btns' })}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
|
@ -96,7 +96,9 @@ const Flags: FC = () => {
|
|||
<td>
|
||||
<Stack>
|
||||
<small className="text-secondary">
|
||||
Flagged {li.object_type}
|
||||
{t('flagged_type', {
|
||||
type: t(li.object_type, { keyPrefix: 'btns' }),
|
||||
})}
|
||||
</small>
|
||||
<BaseUserCard
|
||||
data={li.reported_user}
|
||||
|
|
|
@ -2,18 +2,23 @@ import { useState, useEffect } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { useToast } from '@/hooks';
|
||||
import type * as Types from '@/common/interface';
|
||||
import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
|
||||
import { SchemaForm, JSONSchema, UISchema } from '@/components';
|
||||
import { useQueryPluginConfig, updatePluginConfig } from '@/services';
|
||||
import { InputOptions } from '@/components/SchemaForm';
|
||||
import {
|
||||
InputOptions,
|
||||
FormKit,
|
||||
initFormData,
|
||||
mergeFormData,
|
||||
} from '@/components/SchemaForm';
|
||||
|
||||
const Config = () => {
|
||||
const { t } = useTranslation('translation');
|
||||
const { slug_name } = useParams<{ slug_name: string }>();
|
||||
const { data } = useQueryPluginConfig({ plugin_slug_name: slug_name });
|
||||
const { data, mutate: refreshPluginConfig } = useQueryPluginConfig({
|
||||
plugin_slug_name: slug_name,
|
||||
});
|
||||
const Toast = useToast();
|
||||
const [schema, setSchema] = useState<JSONSchema | null>(null);
|
||||
const [uiSchema, setUISchema] = useState<UISchema>();
|
||||
|
@ -62,7 +67,7 @@ const Config = () => {
|
|||
};
|
||||
setSchema(result);
|
||||
setUISchema(uiConf);
|
||||
setFormData(initFormData(result));
|
||||
setFormData(mergeFormData(initFormData(result), formData));
|
||||
}, [data?.config_fields]);
|
||||
|
||||
const onSubmit = (evt) => {
|
||||
|
@ -86,24 +91,19 @@ const Config = () => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
const refreshConfig: FormKit['refreshConfig'] = async () => {
|
||||
refreshPluginConfig();
|
||||
};
|
||||
const handleOnChange = (form) => {
|
||||
setFormData(form);
|
||||
};
|
||||
|
||||
if (!data || !schema || !formData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEmpty(schema.properties)) {
|
||||
return <h3 className="mb-4">{data?.name}</h3>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{data?.name}</h3>
|
||||
<SchemaForm
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
refreshConfig={refreshConfig}
|
||||
formData={formData}
|
||||
onSubmit={onSubmit}
|
||||
onChange={handleOnChange}
|
||||
|
|
|
@ -80,7 +80,7 @@ const Index: FC = () => {
|
|||
formMeta.robots.value = setting.robots;
|
||||
formMeta.permalink.value = setting.permalink;
|
||||
if (!/[1234]/.test(formMeta.permalink.value)) {
|
||||
formMeta.permalink.value = 1;
|
||||
formMeta.permalink.value = 4;
|
||||
}
|
||||
setFormData(formMeta);
|
||||
}
|
||||
|
|
|
@ -57,8 +57,10 @@ const Users: FC = () => {
|
|||
const currentUser = loggedUserInfoStore((state) => state.user);
|
||||
const { agent: ucAgent } = userCenterStore();
|
||||
const [adminUcAgent, setAdminUcAgent] = useState<AdminUcAgent>({
|
||||
user_status_agent_enabled: false,
|
||||
user_password_agent_enabled: false,
|
||||
allow_create_user: true,
|
||||
allow_update_user_status: true,
|
||||
allow_update_user_password: true,
|
||||
allow_update_user_role: true,
|
||||
});
|
||||
const Toast = useToast();
|
||||
const {
|
||||
|
@ -156,6 +158,18 @@ const Users: FC = () => {
|
|||
});
|
||||
}
|
||||
}, [ucAgent]);
|
||||
const showAddUser =
|
||||
!ucAgent?.enabled || (ucAgent?.enabled && adminUcAgent?.allow_create_user);
|
||||
const showActionPassword =
|
||||
!ucAgent?.enabled ||
|
||||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_password);
|
||||
const showActionRole =
|
||||
!ucAgent?.enabled ||
|
||||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_role);
|
||||
const showActionStatus =
|
||||
!ucAgent?.enabled ||
|
||||
(ucAgent?.enabled && adminUcAgent?.allow_update_user_status);
|
||||
const showAction = showActionPassword || showActionRole || showActionStatus;
|
||||
return (
|
||||
<>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
|
@ -167,7 +181,7 @@ const Users: FC = () => {
|
|||
sortKey="filter"
|
||||
i18nKeyPrefix="admin.users"
|
||||
/>
|
||||
{!ucAgent?.enabled ? (
|
||||
{showAddUser ? (
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
|
@ -252,31 +266,31 @@ const Users: FC = () => {
|
|||
</span>
|
||||
</td>
|
||||
)}
|
||||
{curFilter !== 'deleted' ? (
|
||||
{curFilter !== 'deleted' && showAction ? (
|
||||
<td className="text-end">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="link" className="no-toggle">
|
||||
<Icon name="three-dots-vertical" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{!ucAgent?.enabled ||
|
||||
!adminUcAgent.user_password_agent_enabled ? (
|
||||
{showActionPassword ? (
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('password', user)}>
|
||||
{t('set_new_password')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
{!ucAgent?.enabled ||
|
||||
!adminUcAgent.user_status_agent_enabled ? (
|
||||
{showActionStatus ? (
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('status', user)}>
|
||||
{t('change_status')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('role', user)}>
|
||||
{t('change_role')}
|
||||
</Dropdown.Item>
|
||||
{showActionRole ? (
|
||||
<Dropdown.Item
|
||||
onClick={() => handleAction('role', user)}>
|
||||
{t('change_role')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</td>
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Comment,
|
||||
FormatTime,
|
||||
htmlRender,
|
||||
ImgViewer,
|
||||
} from '@/components';
|
||||
import { scrollToElementTop, bgFadeOut } from '@/utils';
|
||||
import { AnswerItem } from '@/common/interface';
|
||||
|
@ -84,10 +85,12 @@ const Index: FC<Props> = ({
|
|||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<article
|
||||
className="fmt text-break text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
/>
|
||||
<ImgViewer>
|
||||
<article
|
||||
className="fmt text-break text-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: data?.html }}
|
||||
/>
|
||||
</ImgViewer>
|
||||
<div className="d-flex align-items-center mt-4">
|
||||
<Actions
|
||||
source="answer"
|
||||
|
|
|
@ -64,8 +64,8 @@ const Tags = () => {
|
|||
<Row className="mb-4 d-flex justify-content-center">
|
||||
<Col xxl={10} sm={12}>
|
||||
<h3 className="mb-4">{t('title')}</h3>
|
||||
<div className="d-flex justify-content-between align-items-center flex-wrap">
|
||||
<Stack direction="horizontal" gap={3}>
|
||||
<div className="d-block d-sm-flex justify-content-between align-items-center flex-wrap">
|
||||
<Stack direction="horizontal" gap={3} className="mb-3 mb-sm-0">
|
||||
<Form>
|
||||
<Form.Group controlId="formBasicEmail">
|
||||
<Form.Control
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { memo, FC, useState, useEffect } from 'react';
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import QrCode from 'qrcode';
|
||||
|
||||
|
@ -13,6 +14,7 @@ import { getLoginConf, checkLoginResult } from './service';
|
|||
let checkTimer: NodeJS.Timeout;
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins' });
|
||||
const navigate = useNavigate();
|
||||
const ucAgent = userCenterStore().agent;
|
||||
const agentName = ucAgent?.agent_info?.name || '';
|
||||
const [qrcodeDataUrl, setQrCodeDataUrl] = useState('');
|
||||
|
@ -22,7 +24,7 @@ const Index: FC = () => {
|
|||
}
|
||||
checkLoginResult(key).then((res) => {
|
||||
if (res.is_login) {
|
||||
guard.handleLoginWithToken(res.token);
|
||||
guard.handleLoginWithToken(res.token, navigate);
|
||||
return;
|
||||
}
|
||||
clearTimeout(checkTimer);
|
||||
|
@ -73,8 +75,8 @@ const Index: FC = () => {
|
|||
{qrcodeDataUrl ? (
|
||||
<>
|
||||
<img
|
||||
width={240}
|
||||
height={240}
|
||||
className="w-100"
|
||||
style={{ maxWidth: '240px' }}
|
||||
src={qrcodeDataUrl}
|
||||
alt={agentName}
|
||||
/>
|
||||
|
|
|
@ -22,7 +22,7 @@ const Index: React.FC = () => {
|
|||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<SendEmail visible={step === 1} callback={callback} />
|
||||
</Col>
|
||||
)}
|
||||
|
|
|
@ -15,7 +15,7 @@ const Index: FC = () => {
|
|||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<WelcomeTitle />
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<SendEmail />
|
||||
</Col>
|
||||
</Container>
|
||||
|
|
|
@ -175,86 +175,90 @@ const Index: React.FC = () => {
|
|||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<WelcomeTitle />
|
||||
{step === 1 && canOriginalLogin ? (
|
||||
<Col className="mx-auto" md={3}>
|
||||
{step === 1 ? (
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
{ucAgentInfo ? (
|
||||
<PluginUcLogin className="mb-5" />
|
||||
) : (
|
||||
<PluginOauth className="mb-5" />
|
||||
)}
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{t('email.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
tabIndex={1}
|
||||
type="email"
|
||||
value={formData.e_mail.value}
|
||||
isInvalid={formData.e_mail.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
e_mail: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.e_mail.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
{canOriginalLogin ? (
|
||||
<>
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{t('email.label')}</Form.Label>
|
||||
<Form.Control
|
||||
required
|
||||
tabIndex={1}
|
||||
type="email"
|
||||
value={formData.e_mail.value}
|
||||
isInvalid={formData.e_mail.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
e_mail: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.e_mail.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="password" className="mb-3">
|
||||
<div className="d-flex justify-content-between">
|
||||
<Form.Label>{t('password.label')}</Form.Label>
|
||||
<Link to="/users/account-recovery" tabIndex={2}>
|
||||
<small>{t('forgot_pass')}</small>
|
||||
</Link>
|
||||
</div>
|
||||
<Form.Group controlId="password" className="mb-3">
|
||||
<div className="d-flex justify-content-between">
|
||||
<Form.Label>{t('password.label')}</Form.Label>
|
||||
<Link to="/users/account-recovery" tabIndex={2}>
|
||||
<small>{t('forgot_pass')}</small>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Form.Control
|
||||
required
|
||||
tabIndex={1}
|
||||
type="password"
|
||||
// value={formData.pass.value}
|
||||
maxLength={32}
|
||||
isInvalid={formData.pass.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
pass: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.pass.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Control
|
||||
required
|
||||
tabIndex={1}
|
||||
type="password"
|
||||
// value={formData.pass.value}
|
||||
maxLength={32}
|
||||
isInvalid={formData.pass.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
pass: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.pass.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<div className="d-grid">
|
||||
<Button variant="primary" type="submit" tabIndex={1}>
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{loginSetting.allow_new_registrations && (
|
||||
<div className="text-center mt-5">
|
||||
<Trans i18nKey="login.info_sign" ns="translation">
|
||||
Don’t have an account?
|
||||
<Link
|
||||
to={userCenter.getSignUpUrl()}
|
||||
tabIndex={2}
|
||||
onClick={floppyNavigation.handleRouteLinkClick}>
|
||||
Sign up
|
||||
</Link>
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
<div className="d-grid">
|
||||
<Button variant="primary" type="submit" tabIndex={1}>
|
||||
{t('login', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{loginSetting.allow_new_registrations && (
|
||||
<div className="text-center mt-5">
|
||||
<Trans i18nKey="login.info_sign" ns="translation">
|
||||
Don’t have an account?
|
||||
<Link
|
||||
to={userCenter.getSignUpUrl()}
|
||||
tabIndex={2}
|
||||
onClick={floppyNavigation.handleRouteLinkClick}>
|
||||
Sign up
|
||||
</Link>
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</Col>
|
||||
) : null}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ListGroup } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
@ -7,6 +8,7 @@ import { isEmpty } from 'lodash';
|
|||
import { FormatTime, Empty } from '@/components';
|
||||
|
||||
const Inbox = ({ data, handleReadNotification }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'notifications' });
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
@ -40,12 +42,13 @@ const Inbox = ({ data, handleReadNotification }) => {
|
|||
!item.is_read && 'warning',
|
||||
)}>
|
||||
<div>
|
||||
{item.user_info.status !== 'deleted' ? (
|
||||
{item.user_info && item.user_info.status !== 'deleted' ? (
|
||||
<Link to={`/users/${item.user_info.username}`}>
|
||||
{item.user_info.display_name}{' '}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{item.user_info.display_name} </span>
|
||||
// someone for anonymous user display
|
||||
<span>{item.user_info?.display_name || t('someone')} </span>
|
||||
)}
|
||||
{item.notification_action}{' '}
|
||||
<Link to={url} onClick={() => handleReadNotification(item.id)}>
|
||||
|
|
|
@ -148,7 +148,7 @@ const Index: FC = () => {
|
|||
<p>{t('info', { keyPrefix: 'inactive' })}</p>
|
||||
</Col>
|
||||
) : (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<div className="text-center mb-5">{t('subtitle')}</div>
|
||||
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
|
|
|
@ -119,7 +119,7 @@ const Index: React.FC = () => {
|
|||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
{step === 1 && (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<Form noValidate onSubmit={handleSubmit} autoComplete="off">
|
||||
<Form.Group controlId="email" className="mb-3">
|
||||
<Form.Label>{t('password.label')}</Form.Label>
|
||||
|
|
|
@ -26,7 +26,7 @@ const Index: React.FC = () => {
|
|||
<WelcomeTitle />
|
||||
|
||||
{showForm ? (
|
||||
<Col className="mx-auto" md={3}>
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<PluginOauth className="mb-5" />
|
||||
<SignUpForm callback={onStep} />
|
||||
</Col>
|
||||
|
|
|
@ -4,20 +4,37 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import type * as Type from '@/common/interface';
|
||||
import { useToast } from '@/hooks';
|
||||
import { getLoggedUserInfo, changeEmail } from '@/services';
|
||||
import { getLoggedUserInfo, changeEmail, checkImgCode } from '@/services';
|
||||
import { handleFormError } from '@/utils';
|
||||
import { PicAuthCodeModal } from '@/components/Modal';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'settings.account',
|
||||
});
|
||||
const [step, setStep] = useState(1);
|
||||
const [showModal, setModalState] = useState(false);
|
||||
const [imgCode, setImgCode] = useState<Type.ImgCodeRes>({
|
||||
captcha_id: '',
|
||||
captcha_img: '',
|
||||
verify: false,
|
||||
});
|
||||
const [formData, setFormData] = useState<Type.FormDataType>({
|
||||
e_mail: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
pass: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
captcha_code: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
const [userInfo, setUserInfo] = useState<Type.UserInfoRes>();
|
||||
const toast = useToast();
|
||||
|
@ -27,13 +44,21 @@ const Index: FC = () => {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const getImgCode = () => {
|
||||
checkImgCode({
|
||||
action: 'e_mail',
|
||||
}).then((res) => {
|
||||
setImgCode(res);
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (params: Type.FormDataType) => {
|
||||
setFormData({ ...formData, ...params });
|
||||
};
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { e_mail } = formData;
|
||||
const { e_mail, pass } = formData;
|
||||
|
||||
if (!e_mail.value) {
|
||||
bol = false;
|
||||
|
@ -43,41 +68,89 @@ const Index: FC = () => {
|
|||
errorMsg: t('email.msg'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!pass.value) {
|
||||
bol = false;
|
||||
formData.pass = {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('pass.msg'),
|
||||
};
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
});
|
||||
return bol;
|
||||
};
|
||||
|
||||
const initFormData = () => {
|
||||
setFormData({
|
||||
e_mail: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
pass: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
captcha_code: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const postEmail = (event?: any) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
const params: any = {
|
||||
e_mail: formData.e_mail.value,
|
||||
pass: formData.pass.value,
|
||||
};
|
||||
|
||||
if (imgCode.verify) {
|
||||
params.captcha_code = formData.captcha_code.value;
|
||||
params.captcha_id = imgCode.captcha_id;
|
||||
}
|
||||
changeEmail(params)
|
||||
.then(() => {
|
||||
setStep(1);
|
||||
setModalState(false);
|
||||
toast.onShow({
|
||||
msg: t('change_email_info'),
|
||||
variant: 'warning',
|
||||
});
|
||||
initFormData();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
if (!err.list.find((v) => v.error_field.indexOf('captcha') >= 0)) {
|
||||
setModalState(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
getImgCode();
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (event: FormEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!checkValidated()) {
|
||||
return;
|
||||
}
|
||||
changeEmail({
|
||||
e_mail: formData.e_mail.value,
|
||||
})
|
||||
.then(() => {
|
||||
setStep(1);
|
||||
toast.onShow({
|
||||
msg: t('change_email_info'),
|
||||
variant: 'warning',
|
||||
});
|
||||
setFormData({
|
||||
e_mail: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.isError) {
|
||||
const data = handleFormError(err, formData);
|
||||
setFormData({ ...data });
|
||||
}
|
||||
});
|
||||
|
||||
if (imgCode.verify) {
|
||||
setModalState(true);
|
||||
}
|
||||
postEmail();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -96,13 +169,41 @@ const Index: FC = () => {
|
|||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Button variant="outline-secondary" onClick={() => setStep(2)}>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={() => {
|
||||
setStep(2);
|
||||
getImgCode();
|
||||
}}>
|
||||
{t('change_email_btn')}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="currentPass" className="mb-3">
|
||||
<Form.Label>{t('pass.label')}</Form.Label>
|
||||
<Form.Control
|
||||
autoComplete="new-password"
|
||||
required
|
||||
type="password"
|
||||
maxLength={32}
|
||||
isInvalid={formData.pass.isInvalid}
|
||||
onChange={(e) =>
|
||||
handleChange({
|
||||
pass: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.pass.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="newEmail" className="mb-3">
|
||||
<Form.Label>{t('email.label')}</Form.Label>
|
||||
<Form.Control
|
||||
|
@ -126,6 +227,7 @@ const Index: FC = () => {
|
|||
{formData.e_mail.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
|
||||
<div>
|
||||
<Button type="submit" variant="primary" className="me-2">
|
||||
{t('save', { keyPrefix: 'btns' })}
|
||||
|
@ -137,6 +239,18 @@ const Index: FC = () => {
|
|||
</div>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
<PicAuthCodeModal
|
||||
visible={showModal}
|
||||
data={{
|
||||
captcha: formData.captcha_code,
|
||||
imgCode,
|
||||
}}
|
||||
handleCaptcha={handleChange}
|
||||
clickSubmit={postEmail}
|
||||
refreshImgCode={getImgCode}
|
||||
onClose={() => setModalState(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
export interface PluginOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
name: string;
|
||||
type: UIWidget;
|
||||
title: string;
|
||||
description: string;
|
||||
ui_options?: UIOptions;
|
||||
options?: PluginOption[];
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
slug_name: string;
|
||||
config_fields: PluginItem[];
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { FC, ReactNode, useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate, useLoaderData } from 'react-router-dom';
|
||||
import { useNavigate, useLoaderData } from 'react-router-dom';
|
||||
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { TGuardFunc, TGuardResult } from '@/utils/guard';
|
||||
|
@ -13,7 +13,6 @@ const RouteGuard: FC<{
|
|||
page?: string;
|
||||
}> = ({ children, onEnter, path, page }) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const loaderData = useLoaderData();
|
||||
const [gk, setKeeper] = useState<TGuardResult>({
|
||||
ok: true,
|
||||
|
@ -23,6 +22,7 @@ const RouteGuard: FC<{
|
|||
if (typeof onEnter !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const gr = onEnter({
|
||||
loaderData,
|
||||
path,
|
||||
|
@ -48,12 +48,10 @@ const RouteGuard: FC<{
|
|||
};
|
||||
useEffect(() => {
|
||||
/**
|
||||
* NOTICE:
|
||||
* Must be put in `useEffect`,
|
||||
* otherwise `guard` may not get `loggedUserInfo` correctly
|
||||
* By detecting changes to location.href, many unnecessary tests can be avoided
|
||||
*/
|
||||
applyGuard();
|
||||
}, [location]);
|
||||
}, [window.location.href]);
|
||||
|
||||
let asOK = gk.ok;
|
||||
if (gk.ok === false && gk.redirect) {
|
||||
|
@ -62,10 +60,8 @@ const RouteGuard: FC<{
|
|||
* but the current page is already the target page for the route guard jump
|
||||
* This should render `children`!
|
||||
*/
|
||||
|
||||
asOK = floppyNavigation.equalToCurrentHref(gk.redirect);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{asOK ? children : null}
|
||||
|
|
|
@ -2,7 +2,15 @@ export const RouteAlias = {
|
|||
home: '/',
|
||||
login: '/users/login',
|
||||
signUp: '/users/register',
|
||||
activation: '/users/login?status=inactive',
|
||||
inactive: '/users/login?status=inactive',
|
||||
accountRecovery: '/users/account-recovery',
|
||||
changeEmail: '/users/change-email',
|
||||
passwordReset: '/users/password-reset',
|
||||
accountActivation: '/users/account-activation',
|
||||
activationSuccess: '/users/account-activation/success',
|
||||
activationFailed: '/users/account-activation/failed',
|
||||
suspended: '/users/account-suspended',
|
||||
confirmNewEmail: '/users/confirm-new-email',
|
||||
confirmEmail: '/users/confirm-email',
|
||||
authLanding: '/users/auth-landing',
|
||||
};
|
||||
|
|
|
@ -269,7 +269,7 @@ const routes: RouteNode[] = [
|
|||
path: 'admin',
|
||||
page: 'pages/Admin',
|
||||
loader: async () => {
|
||||
await guard.pullLoggedUser(true);
|
||||
await guard.pullLoggedUser();
|
||||
return null;
|
||||
},
|
||||
guard: () => {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import qs from 'qs';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import type * as Types from '@/common/interface';
|
||||
import request from '@/utils/request';
|
||||
import type { PluginConfig } from '@/plugins/types';
|
||||
|
||||
export const useQueryPlugins = (params) => {
|
||||
const apiUrl = `/answer/admin/api/plugins?${qs.stringify(params)}`;
|
||||
|
@ -24,7 +24,7 @@ export const updatePluginStatus = (params) => {
|
|||
|
||||
export const useQueryPluginConfig = (params) => {
|
||||
const apiUrl = `/answer/admin/api/plugin/config?${qs.stringify(params)}`;
|
||||
const { data, error, mutate } = useSWR<Types.PluginConfig, Error>(
|
||||
const { data, error, mutate } = useSWR<PluginConfig, Error>(
|
||||
apiUrl,
|
||||
request.instance.get,
|
||||
);
|
||||
|
|
|
@ -249,7 +249,7 @@ export const closeQuestion = (params: {
|
|||
return request.put('/answer/api/v1/question/status', params);
|
||||
};
|
||||
|
||||
export const changeEmail = (params: { e_mail: string }) => {
|
||||
export const changeEmail = (params: { e_mail: string; pass?: string }) => {
|
||||
return request.post('/answer/api/v1/user/email/change/code', params);
|
||||
};
|
||||
|
||||
|
|
|
@ -40,8 +40,10 @@ export interface UcBranding {
|
|||
}
|
||||
|
||||
export interface AdminUcAgent {
|
||||
user_status_agent_enabled: boolean;
|
||||
user_password_agent_enabled: boolean;
|
||||
allow_create_user: boolean;
|
||||
allow_update_user_status: boolean;
|
||||
allow_update_user_password: boolean;
|
||||
allow_update_user_role: boolean;
|
||||
}
|
||||
|
||||
export const getUcAgent = () => {
|
||||
|
|
|
@ -31,6 +31,9 @@ const initUser: UserInfoRes = {
|
|||
const loggedUserInfo = create<UserInfoStore>((set) => ({
|
||||
user: initUser,
|
||||
update: (params) => {
|
||||
if (typeof params !== 'object' || !params) {
|
||||
return;
|
||||
}
|
||||
if (!params?.language) {
|
||||
params.language = 'Default';
|
||||
}
|
||||
|
|
|
@ -60,17 +60,18 @@ const navigate = (to: string | number, config: NavigateConfig = {}) => {
|
|||
/**
|
||||
* 1. Blocking redirection of two login pages
|
||||
* 2. Auto storage login redirect
|
||||
* Note: The or judgement cannot be missing here, both jumps will be used
|
||||
*/
|
||||
if (to === RouteAlias.login || to === getLoginUrl()) {
|
||||
if (equalToCurrentHref(RouteAlias.login)) {
|
||||
return;
|
||||
}
|
||||
storageLoginRedirect();
|
||||
}
|
||||
|
||||
if (!isRoutableLink(to) && handler !== 'href' && handler !== 'replace') {
|
||||
handler = 'href';
|
||||
}
|
||||
if (handler === 'href' && config.options?.replace) {
|
||||
handler = 'replace';
|
||||
}
|
||||
if (handler === 'href') {
|
||||
window.location.href = to;
|
||||
} else if (handler === 'replace') {
|
||||
|
@ -85,10 +86,11 @@ const navigate = (to: string | number, config: NavigateConfig = {}) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* auto navigate to login page with redirect info
|
||||
* auto navigate to login page
|
||||
* Note: Only the internal login page is jumped here, `userAgent` login is handled on the internal login page.
|
||||
*/
|
||||
const navigateToLogin = (config?: NavigateConfig) => {
|
||||
const loginUrl = getLoginUrl();
|
||||
const loginUrl = RouteAlias.login;
|
||||
navigate(loginUrl, config);
|
||||
};
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import Storage from '@/utils/storage';
|
|||
|
||||
import { setupAppLanguage, setupAppTimeZone } from './localize';
|
||||
import { floppyNavigation, NavigateConfig } from './floppyNavigation';
|
||||
import { pullUcAgent, getLoginUrl, getSignUpUrl } from './userCenter';
|
||||
import { pullUcAgent, getSignUpUrl } from './userCenter';
|
||||
|
||||
type TLoginState = {
|
||||
isLogged: boolean;
|
||||
|
@ -81,6 +81,19 @@ export const deriveLoginState = (): TLoginState => {
|
|||
return ls;
|
||||
};
|
||||
|
||||
export const IGNORE_PATH_LIST = [
|
||||
RouteAlias.login,
|
||||
RouteAlias.signUp,
|
||||
RouteAlias.accountRecovery,
|
||||
RouteAlias.changeEmail,
|
||||
RouteAlias.passwordReset,
|
||||
RouteAlias.accountActivation,
|
||||
RouteAlias.confirmNewEmail,
|
||||
RouteAlias.confirmEmail,
|
||||
RouteAlias.authLanding,
|
||||
'/user-center/',
|
||||
];
|
||||
|
||||
export const isIgnoredPath = (ignoredPath: string | string[]) => {
|
||||
if (!Array.isArray(ignoredPath)) {
|
||||
ignoredPath = [ignoredPath];
|
||||
|
@ -92,15 +105,18 @@ export const isIgnoredPath = (ignoredPath: string | string[]) => {
|
|||
return !!matchingPath;
|
||||
};
|
||||
|
||||
let pluLock = false;
|
||||
let pluTimestamp = 0;
|
||||
export const pullLoggedUser = async (forceRePull = false) => {
|
||||
// only pull once if not force re-pull
|
||||
if (pluLock && !forceRePull) {
|
||||
return;
|
||||
}
|
||||
// dedupe pull requests in this time span in 10 seconds
|
||||
if (Date.now() - pluTimestamp < 1000 * 10) {
|
||||
export const pullLoggedUser = async (isInitPull = false) => {
|
||||
/**
|
||||
* WARN:
|
||||
* - dedupe pull requests in this time span in 10 seconds
|
||||
* - isInitPull:
|
||||
* Requests sent by the initialisation method cannot be throttled
|
||||
* and may cause Promise.allSettled to complete early in React development mode,
|
||||
* resulting in inaccurate application data.
|
||||
*/
|
||||
//
|
||||
if (!isInitPull && Date.now() - pluTimestamp < 1000 * 10) {
|
||||
return;
|
||||
}
|
||||
pluTimestamp = Date.now();
|
||||
|
@ -112,7 +128,6 @@ export const pullLoggedUser = async (forceRePull = false) => {
|
|||
console.error(ex);
|
||||
});
|
||||
if (loggedUserInfo) {
|
||||
pluLock = true;
|
||||
loggedUserInfoStore.getState().update(loggedUserInfo);
|
||||
}
|
||||
};
|
||||
|
@ -152,7 +167,7 @@ export const activated = () => {
|
|||
const us = deriveLoginState();
|
||||
if (us.isNotActivated) {
|
||||
gr.ok = false;
|
||||
gr.redirect = RouteAlias.activation;
|
||||
gr.redirect = RouteAlias.inactive;
|
||||
}
|
||||
return gr;
|
||||
};
|
||||
|
@ -228,16 +243,6 @@ export const allowNewRegistration = () => {
|
|||
return gr;
|
||||
};
|
||||
|
||||
export const loginAgent = () => {
|
||||
const gr: TGuardResult = { ok: true };
|
||||
const loginUrl = getLoginUrl();
|
||||
if (loginUrl !== RouteAlias.login) {
|
||||
gr.ok = false;
|
||||
gr.redirect = loginUrl;
|
||||
}
|
||||
return gr;
|
||||
};
|
||||
|
||||
export const singUpAgent = () => {
|
||||
const gr: TGuardResult = { ok: true };
|
||||
const signUpUrl = getSignUpUrl();
|
||||
|
@ -258,19 +263,7 @@ export const shouldLoginRequired = () => {
|
|||
if (us.isLogged) {
|
||||
return gr;
|
||||
}
|
||||
if (
|
||||
isIgnoredPath([
|
||||
RouteAlias.login,
|
||||
RouteAlias.signUp,
|
||||
'/users/account-recovery',
|
||||
'users/change-email',
|
||||
'users/password-reset',
|
||||
'users/account-activation',
|
||||
'users/account-activation/success',
|
||||
'/users/account-activation/failed',
|
||||
'/users/confirm-new-email',
|
||||
])
|
||||
) {
|
||||
if (isIgnoredPath(IGNORE_PATH_LIST)) {
|
||||
return gr;
|
||||
}
|
||||
gr.ok = false;
|
||||
|
@ -296,7 +289,7 @@ export const tryNormalLogged = (canNavigate: boolean = false) => {
|
|||
return false;
|
||||
}
|
||||
if (us.isNotActivated) {
|
||||
floppyNavigation.navigate(RouteAlias.activation);
|
||||
floppyNavigation.navigate(RouteAlias.inactive);
|
||||
} else if (us.isForbidden) {
|
||||
floppyNavigation.navigate(RouteAlias.suspended, {
|
||||
handler: 'replace',
|
||||
|
@ -337,19 +330,21 @@ export const handleLoginWithToken = (
|
|||
) => {
|
||||
if (token) {
|
||||
Storage.set(LOGGED_TOKEN_STORAGE_KEY, token);
|
||||
getLoggedUserInfo().then((res) => {
|
||||
loggedUserInfoStore.getState().update(res);
|
||||
const userStat = deriveLoginState();
|
||||
if (userStat.isNotActivated) {
|
||||
floppyNavigation.navigate(RouteAlias.activation, {
|
||||
handler,
|
||||
options: {
|
||||
replace: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
handleLoginRedirect(handler);
|
||||
}
|
||||
setTimeout(() => {
|
||||
getLoggedUserInfo().then((res) => {
|
||||
loggedUserInfoStore.getState().update(res);
|
||||
const userStat = deriveLoginState();
|
||||
if (userStat.isNotActivated) {
|
||||
floppyNavigation.navigate(RouteAlias.inactive, {
|
||||
handler,
|
||||
options: {
|
||||
replace: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
handleLoginRedirect(handler);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
floppyNavigation.navigate(RouteAlias.home, {
|
||||
|
@ -364,7 +359,6 @@ export const handleLoginWithToken = (
|
|||
/**
|
||||
* Initialize app configuration
|
||||
*/
|
||||
let appInitialized = false;
|
||||
export const initAppSettingsStore = async () => {
|
||||
const appSettings = await getAppSettings();
|
||||
if (appSettings) {
|
||||
|
@ -386,7 +380,13 @@ export const initAppSettingsStore = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
let appInitialized = false;
|
||||
export const setupApp = async () => {
|
||||
/**
|
||||
* This cannot be removed:
|
||||
* clicking on the current navigation link will trigger a call to the routing loader,
|
||||
* even though the page is not refreshed.
|
||||
*/
|
||||
if (appInitialized) {
|
||||
return;
|
||||
}
|
||||
|
@ -395,9 +395,14 @@ export const setupApp = async () => {
|
|||
* 1. must pre init logged user info for router guard
|
||||
* 2. must pre init app settings for app render
|
||||
*/
|
||||
await Promise.allSettled([pullLoggedUser(), initAppSettingsStore()]);
|
||||
await Promise.allSettled([initAppSettingsStore(), pullLoggedUser(true)]);
|
||||
await Promise.allSettled([pullUcAgent()]);
|
||||
setupAppLanguage();
|
||||
setupAppTimeZone();
|
||||
/**
|
||||
* WARN:
|
||||
* Initialization must be completed after all initialization actions,
|
||||
* otherwise the problem of rendering twice in React development mode can lead to inaccurate data or flickering pages
|
||||
*/
|
||||
appInitialized = true;
|
||||
};
|
||||
|
|
|
@ -3,13 +3,13 @@ import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios';
|
|||
|
||||
import { Modal } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore, errorCodeStore } from '@/stores';
|
||||
import { LOGGED_TOKEN_STORAGE_KEY, IGNORE_PATH_LIST } from '@/common/constants';
|
||||
import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants';
|
||||
import { RouteAlias } from '@/router/alias';
|
||||
import { getCurrentLang } from '@/utils/localize';
|
||||
|
||||
import Storage from './storage';
|
||||
import { floppyNavigation } from './floppyNavigation';
|
||||
import { isIgnoredPath } from './guard';
|
||||
import { isIgnoredPath, IGNORE_PATH_LIST } from './guard';
|
||||
|
||||
const baseConfig = {
|
||||
timeout: 10000,
|
||||
|
@ -52,19 +52,31 @@ class Request {
|
|||
// no content
|
||||
return true;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
(error) => {
|
||||
const {
|
||||
status,
|
||||
data: errModel,
|
||||
data: errBody,
|
||||
config: errConfig,
|
||||
} = error.response || {};
|
||||
const { data = {}, msg = '' } = errModel || {};
|
||||
const { data = {}, msg = '' } = errBody || {};
|
||||
const errorObject: {
|
||||
code: any;
|
||||
msg: string;
|
||||
data: any;
|
||||
// Currently only used for form errors
|
||||
isError?: boolean;
|
||||
// Currently only used for form errors
|
||||
list?: any[];
|
||||
} = {
|
||||
code: status,
|
||||
msg,
|
||||
data,
|
||||
};
|
||||
if (status === 400) {
|
||||
if (data?.err_type && errConfig?.passingError) {
|
||||
return errModel;
|
||||
return Promise.reject(errorObject);
|
||||
}
|
||||
if (data?.err_type) {
|
||||
if (data.err_type === 'toast') {
|
||||
|
@ -94,12 +106,9 @@ class Request {
|
|||
|
||||
if (data instanceof Array && data.length > 0) {
|
||||
// handle form error
|
||||
return Promise.reject({
|
||||
code: status,
|
||||
msg,
|
||||
isError: true,
|
||||
list: data,
|
||||
});
|
||||
errorObject.isError = true;
|
||||
errorObject.list = data;
|
||||
return Promise.reject(errorObject);
|
||||
}
|
||||
|
||||
if (!data || Object.keys(data).length <= 0) {
|
||||
|
@ -129,7 +138,7 @@ class Request {
|
|||
}
|
||||
if (data?.type === 'inactive') {
|
||||
// inactivated
|
||||
floppyNavigation.navigate(RouteAlias.activation);
|
||||
floppyNavigation.navigate(RouteAlias.inactive);
|
||||
return Promise.reject(false);
|
||||
}
|
||||
|
||||
|
@ -177,7 +186,7 @@ class Request {
|
|||
`Request failed with status code ${status}, ${msg || ''}`,
|
||||
);
|
||||
}
|
||||
return Promise.reject(false);
|
||||
return Promise.reject(errorObject);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue