Files
cpdaily_xiaozhushou/backed/cmd/server/main.go
T
becomer 097aed6f40
ci/woodpecker/tag/build Pipeline was successful
fix: 收紧管理端越权, 全量CDK/代理列表/公告管理/系统设置仅超管
实测发现任意代理 token 可访问 /admin/all-cdks、/admin/all-agents、
/agent/list 查看全站数据,并可创建全站强制弹窗公告、篡改系统计费与
SMTP 配置。为这些接口补充 AdminOnly 中间件;公告收取仍走
/my-announcements,代理自管理功能(自有CDK/下级代理)不受影响。
2026-09-03 15:28:43 +08:00

194 lines
7.7 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 main
import (
"fmt"
"log"
"net/http"
"time"
"cpdaily-new/internal/config"
"cpdaily-new/internal/database"
"cpdaily-new/internal/handler"
"cpdaily-new/internal/middleware"
"cpdaily-new/internal/ratelimit"
"cpdaily-new/internal/service"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/filesystem"
)
func main() {
// 加载配置
if err := config.Load(""); err != nil {
log.Fatalf("加载配置失败: %v", err)
}
// 连接数据库
database.Init()
// 连接 Redis(验证码存储/限流)
service.InitRedis()
// 启动限流记录定时清理(每5分钟清理一次)
ratelimit.Cleanup(5 * time.Minute)
// 创建 Fiber 应用
app := fiber.New(fiber.Config{
AppName: "Cpdaily New",
// 信任 openresty 反代(host 网络,经 docker NAT 后源 IP 为 172.22.0.1),
// 从 X-Forwarded-For 取真实客户端 IP(如待适配学校提交记录)
TrustedProxies: []string{"172.22.0.0/16"},
EnableTrustedProxyCheck: true,
ProxyHeader: fiber.HeaderXForwardedFor,
})
// 中间件
app.Use(middleware.CORS())
// 路由
api := app.Group("/api")
// 认证相关
auth := api.Group("/auth")
auth.Get("/captcha", handler.GetCaptcha)
auth.Get("/schools", handler.SearchSchools)
auth.Post("/login", handler.Login)
auth.Post("/refresh", handler.RefreshToken)
auth.Get("/userinfo", middleware.AuthRequired(), handler.GetUserInfo)
auth.Post("/generate-invite", middleware.AuthRequired(), handler.GenerateMyInviteCode)
auth.Get("/my-invite", middleware.AuthRequired(), handler.GetMyInviteInfo)
auth.Post("/redeem-invite", middleware.AuthRequired(), handler.RedeemInviteReward)
// 签到任务相关
task := api.Group("/task", middleware.AuthRequired())
task.Get("/titles", handler.GetTaskTitles)
task.Post("/create", handler.CreateTask)
task.Get("/list", handler.ListTasks)
task.Get("/form/:id", handler.GetTaskForm)
task.Post("/form/:id", handler.SaveTaskForm)
task.Put("/form/:id", handler.SaveTaskForm)
task.Put("/update/:id", handler.UpdateTask)
task.Delete("/delete/:id", handler.DeleteTask)
task.Get("/execute/:id", handler.ExecuteTask)
task.Get("/logs", handler.ListTaskLogs)
task.Delete("/logs/:id", handler.DeleteTaskLog)
task.Post("/upload-policy", handler.GetUploadPolicy)
task.Post("/preview-url", handler.GetPreviewUrl)
// CDK 兑换
cdk := api.Group("/cdk", middleware.AuthRequired())
cdk.Post("/redeem", handler.UserRedeemCDK)
// 设备绑定
device := api.Group("/device")
device.Post("/generate", middleware.AuthRequired(), handler.GenerateDeviceQr)
device.Post("/submit", handler.SubmitDeviceCode)
device.Get("/status", middleware.AuthRequired(), handler.GetDeviceStatus)
// 管理后台 API
admin := api.Group("/admin")
admin.Get("/setup/status", handler.AdminSetupStatus)
admin.Post("/setup/init", handler.AdminSetupInit)
admin.Post("/login", handler.AdminLogin)
admin.Get("/me", middleware.AdminAuthRequired(), handler.AdminMe)
// 管理后台 - 代理/管理员
adminAuth := api.Group("/admin", middleware.AdminAuthRequired())
adminAuth.Get("/cdks", handler.ListCDKs)
adminAuth.Post("/cdks", handler.GenerateCDK)
adminAuth.Put("/cdks/:id/status", handler.UpdateCDKStatus)
// 全量数据仅超管可见(代理只能看自己生成的)
adminAuth.Get("/all-cdks", middleware.AdminOnly(), handler.ListAllCDKs)
adminAuth.Get("/agents", handler.ListSubAgents)
adminAuth.Post("/agents", handler.CreateSubAgent)
adminAuth.Put("/agents/:id", handler.UpdateSubAgent)
adminAuth.Get("/agents/:id", handler.GetAgentDetail)
adminAuth.Put("/agents/:id/password", handler.UpdateAgentPassword)
adminAuth.Put("/agents/:id/status", handler.UpdateAgentStatus)
adminAuth.Get("/all-agents", middleware.AdminOnly(), handler.ListAllAgents)
adminAuth.Get("/agent/list", middleware.AdminOnly(), handler.ListAllAgents)
// 管理后台 - 用户管理(仅admin)
adminAuth.Get("/users", middleware.AdminOnly(), handler.ListUsers)
adminAuth.Get("/users/:id", middleware.AdminOnly(), handler.GetUserDetail)
adminAuth.Put("/users/:id/status", middleware.AdminOnly(), handler.UpdateUserStatus)
// 管理后台 - 公告(公告面向全站代理/用户推送,管理操作仅超管;代理/管理员通过 /my-announcements 收取公告)
adminAuth.Post("/announcements", middleware.AdminOnly(), handler.CreateAnnouncement)
adminAuth.Get("/announcements", middleware.AdminOnly(), handler.ListAnnouncements)
adminAuth.Get("/announcements/:id", middleware.AdminOnly(), handler.GetAnnouncement)
adminAuth.Put("/announcements/:id", middleware.AdminOnly(), handler.UpdateAnnouncement)
adminAuth.Delete("/announcements/:id", middleware.AdminOnly(), handler.DeleteAnnouncement)
// 管理后台 - 系统设置(全站计费/奖励/SMTP 配置,仅超管)
adminAuth.Get("/settings", middleware.AdminOnly(), handler.GetSettings)
adminAuth.Post("/settings", middleware.AdminOnly(), handler.UpdateSetting)
adminAuth.Put("/settings", middleware.AdminOnly(), handler.UpdateSetting)
adminAuth.Post("/settings/test-mail", middleware.AdminOnly(), handler.TestMail)
// 管理后台 - 我的公告(代理/管理员视角,共用普通用户公告处理器,但 target_type=agent)
adminAuth.Get("/my-announcements", handler.ListUserAnnouncements)
adminAuth.Get("/my-announcements/unread-count", handler.GetUnreadCount)
adminAuth.Get("/my-announcements/my-info", handler.GetMyAnnouncementInfo)
adminAuth.Post("/my-announcements/:id/read", handler.MarkAnnouncementRead)
// 用户公告
api.Get("/announcements", middleware.AuthRequired(), handler.ListUserAnnouncements)
api.Get("/announcements/unread-count", middleware.AuthRequired(), handler.GetUnreadCount)
api.Get("/announcements/my-info", middleware.AuthRequired(), handler.GetMyAnnouncementInfo)
api.Post("/announcements/:id/read", middleware.AuthRequired(), handler.MarkAnnouncementRead)
// 设备授权页面(纯 HTML)
api.Get("/device/authorize/:token", handler.AuthorizePage)
api.Get("/device/close", handler.ClosePage)
// 工单系统
ticket := api.Group("/ticket", middleware.AuthRequired())
ticket.Post("/create", handler.CreateTicket)
ticket.Get("/list", handler.ListMyTickets)
ticket.Get("/:id", handler.GetTicketDetail)
ticket.Post("/:id/message", handler.AddTicketMessage)
ticket.Put("/:id/close", handler.CloseTicket)
ticket.Delete("/:id", handler.DeleteTicket)
ticket.Get("/notifications/list", handler.ListTicketNotifications)
ticket.Get("/notifications/unread-count", handler.GetTicketUnreadCount)
ticket.Post("/notifications/:id/read", handler.MarkTicketNotificationRead)
// 用户报告登录失败(无需认证,有独立速率限制 + 验证码校验)
// 使用独立路径避免与 /ticket 组的 AuthRequired 中间件冲突
api.Post("/report-failed-login", handler.ReportFailedLogin)
// 管理后台 - 工单管理
adminAuth.Post("/ticket/reply/:id", handler.AdminReplyTicket)
adminAuth.Put("/ticket/status/:id", handler.AdminUpdateTicketStatus)
adminAuth.Get("/ticket/list", handler.AdminListTickets)
adminAuth.Get("/ticket/:id", handler.AdminGetTicket)
// 管理后台 - 待适配学校
adminAuth.Get("/pending-schools", handler.AdminListPendingSchools)
adminAuth.Post("/pending-schools/:id/mark-done", handler.AdminMarkSchoolDone)
// 健康检查(用于蓝绿部署)
app.Get("/health", func(c *fiber.Ctx) error {
return c.SendString("ok")
})
// 静态文件服务(前端 SPA)
app.Static("/assets", "./dist/assets", fiber.Static{
MaxAge: 86400,
})
app.Use(filesystem.New(filesystem.Config{
Root: http.Dir("./dist"),
Index: "index.html",
NotFoundFile: "index.html",
MaxAge: 3600,
}))
// 启动
addr := fmt.Sprintf(":%d", config.Global.Server.Port)
log.Printf("服务器启动在 %s", addr)
if err := app.Listen(addr); err != nil {
log.Fatalf("服务器启动失败: %v", err)
}
}