Files
Novault-backend/internal/service/app_lock_service.go
2026-01-25 21:59:00 +08:00

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
}