restrict v1
This commit is contained in:
@@ -158,6 +158,20 @@ func AnswerFollowUp(ctx context.Context, pageUser *db.User, question *db.Questio
|
||||
return
|
||||
}
|
||||
|
||||
// 站方处置 L2 起拒绝回答追问:追问回答同样是公开内容,且没有「私密」这一档可以降级。
|
||||
restrictionLevel, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), pageUser.ID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to query user restriction")
|
||||
ctx.SetInternalError()
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
if db.RestrictionAtLeast(restrictionLevel, db.RestrictionL2) {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "question.errRestrictedPublish"))
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
|
||||
followUpID := uint(ctx.ParamInt("followUpID"))
|
||||
|
||||
// 内容安全检测:统一守卫,箱主本人回复同样按自己的审核严格度裁决。
|
||||
|
||||
@@ -59,6 +59,16 @@ func Pager(ctx context.Context) {
|
||||
}
|
||||
ctx.Map(pageUser)
|
||||
|
||||
// 站方处置 L4:整页停止对外访问(数据保留)。
|
||||
// 查询失败不拦(fail-open):DB 异常时整站都不可用,这里再 fail-closed 只会让所有
|
||||
// 提问箱一起白屏;与分享卡不同,这一层不是内容外传通道,宁可放行也不要制造大面积故障。
|
||||
if level, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), pageUser.ID); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to query user restriction")
|
||||
} else if level == db.RestrictionL4 {
|
||||
ctx.Redirect("/restricted")
|
||||
return
|
||||
}
|
||||
|
||||
// 个人页语言:永远跟随提问箱主人(pageUser)配置的语言,忽略访客 cookie;未配置时回退站点默认语言
|
||||
if pageUser.Language != "" {
|
||||
lang := i18n.Resolve(pageUser.Language)
|
||||
|
||||
@@ -164,6 +164,30 @@ func PublishAnswer(ctx context.Context, pageUser *db.User, question *db.Question
|
||||
|
||||
answer := f.Answer
|
||||
|
||||
// 站方处置:L3 起冻结发布,L2 收窄公开(回答强制转私密)。
|
||||
// 处置记录只能由运维经 CLI 写入,箱主无法自行解除 —— 与 feature_controls 的自主开关严格区分。
|
||||
restrictionLevel, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), pageUser.ID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to query user restriction")
|
||||
ctx.SetInternalError()
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
if db.RestrictionAtLeast(restrictionLevel, db.RestrictionL3) {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "question.errRestrictedPublish"))
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
// L2:先把该题置为私密再写入回答,任何时刻都不会出现「公开 + 已作答」的中间态。
|
||||
if db.RestrictionAtLeast(restrictionLevel, db.RestrictionL2) {
|
||||
if err := db.Questions.SetPrivate(ctx.Request().Context(), question.ID); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to force question private")
|
||||
ctx.SetInternalError()
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 🚨 Content security check.
|
||||
// 统一守卫:按箱主「审核严格度」裁决是否放行(审核源不可用仍为 fail-closed,
|
||||
// 拒绝发布而非免审放行)。返回的 censorResponse 用于下方落审计字段。
|
||||
@@ -259,6 +283,20 @@ func SetPublic(ctx context.Context, pageUser *db.User, question *db.Question, ca
|
||||
return
|
||||
}
|
||||
|
||||
// 站方处置 L2 起禁止「设为公开」:L2 的语义就是收窄公开,放开等于自行解除处置。
|
||||
restrictionLevel, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), pageUser.ID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to query user restriction")
|
||||
ctx.SetInternalError()
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
if db.RestrictionAtLeast(restrictionLevel, db.RestrictionL2) {
|
||||
ctx.SetErrorFlash(i18n.T(ctx.Lang(), "question.errRestrictedPublic"))
|
||||
ctx.Success("question/item")
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Questions.SetPublic(ctx.Request().Context(), question.ID); err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to set question public")
|
||||
ctx.SetInternalError()
|
||||
|
||||
@@ -810,6 +810,26 @@ func QuestionShareCard(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 站方处置:任何级别都禁止生成分享图。
|
||||
// 分享卡会把「提问 + 回答」渲染成一张带箱主水印与二维码的图片,是挂人内容外流的主要通道
|
||||
// —— 图片离开原页面后没有上下文、没有原链接、无法回溯,比页面本身传播得更远。
|
||||
// 因此 L1「停止分发」首先就落在这里。查询失败按已处置处理(fail-closed):
|
||||
// 宁可少出一张卡,也不要让处置在 DB 抖动时静默失效。
|
||||
level, _, err := db.Restrictions.ActiveLevel(ctx.Request().Context(), user.ID)
|
||||
if err != nil {
|
||||
logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to query user restriction")
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
if level != "" {
|
||||
logrus.WithContext(ctx.Request().Context()).
|
||||
WithField("domain", domain).
|
||||
WithField("level", level).
|
||||
Info("Share card blocked by restriction")
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
|
||||
cacheKey := questionShareCardCacheKey(user, question)
|
||||
if cached, ok := questionShareCache.Get(cacheKey); ok {
|
||||
writeShareCardResponse(ctx, cached.([]byte))
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026 tamakyi. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/tamakyi/TamaBox/internal/context"
|
||||
"github.com/tamakyi/TamaBox/internal/i18n"
|
||||
)
|
||||
|
||||
// RestrictedProfile 提问箱暂停访问页(站方处置 L4)。
|
||||
//
|
||||
// 为什么单独一个公开页,而不是跳首页或登录页:
|
||||
// - 跳首页会让访客以为链接失效;
|
||||
// - 跳登录页会让人以为需要登录,而被处置箱主自己登录后仍然进不去,反复尝试;
|
||||
// - 该页不暴露「谁被处置、为什么被处置」的细节,只给一句通用说明,避免二次传播。
|
||||
//
|
||||
// 路由必须注册在公开区(reqUserSignIn 之外),否则被处置箱主会先被要求登录再看到 404。
|
||||
func RestrictedProfile(ctx context.Context) {
|
||||
ctx.SetTitle(i18n.T(ctx.Lang(), "restricted.title"))
|
||||
ctx.Success("user/restricted")
|
||||
}
|
||||
Reference in New Issue
Block a user