Files
opencatd-open/backend/internal/api/api.go
T
Sakurasan 094230a293 feat: API key management improvements
- Rename routes: /dashboard/tokens → /dashboard/apikeys, /dashboard/manager/keys → /dashboard/manager/channels
- Add KeyPlain field to store plaintext API keys for re-viewing
- API key list shows masked key (sk-ot-123456****abcd) with eye toggle to reveal
- Copy button with 2s feedback on key list
- TokenNew shows full key + copy after creation
- Increase key prefix display to 12 characters
- Fix SQLite driver: replace gorm.io/driver/sqlite with ncruces/go-sqlite3/gormlite
- Fix user.status === 'active' checks across frontend views
- Add channel store for new channels API
- Update Makefile frontend build to work reliably

BREAKING CHANGE: Existing API keys created before this change will not show their plaintext value (only prefix visible).
2026-08-30 15:21:49 +08:00

884 lines
22 KiB
Go

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),
}
}
// --- Helpers ---
func userToResponse(user *store.User) gin.H {
roleNum := 1
if user.Role == store.RoleAdmin {
roleNum = 10
}
return gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": roleNum,
"status": user.Status,
"balance": user.Balance,
"created_at": user.CreatedAt,
"updated_at": user.UpdatedAt,
"last_login_at": user.LastLoginAt,
}
}
// --- 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:"omitempty,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)
email := req.Email
if email == "" {
email = req.Username + "@placeholder.local"
}
user := &store.User{
Username: req.Username,
Email: 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
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": userToResponse(user),
})
}
func (h *Handler) UpdateProfile(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
}
var req struct {
Email string `json:"email"`
Name string `json:"name"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Email != "" {
user.Email = req.Email
}
if err := h.userDAO.Update(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "profile updated"})
}
func (h *Handler) UpdatePassword(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
}
var req struct {
Password string `json:"password" binding:"required"`
NewPassword string `json:"newpassword" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Verify old password
oldHash := crypto.Sha256Hex(req.Password)
if user.PasswordHash != oldHash {
c.JSON(http.StatusBadRequest, gin.H{"error": "incorrect password"})
return
}
// Update to new password
user.PasswordHash = crypto.Sha256Hex(req.NewPassword)
if err := h.userDAO.Update(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "password updated"})
}
// --- Users ---
func (h *Handler) ListUsers(c *gin.Context) {
// Support both limit/offset and pageSize/page parameters
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
// If pageSize/page are provided, use them instead
if pageSize := c.Query("pageSize"); pageSize != "" {
if size, err := strconv.Atoi(pageSize); err == nil && size > 0 {
limit = size
}
}
if page := c.Query("page"); page != "" {
if p, err := strconv.Atoi(page); err == nil && p > 0 {
offset = (p - 1) * limit
}
}
users, total, err := h.userDAO.List(limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
data := make([]gin.H, len(users))
for i, u := range users {
data[i] = userToResponse(u)
}
c.JSON(http.StatusOK, gin.H{"data": data, "total": total})
}
func (h *Handler) GetUser(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
}
user, err := h.userDAO.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": userToResponse(user),
})
}
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, userToResponse(user))
}
func (h *Handler) UpdateUser(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
}
user, err := h.userDAO.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
var req struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Role string `json:"role"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Username != "" {
user.Username = req.Username
}
if req.Email != "" {
user.Email = req.Email
}
if req.Password != "" {
user.PasswordHash = crypto.Sha256Hex(req.Password)
}
if req.Role != "" {
user.Role = req.Role
}
if req.Status != "" {
user.Status = req.Status
}
if err := h.userDAO.Update(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, userToResponse(user))
}
func (h *Handler) BatchUsers(c *gin.Context) {
option := c.Param("option")
var req struct {
IDs []uint64 `json:"ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for _, id := range req.IDs {
user, err := h.userDAO.GetByID(id)
if err != nil {
continue
}
switch option {
case "enable":
user.Status = store.UserStatusActive
case "disable":
user.Status = store.UserStatusDisabled
case "delete":
h.userDAO.Delete(id)
continue
}
h.userDAO.Update(user)
}
c.JSON(http.StatusOK, gin.H{"message": "batch operation completed"})
}
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")
// Support both limit/offset and pageSize/page parameters
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
// If pageSize/page are provided, use them instead
if pageSize := c.Query("pageSize"); pageSize != "" {
if size, err := strconv.Atoi(pageSize); err == nil && size > 0 {
limit = size
}
}
if page := c.Query("page"); page != "" {
if p, err := strconv.Atoi(page); err == nil && p > 0 {
offset = (p - 1) * limit
}
}
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) GetApiKey(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
}
key, err := h.apiKeyDAO.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "key not found"})
return
}
c.JSON(http.StatusOK, gin.H{"data": key})
}
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),
KeyPlain: keyValue,
KeyPrefix: keyValue[:12],
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,
"data": key,
})
}
func (h *Handler) UpdateApiKey(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
}
key, err := h.apiKeyDAO.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "key not found"})
return
}
var req struct {
Name string `json:"name"`
QuotaTokensPerDay *int64 `json:"quota_tokens_per_day"`
QuotaRequestsPerDay *int `json:"quota_requests_per_day"`
AllowedModels []string `json:"allowed_models"`
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Name != "" {
key.Name = req.Name
}
if req.QuotaTokensPerDay != nil {
key.QuotaTokensPerDay = req.QuotaTokensPerDay
}
if req.QuotaRequestsPerDay != nil {
key.QuotaRequestsPerDay = req.QuotaRequestsPerDay
}
if req.AllowedModels != nil {
key.AllowedModels = req.AllowedModels
}
if req.Status != "" {
key.Status = req.Status
}
if err := h.apiKeyDAO.Update(key); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": key})
}
func (h *Handler) BatchApiKeys(c *gin.Context) {
option := c.Param("option")
var req struct {
IDs []uint64 `json:"ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
switch option {
case "delete":
if err := h.apiKeyDAO.BatchDelete(req.IDs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
c.JSON(http.StatusOK, gin.H{"message": "batch operation completed"})
}
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) {
// Support both limit/offset and pageSize/page parameters
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
// If pageSize/page are provided, use them instead
if pageSize := c.Query("pageSize"); pageSize != "" {
if size, err := strconv.Atoi(pageSize); err == nil && size > 0 {
limit = size
}
}
if page := c.Query("page"); page != "" {
if p, err := strconv.Atoi(page); err == nil && p > 0 {
offset = (p - 1) * limit
}
}
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) {
// Support both limit/offset and pageSize/page parameters
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
// If pageSize/page are provided, use them instead
if pageSize := c.Query("pageSize"); pageSize != "" {
if size, err := strconv.Atoi(pageSize); err == nil && size > 0 {
limit = size
}
}
if page := c.Query("page"); page != "" {
if p, err := strconv.Atoi(page); err == nil && p > 0 {
offset = (p - 1) * limit
}
}
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})
}