448 lines
14 KiB
PHP
448 lines
14 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Authentication Service
|
|
* Professional Resume Builder - Enterprise Auth System
|
|
*
|
|
* @author David Valera Melendez <david@valera-melendez.de>
|
|
* @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);
|
|
}
|
|
}
|