feat: opaque session、安装/发帖 SSR 与最小 Admin 后台

浏览器登录改为 DB sessions(可吊销);敏感词与 OIDC PEM 入 settings;
落地安装向导、注册发帖与 /admin 仪表盘/板块/审核/设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 05:44:16 +08:00
parent 3f50316ad0
commit fde5f628ec
80 changed files with 4148 additions and 1849 deletions

124
routers/install/install.go Normal file
View File

@@ -0,0 +1,124 @@
package install
import (
"net/http"
"strings"
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
"git.iioio.com/freefire/jiang13-forum/services"
"github.com/gin-gonic/gin"
)
// Deps 安装依赖
type Deps struct {
DataDir string
JWTSecret string
Auth *services.AuthService
Settings *services.ForumSettingsService
}
type pageData struct {
Title string
Description string
SiteName string
Slogan string
LogoMark string
CSRF string
Error string
Flash string
AdminUsername string
AdminEmail string
AdminNickname string
}
// Register 未安装时的路由(仅 /install
func Register(r *gin.Engine, deps Deps) {
r.GET("/install", deps.Get)
r.POST("/install", deps.Post)
}
// Get 安装页
func (d Deps) Get(c *gin.Context) {
if services.IsInstalled(d.DataDir) {
c.Redirect(http.StatusFound, "/")
return
}
ctx := webctx.New(c, d.JWTSecret)
brand := d.Settings.SiteBranding()
name := brand.Name
if name == "" {
name = "姜十三论坛"
}
ctx.HTML(http.StatusOK, "install", pageData{
Title: "安装 · " + name, SiteName: name, LogoMark: "姜",
CSRF: ctx.EnsureCSRF(), AdminUsername: "admin",
})
}
// Post 提交安装
func (d Deps) Post(c *gin.Context) {
if services.IsInstalled(d.DataDir) {
c.Redirect(http.StatusSeeOther, "/")
return
}
ctx := webctx.New(c, d.JWTSecret)
brand := d.Settings.SiteBranding()
siteName := strings.TrimSpace(c.PostForm("site_name"))
if siteName == "" {
siteName = "姜十三论坛"
}
data := pageData{
Title: "安装 · " + siteName, SiteName: siteName, LogoMark: "姜",
CSRF: ctx.EnsureCSRF(),
AdminUsername: strings.TrimSpace(c.PostForm("admin_username")),
AdminEmail: strings.TrimSpace(c.PostForm("admin_email")),
AdminNickname: strings.TrimSpace(c.PostForm("admin_nickname")),
}
if !ctx.CheckCSRF() {
data.Error = "无效请求,请重试"
ctx.HTML(http.StatusBadRequest, "install", data)
return
}
pass := c.PostForm("admin_password")
pass2 := c.PostForm("admin_password2")
if pass != pass2 {
data.Error = "两次密码不一致"
ctx.HTML(http.StatusBadRequest, "install", data)
return
}
if _, err := d.Auth.CreateAdmin(data.AdminUsername, pass, data.AdminNickname, data.AdminEmail); err != nil {
data.Error = err.Error()
ctx.HTML(http.StatusBadRequest, "install", data)
return
}
b := d.Settings.SiteBranding()
b.Name = siteName
_ = d.Settings.UpdateSiteBranding(b)
if err := services.WriteInstallLock(d.DataDir); err != nil {
data.Error = "写入安装锁失败: " + err.Error()
ctx.HTML(http.StatusInternalServerError, "install", data)
return
}
ctx.HTML(http.StatusOK, "post-install", pageData{
Title: "安装完成", SiteName: siteName, LogoMark: "姜",
})
_ = brand
}
// Guard 未安装则只允许 install / assets / health
func Guard(dataDir string) gin.HandlerFunc {
return func(c *gin.Context) {
if services.IsInstalled(dataDir) {
c.Next()
return
}
path := c.Request.URL.Path
if path == "/install" || path == "/health" ||
strings.HasPrefix(path, "/ssr-assets/") {
c.Next()
return
}
c.Redirect(http.StatusFound, "/install")
c.Abort()
}
}