feat: Implement User Management API (Issue #6) and wire up router
- Create UserService with CRUD operations - Implement ListUsers with pagination - Implement GetUser, CreateUser, UpdateUser, DeleteUser - Add ResetUserPassword for admin password resets - Create UserHandler with HTTP endpoints - Update router to wire up all handlers and middleware - Add authentication middleware to protected routes - Add admin-only middleware to user management routes Closes #6 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
224
backend/internal/api/handlers/users.go
Normal file
224
backend/internal/api/handlers/users.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yourusername/victorialogs-manager/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserHandler handles user management endpoints
|
||||||
|
type UserHandler struct {
|
||||||
|
userService *services.UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserHandler creates a new user handler
|
||||||
|
func NewUserHandler(userService *services.UserService) *UserHandler {
|
||||||
|
return &UserHandler{
|
||||||
|
userService: userService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers returns a paginated list of users
|
||||||
|
// @Summary List users
|
||||||
|
// @Description Get paginated list of users (admin only)
|
||||||
|
// @Tags users
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Limit" default(20)
|
||||||
|
// @Param offset query int false "Offset" default(0)
|
||||||
|
// @Success 200 {object} services.ListUsersResponse
|
||||||
|
// @Failure 401 {object} ErrorResponse
|
||||||
|
// @Failure 403 {object} ErrorResponse
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/users [get]
|
||||||
|
func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||||
|
var req services.ListUsersRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Error: "invalid request",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.userService.ListUsers(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||||
|
Error: "failed to list users",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns a user by ID
|
||||||
|
// @Summary Get user
|
||||||
|
// @Description Get user by ID (admin only)
|
||||||
|
// @Tags users
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "User ID"
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 401 {object} ErrorResponse
|
||||||
|
// @Failure 403 {object} ErrorResponse
|
||||||
|
// @Failure 404 {object} ErrorResponse
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/users/{id} [get]
|
||||||
|
func (h *UserHandler) GetUser(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
user, err := h.userService.GetUser(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, ErrorResponse{
|
||||||
|
Error: "user not found",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user
|
||||||
|
// @Summary Create user
|
||||||
|
// @Description Create a new user (admin only)
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param request body services.CreateUserRequest true "User data"
|
||||||
|
// @Success 201 {object} models.User
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 401 {object} ErrorResponse
|
||||||
|
// @Failure 403 {object} ErrorResponse
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/users [post]
|
||||||
|
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||||
|
var req services.CreateUserRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Error: "invalid request",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.userService.CreateUser(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Error: "failed to create user",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser updates a user
|
||||||
|
// @Summary Update user
|
||||||
|
// @Description Update user information (admin only)
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "User ID"
|
||||||
|
// @Param request body services.UpdateUserRequest true "User data"
|
||||||
|
// @Success 200 {object} models.User
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 401 {object} ErrorResponse
|
||||||
|
// @Failure 403 {object} ErrorResponse
|
||||||
|
// @Failure 404 {object} ErrorResponse
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/users/{id} [put]
|
||||||
|
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var req services.UpdateUserRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Error: "invalid request",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := h.userService.UpdateUser(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Error: "failed to update user",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes a user
|
||||||
|
// @Summary Delete user
|
||||||
|
// @Description Delete a user (admin only)
|
||||||
|
// @Tags users
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "User ID"
|
||||||
|
// @Success 200 {object} SuccessResponse
|
||||||
|
// @Failure 401 {object} ErrorResponse
|
||||||
|
// @Failure 403 {object} ErrorResponse
|
||||||
|
// @Failure 404 {object} ErrorResponse
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/users/{id} [delete]
|
||||||
|
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
err := h.userService.DeleteUser(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Error: "failed to delete user",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, SuccessResponse{
|
||||||
|
Message: "user deleted successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserPassword resets a user's password
|
||||||
|
// @Summary Reset user password
|
||||||
|
// @Description Reset a user's password (admin only)
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "User ID"
|
||||||
|
// @Param request body services.ResetPasswordRequest true "New password"
|
||||||
|
// @Success 200 {object} SuccessResponse
|
||||||
|
// @Failure 400 {object} ErrorResponse
|
||||||
|
// @Failure 401 {object} ErrorResponse
|
||||||
|
// @Failure 403 {object} ErrorResponse
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Router /api/v1/users/{id}/reset-password [post]
|
||||||
|
func (h *UserHandler) ResetUserPassword(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
var req services.ResetPasswordRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Error: "invalid request",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.userService.ResetUserPassword(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Error: "failed to reset password",
|
||||||
|
Message: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, SuccessResponse{
|
||||||
|
Message: "password reset successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,7 +5,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yourusername/victorialogs-manager/internal/api/handlers"
|
||||||
|
"github.com/yourusername/victorialogs-manager/internal/api/middleware"
|
||||||
"github.com/yourusername/victorialogs-manager/internal/config"
|
"github.com/yourusername/victorialogs-manager/internal/config"
|
||||||
|
"github.com/yourusername/victorialogs-manager/internal/repository"
|
||||||
|
"github.com/yourusername/victorialogs-manager/internal/services"
|
||||||
|
"github.com/yourusername/victorialogs-manager/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewRouter creates and configures the main router
|
// NewRouter creates and configures the main router
|
||||||
@@ -13,15 +18,35 @@ func NewRouter(cfg *config.Config, db *sql.DB) *gin.Engine {
|
|||||||
// Set Gin mode
|
// Set Gin mode
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
router := gin.Default()
|
router := gin.New()
|
||||||
|
|
||||||
// CORS middleware
|
// Global middleware
|
||||||
router.Use(corsMiddleware())
|
router.Use(gin.Recovery())
|
||||||
|
router.Use(middleware.CORSMiddleware())
|
||||||
|
router.Use(middleware.RequestLogger())
|
||||||
|
|
||||||
|
// Initialize JWT manager
|
||||||
|
jwtManager := utils.NewJWTManager(
|
||||||
|
cfg.Auth.JWTSecret,
|
||||||
|
cfg.Auth.AccessTokenDuration,
|
||||||
|
cfg.Auth.RefreshTokenDuration,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize repositories
|
||||||
|
userRepo := repository.NewUserRepository(db)
|
||||||
|
|
||||||
|
// Initialize services
|
||||||
|
authService := services.NewAuthService(userRepo, jwtManager)
|
||||||
|
userService := services.NewUserService(userRepo)
|
||||||
|
|
||||||
|
// Initialize handlers
|
||||||
|
authHandler := handlers.NewAuthHandler(authService)
|
||||||
|
userHandler := handlers.NewUserHandler(userService)
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
router.GET("/health", func(c *gin.Context) {
|
router.GET("/health", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"service": "victorialogs-manager",
|
"service": "victorialogs-manager",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -32,45 +57,34 @@ func NewRouter(cfg *config.Config, db *sql.DB) *gin.Engine {
|
|||||||
// Authentication routes (public)
|
// Authentication routes (public)
|
||||||
auth := v1.Group("/auth")
|
auth := v1.Group("/auth")
|
||||||
{
|
{
|
||||||
auth.POST("/login", func(c *gin.Context) {
|
auth.POST("/login", authHandler.Login)
|
||||||
// TODO: Implement login handler
|
auth.POST("/refresh", authHandler.RefreshToken)
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
|
||||||
})
|
|
||||||
auth.POST("/refresh", func(c *gin.Context) {
|
|
||||||
// TODO: Implement refresh handler
|
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
|
||||||
})
|
|
||||||
auth.GET("/me", func(c *gin.Context) {
|
|
||||||
// TODO: Implement me handler (requires auth)
|
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protected routes (require authentication)
|
// Protected auth routes (require authentication)
|
||||||
// TODO: Add auth middleware here
|
authProtected := v1.Group("/auth")
|
||||||
|
authProtected.Use(middleware.AuthMiddleware(jwtManager))
|
||||||
// User management routes
|
|
||||||
users := v1.Group("/users")
|
|
||||||
{
|
{
|
||||||
users.GET("", func(c *gin.Context) {
|
authProtected.GET("/me", authHandler.GetMe)
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
authProtected.POST("/change-password", authHandler.ChangePassword)
|
||||||
})
|
|
||||||
users.POST("", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
|
||||||
})
|
|
||||||
users.GET("/:id", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
|
||||||
})
|
|
||||||
users.PUT("/:id", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
|
||||||
})
|
|
||||||
users.DELETE("/:id", func(c *gin.Context) {
|
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log querying routes
|
// User management routes (admin only)
|
||||||
|
users := v1.Group("/users")
|
||||||
|
users.Use(middleware.AuthMiddleware(jwtManager))
|
||||||
|
users.Use(middleware.RequireAdmin())
|
||||||
|
{
|
||||||
|
users.GET("", userHandler.ListUsers)
|
||||||
|
users.POST("", userHandler.CreateUser)
|
||||||
|
users.GET("/:id", userHandler.GetUser)
|
||||||
|
users.PUT("/:id", userHandler.UpdateUser)
|
||||||
|
users.DELETE("/:id", userHandler.DeleteUser)
|
||||||
|
users.POST("/:id/reset-password", userHandler.ResetUserPassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log querying routes (protected)
|
||||||
logs := v1.Group("/logs")
|
logs := v1.Group("/logs")
|
||||||
|
logs.Use(middleware.AuthMiddleware(jwtManager))
|
||||||
{
|
{
|
||||||
logs.POST("/query", func(c *gin.Context) {
|
logs.POST("/query", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||||
@@ -89,8 +103,9 @@ func NewRouter(cfg *config.Config, db *sql.DB) *gin.Engine {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alert management routes
|
// Alert management routes (protected)
|
||||||
alerts := v1.Group("/alerts")
|
alerts := v1.Group("/alerts")
|
||||||
|
alerts.Use(middleware.AuthMiddleware(jwtManager))
|
||||||
{
|
{
|
||||||
alerts.GET("/rules", func(c *gin.Context) {
|
alerts.GET("/rules", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||||
@@ -115,8 +130,9 @@ func NewRouter(cfg *config.Config, db *sql.DB) *gin.Engine {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pattern detection routes
|
// Pattern detection routes (protected)
|
||||||
patterns := v1.Group("/patterns")
|
patterns := v1.Group("/patterns")
|
||||||
|
patterns.Use(middleware.AuthMiddleware(jwtManager))
|
||||||
{
|
{
|
||||||
patterns.GET("", func(c *gin.Context) {
|
patterns.GET("", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||||
@@ -141,8 +157,9 @@ func NewRouter(cfg *config.Config, db *sql.DB) *gin.Engine {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report routes
|
// Report routes (protected)
|
||||||
reports := v1.Group("/reports")
|
reports := v1.Group("/reports")
|
||||||
|
reports.Use(middleware.AuthMiddleware(jwtManager))
|
||||||
{
|
{
|
||||||
reports.GET("", func(c *gin.Context) {
|
reports.GET("", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
c.JSON(http.StatusNotImplemented, gin.H{"message": "not implemented yet"})
|
||||||
@@ -167,20 +184,3 @@ func NewRouter(cfg *config.Config, db *sql.DB) *gin.Engine {
|
|||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
// corsMiddleware handles CORS headers
|
|
||||||
func corsMiddleware() gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
|
||||||
|
|
||||||
if c.Request.Method == "OPTIONS" {
|
|
||||||
c.AbortWithStatus(204)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
228
backend/internal/services/user_service.go
Normal file
228
backend/internal/services/user_service.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"github.com/yourusername/victorialogs-manager/internal/models"
|
||||||
|
"github.com/yourusername/victorialogs-manager/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserService handles user management business logic
|
||||||
|
type UserService struct {
|
||||||
|
userRepo *repository.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserService creates a new user service
|
||||||
|
func NewUserService(userRepo *repository.UserRepository) *UserService {
|
||||||
|
return &UserService{
|
||||||
|
userRepo: userRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUserRequest represents a user update request
|
||||||
|
type UpdateUserRequest struct {
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Role models.Role `json:"role,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsersRequest represents a list users request
|
||||||
|
type ListUsersRequest struct {
|
||||||
|
Limit int `json:"limit" form:"limit"`
|
||||||
|
Offset int `json:"offset" form:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsersResponse represents a list users response
|
||||||
|
type ListUsersResponse struct {
|
||||||
|
Users []*models.User `json:"users"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers returns a paginated list of users
|
||||||
|
func (s *UserService) ListUsers(ctx context.Context, req *ListUsersRequest) (*ListUsersResponse, error) {
|
||||||
|
// Set defaults
|
||||||
|
if req.Limit <= 0 {
|
||||||
|
req.Limit = 20
|
||||||
|
}
|
||||||
|
if req.Limit > 100 {
|
||||||
|
req.Limit = 100
|
||||||
|
}
|
||||||
|
if req.Offset < 0 {
|
||||||
|
req.Offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users
|
||||||
|
users, err := s.userRepo.List(ctx, req.Limit, req.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
total, err := s.userRepo.Count(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to count users: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear password hashes
|
||||||
|
for _, user := range users {
|
||||||
|
user.PasswordHash = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ListUsersResponse{
|
||||||
|
Users: users,
|
||||||
|
Total: total,
|
||||||
|
Limit: req.Limit,
|
||||||
|
Offset: req.Offset,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUser returns a user by ID
|
||||||
|
func (s *UserService) GetUser(ctx context.Context, id string) (*models.User, error) {
|
||||||
|
user, err := s.userRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear password hash
|
||||||
|
user.PasswordHash = ""
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user
|
||||||
|
func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (*models.User, error) {
|
||||||
|
// Check if username exists
|
||||||
|
exists, err := s.userRepo.UsernameExists(ctx, req.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check username: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("username already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email exists
|
||||||
|
exists, err = s.userRepo.EmailExists(ctx, req.Email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check email: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("email already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
user := &models.User{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
Username: req.Username,
|
||||||
|
Email: req.Email,
|
||||||
|
PasswordHash: string(hashedPassword),
|
||||||
|
Role: req.Role,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.userRepo.Create(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear password hash
|
||||||
|
user.PasswordHash = ""
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser updates a user
|
||||||
|
func (s *UserService) UpdateUser(ctx context.Context, id string, req *UpdateUserRequest) (*models.User, error) {
|
||||||
|
// Get existing user
|
||||||
|
user, err := s.userRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields if provided
|
||||||
|
if req.Username != "" && req.Username != user.Username {
|
||||||
|
// Check if new username exists
|
||||||
|
exists, err := s.userRepo.UsernameExists(ctx, req.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check username: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("username already exists")
|
||||||
|
}
|
||||||
|
user.Username = req.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Email != "" && req.Email != user.Email {
|
||||||
|
// Check if new email exists
|
||||||
|
exists, err := s.userRepo.EmailExists(ctx, req.Email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check email: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("email already exists")
|
||||||
|
}
|
||||||
|
user.Email = req.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Role != "" {
|
||||||
|
user.Role = req.Role
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.IsActive != nil {
|
||||||
|
user.IsActive = *req.IsActive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user
|
||||||
|
err = s.userRepo.Update(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear password hash
|
||||||
|
user.PasswordHash = ""
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes a user
|
||||||
|
func (s *UserService) DeleteUser(ctx context.Context, id string) error {
|
||||||
|
err := s.userRepo.Delete(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserPassword resets a user's password (admin only)
|
||||||
|
type ResetPasswordRequest struct {
|
||||||
|
NewPassword string `json:"new_password" binding:"required,min=8"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserPassword resets a user's password
|
||||||
|
func (s *UserService) ResetUserPassword(ctx context.Context, userID string, req *ResetPasswordRequest) error {
|
||||||
|
// Hash new password
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to hash password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password
|
||||||
|
err = s.userRepo.UpdatePassword(ctx, userID, string(hashedPassword))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to reset password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user