package admin import ( "net/http" "strings" "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 UsersHandler struct { db *gorm.DB } func NewUsersHandler(db *gorm.DB) *UsersHandler { return &UsersHandler{db: db} } type CreateUserRequest struct { Username string `json:"username" binding:"required,min=3,max=64"` Password string `json:"password" binding:"required,min=8"` } type UpdateUserRequest struct { Password *string `json:"password"` IsActive *bool `json:"is_active"` } func (h *UsersHandler) List(c *gin.Context) { var users []db.AdminUser if err := h.db.Order("created_at DESC").Find(&users).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "internal_error", "message": "Failed to list users"}}) return } result := make([]gin.H, len(users)) for i, u := range users { result[i] = gin.H{ "id": u.ID, "username": u.Username, "is_active": u.IsActive, "created_at": u.CreatedAt, "last_login_at": u.LastLoginAt, } } c.JSON(http.StatusOK, result) } func (h *UsersHandler) Create(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 CreateUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "validation_error", "message": "username (3-64 chars) and password (min 8 chars) required"}}) return } username := strings.ToLower(strings.TrimSpace(req.Username)) var count int64 h.db.Model(&db.AdminUser{}).Where("username = ?", username).Count(&count) if count > 0 { c.JSON(http.StatusConflict, gin.H{"error": gin.H{"code": "conflict", "message": "Username already exists"}}) return } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "internal_error", "message": "Failed to hash password"}}) return } user := &db.AdminUser{ ID: uuid.New(), Username: username, PasswordHash: string(hash), IsActive: true, } if err := h.db.Create(user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "internal_error", "message": "Failed to create user"}}) return } c.JSON(http.StatusCreated, gin.H{ "id": user.ID, "username": user.Username, "is_active": user.IsActive, "created_at": user.CreatedAt, }) } func (h *UsersHandler) Update(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 } idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "validation_error", "message": "Invalid user ID"}}) return } currentUser, _ := uuid.Parse(claims.Subject) if currentUser == id { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "validation_error", "message": "Cannot modify your own account via this endpoint"}}) return } var req UpdateUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "validation_error", "message": "Invalid request body"}}) return } var user db.AdminUser if err := h.db.Where("id = ?", id).First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"code": "not_found", "message": "User not found"}}) return } updates := map[string]interface{}{"updated_at": time.Now()} if req.Password != nil && *req.Password != "" { if len(*req.Password) < 8 { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "validation_error", "message": "Password must be at least 8 characters"}}) return } hash, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "internal_error", "message": "Failed to hash password"}}) return } updates["password_hash"] = string(hash) } if req.IsActive != nil { updates["is_active"] = *req.IsActive } h.db.Model(&user).Updates(updates) c.JSON(http.StatusOK, gin.H{ "id": user.ID, "username": user.Username, "is_active": user.IsActive, "updated_at": user.UpdatedAt, }) } func (h *UsersHandler) Delete(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 } idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "validation_error", "message": "Invalid user ID"}}) return } currentUser, _ := uuid.Parse(claims.Subject) if currentUser == id { c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{"code": "validation_error", "message": "Cannot delete your own account"}}) return } var user db.AdminUser if err := h.db.Where("id = ?", id).First(&user).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": gin.H{"code": "not_found", "message": "User not found"}}) return } h.db.Delete(&user) c.Status(http.StatusNoContent) }