411 lines
12 KiB
PHP
411 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Interfaces\UserRepositoryInterface;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Carbon\Carbon;
|
|
|
|
/**
|
|
* User Repository Implementation
|
|
*
|
|
* Handles all user-related data operations.
|
|
* Implements UserRepositoryInterface with user-specific business logic.
|
|
*
|
|
* @author David Valera Melendez <david@valera-melendez.de>
|
|
* @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;
|
|
}
|
|
}
|