163 lines
4.1 KiB
Go
163 lines
4.1 KiB
Go
package service
|
|
|
|
import (
|
|
"accounting-app/internal/models"
|
|
"accounting-app/internal/repository"
|
|
"errors"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const (
|
|
// MaxFailedAttempts is the maximum number of failed login attempts before locking
|
|
MaxFailedAttempts = 5
|
|
// LockDuration is how long the app remains locked after max failed attempts
|
|
LockDuration = 5 * time.Minute
|
|
)
|
|
|
|
var (
|
|
ErrAppLocked = errors.New("app is locked due to too many failed attempts")
|
|
ErrAppLockInvalidPassword = errors.New("invalid password")
|
|
ErrAppLockNotEnabled = errors.New("app lock is not enabled")
|
|
ErrPasswordRequired = errors.New("password is required")
|
|
)
|
|
|
|
// AppLockService handles business logic for app lock
|
|
type AppLockService struct {
|
|
repo *repository.AppLockRepository
|
|
}
|
|
|
|
// NewAppLockService creates a new app lock service
|
|
func NewAppLockService(repo *repository.AppLockRepository) *AppLockService {
|
|
return &AppLockService{repo: repo}
|
|
}
|
|
|
|
// GetStatus returns the current app lock status
|
|
func (s *AppLockService) GetStatus(userID uint) (*models.AppLock, error) {
|
|
return s.repo.GetOrCreate(userID)
|
|
}
|
|
|
|
// SetPassword sets or updates the app lock password
|
|
func (s *AppLockService) SetPassword(userID uint, password string) error {
|
|
if password == "" {
|
|
return ErrPasswordRequired
|
|
}
|
|
|
|
// Hash the password using bcrypt
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
appLock, err := s.repo.GetOrCreate(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
appLock.PasswordHash = string(hashedPassword)
|
|
appLock.IsEnabled = true
|
|
appLock.FailedAttempts = 0
|
|
appLock.LockedUntil = nil
|
|
appLock.LastFailedAttempt = nil
|
|
|
|
return s.repo.Update(appLock)
|
|
}
|
|
|
|
// VerifyPassword verifies the provided password against the stored hash
|
|
func (s *AppLockService) VerifyPassword(userID uint, password string) error {
|
|
appLock, err := s.repo.GetOrCreate(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !appLock.IsEnabled {
|
|
return ErrAppLockNotEnabled
|
|
}
|
|
|
|
// Check if app is currently locked
|
|
if appLock.IsLocked() {
|
|
return ErrAppLocked
|
|
}
|
|
|
|
// Verify password
|
|
err = bcrypt.CompareHashAndPassword([]byte(appLock.PasswordHash), []byte(password))
|
|
if err != nil {
|
|
// Password is incorrect, increment failed attempts
|
|
return s.handleFailedAttempt(appLock)
|
|
}
|
|
|
|
// Password is correct, reset failed attempts
|
|
if appLock.FailedAttempts > 0 {
|
|
if err := s.repo.ResetFailedAttempts(appLock); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleFailedAttempt handles a failed password attempt
|
|
func (s *AppLockService) handleFailedAttempt(appLock *models.AppLock) error {
|
|
now := time.Now()
|
|
appLock.FailedAttempts++
|
|
appLock.LastFailedAttempt = &now
|
|
|
|
// Lock the app if max attempts reached
|
|
if appLock.FailedAttempts >= MaxFailedAttempts {
|
|
lockUntil := now.Add(LockDuration)
|
|
appLock.LockedUntil = &lockUntil
|
|
}
|
|
|
|
if err := s.repo.IncrementFailedAttempts(appLock); err != nil {
|
|
return err
|
|
}
|
|
|
|
if appLock.FailedAttempts >= MaxFailedAttempts {
|
|
return ErrAppLocked
|
|
}
|
|
|
|
return ErrAppLockInvalidPassword
|
|
}
|
|
|
|
// DisableLock disables the app lock (requires password verification first)
|
|
func (s *AppLockService) DisableLock(userID uint) error {
|
|
appLock, err := s.repo.GetOrCreate(userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
appLock.IsEnabled = false
|
|
appLock.FailedAttempts = 0
|
|
appLock.LockedUntil = nil
|
|
appLock.LastFailedAttempt = nil
|
|
|
|
return s.repo.Update(appLock)
|
|
}
|
|
|
|
// ChangePassword changes the app lock password (requires old password verification first)
|
|
func (s *AppLockService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
|
// Verify old password first
|
|
if err := s.VerifyPassword(userID, oldPassword); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set new password
|
|
return s.SetPassword(userID, newPassword)
|
|
}
|
|
|
|
// GetRemainingLockTime returns the remaining lock time in seconds, or 0 if not locked
|
|
func (s *AppLockService) GetRemainingLockTime(userID uint) (int, error) {
|
|
appLock, err := s.repo.GetOrCreate(userID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if !appLock.IsLocked() {
|
|
return 0, nil
|
|
}
|
|
|
|
remaining := time.Until(*appLock.LockedUntil)
|
|
return int(remaining.Seconds()), nil
|
|
}
|