Initial commit: CarrotAssistant backend (Go + Gin + SQLite, OpenAI-compatible agent runtime)
This commit is contained in:
110
internal/server/auth.go
Normal file
110
internal/server/auth.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"code.littlelan.cn/CarrotAssistant/backend/internal/store"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// tokenLifetime bounds how long an admin session token remains valid. The
|
||||
// SPA re-logs-in after expiry rather than refreshing, keeping the flow simple.
|
||||
const tokenLifetime = 24 * time.Hour
|
||||
|
||||
// issueToken signs a JWT for the given admin username.
|
||||
func (d Deps) issueToken(username string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"sub": username,
|
||||
"exp": time.Now().Add(tokenLifetime).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return tok.SignedString([]byte(d.Config.JWTSecret))
|
||||
}
|
||||
|
||||
// RequireAdmin is the middleware guarding /admin routes (other than login).
|
||||
// It validates the Bearer JWT and loads the AdminUser row onto the context
|
||||
// as "admin_user".
|
||||
func (d Deps) RequireAdmin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
u, err := d.adminFromToken(c)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录或会话已过期"})
|
||||
return
|
||||
}
|
||||
c.Set("admin_user", u)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func (d Deps) adminFromToken(c *gin.Context) (*store.AdminUser, error) {
|
||||
auth := c.GetHeader("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
return nil, errors.New("missing bearer token")
|
||||
}
|
||||
raw := strings.TrimPrefix(auth, "Bearer ")
|
||||
|
||||
tok, err := jwt.Parse(raw, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
||||
}
|
||||
return []byte(d.Config.JWTSecret), nil
|
||||
})
|
||||
if err != nil || !tok.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
claims, ok := tok.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid claims")
|
||||
}
|
||||
username, _ := claims["sub"].(string)
|
||||
if username == "" {
|
||||
return nil, errors.New("missing subject")
|
||||
}
|
||||
|
||||
var u store.AdminUser
|
||||
if err := d.DB.Where("username = ?", username).First(&u).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("admin not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// adminCtx returns the authenticated admin user placed by RequireAdmin.
|
||||
func adminCtx(c *gin.Context) *store.AdminUser {
|
||||
u, _ := c.Get("admin_user")
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
return u.(*store.AdminUser)
|
||||
}
|
||||
|
||||
// audit is a small helper to record an audit log row from any handler. It
|
||||
// never fails the request: auditing is best-effort but logged on error.
|
||||
func (d Deps) audit(c *gin.Context, appID *int64, sessionID *string, action string, detail map[string]any) {
|
||||
actor := "system"
|
||||
if u := adminCtx(c); u != nil {
|
||||
actor = u.Username
|
||||
}
|
||||
d.auditBy(actor, appID, sessionID, action, detail)
|
||||
}
|
||||
|
||||
func (d Deps) auditBy(actor string, appID *int64, sessionID *string, action string, detail map[string]any) {
|
||||
row := store.AuditLog{AppID: appID, SessionID: sessionID, Actor: actor, Action: action}
|
||||
if detail != nil {
|
||||
row.Detail = marshalJSON(detail)
|
||||
}
|
||||
if err := d.DB.Create(&row).Error; err != nil {
|
||||
// Best-effort: surface to stderr but never interrupt the caller.
|
||||
gin.DefaultErrorWriter.Write([]byte("audit log: " + err.Error() + "\n"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user