package api import ( "net/http" "opencatd-open/internal/dao" "opencatd-open/internal/store" "opencatd-open/internal/pkg/apikey" "opencatd-open/internal/pkg/crypto" "opencatd-open/internal/pkg/jwt" "opencatd-open/internal/auth" "strconv" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type Handler struct { db *gorm.DB userDAO *dao.UserDAO apiKeyDAO *dao.ApiKeyDAO channelDAO *dao.ChannelDAO modelDAO *dao.ModelDAO usageDAO *dao.UsageDAO dailyDAO *dao.DailyUsageDAO } func NewHandler(db *gorm.DB) *Handler { return &Handler{ db: db, userDAO: dao.NewUserDAO(db), apiKeyDAO: dao.NewApiKeyDAO(db), channelDAO: dao.NewChannelDAO(db), modelDAO: dao.NewModelDAO(db), usageDAO: dao.NewUsageDAO(db), dailyDAO: dao.NewDailyUsageDAO(db), } } // --- Auth --- func (h *Handler) Register(c *gin.Context) { var req struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` Email string `json:"email" binding:"required,email"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Check if first user (becomes admin) var count int64 h.db.Model(&store.User{}).Count(&count) role := store.RoleUser if count == 0 { role = store.RoleAdmin } hash := crypto.Sha256Hex(req.Password) user := &store.User{ Username: req.Username, Email: req.Email, PasswordHash: hash, Role: role, Status: store.UserStatusActive, } if err := h.userDAO.Create(user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "username or email already exists"}) return } c.JSON(http.StatusOK, gin.H{"message": "registered"}) } func (h *Handler) Login(c *gin.Context) { var req struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } user, err := h.userDAO.GetByUsername(req.Username) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } hash := crypto.Sha256Hex(req.Password) if user.PasswordHash != hash { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } secret := auth.GetSecretKey() accessToken, refreshToken, err := jwt.GenerateTokenPair(user.ID, user.Username, user.Role, secret, 24*time.Hour, 7*24*time.Hour) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate token"}) return } // Update last login now := time.Now() user.LastLoginAt = &now h.userDAO.Update(user) c.JSON(http.StatusOK, gin.H{ "code": 200, "data": gin.H{ "token": accessToken, "access_token": accessToken, "refresh_token": refreshToken, }, }) } func (h *Handler) Me(c *gin.Context) { userID, _ := c.Get("user_id") user, err := h.userDAO.GetByID(userID.(uint64)) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } // Map role string to number for frontend compatibility roleNum := 1 // default user if user.Role == store.RoleAdmin { roleNum = 10 } c.JSON(http.StatusOK, gin.H{ "code": 200, "data": gin.H{ "id": user.ID, "username": user.Username, "email": user.Email, "role": roleNum, "status": user.Status, }, }) } // --- Users --- func (h *Handler) ListUsers(c *gin.Context) { limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) users, total, err := h.userDAO.List(limit, offset) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": users, "total": total}) } func (h *Handler) CreateUser(c *gin.Context) { var req struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` Email string `json:"email" binding:"required,email"` Role string `json:"role"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } role := store.RoleUser if req.Role != "" { role = req.Role } hash := crypto.Sha256Hex(req.Password) user := &store.User{ Username: req.Username, Email: req.Email, PasswordHash: hash, Role: role, Status: store.UserStatusActive, } if err := h.userDAO.Create(user); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "username or email already exists"}) return } c.JSON(http.StatusOK, user) } func (h *Handler) DeleteUser(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } if err := h.userDAO.Delete(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } // --- API Keys --- func (h *Handler) ListApiKeys(c *gin.Context) { userID, _ := c.Get("user_id") limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) keys, total, err := h.apiKeyDAO.ListByUserID(userID.(uint64), limit, offset) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": keys, "total": total}) } func (h *Handler) CreateApiKey(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` QuotaTokensPerDay *int64 `json:"quota_tokens_per_day"` QuotaRequestsPerDay *int `json:"quota_requests_per_day"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID, _ := c.Get("user_id") keyValue, _ := apikey.Generate() key := &store.APIKey{ UserID: userID.(uint64), Name: req.Name, KeyHash: apikey.Hash(keyValue), KeyPrefix: keyValue[:8], QuotaTokensPerDay: req.QuotaTokensPerDay, QuotaRequestsPerDay: req.QuotaRequestsPerDay, Status: store.KeyStatusActive, } if err := h.apiKeyDAO.Create(key); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "key": keyValue, "id": key.ID, }) } func (h *Handler) DeleteApiKey(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } if err := h.apiKeyDAO.Delete(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } // --- Channels --- func (h *Handler) ListChannels(c *gin.Context) { limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) channels, total, err := h.channelDAO.List(limit, offset) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": channels, "total": total}) } func (h *Handler) CreateChannel(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` Provider string `json:"provider" binding:"required"` BaseURL string `json:"base_url" binding:"required"` APIKey string `json:"api_key" binding:"required"` Priority int `json:"priority"` Weight int `json:"weight"` Formats []string `json:"formats"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } encrypted, err := crypto.Encrypt(req.APIKey) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"}) return } if req.Weight == 0 { req.Weight = 1 } ch := &store.Channel{ Name: req.Name, Provider: req.Provider, BaseURL: req.BaseURL, APIKeyEnc: encrypted, Weight: req.Weight, Priority: req.Priority, Formats: req.Formats, Enabled: true, } if err := h.channelDAO.Create(ch); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "channel name already exists"}) return } c.JSON(http.StatusOK, ch) } func (h *Handler) UpdateChannel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } ch, err := h.channelDAO.GetByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"}) return } var req struct { Name string `json:"name"` BaseURL string `json:"base_url"` APIKey string `json:"api_key"` Priority *int `json:"priority"` Weight *int `json:"weight"` Formats []string `json:"formats"` Enabled *bool `json:"enabled"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Name != "" { ch.Name = req.Name } if req.BaseURL != "" { ch.BaseURL = req.BaseURL } if req.APIKey != "" { encrypted, err := crypto.Encrypt(req.APIKey) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to encrypt API key"}) return } ch.APIKeyEnc = encrypted } if req.Priority != nil { ch.Priority = *req.Priority } if req.Weight != nil { ch.Weight = *req.Weight } if req.Formats != nil { ch.Formats = req.Formats } if req.Enabled != nil { ch.Enabled = *req.Enabled } if err := h.channelDAO.Update(ch); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, ch) } func (h *Handler) DeleteChannel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } if err := h.channelDAO.Delete(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } // --- Models --- func (h *Handler) ListModels(c *gin.Context) { limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) models, total, err := h.modelDAO.List(limit, offset) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": models, "total": total}) } func (h *Handler) CreateModel(c *gin.Context) { var req struct { Name string `json:"name" binding:"required"` DisplayName string `json:"display_name"` InputPrice float64 `json:"input_price"` OutputPrice float64 `json:"output_price"` CacheReadPrice float64 `json:"cache_read_price"` Sort int `json:"sort"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } m := &store.Model{ Name: req.Name, DisplayName: req.DisplayName, InputPrice: req.InputPrice, OutputPrice: req.OutputPrice, CacheReadPrice: req.CacheReadPrice, Sort: req.Sort, Enabled: true, } if err := h.modelDAO.Create(m); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "model name already exists"}) return } c.JSON(http.StatusOK, m) } func (h *Handler) UpdateModel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } m, err := h.modelDAO.GetByID(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "model not found"}) return } var req struct { DisplayName string `json:"display_name"` InputPrice *float64 `json:"input_price"` OutputPrice *float64 `json:"output_price"` CacheReadPrice *float64 `json:"cache_read_price"` Sort *int `json:"sort"` Enabled *bool `json:"enabled"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.DisplayName != "" { m.DisplayName = req.DisplayName } if req.InputPrice != nil { m.InputPrice = *req.InputPrice } if req.OutputPrice != nil { m.OutputPrice = *req.OutputPrice } if req.CacheReadPrice != nil { m.CacheReadPrice = *req.CacheReadPrice } if req.Sort != nil { m.Sort = *req.Sort } if req.Enabled != nil { m.Enabled = *req.Enabled } if err := h.modelDAO.Update(m); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, m) } func (h *Handler) DeleteModel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } if err := h.modelDAO.Delete(id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } // --- Channel-Model Bindings --- func (h *Handler) BindChannelModels(c *gin.Context) { channelID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid channel id"}) return } var req struct { Bindings []struct { ModelID uint64 `json:"model_id"` UpstreamModel string `json:"upstream_model"` Weight int `json:"weight"` } `json:"bindings"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } bindings := make([]store.ChannelModelBinding, len(req.Bindings)) for i, b := range req.Bindings { bindings[i] = store.ChannelModelBinding{ ChannelID: channelID, ModelID: b.ModelID, UpstreamModel: b.UpstreamModel, Weight: b.Weight, } } if err := h.channelDAO.BindModels(channelID, bindings); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "bound"}) } func (h *Handler) GetChannelModels(c *gin.Context) { channelID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid channel id"}) return } bindings, err := h.channelDAO.GetChannelModels(channelID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": bindings}) }