- 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>
229 lines
5.6 KiB
Go
229 lines
5.6 KiB
Go
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
|
|
}
|