Files
jiang13-forum/router/router.go
freefire 060b7707cb 支持公开用户主页、帖子图缩略图与编辑器图组排版。
新增用户签名与活动统计、图片灯箱;正文按需生成缩略图;TipTap 支持多图分组与环绕排版,并注入站点标题避免刷新闪烁。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-01 12:29:50 +08:00

206 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package router
import (
"net/http"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/config"
"git.iioio.com/freefire/jiang13-forum/embed_static"
"git.iioio.com/freefire/jiang13-forum/handler"
"git.iioio.com/freefire/jiang13-forum/middleware"
"git.iioio.com/freefire/jiang13-forum/service"
)
func Setup(cfg *config.Config) (*gin.Engine, error) {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery())
r.Use(gin.Logger())
if err := embed_static.SetupEmbed(r); err != nil {
return nil, err
}
filter := service.NewSensitiveFilter()
_ = service.WriteDefaultFilterWords(cfg.FilterWordsPath())
filter.LoadFromFile(cfg.FilterWordsPath())
settingsSvc := service.NewForumSettingsService()
settingsSvc.SeedOIDCFromINI(
cfg.RootURL,
cfg.OAuthClientID,
cfg.OAuthClientSecret,
strings.Join(cfg.OAuthRedirectURIs, ","),
)
settingsSvc.MigrateLegacyOIDCClient()
settingsSvc.SeedGiteaFromINI(cfg.GiteaBaseURL, cfg.GiteaToken, cfg.GiteaSyncEnabled)
// SPA 入口 HTML 注入后台配置的标签标题,避免刷新时先闪默认文案
embed_static.SetSPADocumentTitle(func() string {
return settingsSvc.SiteBranding().DocumentTitle()
})
authSvc := service.NewAuthService(cfg.JWTSecret, filter, settingsSvc)
userSvc := service.NewUserService(filter, settingsSvc)
boardSvc := service.NewBoardService()
postSvc := service.NewPostService(filter, settingsSvc)
commentSvc := service.NewCommentService(filter, settingsSvc)
backupSvc := service.NewBackupService(cfg.DBPath(), cfg.DataDir)
limiter := service.NewRateLimiter(settingsSvc)
captchaSvc := service.NewCaptchaService()
mailSvc := service.NewMailService(settingsSvc)
emailCodeSvc := service.NewEmailCodeService(mailSvc)
oidcSvc, err := service.NewOIDCService(cfg, settingsSvc)
if err != nil {
return nil, err
}
giteaSvc := service.NewGiteaService(settingsSvc)
giteaSvc.StartBackgroundSync()
h := &handler.Handlers{
Cfg: cfg, Auth: authSvc, User: userSvc, Board: boardSvc,
Post: postSvc, Comment: commentSvc, Backup: backupSvc,
Filter: filter, Limiter: limiter, Settings: settingsSvc,
Captcha: captchaSvc, Mail: mailSvc, EmailCode: emailCodeSvc,
OIDC: oidcSvc, Gitea: giteaSvc,
}
authMW := middleware.NewAuthMiddleware(authSvc)
// 缩略图使用独立前缀,避免与 Static("/uploads/*filepath") 路由冲突
r.GET("/media/thumb/*filepath", h.ServeImageThumb)
r.Static("/uploads", filepath.Join(cfg.DataDir, "uploads"))
// OIDC ProviderGitea 等外部站点 SSO
r.GET("/.well-known/openid-configuration", h.OIDCDiscovery)
r.GET("/oauth/jwks", h.OIDCJWKS)
r.GET("/oauth/authorize", authMW.OptionalAuth(), h.OIDCAuthorize)
r.POST("/oauth/token", h.OIDCToken)
r.GET("/oauth/userinfo", h.OIDCUserInfo)
r.POST("/oauth/userinfo", h.OIDCUserInfo)
r.GET("/oauth/logout", h.OIDCLogout)
r.POST("/oauth/logout", h.OIDCLogout)
// 公开 JSON API可选登录
pubAPI := r.Group("/api", authMW.OptionalAuth())
{
pubAPI.GET("/me", h.APIMe)
pubAPI.GET("/boards", h.APIBoards)
pubAPI.GET("/stats", h.APIStats)
pubAPI.GET("/forum-limits", h.APIForumLimits)
pubAPI.GET("/site-branding", h.APISiteBranding)
pubAPI.GET("/captcha", h.APICaptcha)
pubAPI.GET("/register/config", h.APIRegisterConfig)
pubAPI.POST("/register/email-code", middleware.RateLimitMiddleware(limiter, "register"), h.APISendRegisterEmailCode)
pubAPI.GET("/posts", h.APIPosts)
pubAPI.GET("/posts/hot", h.APIHotPosts)
pubAPI.GET("/tags", h.APITags)
pubAPI.GET("/comments/recent", h.APIRecentComments)
pubAPI.GET("/users/:id", h.APIUserPublic)
pubAPI.GET("/posts/:id", h.APIPostDetail)
pubAPI.GET("/posts/:id/comments", h.APIPostComments)
pubAPI.POST("/posts/:id/comments", middleware.RateLimitMiddleware(limiter, "comment"), h.APICreateComment)
pubAPI.GET("/projects", h.APIProjects)
pubAPI.POST("/register", middleware.RateLimitMiddleware(limiter, "register"), h.APIRegister)
pubAPI.POST("/login", middleware.RateLimitMiddleware(limiter, "login"), h.APILogin)
}
// 需登录 API
api := r.Group("/api", authMW.RequireAuth())
{
api.POST("/logout", h.APILogout)
api.GET("/favorites", h.APIFavorites)
api.GET("/profile/stats", h.APIProfileStats)
api.POST("/profile/nickname", h.APIUpdateProfile)
api.POST("/profile/signature", h.APIUpdateSignature)
api.POST("/profile/password", h.APIUpdatePassword)
api.POST("/profile/avatar", h.APIUploadAvatar)
api.POST("/uploads/image", h.APIUploadPostImage)
api.POST("/posts", middleware.RateLimitMiddleware(limiter, "post"), h.APICreatePost)
api.PUT("/posts/:id", h.APIUpdatePost)
api.DELETE("/posts/:id", h.APIDeletePost)
api.GET("/posts/:id/revisions", h.APIPostRevisions)
api.GET("/posts/:id/revisions/:revId", h.APIPostRevisionDetail)
api.POST("/posts/:id/like", h.APIToggleLike)
api.POST("/posts/:id/favorite", h.APIToggleFavorite)
api.DELETE("/comments/:id", h.APIDeleteComment)
api.PUT("/comments/:id", h.APIUpdateComment)
}
// 管理员 APIReact SPA 后台统一使用 JSON
adminAPI := r.Group("/api/admin", authMW.RequireAuth(), authMW.RequireAdmin())
{
adminAPI.GET("/dashboard", h.APIAdminDashboard)
adminAPI.GET("/settings", h.APIAdminSettings)
adminAPI.PUT("/settings/forum", h.APIAdminUpdateForumSettings)
adminAPI.PUT("/settings/mail", h.APIAdminUpdateMailSettings)
adminAPI.POST("/settings/mail/test", h.APIAdminTestMail)
adminAPI.PUT("/settings/oidc", h.APIAdminUpdateOIDCSettings)
adminAPI.PUT("/settings/gitea", h.APIAdminUpdateGiteaSettings)
adminAPI.POST("/settings/gitea/sync", h.APIAdminSyncGitea)
adminAPI.PUT("/settings/branding", h.APIAdminUpdateBranding)
adminAPI.POST("/settings/branding/upload", h.APIAdminUploadBrandingAsset)
adminAPI.POST("/settings/branding/clear", h.APIAdminClearBrandingAsset)
adminAPI.GET("/oauth/clients", h.APIAdminListOAuthClients)
adminAPI.POST("/oauth/clients", h.APIAdminCreateOAuthClient)
adminAPI.PUT("/oauth/clients/:id", h.APIAdminUpdateOAuthClient)
adminAPI.DELETE("/oauth/clients/:id", h.APIAdminDeleteOAuthClient)
adminAPI.GET("/settings/filter-words", h.APIAdminFilterWords)
adminAPI.PUT("/settings/filter-words", h.APIAdminUpdateFilterWords)
adminAPI.POST("/boards", h.APIAdminCreateBoard)
adminAPI.PUT("/boards/:id", h.APIAdminUpdateBoard)
adminAPI.DELETE("/boards/:id", h.APIAdminDeleteBoard)
adminAPI.GET("/posts", h.APIAdminPosts)
adminAPI.POST("/posts/:id/pin", h.APIAdminPinPost)
adminAPI.POST("/posts/:id/lock", h.APIAdminLockPost)
adminAPI.DELETE("/posts/:id", h.APIAdminDeletePost)
adminAPI.GET("/comments", h.APIAdminComments)
adminAPI.DELETE("/comments/:id", h.APIAdminDeleteComment)
adminAPI.GET("/users", h.APIAdminUsers)
adminAPI.POST("/users/:id/ban", h.APIAdminBanUser)
adminAPI.POST("/backup", h.APIAdminBackup)
adminAPI.GET("/backup/download/:name", h.APIAdminDownloadBackup)
}
// 后台管理API 保留兼容,页面统一由 React SPA 渲染
admin := r.Group("/admin")
{
admin.GET("/login", func(c *gin.Context) {
c.Redirect(http.StatusFound, "/login")
})
admin.POST("/api/login", middleware.RateLimitMiddleware(limiter, "admin_login"), h.AdminAPILogin)
adminAuth := admin.Group("/", authMW.RequireAuth(), authMW.RequireAdmin())
{
adminAuth.POST("/api/logout", h.AdminAPILogout)
// 遗留 form API旧模板脚本仍可能调用
adminAuth.POST("/api/boards", h.AdminAPICreateBoard)
adminAuth.PUT("/api/boards/:id", h.AdminAPIUpdateBoard)
adminAuth.DELETE("/api/boards/:id", h.AdminAPIDeleteBoard)
adminAuth.POST("/api/posts/:id/pin", h.AdminAPIPinPost)
adminAuth.DELETE("/api/posts/:id", h.AdminAPIDeletePost)
adminAuth.DELETE("/api/comments/:id", h.AdminAPIDeleteComment)
adminAuth.POST("/api/users/:id/ban", h.AdminAPIBanUser)
adminAuth.POST("/api/backup", h.AdminAPIBackup)
adminAuth.GET("/api/backup/download/:name", h.AdminDownloadBackup)
// React SPA 管理页面
adminAuth.GET("/", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/dashboard") })
for _, page := range []string{"dashboard", "boards", "posts", "comments", "users", "settings"} {
adminAuth.GET("/"+page, embed_static.ServeSPA)
}
}
}
// React SPA 入口
r.GET("/", embed_static.ServeSPA)
r.NoRoute(func(c *gin.Context) {
if embed_static.IsSPARoute(c.Request.URL.Path) {
embed_static.ServeSPA(c)
return
}
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
})
return r, nil
}