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 }