Files
backend/internal/server/server.go

150 lines
4.2 KiB
Go

// Package server assembles the HTTP application: it wires the gin engine,
// mounts route groups, and exposes helpers used by main for bootstrap.
package server
import (
"net/http"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"code.littlelan.cn/CarrotAssistant/backend/internal/config"
"code.littlelan.cn/CarrotAssistant/backend/internal/security"
"code.littlelan.cn/CarrotAssistant/backend/internal/store"
)
// Deps bundles the shared dependencies every handler group needs. Keeping
// them in one struct avoids a long parameter list as routes grow.
type Deps struct {
DB *gorm.DB
Config *config.Config
Crypto *security.Crypto
SkillsDir string
}
// New builds the gin engine with all route groups mounted.
func New(d Deps) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery())
r.Use(requestLogger())
// Permissive CORS for development; the admin SPA and any embedded client
// run on different origins. Tighten AllowedOrigins in production.
r.Use(cors.New(cors.Config{
AllowOriginFunc: func(string) bool { return true },
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-App-Token"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// Liveness probe for container orchestrators.
r.GET("/healthz", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true, "ts": time.Now().Unix()})
})
d.mountAdminRoutes(r)
d.mountChatRoutes(r)
return r
}
// mountChatRoutes wires the public end-user Chat API under /api/v1. Every
// route requires a valid X-App-Token.
func (d Deps) mountChatRoutes(r *gin.Engine) {
g := r.Group("/api/v1")
g.Use(d.RequireAppToken())
g.POST("/chat", d.handleChat)
g.POST("/chat/confirm", d.handleConfirm)
}
// mountAdminRoutes wires everything under /admin. Login is public; every
// other route sits behind RequireAdmin.
func (d Deps) mountAdminRoutes(r *gin.Engine) {
g := r.Group("/admin")
g.POST("/login", d.handleLogin)
auth := g.Group("")
auth.Use(d.RequireAdmin())
auth.GET("/me", d.handleMe)
// Apps
auth.GET("/apps", d.handleListApps)
auth.POST("/apps", d.handleCreateApp)
auth.GET("/apps/:id", d.handleGetApp)
auth.PUT("/apps/:id", d.handleUpdateApp)
auth.DELETE("/apps/:id", d.handleDeleteApp)
auth.POST("/apps/:id/token/rotate", d.handleRotateToken)
// Model configs
auth.GET("/model-configs", d.handleListModels)
auth.POST("/model-configs", d.handleCreateModel)
auth.PUT("/model-configs/:id", d.handleUpdateModel)
auth.DELETE("/model-configs/:id", d.handleDeleteModel)
// Skills
auth.GET("/apps/:id/skills", d.handleListSkills)
auth.POST("/apps/:id/skills/generate", d.handleGenerateSkill)
auth.GET("/skills/:id", d.handleGetSkill)
auth.PUT("/skills/:id", d.handleUpdateSkill)
auth.POST("/skills/:id/publish", d.handlePublishSkill)
auth.DELETE("/skills/:id", d.handleDeleteSkill)
auth.POST("/skills/:id/test", d.handleTestSkill)
// Audit logs
auth.GET("/audit-logs", d.handleListAuditLogs)
}
func requestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
gin.DefaultErrorWriter.Write([]byte(
c.Request.Method + " " + c.Request.URL.RequestURI() +
" " + itoa(c.Writer.Status()) + " " + time.Since(start).String() + "\n",
))
}
}
// itoa keeps a tiny stdlib-only int->string here so the logger above does
// not pull in strconv just for one line. Using strconv would be equally fine.
func itoa(n int) string {
if n == 0 {
return "0"
}
neg := n < 0
if neg {
n = -n
}
var b [12]byte
i := len(b)
for n > 0 {
i--
b[i] = byte('0' + n%10)
n /= 10
}
if neg {
i--
b[i] = '-'
}
return string(b[i:])
}
// EnsureAdmin creates username with the given password if no admin exists.
// It is a no-op when the username is already taken.
func EnsureAdmin(db *gorm.DB, username, password string) error {
var count int64
if err := db.Model(&store.AdminUser{}).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
hash, err := security.HashPassword(password)
if err != nil {
return err
}
return db.Create(&store.AdminUser{Username: username, PasswordHash: hash}).Error
}