package routers import ( "fmt" "io/fs" "net/http" "os" "path/filepath" "git.iioio.com/freefire/jiang13-forum/config" "git.iioio.com/freefire/jiang13-forum/modules/auth" webpublic "git.iioio.com/freefire/jiang13-forum/public" "git.iioio.com/freefire/jiang13-forum/routers/api" "git.iioio.com/freefire/jiang13-forum/routers/install" webpages "git.iioio.com/freefire/jiang13-forum/routers/web" "git.iioio.com/freefire/jiang13-forum/services" "github.com/gin-gonic/gin" ) 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 := services.EnsureInstallLockFromExistingData(cfg.DataDir); err != nil { fmt.Fprintf(os.Stderr, "警告: 安装锁检查失败: %v\n", err) } if sub, err := fs.Sub(webpublic.Assets, "assets"); err == nil { ssrFiles := http.StripPrefix("/ssr-assets", http.FileServer(http.FS(sub))) r.GET("/ssr-assets/*filepath", func(c *gin.Context) { c.Header("Cache-Control", "public, max-age=86400") ssrFiles.ServeHTTP(c.Writer, c.Request) }) } r.Use(install.Guard(cfg.DataDir)) filter := services.NewSensitiveFilter() settingsSvc := services.NewForumSettingsService() services.EnsureFilterWordsInSettings(settingsSvc, cfg.FilterWordsPath(), filter) authSvc := services.NewAuthService(cfg.JWTSecret, filter, settingsSvc) userSvc := services.NewUserService(filter, settingsSvc) boardSvc := services.NewBoardService() boardSvc.EnsureDefaultBoard() postSvc := services.NewPostService(filter, settingsSvc) commentSvc := services.NewCommentService(filter, settingsSvc) messageSvc := services.NewMessageService(filter, settingsSvc) reportSvc := services.NewReportService(filter, settingsSvc, messageSvc, postSvc, commentSvc) backupSvc := services.NewBackupService(cfg.DBPath(), cfg.DataDir) limiter := services.NewRateLimiter(settingsSvc) captchaSvc := services.NewCaptchaService() mailSvc := services.NewMailService(settingsSvc) emailCodeSvc := services.NewEmailCodeService(mailSvc) notifySvc := services.NewNotifyService(messageSvc, mailSvc, settingsSvc) friendLinkApplySvc := services.NewFriendLinkApplyService(settingsSvc, messageSvc) oidcSvc, err := services.NewOIDCService(cfg, settingsSvc) if err != nil { return nil, err } // Gitea 仓库同步后置:本阶段不启动后台同步,亦不挂管理入口 giteaSvc := services.NewGiteaService(settingsSvc) uploadStore := services.NewUploadStore(cfg.DataDir, settingsSvc) if err := uploadStore.ReloadFromSettings(settingsSvc); err != nil { fmt.Fprintf(os.Stderr, "警告: 对象存储初始化失败,暂用本地磁盘: %v\n", err) _ = uploadStore.Apply(services.StorageConfig{Type: "local"}) } go func() { if n, err := uploadStore.SyncMediaIndex(); err != nil { fmt.Fprintf(os.Stderr, "警告: 媒体索引同步失败: %v\n", err) } else if n > 0 { fmt.Fprintf(os.Stderr, "媒体索引已同步 %d 条\n", n) } }() sitePageSvc := services.NewSitePageService(filter) h := &api.Handlers{ Cfg: cfg, Store: uploadStore, Auth: authSvc, User: userSvc, Board: boardSvc, Post: postSvc, Comment: commentSvc, Message: messageSvc, Notify: notifySvc, Report: reportSvc, Backup: backupSvc, Filter: filter, Limiter: limiter, Settings: settingsSvc, Captcha: captchaSvc, Mail: mailSvc, EmailCode: emailCodeSvc, OIDC: oidcSvc, Gitea: giteaSvc, Points: services.NewPointsService(), Badge: services.NewBadgeService(), SitePage: sitePageSvc, FriendLinkApply: friendLinkApplySvc, } authMW := auth.NewAuthMiddleware(authSvc) install.Register(r, install.Deps{ DataDir: cfg.DataDir, JWTSecret: cfg.JWTSecret, Auth: authSvc, Settings: settingsSvc, }) webpages.Register(r, webpages.Deps{ DataDir: cfg.DataDir, JWTSecret: cfg.JWTSecret, Settings: settingsSvc, Auth: authSvc, User: userSvc, Board: boardSvc, Post: postSvc, Comment: commentSvc, Message: messageSvc, Filter: filter, Limiter: limiter, EmailCode: emailCodeSvc, Store: uploadStore, Points: services.NewPointsService(), Badge: services.NewBadgeService(), FriendLink: friendLinkApplySvc, Mail: mailSvc, SitePage: sitePageSvc, Report: reportSvc, Captcha: captchaSvc, Notify: notifySvc, Backup: backupSvc, }, authMW) r.GET("/media/thumb/*filepath", h.ServeImageThumb) r.Static("/uploads", filepath.Join(cfg.DataDir, "uploads")) r.GET("/health", h.APIHealth) r.GET("/robots.txt", h.RobotsTxt) r.GET("/sitemap.xml", h.SitemapXML) // 机器注册辅助(与 SSR 注册共用 CaptchaService) r.GET("/api/captcha", h.APICaptcha) r.POST("/api/register", h.APIRegister) // OIDC Provider(外部机器 / Gitea 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) // 精简机器 API:健康检查已注册;保留只读探测与 OIDC,论坛 UI 不再走 /api r.NoRoute(webpages.Deps{ DataDir: cfg.DataDir, JWTSecret: cfg.JWTSecret, Settings: settingsSvc, Auth: authSvc, Board: boardSvc, Post: postSvc, Comment: commentSvc, }.NotFound) return r, nil }