feat: Phase 3 - architecture & maintainability
- Split 1474-line dashboard.go into 5 domain files (clients, providers, users, system) - Unit tests for ModelRegistry.FindModel and CalculateCost - go mod tidy + verify (deps clean) - .gitignore excludes tool cache dirs (.pi-lens/, .opencode/)
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gophergate/internal/db"
|
||||
"gophergate/internal/models"
|
||||
"gophergate/internal/utils"
|
||||
"log/slog"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
|
||||
func (s *Server) handleGetClients(c *gin.Context) {
|
||||
var clients []db.Client
|
||||
err := s.database.Select(&clients, "SELECT * FROM clients ORDER BY created_at DESC")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
type UIClient struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsed *time.Time `json:"last_used"`
|
||||
RequestsCount int `json:"requests_count"`
|
||||
TokensCount int `json:"tokens_count"`
|
||||
Status string `json:"status"`
|
||||
RateLimitPerMinute int `json:"rate_limit_per_minute"`
|
||||
}
|
||||
|
||||
uiClients := make([]UIClient, len(clients))
|
||||
for i, cl := range clients {
|
||||
status := "active"
|
||||
if !cl.IsActive {
|
||||
status = "disabled"
|
||||
}
|
||||
|
||||
name := ""
|
||||
if cl.Name != nil {
|
||||
name = *cl.Name
|
||||
}
|
||||
desc := ""
|
||||
if cl.Description != nil {
|
||||
desc = *cl.Description
|
||||
}
|
||||
|
||||
var lastUsedTime sql.NullTime
|
||||
_ = s.database.Get(&lastUsedTime, "SELECT MAX(last_used_at) FROM client_tokens WHERE client_id = ?", cl.ClientID)
|
||||
|
||||
var lastUsed *time.Time
|
||||
if lastUsedTime.Valid && !lastUsedTime.Time.IsZero() {
|
||||
t := lastUsedTime.Time
|
||||
lastUsed = &t
|
||||
}
|
||||
|
||||
uiClients[i] = UIClient{
|
||||
ID: cl.ClientID,
|
||||
Name: name,
|
||||
Description: desc,
|
||||
CreatedAt: cl.CreatedAt,
|
||||
LastUsed: lastUsed,
|
||||
RequestsCount: cl.TotalRequests,
|
||||
TokensCount: cl.TotalTokens,
|
||||
Status: status,
|
||||
RateLimitPerMinute: cl.RateLimitPerMinute,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(uiClients))
|
||||
}
|
||||
|
||||
func (s *Server) handleGetClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var cl db.Client
|
||||
err := s.database.Get(&cl, "SELECT * FROM clients WHERE client_id = ?", id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, ErrorResponse("Client not found"))
|
||||
return
|
||||
}
|
||||
|
||||
name := ""
|
||||
if cl.Name != nil {
|
||||
name = *cl.Name
|
||||
}
|
||||
desc := ""
|
||||
if cl.Description != nil {
|
||||
desc = *cl.Description
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||
"id": cl.ClientID,
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"is_active": cl.IsActive,
|
||||
"rate_limit_per_minute": cl.RateLimitPerMinute,
|
||||
"created_at": cl.CreatedAt,
|
||||
}))
|
||||
}
|
||||
|
||||
type UpdateClientRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
RateLimitPerMinute *int `json:"rate_limit_per_minute"`
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var req UpdateClientRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
_, err := s.database.Exec(`
|
||||
UPDATE clients SET
|
||||
name = ?,
|
||||
description = ?,
|
||||
is_active = ?,
|
||||
rate_limit_per_minute = COALESCE(?, rate_limit_per_minute),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE client_id = ?
|
||||
`, req.Name, req.Description, req.IsActive, req.RateLimitPerMinute, id)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Client updated"}))
|
||||
}
|
||||
|
||||
type CreateClientRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ClientID *string `json:"client_id"`
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateClient(c *gin.Context) {
|
||||
var req CreateClientRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request"))
|
||||
return
|
||||
}
|
||||
|
||||
clientID := ""
|
||||
if req.ClientID != nil {
|
||||
clientID = *req.ClientID
|
||||
} else {
|
||||
clientID = "client-" + uuid.New().String()[:8]
|
||||
}
|
||||
|
||||
_, err := s.database.Exec("INSERT INTO clients (client_id, name, is_active) VALUES (?, ?, 1)", clientID, req.Name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
token := "sk-" + uuid.New().String() + uuid.New().String()
|
||||
token = token[:51]
|
||||
|
||||
_, err = s.database.Exec("INSERT INTO client_tokens (client_id, token, name) VALUES (?, ?, 'default')", clientID, token)
|
||||
if err != nil {
|
||||
// Log error
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||
"id": clientID,
|
||||
"name": req.Name,
|
||||
"status": "active",
|
||||
"token": token,
|
||||
"created_at": time.Now(),
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "default" {
|
||||
c.JSON(http.StatusBadRequest, ErrorResponse("Cannot delete default client"))
|
||||
return
|
||||
}
|
||||
|
||||
_, err := s.database.Exec("DELETE FROM clients WHERE client_id = ?", id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Client deleted"}))
|
||||
}
|
||||
|
||||
func (s *Server) handleGetClientTokens(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var tokens []db.ClientToken
|
||||
err := s.database.Select(&tokens, "SELECT * FROM client_tokens WHERE client_id = ? ORDER BY created_at DESC", id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
type MaskedToken struct {
|
||||
ID int `json:"id"`
|
||||
TokenMasked string `json:"token_masked"`
|
||||
Name string `json:"name"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
}
|
||||
|
||||
masked := make([]MaskedToken, len(tokens))
|
||||
for i, t := range tokens {
|
||||
maskedToken := "••••"
|
||||
if len(t.Token) > 8 {
|
||||
maskedToken = t.Token[:3] + "••••" + t.Token[len(t.Token)-8:]
|
||||
}
|
||||
masked[i] = MaskedToken{
|
||||
ID: t.ID,
|
||||
TokenMasked: maskedToken,
|
||||
Name: t.Name,
|
||||
IsActive: t.IsActive,
|
||||
CreatedAt: t.CreatedAt,
|
||||
LastUsedAt: t.LastUsedAt,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(masked))
|
||||
}
|
||||
|
||||
type CreateTokenRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (s *Server) handleCreateClientToken(c *gin.Context) {
|
||||
clientID := c.Param("id")
|
||||
var req CreateTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
// optional name
|
||||
}
|
||||
|
||||
name := "default"
|
||||
if req.Name != "" {
|
||||
name = req.Name
|
||||
}
|
||||
|
||||
token := "sk-" + uuid.New().String() + uuid.New().String()
|
||||
token = token[:51]
|
||||
|
||||
_, err := s.database.Exec("INSERT INTO client_tokens (client_id, token, name) VALUES (?, ?, ?)", clientID, token, name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{
|
||||
"token": token,
|
||||
"name": name,
|
||||
"created_at": time.Now(),
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteClientToken(c *gin.Context) {
|
||||
tokenID := c.Param("token_id")
|
||||
|
||||
_, err := s.database.Exec("DELETE FROM client_tokens WHERE id = ?", tokenID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Token revoked"}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user