* @created 2025-08-08 * @location Made in Germany 🇩🇪 */ namespace App\Services; use App\Interfaces\UserRepositoryInterface; use App\Models\User; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\RateLimiter; use Illuminate\Validation\ValidationException; use Exception; /** * Authentication Service * Handles user authentication, registration, and security features with repository pattern */ class AuthService { /** * User repository instance */ private UserRepositoryInterface $userRepository; /** * Maximum login attempts per minute */ protected const MAX_LOGIN_ATTEMPTS = 5; /** * Login throttle key prefix */ protected const THROTTLE_KEY_PREFIX = 'login_attempts:'; /** * Account lockout duration in minutes */ protected const LOCKOUT_DURATION = 15; /** * Create a new service instance with dependency injection */ public function __construct(UserRepositoryInterface $userRepository) { $this->userRepository = $userRepository; } /** * Attempt user authentication with comprehensive security measures * * Implements enterprise-grade authentication with: * - Rate limiting to prevent brute force attacks * - Account lockout mechanisms for security * - Detailed security event logging * - Structured response format for consistent handling * * @param array $credentials User login credentials (email, password, remember) * @return array Structured authentication result with success status and details * @throws ValidationException When rate limiting is exceeded or validation fails * @throws Exception When system errors occur during authentication */ public function attemptLogin(array $credentials): array { $email = $credentials['email']; $password = $credentials['password']; $remember = $credentials['remember'] ?? false; try { $this->checkRateLimit($email); if ($this->userRepository->isAccountLocked($email)) { Log::warning('Login attempt on locked account', [ 'email' => $email, 'ip_address' => request()->ip() ]); return [ 'success' => false, 'reason' => 'account_locked', 'message' => 'Account is temporarily locked' ]; } $user = $this->userRepository->findByEmail($email); if (!$user) { $this->incrementRateLimit($email); return [ 'success' => false, 'reason' => 'invalid_credentials', 'message' => 'User not found' ]; } if (!$user->is_active) { $this->incrementRateLimit($email); Log::warning('Login attempt on inactive account', [ 'user_id' => $user->id, 'email' => $email, 'ip_address' => request()->ip() ]); return [ 'success' => false, 'reason' => 'account_inactive', 'message' => 'Account is deactivated' ]; } if (!Hash::check($password, $user->password)) { $this->incrementRateLimit($email); $this->userRepository->incrementLoginAttempts($email); $updatedUser = $this->userRepository->findByEmail($email); if ($updatedUser && $updatedUser->login_attempts >= self::MAX_LOGIN_ATTEMPTS) { $this->userRepository->lockAccount($email, self::LOCKOUT_DURATION); Log::warning('Account locked due to excessive failed attempts', [ 'user_id' => $user->id, 'email' => $email, 'attempts' => $updatedUser->login_attempts ]); return [ 'success' => false, 'reason' => 'too_many_attempts', 'message' => 'Account locked due to failed attempts' ]; } return [ 'success' => false, 'reason' => 'invalid_credentials', 'message' => 'Invalid password' ]; } Auth::login($user, $remember); $this->userRepository->updateLastLogin($user->id, request()->ip()); $this->userRepository->resetLoginAttempts($email); $this->clearRateLimit($email); Log::info('Successful user authentication', [ 'user_id' => $user->id, 'email' => $email, 'ip_address' => request()->ip(), 'user_agent' => request()->userAgent() ]); return [ 'success' => true, 'user' => $user, 'message' => 'Login successful' ]; } catch (ValidationException $e) { throw $e; } catch (Exception $e) { Log::error('Authentication service error', [ 'error_message' => $e->getMessage(), 'email' => $email, 'ip_address' => request()->ip(), 'trace' => $e->getTraceAsString() ]); return [ 'success' => false, 'reason' => 'system_error', 'message' => 'System error occurred' ]; } } /** * Create new user account with comprehensive security validation * * Implements secure user registration with: * - Duplicate email detection using repository layer * - Secure password hashing with bcrypt * - Security-focused default settings * - Comprehensive audit logging * - Prepared data structure for repository storage * * @param array $userData User registration data (first_name, last_name, email, password) * @return User The created user instance * @throws ValidationException When email already exists or validation fails * @throws Exception When user creation fails due to system errors */ public function createUser(array $userData): User { try { if ($this->userRepository->findByEmail($userData['email'])) { Log::warning('Registration attempt with existing email', [ 'email' => $userData['email'], 'ip_address' => request()->ip() ]); throw ValidationException::withMessages([ 'email' => 'An account with this email address already exists.', ]); } $preparedData = [ 'first_name' => $userData['first_name'], 'last_name' => $userData['last_name'], 'email' => $userData['email'], 'password' => Hash::make($userData['password']), 'status' => 'active', 'locale' => $userData['locale'] ?? 'en', 'timezone' => $userData['timezone'] ?? 'UTC', 'login_attempts' => 0, 'locked_until' => null, 'email_verified_at' => null, 'last_login_at' => null, 'last_login_ip' => null ]; $user = $this->userRepository->create($preparedData); if ($user) { Log::info('New user account created successfully', [ 'user_id' => $user->id, 'email' => $user->email, 'ip_address' => request()->ip(), 'user_agent' => request()->userAgent() ]); } return $user; } catch (ValidationException $e) { throw $e; } catch (Exception $e) { Log::error('User creation failed', [ 'error_message' => $e->getMessage(), 'email' => $userData['email'] ?? 'unknown', 'ip_address' => request()->ip(), 'trace' => $e->getTraceAsString() ]); throw new Exception('Failed to create user account'); } } /** * Update user profile information */ public function updateProfile(User $user, array $profileData): User { unset($profileData['password'], $profileData['email'], $profileData['is_active']); $user->update($profileData); logger()->info('User profile updated', [ 'user_id' => $user->id, 'email' => $user->email, 'ip' => request()->ip(), ]); return $user; } /** * Change user password */ public function changePassword(User $user, string $currentPassword, string $newPassword): bool { if (!Hash::check($currentPassword, $user->password)) { throw new \InvalidArgumentException('Current password is incorrect.'); } $user->update(['password' => Hash::make($newPassword)]); logger()->info('User password changed', [ 'user_id' => $user->id, 'email' => $user->email ]); return true; } /** * Deactivate user account */ public function deactivateAccount(User $user): bool { $user->update(['is_active' => false]); logger()->info('User account deactivated', [ 'user_id' => $user->id, 'email' => $user->email, 'ip' => request()->ip(), ]); return true; } /** * Activate user account */ public function activateAccount(User $user): bool { $user->update(['is_active' => true]); logger()->info('User account activated', [ 'user_id' => $user->id, 'email' => $user->email, 'ip' => request()->ip(), ]); return true; } /** * Check if user has exceeded login rate limit */ protected function checkRateLimit(string $email): void { $key = $this->getRateLimitKey($email); if (RateLimiter::tooManyAttempts($key, self::MAX_LOGIN_ATTEMPTS)) { $seconds = RateLimiter::availableIn($key); throw ValidationException::withMessages([ 'email' => "Too many login attempts. Please try again in {$seconds} seconds.", ]); } } /** * Increment rate limit counter */ protected function incrementRateLimit(string $email): void { $key = $this->getRateLimitKey($email); RateLimiter::hit($key, 60); } /** * Clear rate limit counter */ protected function clearRateLimit(string $email): void { $key = $this->getRateLimitKey($email); RateLimiter::clear($key); } /** * Get rate limit key for email */ protected function getRateLimitKey(string $email): string { return self::THROTTLE_KEY_PREFIX . $email . '|' . request()->ip(); } /** * Send password reset notification */ public function sendPasswordResetNotification(string $email): bool { $user = User::where('email', $email)->first(); if (!$user) { // Don't reveal if email exists or not for security return true; } // Generate and send password reset token // Implementation depends on your notification preferences // $user->sendPasswordResetNotification($token); logger()->info('Password reset requested', [ 'user_id' => $user->id, 'email' => $user->email, 'ip' => request()->ip(), ]); return true; } /** * Verify email address */ public function verifyEmail(User $user): bool { if ($user->hasVerifiedEmail()) { return true; } $user->markEmailAsVerified(); logger()->info('Email verified', [ 'user_id' => $user->id, 'email' => $user->email, 'ip' => request()->ip(), ]); return true; } /** * Get user statistics */ public function getUserStats(User $user): array { return [ 'account_created' => $user->created_at, 'last_login' => $user->last_login_at, 'profile_completion' => $this->calculateProfileCompletion($user), 'total_resumes' => $user->resumes()->count(), 'completed_resumes' => $user->completedResumes()->count(), 'is_verified' => $user->hasVerifiedEmail(), ]; } /** * Calculate profile completion percentage */ protected function calculateProfileCompletion(User $user): int { $fields = [ 'first_name', 'last_name', 'email', 'phone', 'address', 'city', 'country', 'profession' ]; $completed = 0; foreach ($fields as $field) { if (!empty($user->$field)) { $completed++; } } return (int) (($completed / count($fields)) * 100); } }