Files
Atlas/internal/api/handlers.go
T
renjue 6e6b899071
CI / docker (push) Successful in 7m37s
实现 Atlas AI 平台一期能力并完成品牌化改造。
引入用户端对话/生图与管理端 Provider 配置,统一 invoke 编排、Gateway、请求日志与模型管理;前端按设计规范拆分布局,Go module 更名为 github.com/rose_cat707/Atlas;生图长任务 SSE 推送;管理端登录防爆破;协议与服务类型联动;Replicate 模型同步修复;用户端创作台 UI 重构;管理端创作历史替代请求日志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 22:13:52 +08:00

235 lines
5.8 KiB
Go

package api
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"github.com/rose_cat707/Atlas/internal/service"
"github.com/rose_cat707/Atlas/internal/settings"
)
type Server struct {
app *service.App
loginGuard *loginGuard
}
func NewServer(app *service.App) *Server {
return &Server{
app: app,
loginGuard: newLoginGuard(),
}
}
func (s *Server) Router() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery(), s.corsMiddleware())
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
api := r.Group("/api/v1")
api.POST("/auth/login", s.login)
api.GET("/auth/status", s.authStatus)
protected := api.Group("")
protected.Use(s.authMiddleware())
{
protected.GET("/dashboard", s.dashboard)
protected.GET("/settings", s.getSettings)
protected.PUT("/settings", s.putSettings)
protected.GET("/audit-logs", s.listAuditLogs)
s.registerAdminRoutes(protected)
}
user := api.Group("")
s.registerUserRoutes(user)
s.registerGatewayRoutes(r)
r.NoRoute(s.serveSPA)
return r
}
func (s *Server) corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type")
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func (s *Server) authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
cfg, err := s.app.Store.GetSettings(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
c.Abort()
return
}
if cfg[settings.KeyAuthEnabled] != "true" {
c.Next()
return
}
token := bearerToken(c)
if token == "" || !verifyToken(cfg[settings.KeyAdminPassword], token) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
c.Abort()
return
}
c.Next()
}
}
type loginReq struct {
Password string `json:"password"`
}
func (s *Server) authStatus(c *gin.Context) {
cfg, err := s.app.Store.GetSettings(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"enabled": cfg[settings.KeyAuthEnabled] == "true"})
}
func (s *Server) login(c *gin.Context) {
ip := c.ClientIP()
if locked, retryAfter := s.loginGuard.check(ip); locked {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "too many login attempts",
"retry_after": retryAfter,
})
return
}
var req loginReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx := c.Request.Context()
cfg, err := s.app.Store.GetSettings(ctx)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
hash := cfg[settings.KeyAdminPassword]
if !checkPassword(req.Password, hash) {
time.Sleep(loginMinDelay)
if locked, retryAfter := s.loginGuard.recordFailure(ip); locked {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "too many login attempts",
"retry_after": retryAfter,
})
return
}
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid password"})
return
}
s.loginGuard.reset(ip)
token := issueToken(hash, 24*time.Hour)
c.JSON(http.StatusOK, gin.H{"token": token})
}
func (s *Server) dashboard(c *gin.Context) {
cfg, err := s.app.Store.GetSettings(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"app_name": cfg[settings.KeyAppName],
"data_dir": s.app.DataDir,
})
}
func (s *Server) getSettings(c *gin.Context) {
cfg, err := s.app.Store.GetSettings(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, settings.Display(cfg))
}
func (s *Server) putSettings(c *gin.Context) {
var in map[string]string
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
normalized, err := settings.NormalizeSubmit(in)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(normalized) == 0 {
c.JSON(http.StatusOK, gin.H{"ok": true})
return
}
ctx := c.Request.Context()
if err := s.app.Store.SetSettings(ctx, normalized); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
_ = s.app.Store.InsertAuditLog(ctx, "update", "settings", "settings updated")
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (s *Server) listAuditLogs(c *gin.Context) {
limit, offset := parseLimitOffset(c, 50, 500)
items, total, err := s.app.Store.ListAuditLogs(c.Request.Context(), limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"items": items, "total": total})
}
func bearerToken(c *gin.Context) string {
h := c.GetHeader("Authorization")
if strings.HasPrefix(h, "Bearer ") {
return strings.TrimPrefix(h, "Bearer ")
}
return ""
}
func checkPassword(password, stored string) bool {
if strings.HasPrefix(stored, "$2a$") || strings.HasPrefix(stored, "$2b$") {
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(password)) == nil
}
return password == stored
}
func parseLimitOffset(c *gin.Context, defaultLimit, maxLimit int) (int, int) {
limit := defaultLimit
offset := 0
if v := c.Query("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
limit = n
}
}
if limit > maxLimit {
limit = maxLimit
}
if v := c.Query("offset"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n >= 0 {
offset = n
}
}
return limit, offset
}