mirror of
https://github.com/R0m1k3/FlowReader.git
synced 2026-10-15 19:26:31 +02:00
feat(story-1.3): add user registration API with Argon2 password hashing
This commit is contained in:
1 parent
caa1d885b6
commit
e186a4dd87
7 files changed
+361
-4
No files matched your search
@@ -0,0 +1,133 @@
|
||||
// Package service contains business logic services.
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/michael/flowreader/internal/domain"
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrInvalidEmail = errors.New("invalid email format")
|
||||
ErrPasswordTooShort = errors.New("password must be at least 8 characters")
|
||||
ErrEmailAlreadyExists = errors.New("email already registered")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
)
|
||||
|
||||
// Argon2 parameters (OWASP recommended)
|
||||
const (
|
||||
argon2Time = 1
|
||||
argon2Memory = 64 * 1024 // 64MB
|
||||
argon2Threads = 4
|
||||
argon2KeyLen = 32
|
||||
saltLength = 16
|
||||
)
|
||||
|
||||
// AuthService handles user authentication business logic.
|
||||
type AuthService struct {
|
||||
userRepo domain.UserRepository
|
||||
}
|
||||
|
||||
// NewAuthService creates a new authentication service.
|
||||
func NewAuthService(userRepo domain.UserRepository) *AuthService {
|
||||
return &AuthService{userRepo: userRepo}
|
||||
}
|
||||
|
||||
// RegisterRequest contains the data needed to register a new user.
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// RegisterResponse contains the registered user data.
|
||||
type RegisterResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// Register creates a new user account.
|
||||
func (s *AuthService) Register(req RegisterRequest) (*RegisterResponse, error) {
|
||||
// Validate email format
|
||||
if !isValidEmail(req.Email) {
|
||||
return nil, ErrInvalidEmail
|
||||
}
|
||||
|
||||
// Validate password length
|
||||
if len(req.Password) < 8 {
|
||||
return nil, ErrPasswordTooShort
|
||||
}
|
||||
|
||||
// Check if email already exists
|
||||
exists, err := s.userRepo.Exists(req.Email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking email: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrEmailAlreadyExists
|
||||
}
|
||||
|
||||
// Hash password with Argon2id
|
||||
passwordHash, err := hashPassword(req.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hashing password: %w", err)
|
||||
}
|
||||
|
||||
// Create user
|
||||
now := time.Now()
|
||||
user := &domain.User{
|
||||
ID: uuid.New(),
|
||||
Email: req.Email,
|
||||
PasswordHash: passwordHash,
|
||||
IsAdmin: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, fmt.Errorf("creating user: %w", err)
|
||||
}
|
||||
|
||||
return &RegisterResponse{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
CreatedAt: user.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// hashPassword creates an Argon2id hash of the password.
|
||||
func hashPassword(password string) (string, error) {
|
||||
salt := make([]byte, saltLength)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := argon2.IDKey([]byte(password), salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)
|
||||
|
||||
// Encode salt and hash together
|
||||
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version,
|
||||
argon2Memory,
|
||||
argon2Time,
|
||||
argon2Threads,
|
||||
base64.RawStdEncoding.EncodeToString(salt),
|
||||
base64.RawStdEncoding.EncodeToString(hash),
|
||||
)
|
||||
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
// isValidEmail checks if the email has a valid format.
|
||||
func isValidEmail(email string) bool {
|
||||
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
return emailRegex.MatchString(email)
|
||||
}
|
||||
Reference in new issue
Block a user