diff --git a/internal/captcha/pages/geetest.html b/internal/captcha/pages/geetest.html
index 3592e4f0a..a8fb4575b 100644
--- a/internal/captcha/pages/geetest.html
+++ b/internal/captcha/pages/geetest.html
@@ -29,21 +29,20 @@
gt.appendTo("#captcha").onSuccess(function (e) {
var result = gt.getValidate();
- var form = new FormData()
- form.append("value", JSON.stringify(result))
console.log("[极验验证结果] ", result)
- fetch("./check", {
+ fetch("./verify", {
method: 'POST',
- body: form,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ value: JSON.stringify(result) }),
})
- .then(function (resp) {
- return resp.json()
- })
- .then(function (json) {
- if (json.success) {
- console.log("验证成功:" + json)
- } else {
- alert("验证失败:" + json.msg || '')
+ .then(function (res) {
+ if (!res.ok) {
+ res.json().then(json => {
+ alert("验证失败:" + res.status + " " + json.msg || '')
+ })
}
})
.catch(function (err) {
diff --git a/internal/captcha/pages/hcaptcha.html b/internal/captcha/pages/hcaptcha.html
index cf3d6c38a..dd652c70c 100644
--- a/internal/captcha/pages/hcaptcha.html
+++ b/internal/captcha/pages/hcaptcha.html
@@ -20,20 +20,19 @@
hcaptcha.render("container", {
sitekey: "{{.site_key}}",
callback: function(token) {
- var form = new FormData()
- form.append("value", token)
- fetch("./check", {
+ fetch("./verify", {
method: 'POST',
- body: form,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ value: token }),
})
- .then(function (resp) {
- return resp.json()
- })
- .then(function (json) {
- if (json.success) {
- console.log("验证成功:" + json)
- } else {
- alert("验证失败:" + json.msg || '')
+ .then(function (res) {
+ if (!res.ok) {
+ res.json().then(json => {
+ alert("验证失败:" + res.status + " " + json.msg || '')
+ })
}
})
.catch(function (err) {
diff --git a/internal/captcha/pages/recaptcha.html b/internal/captcha/pages/recaptcha.html
index d539967fe..bb5715b15 100644
--- a/internal/captcha/pages/recaptcha.html
+++ b/internal/captcha/pages/recaptcha.html
@@ -18,20 +18,19 @@
grecaptcha.render("container", {
sitekey: "{{.site_key}}",
callback: function(token) {
- var form = new FormData()
- form.append("value", token)
- fetch("./check", {
+ fetch("./verify", {
method: 'POST',
- body: form,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ value: token }),
})
- .then(function (resp) {
- return resp.json()
- })
- .then(function (json) {
- if (json.success) {
- console.log("验证成功:" + json)
- } else {
- alert("验证失败:" + json.msg || '')
+ .then(function (res) {
+ if (!res.ok) {
+ res.json().then(json => {
+ alert("验证失败:" + res.status + " " + json.msg || '')
+ })
}
})
.catch(function (err) {
diff --git a/internal/captcha/pages/turnstile.html b/internal/captcha/pages/turnstile.html
index 229b25d77..a91883175 100644
--- a/internal/captcha/pages/turnstile.html
+++ b/internal/captcha/pages/turnstile.html
@@ -18,20 +18,19 @@
const turnstileOptions = {
sitekey: '{{.site_key}}',
callback: function(token) {
- var form = new FormData()
- form.append("value", token)
- fetch("./check", {
+ fetch("./verify", {
method: 'POST',
- body: form,
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ value: token }),
})
- .then(function (resp) {
- return resp.json()
- })
- .then(function (json) {
- if (json.success) {
- console.log("验证成功:" + json)
- } else {
- alert("验证失败:" + json.msg || '')
+ .then(function (res) {
+ if (!res.ok) {
+ res.json().then(json => {
+ alert("验证失败:" + res.status + " " + json.msg || '')
+ })
}
})
.catch(function (err) {
diff --git a/server/common/captcha.go b/server/common/captcha.go
index b47efe31f..e2ca2124a 100644
--- a/server/common/captcha.go
+++ b/server/common/captcha.go
@@ -5,13 +5,15 @@ import (
"github.com/ArtalkJS/Artalk/internal/captcha"
"github.com/ArtalkJS/Artalk/internal/core"
+ "github.com/ArtalkJS/Artalk/internal/i18n"
+ "github.com/ArtalkJS/Artalk/server/middleware/limiter"
"github.com/gofiber/fiber/v2"
)
-func GetLimiter[T any](c *fiber.Ctx) (lmt *T, error error) {
- l := c.Locals("limiter").(*T)
- if l == nil {
- return nil, RespError(c, 500, "limiter is not initialize")
+func GetLimiter(c *fiber.Ctx) (lmt *limiter.Limiter, err error) {
+ l, ok := c.Locals("limiter").(*limiter.Limiter)
+ if l == nil || !ok {
+ return nil, RespError(c, 500, "limiter is not initialize, but middleware is used")
}
return l, nil
}
@@ -25,3 +27,55 @@ func NewCaptchaChecker(app *core.App, c *fiber.Ctx) captcha.Checker {
},
})
}
+
+func LimiterGuard(app *core.App, handler fiber.Handler) fiber.Handler {
+ return func(c *fiber.Ctx) error {
+ limiter, err := GetLimiter(c)
+ if err != nil {
+ return err
+ }
+
+ // 关闭验证码功能,直接 Skip
+ if !app.Conf().Captcha.Enabled {
+ return handler(c)
+ }
+
+ // 管理员直接忽略
+ if CheckIsAdminReq(app, c) {
+ return handler(c)
+ }
+
+ // 检测是否需要验证码
+ ip := c.IP()
+ if limiter.IsPass(ip) {
+ // 无需验证码
+ err := handler(c)
+
+ if c.Method() != fiber.MethodOptions { // 忽略 Options 请求
+ limiter.Log(ip) // 记录操作
+ }
+
+ return err
+ } else {
+ // create new captcha checker instance
+ cap := NewCaptchaChecker(app, c)
+
+ // response need captcha check
+ respData := Map{
+ "need_captcha": true,
+ }
+
+ switch cap.Type() {
+ case captcha.Image:
+ // 图片验证码
+ img, _ := cap.Get()
+ respData["img_data"] = string(img)
+ case captcha.IFrame:
+ // iFrame 验证模式
+ respData["iframe"] = true
+ }
+
+ return RespError(c, 403, i18n.T("Captcha required"), respData)
+ }
+ }
+}
diff --git a/server/handler/captcha_status.go b/server/handler/captcha_status.go
index 7686993cb..9bc414a0b 100644
--- a/server/handler/captcha_status.go
+++ b/server/handler/captcha_status.go
@@ -2,7 +2,6 @@ package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
- "github.com/ArtalkJS/Artalk/internal/limiter"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
@@ -20,8 +19,8 @@ type ResponseCaptchaStatus struct {
// @Router /captcha/status [get]
func CaptchaStatus(app *core.App, router fiber.Router) {
router.Get("/captcha/status", func(c *fiber.Ctx) error {
- limiter, err := common.GetLimiter[limiter.Limiter](c)
- if limiter == nil {
+ limiter, err := common.GetLimiter(c)
+ if err != nil {
return err
}
diff --git a/server/handler/captcha_verify.go b/server/handler/captcha_verify.go
index 80d6a4b66..b34bdf791 100644
--- a/server/handler/captcha_verify.go
+++ b/server/handler/captcha_verify.go
@@ -4,7 +4,6 @@ import (
"github.com/ArtalkJS/Artalk/internal/captcha"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
- "github.com/ArtalkJS/Artalk/internal/limiter"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
@@ -31,8 +30,8 @@ func CaptchaVerify(app *core.App, router fiber.Router) {
return resp
}
- limiter, err := common.GetLimiter[limiter.Limiter](c)
- if limiter == nil {
+ limiter, err := common.GetLimiter(c)
+ if err != nil {
return err
}
diff --git a/server/handler/comment_create.go b/server/handler/comment_create.go
index 016555529..98c4ed88e 100644
--- a/server/handler/comment_create.go
+++ b/server/handler/comment_create.go
@@ -43,7 +43,7 @@ type ResponseCommentCreate struct {
// @Produce json
// @Router /comments [post]
func CommentCreate(app *core.App, router fiber.Router) {
- router.Post("/comments", func(c *fiber.Ctx) error {
+ router.Post("/comments", common.LimiterGuard(app, func(c *fiber.Ctx) error {
var p ParamsCommentCreate
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
@@ -192,7 +192,7 @@ func CommentCreate(app *core.App, router fiber.Router) {
return common.RespData(c, ResponseCommentCreate{
CookedComment: cookedComment,
})
- })
+ }))
}
func isAllowComment(app *core.App, c *fiber.Ctx, name string, email string, page entity.Page) (bool, error) {
diff --git a/server/handler/upload.go b/server/handler/upload.go
index e8e4a5560..b606c9b19 100644
--- a/server/handler/upload.go
+++ b/server/handler/upload.go
@@ -43,7 +43,7 @@ type ResponseUpload struct {
// @Failure 500 {object} Map{msg=string}
// @Router /upload [post]
func Upload(app *core.App, router fiber.Router) {
- router.Post("/upload", func(c *fiber.Ctx) error {
+ router.Post("/upload", common.LimiterGuard(app, func(c *fiber.Ctx) error {
// 功能开关 (管理员始终开启)
if !app.Conf().ImgUpload.Enabled && !common.CheckIsAdminReq(app, c) {
return common.RespError(c, 403, i18n.T("Image upload forbidden"), common.Map{
@@ -196,7 +196,7 @@ func Upload(app *core.App, router fiber.Router) {
FileName: filename,
PublicURL: imgURL,
})
- })
+ }))
}
// 调用 upgit 上传图片获得 URL
diff --git a/server/handler/user_login.go b/server/handler/user_login.go
index 3ea5d9689..8f3cde4fc 100644
--- a/server/handler/user_login.go
+++ b/server/handler/user_login.go
@@ -38,7 +38,7 @@ type ResponseUserLogin struct {
// @Failure 500 {object} Map{msg=string}
// @Router /user/access_token [post]
func UserLogin(app *core.App, router fiber.Router) {
- router.Post("/user/access_token", func(c *fiber.Ctx) error {
+ router.Post("/user/access_token", common.LimiterGuard(app, func(c *fiber.Ctx) error {
var p ParamsUserLogin
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
@@ -112,5 +112,5 @@ func UserLogin(app *core.App, router fiber.Router) {
Token: jwtToken,
User: app.Dao().CookUser(&user),
})
- })
+ }))
}
diff --git a/server/handler/vote.go b/server/handler/vote.go
index f49d05c12..27f7ad1d3 100644
--- a/server/handler/vote.go
+++ b/server/handler/vote.go
@@ -35,7 +35,7 @@ type ResponseVote struct {
// @Failure 500 {object} Map{msg=string}
// @Router /votes/{type}/{target_id} [post]
func Vote(app *core.App, router fiber.Router) {
- router.Post("/votes/:type/:target_id", func(c *fiber.Ctx) error {
+ router.Post("/votes/:type/:target_id", common.LimiterGuard(app, func(c *fiber.Ctx) error {
rawType := c.Params("type")
targetID, _ := c.ParamsInt("target_id")
@@ -135,5 +135,5 @@ func Vote(app *core.App, router fiber.Router) {
Up: up,
Down: down,
})
- })
+ }))
}
diff --git a/server/middleware/admin.go b/server/middleware/admin.go
deleted file mode 100644
index b66ea4285..000000000
--- a/server/middleware/admin.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package middleware
-
-import (
- "github.com/ArtalkJS/Artalk/internal/core"
- "github.com/ArtalkJS/Artalk/internal/i18n"
- "github.com/ArtalkJS/Artalk/server/common"
- "github.com/gofiber/fiber/v2"
-)
-
-func AdminOnlyMiddleware(app *core.App) fiber.Handler {
- return func(c *fiber.Ctx) error {
- if !common.CheckIsAdminReq(app, c) {
- return common.RespError(c, 403, i18n.T("Admin access required"), common.Map{"need_login": true})
- }
-
- return c.Next()
- }
-}
diff --git a/server/middleware/limiter/limiter.go b/server/middleware/limiter/limiter.go
index cbc71d461..987f3aee6 100644
--- a/server/middleware/limiter/limiter.go
+++ b/server/middleware/limiter/limiter.go
@@ -1,22 +1,18 @@
package limiter
import (
- "path"
-
- "github.com/ArtalkJS/Artalk/internal/captcha"
"github.com/ArtalkJS/Artalk/internal/core"
- "github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/limiter"
- "github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ActionLimitConf struct {
- ProtectPaths [][]string
}
const LimiterLocalKey = "limiter"
+type Limiter = limiter.Limiter
+
// 操作限制 中间件
func ActionLimitMiddleware(app *core.App, conf ActionLimitConf) fiber.Handler {
limiter := limiter.NewLimiter(&limiter.LimiterConf{
@@ -29,63 +25,6 @@ func ActionLimitMiddleware(app *core.App, conf ActionLimitConf) fiber.Handler {
// 任何页面都保存 limiter 实例
c.Locals(LimiterLocalKey, limiter)
- // 关闭验证码功能,直接 Skip
- if !app.Conf().Captcha.Enabled {
- return c.Next()
- }
-
- // 路径跳过
- if !isProtectPath(c.Method(), c.Path(), conf.ProtectPaths) {
- return c.Next()
- }
-
- // 管理员直接忽略
- if common.CheckIsAdminReq(app, c) {
- return c.Next()
- }
-
- // 检测是否需要验证码
- ip := c.IP()
- if limiter.IsPass(ip) {
- // 无需验证码
- err := c.Next()
-
- if c.Method() != fiber.MethodOptions { // 忽略 Options 请求
- limiter.Log(ip) // 记录操作
- }
-
- return err
- } else {
- // create new captcha checker instance
- cap := common.NewCaptchaChecker(app, c)
-
- // response need captcha check
- respData := common.Map{
- "need_captcha": true,
- }
-
- switch cap.Type() {
- case captcha.Image:
- // 图片验证码
- img, _ := cap.Get()
- respData["img_data"] = string(img)
- case captcha.IFrame:
- // iFrame 验证模式
- respData["iframe"] = true
- // 前端新版不会再用到 img_data,向旧版响应 ArtalkFrontend out-of-date 图片
- respData["img_data"] = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 40'%3E%3Cdefs%3E%3Cstyle%3E.a%7Bfill:%23328ce6%3B%7D.b%7Bfont-size:12px%3Bfill:%23fff%3Bfont-family:sans-serif%3B%7D%3C/style%3E%3C/defs%3E%3Crect class='a' width='160' height='40'/%3E%3Ctext class='b' transform='translate(18.37 16.67)'%3EArtalk Frontend%3Ctspan x='0' y='14.4'%3EOut-Of-Date.%3C/tspan%3E%3C/text%3E%3C/svg%3E"
- }
-
- return common.RespError(c, 403, i18n.T("Captcha required"), respData)
- }
- }
-}
-
-func isProtectPath(targetMethod string, targetPath string, protectPaths [][]string) bool {
- for _, p := range protectPaths {
- if targetMethod == p[0] && path.Clean(targetPath) == path.Clean(p[1]) {
- return true
- }
+ return c.Next()
}
- return false
}
diff --git a/server/server.go b/server/server.go
index 8a0e6865d..08f501e30 100644
--- a/server/server.go
+++ b/server/server.go
@@ -153,15 +153,7 @@ func cors(app *core.App, f fiber.Router) {
}
func actionLimit(app *core.App, f fiber.Router) {
- f.Use(limiter.ActionLimitMiddleware(app, limiter.ActionLimitConf{
- // 保护的路径
- ProtectPaths: [][]string{
- {fiber.MethodPost, "/api/v2/comments"},
- {fiber.MethodPost, "/api/v2/access_token"},
- {fiber.MethodPost, "/api/v2/votes"},
- {fiber.MethodPost, "/api/v2/upload"},
- },
- }))
+ f.Use(limiter.ActionLimitMiddleware(app, limiter.ActionLimitConf{}))
}
func static(f fiber.Router) {
diff --git a/ui/artalk/src/api/fetch.ts b/ui/artalk/src/api/fetch.ts
index 44f7153ca..23b42aef4 100644
--- a/ui/artalk/src/api/fetch.ts
+++ b/ui/artalk/src/api/fetch.ts
@@ -27,7 +27,7 @@ export const Fetch = async (opts: ApiOptions, input: string | URL | Request, ini
if (json.need_captcha) {
// 请求需要验证码
opts.onNeedCheckCaptcha && await opts.onNeedCheckCaptcha({
- data: { imgData: json.data.img_data, iframe: json.data.iframe }
+ data: { imgData: json.img_data, iframe: json.iframe }
})
return Fetch(opts, input, init) // retry
}
diff --git a/ui/artalk/src/api/options.ts b/ui/artalk/src/api/options.ts
index 69a5419f6..cee57334b 100644
--- a/ui/artalk/src/api/options.ts
+++ b/ui/artalk/src/api/options.ts
@@ -16,8 +16,8 @@ export interface ApiOptions {
onNeedCheckCaptcha?: (payload: {
data: {
- imgData: string
- iframe: string
+ imgData?: string
+ iframe?: string
}
}) => Promise
diff --git a/ui/artalk/src/layer/sidebar-layer.ts b/ui/artalk/src/layer/sidebar-layer.ts
index 5d86ad5bc..0b6d3f04c 100644
--- a/ui/artalk/src/layer/sidebar-layer.ts
+++ b/ui/artalk/src/layer/sidebar-layer.ts
@@ -136,7 +136,6 @@ export default class SidebarLayer extends Component {
if (view) query.view = view
if (this.conf.darkMode) query.darkMode = '1'
- if (typeof this.conf.locale === 'string') query.locale = this.conf.locale
const urlParams = new URLSearchParams(query);
this.iframeLoad($iframe, `${baseURL}?${urlParams.toString()}`)
diff --git a/ui/artalk/src/lib/component.ts b/ui/artalk/src/lib/component.ts
index fe1cc1cac..167ed2631 100644
--- a/ui/artalk/src/lib/component.ts
+++ b/ui/artalk/src/lib/component.ts
@@ -1,12 +1,12 @@
-import type { ArtalkConfig, ContextApi } from '@/types'
+import type { ContextApi } from '@/types'
export default abstract class Component {
public $el!: HTMLElement
- public readonly conf: ArtalkConfig
+ public get conf() {
+ return this.ctx.conf
+ }
public constructor(
public ctx: ContextApi
- ) {
- this.conf = ctx.conf
- }
+ ) {}
}
diff --git a/ui/artalk/src/lib/user.ts b/ui/artalk/src/lib/user.ts
index fb4e0c1fd..bb173b2aa 100644
--- a/ui/artalk/src/lib/user.ts
+++ b/ui/artalk/src/lib/user.ts
@@ -12,8 +12,10 @@ class User {
constructor(
private opts: UserOpts
) {
- // 从 localStorage 导入
+ // Import from localStorage
const localUser = JSON.parse(window.localStorage.getItem(LOCAL_USER_KEY) || '{}')
+
+ // Initialize
this.data = {
nick: localUser.nick || '',
email: localUser.email || '',
@@ -27,7 +29,7 @@ class User {
return this.data
}
- /** 保存用户到 localStorage 中 */
+ /** Update user data and save to localStorage */
update(obj: Partial = {}) {
Object.entries(obj).forEach(([key, value]) => {
this.data[key] = value
@@ -49,7 +51,7 @@ class User {
})
}
- /** 是否已填写基本用户信息 */
+ /** Check if user has filled basic data */
checkHasBasicUserInfo() {
return !!this.data.nick && !!this.data.email
}
diff --git a/ui/artalk/src/plugins/list/fetch.ts b/ui/artalk/src/plugins/list/fetch.ts
index 41503c29e..11c5d5a58 100644
--- a/ui/artalk/src/plugins/list/fetch.ts
+++ b/ui/artalk/src/plugins/list/fetch.ts
@@ -35,6 +35,7 @@ export const Fetch: ArtalkPlugin = (ctx) => {
ctx.getApi().comments
.getComments({
...reqParams,
+ ...ctx.getApi().getUserFields()
})
.then(({ data }) => {
// Must before all other function call and event trigger,
diff --git a/ui/artalk/src/service.ts b/ui/artalk/src/service.ts
index bd19ab7bc..1bc6a23c2 100644
--- a/ui/artalk/src/service.ts
+++ b/ui/artalk/src/service.ts
@@ -46,7 +46,9 @@ const services = {
getCtx: () => ctx,
getApi: () => ctx.getApi(),
onReload: () => ctx.reload(),
- getCaptchaIframeURL: () => `${ctx.conf.server}/api/v2/captcha?t=${+new Date()}`
+
+ // make sure suffix with a slash, because it will be used as a base url when call `fetch`
+ getCaptchaIframeURL: () => `${ctx.conf.server}/api/v2/captcha/?t=${+new Date()}`
})
return checkerLauncher
},