Files
Claude Code d0624a2bc2 feat: Implement Authentication Handlers (Issue #3)
- Create AuthService with login, refresh token, and user management
- Implement Login with password verification
- Implement RefreshToken for token renewal
- Implement GetCurrentUser endpoint
- Add ChangePassword functionality
- Add CreateUser for admin user creation
- Create AuthHandler with HTTP endpoints
- Add comprehensive validation and error handling
- Add google/uuid dependency to go.mod

Closes #3

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-05 00:48:55 +03:00

232 lines
6.4 KiB
Go

package services
import (
"context"
"fmt"
"golang.org/x/crypto/bcrypt"
"github.com/google/uuid"
"github.com/yourusername/victorialogs-manager/internal/models"
"github.com/yourusername/victorialogs-manager/internal/repository"
"github.com/yourusername/victorialogs-manager/internal/utils"
)
// AuthService handles authentication business logic
type AuthService struct {
userRepo *repository.UserRepository
jwtManager *utils.JWTManager
}
// NewAuthService creates a new auth service
func NewAuthService(userRepo *repository.UserRepository, jwtManager *utils.JWTManager) *AuthService {
return &AuthService{
userRepo: userRepo,
jwtManager: jwtManager,
}
}
// LoginRequest represents a login request
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse represents a login response
type LoginResponse struct{
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
User *models.User `json:"user"`
}
// Login authenticates a user and returns JWT tokens
func (s *AuthService) Login(ctx context.Context, req *LoginRequest) (*LoginResponse, error) {
// Get user by username
user, err := s.userRepo.GetByUsername(ctx, req.Username)
if err != nil {
return nil, fmt.Errorf("invalid credentials")
}
// Check if user is active
if !user.IsActive {
return nil, fmt.Errorf("user account is disabled")
}
// Verify password
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
if err != nil {
return nil, fmt.Errorf("invalid credentials")
}
// Generate access token
accessToken, err := s.jwtManager.GenerateAccessToken(user)
if err != nil {
return nil, fmt.Errorf("failed to generate access token: %w", err)
}
// Generate refresh token
refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID)
if err != nil {
return nil, fmt.Errorf("failed to generate refresh token: %w", err)
}
// Clear password hash from response
user.PasswordHash = ""
return &LoginResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
User: user,
}, nil
}
// RefreshTokenRequest represents a refresh token request
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// RefreshTokenResponse represents a refresh token response
type RefreshTokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// RefreshToken generates new tokens from a refresh token
func (s *AuthService) RefreshToken(ctx context.Context, req *RefreshTokenRequest) (*RefreshTokenResponse, error) {
// Validate refresh token
userID, err := s.jwtManager.ValidateRefreshToken(req.RefreshToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token: %w", err)
}
// Get user
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("user not found")
}
// Check if user is active
if !user.IsActive {
return nil, fmt.Errorf("user account is disabled")
}
// Generate new access token
accessToken, err := s.jwtManager.GenerateAccessToken(user)
if err != nil {
return nil, fmt.Errorf("failed to generate access token: %w", err)
}
// Generate new refresh token
refreshToken, err := s.jwtManager.GenerateRefreshToken(user.ID)
if err != nil {
return nil, fmt.Errorf("failed to generate refresh token: %w", err)
}
return &RefreshTokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
}, nil
}
// GetCurrentUser returns the current user from claims
func (s *AuthService) GetCurrentUser(ctx context.Context, userID string) (*models.User, error) {
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("user not found")
}
// Clear password hash
user.PasswordHash = ""
return user, nil
}
// CreateUserRequest represents a user creation request
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
Role models.Role `json:"role" binding:"required"`
}
// CreateUser creates a new user (admin only)
func (s *AuthService) 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
}
// ChangePasswordRequest represents a password change request
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=8"`
}
// ChangePassword changes a user's password
func (s *AuthService) ChangePassword(ctx context.Context, userID string, req *ChangePasswordRequest) error {
// Get user
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found")
}
// Verify old password
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword))
if err != nil {
return fmt.Errorf("invalid old password")
}
// 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 update password: %w", err)
}
return nil
}