* @since February 2025 */ class UserRepository extends BaseRepository implements UserRepositoryInterface { /** * Create a new UserRepository instance * * @param User $model */ public function __construct(User $model) { parent::__construct($model); } /** * Find a user by email address * * @param string $email * @return User|null */ public function findByEmail(string $email): ?User { return $this->findOneBy(['email' => $email]); } /** * Find users by status * * @param string $status * @return Collection */ public function findByStatus(string $status): Collection { return $this->findBy(['status' => $status]); } /** * Get users who haven't logged in recently * * @param int $days * @return Collection */ public function getInactiveUsers(int $days = 30): Collection { $cutoffDate = Carbon::now()->subDays($days); $result = $this->query ->where(function ($query) use ($cutoffDate) { $query->where('last_login_at', '<', $cutoffDate) ->orWhereNull('last_login_at'); }) ->where('status', 'active') ->orderBy('last_login_at', 'asc') ->get(); $this->resetQuery(); return $result; } /** * Get users with completed profiles * * @return Collection */ public function getUsersWithCompletedProfiles(): Collection { $result = $this->query ->whereNotNull('profile_completed_at') ->where('status', 'active') ->orderBy('profile_completed_at', 'desc') ->get(); $this->resetQuery(); return $result; } /** * Get users with incomplete profiles * * @return Collection */ public function getUsersWithIncompleteProfiles(): Collection { $result = $this->query ->whereNull('profile_completed_at') ->where('status', 'active') ->orderBy('created_at', 'desc') ->get(); $this->resetQuery(); return $result; } /** * Get users subscribed to newsletter * * @return Collection */ public function getNewsletterSubscribers(): Collection { return $this->findBy([ 'newsletter_subscribed' => true, 'status' => 'active' ]); } /** * Search users by name or email * * @param string $query * @param int $limit * @return Collection */ public function searchUsers(string $query, int $limit = 10): Collection { $searchTerm = '%' . $query . '%'; $result = $this->query ->where(function ($q) use ($searchTerm) { $q->where('first_name', 'like', $searchTerm) ->orWhere('last_name', 'like', $searchTerm) ->orWhere('email', 'like', $searchTerm) ->orWhereRaw("CONCAT(first_name, ' ', last_name) LIKE ?", [$searchTerm]); }) ->where('status', 'active') ->orderBy('last_login_at', 'desc') ->limit($limit) ->get(); $this->resetQuery(); return $result; } /** * Retrieve comprehensive user statistics and analytics * * Aggregates key user metrics including: * - Total users by status (active, inactive, suspended) * - Email verification status counts * - Registration trends (30-day window) * - Profile completion metrics * - Newsletter subscription data * - Daily active user statistics * * Used for admin dashboards, reporting, and user behavior analysis. * * @return array Comprehensive statistics array with user metrics * @throws Exception When database queries fail */ public function getUserStatistics(): array { $stats = [ 'total_users' => $this->count(), 'active_users' => $this->count(['status' => 'active']), 'inactive_users' => $this->count(['status' => 'inactive']), 'suspended_users' => $this->count(['status' => 'suspended']), 'verified_users' => $this->query->whereNotNull('email_verified_at')->count(), 'unverified_users' => $this->query->whereNull('email_verified_at')->count(), ]; $thirtyDaysAgo = Carbon::now()->subDays(30); $stats['new_users_30_days'] = $this->query ->where('created_at', '>=', $thirtyDaysAgo) ->count(); $stats['completed_profiles'] = $this->query ->whereNotNull('profile_completed_at') ->count(); $stats['newsletter_subscribers'] = $this->count(['newsletter_subscribed' => true]); $stats['active_today'] = $this->query ->whereDate('last_login_at', Carbon::today()) ->count(); $this->resetQuery(); return $stats; } /** * Update user's last login metadata with security tracking * * Records successful login timestamp, IP address, and resets * security-related fields including login attempts and account locks. * Essential for authentication audit trails and security monitoring. * * @param int $userId The user ID to update * @param string $ipAddress Client IP address for security tracking * @return bool True if update succeeded, false otherwise * @throws Exception When user record update fails */ public function updateLastLogin(int $userId, string $ipAddress): bool { return $this->update($userId, [ 'last_login_at' => $this->freshTimestamp(), 'last_login_ip' => $ipAddress, 'login_attempts' => 0, 'locked_until' => null, ]); } /** * Increment failed login attempt counter for user account * * Increases the login_attempts field by 1 for security tracking. * Used in conjunction with account locking mechanisms to prevent * brute force attacks. Should be called after each failed login. * * @param string $email User email address to track attempts for * @return bool True if increment succeeded, false if user not found */ public function incrementLoginAttempts(string $email): bool { $user = $this->findByEmail($email); if (!$user) { return false; } return $user->increment('login_attempts'); } /** * Reset user's login attempts * * @param string $email * @return bool */ public function resetLoginAttempts(string $email): bool { $user = $this->findByEmail($email); if (!$user) { return false; } return $user->update([ 'login_attempts' => 0, 'locked_until' => null, ]); } /** * Temporarily lock user account for security purposes * * Implements account lockout mechanism to prevent brute force attacks * by setting locked_until timestamp. Account remains inaccessible * until the lockout period expires or is manually cleared. * * @param string $email User email address to lock * @param int $minutes Lockout duration in minutes (default: 15) * @return bool True if lock applied successfully, false if user not found */ public function lockAccount(string $email, int $minutes = 15): bool { $user = $this->findByEmail($email); if (!$user) { return false; } return $user->update([ 'locked_until' => Carbon::now()->addMinutes($minutes), ]); } /** * Determine if user account is currently locked * * Validates account lockout status by checking if locked_until * timestamp exists and is still in the future. Used before * authentication attempts to enforce security policies. * * @param string $email User email address to check * @return bool True if account is locked, false if accessible or user not found */ public function isAccountLocked(string $email): bool { $user = $this->findByEmail($email); if (!$user || !$user->locked_until) { return false; } return Carbon::now()->lessThan($user->locked_until); } /** * Suspend user account * * @param int $userId * @param string $reason * @return bool */ public function suspendAccount(int $userId, string $reason): bool { return $this->update($userId, [ 'status' => 'suspended', 'suspended_at' => $this->freshTimestamp(), 'suspension_reason' => $reason, ]); } /** * Reactivate user account * * @param int $userId * @return bool */ public function reactivateAccount(int $userId): bool { return $this->update($userId, [ 'status' => 'active', 'suspended_at' => null, 'suspension_reason' => null, ]); } /** * Get users registered in date range * * @param string $startDate * @param string $endDate * @return Collection */ public function getUsersRegisteredBetween(string $startDate, string $endDate): Collection { $result = $this->query ->whereBetween('created_at', [$startDate, $endDate]) ->orderBy('created_at', 'desc') ->get(); $this->resetQuery(); return $result; } /** * Get paginated users with filters * * @param array $filters * @param int $perPage * @return LengthAwarePaginator */ public function getPaginatedUsersWithFilters(array $filters = [], int $perPage = 15): LengthAwarePaginator { $query = $this->query; // Apply filters if (isset($filters['status']) && !empty($filters['status'])) { $query = $query->where('status', $filters['status']); } if (isset($filters['verified']) && $filters['verified'] !== '') { if ($filters['verified']) { $query = $query->whereNotNull('email_verified_at'); } else { $query = $query->whereNull('email_verified_at'); } } if (isset($filters['newsletter']) && $filters['newsletter'] !== '') { $query = $query->where('newsletter_subscribed', (bool) $filters['newsletter']); } if (isset($filters['search']) && !empty($filters['search'])) { $searchTerm = '%' . $filters['search'] . '%'; $query = $query->where(function ($q) use ($searchTerm) { $q->where('first_name', 'like', $searchTerm) ->orWhere('last_name', 'like', $searchTerm) ->orWhere('email', 'like', $searchTerm); }); } if (isset($filters['date_from']) && !empty($filters['date_from'])) { $query = $query->whereDate('created_at', '>=', $filters['date_from']); } if (isset($filters['date_to']) && !empty($filters['date_to'])) { $query = $query->whereDate('created_at', '<=', $filters['date_to']); } // Default ordering $sortBy = $filters['sort_by'] ?? 'created_at'; $sortDirection = $filters['sort_direction'] ?? 'desc'; $query = $query->orderBy($sortBy, $sortDirection); $result = $query->paginate($perPage); $this->resetQuery(); return $result; } }