chore: 去掉未上线前的旧兼容垫片,板块流收口到首页
鉴权只认 cookie、SEO 只走 Next、sort 与推荐对齐;删除 Bearer、Go sitemap、post_type 回填等冗余路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -41,7 +41,7 @@ func (h *Handlers) AnnouncementDetail(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"announcement": a})
|
||||
}
|
||||
|
||||
// ===== 管理接口(RequireAdmin 兜底,前端不做权限判定) =====
|
||||
// ===== 管理接口(RequireStaff + PermAnnouncements,前端不做权限判定) =====
|
||||
|
||||
// AdminListAnnouncements 全部公告(含草稿)
|
||||
func (h *Handlers) AdminListAnnouncements(c *gin.Context) {
|
||||
|
||||
@@ -57,34 +57,23 @@ func setAuthCookies(c *gin.Context, accessToken, refreshToken string, secure boo
|
||||
})
|
||||
}
|
||||
|
||||
// clearAuthCookies 清除所有认证 cookie。
|
||||
// 除当前名称外,还需兼容清除:
|
||||
// - 启用 __Host- 前缀之前的旧无前缀名称(Path=/)
|
||||
// - 更早期 Path=/api/auth 的旧 refresh cookie
|
||||
// clearAuthCookies 清除当前契约下的三枚认证 cookie(名称随 ConfigureCookieNames,Path=/)。
|
||||
func clearAuthCookies(c *gin.Context, secure bool) {
|
||||
cookies := []struct{ name, path string }{
|
||||
{service.CookieName, "/"},
|
||||
{service.RefreshCookieName, "/"},
|
||||
{service.CSRFCookieName, "/"},
|
||||
cookies := []struct {
|
||||
name string
|
||||
httpOnly bool
|
||||
}{
|
||||
{service.CookieName, true},
|
||||
{service.RefreshCookieName, true},
|
||||
{service.CSRFCookieName, false},
|
||||
}
|
||||
// 当前生产名称带 __Host- 前缀时,旧无前缀 cookie 仍残留在浏览器中
|
||||
if service.CookieName != "j13_token" {
|
||||
cookies = append(cookies,
|
||||
struct{ name, path string }{"j13_token", "/"},
|
||||
struct{ name, path string }{"j13_refresh", "/"},
|
||||
struct{ name, path string }{"j13_csrf", "/"},
|
||||
)
|
||||
}
|
||||
// 更早期的 refresh cookie 只挂在 /api/auth 下
|
||||
cookies = append(cookies, struct{ name, path string }{"j13_refresh", "/api/auth"})
|
||||
|
||||
for _, ck := range cookies {
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: ck.name,
|
||||
Value: "",
|
||||
Path: ck.path,
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
HttpOnly: ck.httpOnly,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Unix(0, 0),
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/middleware"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Boards 获取板块列表
|
||||
@@ -15,3 +19,43 @@ func (h *Handlers) Boards(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"boards": nonNilSlice(boards)})
|
||||
}
|
||||
|
||||
// BoardSidebar 首页选中板块后的右栏聚合数据
|
||||
func (h *Handlers) BoardSidebar(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的板块 ID"})
|
||||
return
|
||||
}
|
||||
data, err := h.Board.Sidebar(uint(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "板块不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 热门榜点赞态 + 登录用户签到
|
||||
if claims := middleware.CurrentUser(c); claims != nil {
|
||||
ids := make([]uint, 0, len(data.Hot))
|
||||
for _, p := range data.Hot {
|
||||
ids = append(ids, p.ID)
|
||||
}
|
||||
likedMap := h.Like.BatchHasLiked(ids, claims.ID)
|
||||
for i := range data.Hot {
|
||||
data.Hot[i].Liked = likedMap[data.Hot[i].ID]
|
||||
}
|
||||
if status, err := h.Checkin.Status(claims.ID); err == nil {
|
||||
data.Checkin = status
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"board": data.Board,
|
||||
"moderators": data.Moderators,
|
||||
"stats": data.Stats,
|
||||
"hot": data.Hot,
|
||||
"active_users": data.ActiveUsers,
|
||||
"checkin": data.Checkin,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,18 +25,13 @@ func parseWSChatRoom(room string) (uint, bool) {
|
||||
|
||||
// 实时总线 WebSocket 入口:GET /api/ws
|
||||
//
|
||||
// 鉴权:与 REST 一致,优先读 HttpOnly 的 j13_token cookie(浏览器握手自动携带),
|
||||
// 回退 Authorization 头;ParseToken + ValidateClaims 实时校验 token_version 与封禁。
|
||||
// 鉴权:与 REST 一致,读 HttpOnly 的 j13_token cookie(浏览器握手自动携带);
|
||||
// ParseToken + ValidateClaims 实时校验 token_version 与封禁。
|
||||
// WebSocket 握手是 GET 请求,天然不受 CSRF 约束,但必须严格校验 Origin,
|
||||
// 防止跨站页面在用户浏览器中发起握手。
|
||||
func (h *Handlers) RealtimeWS(c *gin.Context) {
|
||||
tokenStr, err := c.Cookie(service.CookieName)
|
||||
if err != nil || tokenStr == "" {
|
||||
if auth := c.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
||||
tokenStr = strings.TrimPrefix(auth, "Bearer ")
|
||||
}
|
||||
}
|
||||
if tokenStr == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
|
||||
return
|
||||
}
|
||||
@@ -105,17 +100,17 @@ func (h *Handlers) RealtimeWS(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// checkWSOrigin 校验握手 Origin:
|
||||
// - 无 Origin(非浏览器/native 客户端)放行
|
||||
// - dev:允许 localhost / 127.0.0.1 任意端口(前端 :3000 直连后端 :3001)
|
||||
// - 生产:Origin 主机必须与请求 Host 同源
|
||||
// checkWSOrigin 校验握手 Origin(仅服务浏览器前端):
|
||||
// - 必须带 Origin(浏览器 WS 握手总会带;拒绝空 Origin 的脚本/非浏览器探测)
|
||||
// - dev:允许 localhost / 127.0.0.1 任意端口(页面 :3000 直连后端 :3001,端口不同但 cookie 仍会带上)
|
||||
// - 生产:Origin 的 host 必须与请求 Host 完全一致(需反向代理把页面与 /api/ws 挂到同一 Host)
|
||||
func (h *Handlers) checkWSOrigin(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil || u.Scheme != "http" && u.Scheme != "https" {
|
||||
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
|
||||
return false
|
||||
}
|
||||
if h.Cfg.DevMode {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const siteBaseURL = "http://localhost:3000"
|
||||
|
||||
// RobotsTxt robots.txt
|
||||
func (h *Handlers) RobotsTxt(c *gin.Context) {
|
||||
content := fmt.Sprintf(`User-agent: *
|
||||
Allow: /
|
||||
Sitemap: %s/sitemap.xml
|
||||
`, siteBaseURL)
|
||||
c.String(http.StatusOK, content)
|
||||
}
|
||||
|
||||
// SitemapXML 生成 sitemap
|
||||
func (h *Handlers) SitemapXML(c *gin.Context) {
|
||||
type urlEntry struct {
|
||||
Loc string `xml:"loc"`
|
||||
Lastmod string `xml:"lastmod"`
|
||||
Changefreq string `xml:"changefreq"`
|
||||
Priority string `xml:"priority"`
|
||||
}
|
||||
type urlset struct {
|
||||
XMLName xml.Name `xml:"urlset"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
URLs []urlEntry `xml:"url"`
|
||||
}
|
||||
|
||||
now := time.Now().Format("2006-01-02")
|
||||
entries := []urlEntry{
|
||||
{Loc: siteBaseURL + "/", Lastmod: now, Changefreq: "daily", Priority: "1.0"},
|
||||
}
|
||||
|
||||
// 板块页
|
||||
var boards []model.Board
|
||||
model.DB.Find(&boards)
|
||||
for _, b := range boards {
|
||||
entries = append(entries, urlEntry{
|
||||
Loc: fmt.Sprintf("%s/board/%d", siteBaseURL, b.ID),
|
||||
Lastmod: now,
|
||||
Changefreq: "daily",
|
||||
Priority: "0.8",
|
||||
})
|
||||
}
|
||||
|
||||
// 帖子详情页
|
||||
var posts []model.Post
|
||||
model.DB.Where("status = ?", model.ContentStatusPublished).
|
||||
Order("created_at DESC").Limit(1000).Find(&posts)
|
||||
for _, p := range posts {
|
||||
entries = append(entries, urlEntry{
|
||||
Loc: fmt.Sprintf("%s/post/%d", siteBaseURL, p.ID),
|
||||
Lastmod: p.UpdatedAt.Format("2006-01-02"),
|
||||
Changefreq: "weekly",
|
||||
Priority: "0.6",
|
||||
})
|
||||
}
|
||||
|
||||
// 站点公告
|
||||
var anns []model.Announcement
|
||||
model.DB.Where("published = ?", true).Order("created_at DESC").Limit(200).Find(&anns)
|
||||
for _, a := range anns {
|
||||
entries = append(entries, urlEntry{
|
||||
Loc: fmt.Sprintf("%s/announcement/%d", siteBaseURL, a.ID),
|
||||
Lastmod: a.UpdatedAt.Format("2006-01-02"),
|
||||
Changefreq: "weekly",
|
||||
Priority: "0.5",
|
||||
})
|
||||
}
|
||||
|
||||
us := urlset{
|
||||
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
|
||||
URLs: entries,
|
||||
}
|
||||
output, err := xml.MarshalIndent(us, "", " ")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "application/xml")
|
||||
c.String(http.StatusOK, xml.Header+string(output))
|
||||
}
|
||||
Reference in New Issue
Block a user