This commit is contained in:
2025-08-31 22:35:23 -04:00
parent ac51f75b5c
commit 1430c97ae7
36 changed files with 9962 additions and 73 deletions

View File

@ -0,0 +1,52 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
// HealthHandler handles health check requests
type HealthHandler struct {
db *sqlx.DB
logger *zap.Logger
}
// NewHealthHandler creates a new health handler
func NewHealthHandler(db *sqlx.DB, logger *zap.Logger) *HealthHandler {
return &HealthHandler{
db: db,
logger: logger,
}
}
// Health handles GET /health
func (h *HealthHandler) Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"service": "user-service",
"version": "1.0.0",
})
}
// Ready handles GET /ready
func (h *HealthHandler) Ready(c *gin.Context) {
// Check database connection
if err := h.db.Ping(); err != nil {
h.logger.Error("Database health check failed", zap.Error(err))
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "not ready",
"reason": "database connection failed",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "ready",
"service": "user-service",
"database": "connected",
})
}

View File

@ -0,0 +1,280 @@
package handlers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/user/internal/domain"
"github.com/RyanCopley/skybridge/user/internal/services"
)
// UserHandler handles HTTP requests for user operations
type UserHandler struct {
userService services.UserService
logger *zap.Logger
}
// NewUserHandler creates a new user handler
func NewUserHandler(userService services.UserService, logger *zap.Logger) *UserHandler {
return &UserHandler{
userService: userService,
logger: logger,
}
}
// Create handles POST /users
func (h *UserHandler) Create(c *gin.Context) {
var req domain.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Invalid request body", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request body",
"details": err.Error(),
})
return
}
// Get actor from context (set by auth middleware)
actorID := getActorFromContext(c)
user, err := h.userService.Create(c.Request.Context(), &req, actorID)
if err != nil {
h.logger.Error("Failed to create user", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to create user",
"details": err.Error(),
})
return
}
c.JSON(http.StatusCreated, user)
}
// GetByID handles GET /users/:id
func (h *UserHandler) GetByID(c *gin.Context) {
idParam := c.Param("id")
id, err := uuid.Parse(idParam)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid user ID",
"details": "User ID must be a valid UUID",
})
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"error": "User not found",
})
return
}
h.logger.Error("Failed to get user", zap.String("id", id.String()), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get user",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, user)
}
// Update handles PUT /users/:id
func (h *UserHandler) Update(c *gin.Context) {
idParam := c.Param("id")
id, err := uuid.Parse(idParam)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid user ID",
"details": "User ID must be a valid UUID",
})
return
}
var req domain.UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Invalid request body", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid request body",
"details": err.Error(),
})
return
}
// Get actor from context (set by auth middleware)
actorID := getActorFromContext(c)
user, err := h.userService.Update(c.Request.Context(), id, &req, actorID)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"error": "User not found",
})
return
}
h.logger.Error("Failed to update user", zap.String("id", id.String()), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to update user",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, user)
}
// Delete handles DELETE /users/:id
func (h *UserHandler) Delete(c *gin.Context) {
idParam := c.Param("id")
id, err := uuid.Parse(idParam)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid user ID",
"details": "User ID must be a valid UUID",
})
return
}
// Get actor from context (set by auth middleware)
actorID := getActorFromContext(c)
err = h.userService.Delete(c.Request.Context(), id, actorID)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"error": "User not found",
})
return
}
h.logger.Error("Failed to delete user", zap.String("id", id.String()), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to delete user",
"details": err.Error(),
})
return
}
c.JSON(http.StatusNoContent, nil)
}
// List handles GET /users
func (h *UserHandler) List(c *gin.Context) {
var req domain.ListUsersRequest
// Parse query parameters
if status := c.Query("status"); status != "" {
s := domain.UserStatus(status)
req.Status = &s
}
if role := c.Query("role"); role != "" {
r := domain.UserRole(role)
req.Role = &r
}
req.Search = c.Query("search")
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil && l > 0 {
req.Limit = l
}
}
if offset := c.Query("offset"); offset != "" {
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
req.Offset = o
}
}
req.OrderBy = c.DefaultQuery("order_by", "created_at")
req.OrderDir = c.DefaultQuery("order_dir", "desc")
response, err := h.userService.List(c.Request.Context(), &req)
if err != nil {
h.logger.Error("Failed to list users", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to list users",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, response)
}
// GetByEmail handles GET /users/email/:email
func (h *UserHandler) GetByEmail(c *gin.Context) {
email := c.Param("email")
if email == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Email parameter is required",
})
return
}
user, err := h.userService.GetByEmail(c.Request.Context(), email)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{
"error": "User not found",
})
return
}
h.logger.Error("Failed to get user by email", zap.String("email", email), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get user",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, user)
}
// ExistsByEmail handles GET /users/exists/:email
func (h *UserHandler) ExistsByEmail(c *gin.Context) {
email := c.Param("email")
if email == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Email parameter is required",
})
return
}
exists, err := h.userService.ExistsByEmail(c.Request.Context(), email)
if err != nil {
h.logger.Error("Failed to check user existence", zap.String("email", email), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to check user existence",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"exists": exists,
"email": email,
})
}
// Helper function to get actor from gin context
func getActorFromContext(c *gin.Context) string {
if actor, exists := c.Get("actor_id"); exists {
if actorStr, ok := actor.(string); ok {
return actorStr
}
}
// Fallback to email header if available
if email := c.GetHeader("X-User-Email"); email != "" {
return email
}
return "system"
}