Files
llama-link/internal/api/handlers/admin/me.go
T
darroyo 4d34c6d31a
CI / test (push) Failing after 12m45s
Replace token-based admin auth with JWT session authentication
- Add AdminUser model (bcrypt hashed passwords) and admin_users table
- Add AdminJWTService for HS256 JWT sessions (24h TTL)
- Add AdminSessionAuth middleware for /api/v1/admin/* routes
- Add admin handlers: login, logout, me, change-password, users CRUD
- Keys and model management routes now require admin JWT session
- Remove ADMIN_TOKEN, add ADMIN_USERNAME, ADMIN_PASSWORD env vars
- Update frontend: username/password login, admin_session storage, AdminUsers CRUD view
2026-07-31 17:15:29 -04:00

94 lines
2.7 KiB
Go

package admin
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"github.com/llamalink/llamalink/internal/api/middleware"
"github.com/llamalink/llamalink/internal/db"
)
type MeHandler struct {
db *gorm.DB
}
func NewMeHandler(db *gorm.DB) *MeHandler {
return &MeHandler{db: db}
}
func (h *MeHandler) Me(c *gin.Context) {
claims := middleware.GetAdminClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "unauthorized", "message": "Not authenticated"}})
return
}
var user db.AdminUser
if err := h.db.Where("username = ?", claims.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "unauthorized", "message": "User not found"}})
return
}
c.JSON(http.StatusOK, gin.H{
"id": user.ID,
"username": user.Username,
"is_active": user.IsActive,
"created_at": user.CreatedAt,
"last_login_at": user.LastLoginAt,
})
}
type ChangePasswordRequest struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
func (h *MeHandler) ChangePassword(c *gin.Context) {
claims := middleware.GetAdminClaims(c)
if claims == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "unauthorized", "message": "Not authenticated"}})
return
}
var req ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "validation_error", "message": "current_password and new_password required (min 8 chars)"}})
return
}
userID, err := uuid.Parse(claims.Subject)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "invalid_request", "message": "Invalid token claims"}})
return
}
var user db.AdminUser
if err := h.db.Where("id = ?", userID).First(&user).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"code": "not_found", "message": "User not found"}})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "invalid_credentials", "message": "Current password is incorrect"}})
return
}
newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "internal_error", "message": "Failed to hash password"}})
return
}
h.db.Model(&user).Updates(map[string]interface{}{
"password_hash": string(newHash),
"updated_at": time.Now(),
})
c.Status(http.StatusOK)
}