de5f7ca4ee
CI / test (push) Failing after 12m28s
- Remove dead AdminOnly() and AdminTokenAuth() middleware (unused since JWT switch) - Remove dead RequireScope() middleware (also unused) - Fix seed.go: only log password when auto-generated (was logging every time) - Fix seed.go: use admin.Username (normalized) instead of raw username in log - Simplify generateRandomPassword: 12 bytes (24 hex chars) instead of 24 bytes then truncate - Me handler: use claims.Subject (UUID) for lookup instead of username - Remove unused /logout endpoint (stateless JWT)
99 lines
2.9 KiB
Go
99 lines
2.9 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
|
|
userID, err := uuid.Parse(claims.Subject)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "unauthorized", "message": "Invalid token claims"}})
|
|
return
|
|
}
|
|
if err := h.db.Where("id = ?", userID).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)
|
|
}
|