init commit

This commit is contained in:
David Melendez
2026-01-14 22:38:44 +01:00
parent 4e0c415f0b
commit e25d53d054
124 changed files with 21653 additions and 1 deletions

View File

@@ -0,0 +1,447 @@
<?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);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Services;
use App\Models\User;
/**
* Profile Completion Service
*
* Handles user profile completion calculations and analytics.
*
* @author David Valera Melendez <david@valera-melendez.de>
* @since February 2025
*/
class ProfileCompletionService
{
/**
* Required fields for profile completion
*/
private const REQUIRED_FIELDS = [
'first_name' => 10,
'last_name' => 10,
'email' => 15,
'phone' => 10,
'bio' => 20,
'website' => 10,
'linkedin' => 15,
'github' => 10,
];
/**
* Calculate profile completion percentage
*/
public function calculateCompletion(User $user): int
{
$totalWeight = array_sum(self::REQUIRED_FIELDS);
$completedWeight = 0;
foreach (self::REQUIRED_FIELDS as $field => $weight) {
if (!empty($user->$field)) {
$completedWeight += $weight;
}
}
return (int) (($completedWeight / $totalWeight) * 100);
}
/**
* Get missing profile fields
*/
public function getMissingFields(User $user): array
{
$missing = [];
foreach (self::REQUIRED_FIELDS as $field => $weight) {
if (empty($user->$field)) {
$missing[] = [
'field' => $field,
'label' => $this->getFieldLabel($field),
'weight' => $weight,
];
}
}
return $missing;
}
/**
* Get user-friendly field labels
*/
private function getFieldLabel(string $field): string
{
$labels = [
'first_name' => 'First Name',
'last_name' => 'Last Name',
'email' => 'Email Address',
'phone' => 'Phone Number',
'bio' => 'Professional Bio',
'website' => 'Website',
'linkedin' => 'LinkedIn Profile',
'github' => 'GitHub Profile',
];
return $labels[$field] ?? ucfirst(str_replace('_', ' ', $field));
}
/**
* Check if profile is considered complete
*/
public function isProfileComplete(User $user): bool
{
return $this->calculateCompletion($user) >= 80;
}
/**
* Get profile completion statistics
*/
public function getCompletionStats(User $user): array
{
$completion = $this->calculateCompletion($user);
$missingFields = $this->getMissingFields($user);
return [
'completion_percentage' => $completion,
'is_complete' => $this->isProfileComplete($user),
'missing_fields' => $missingFields,
'missing_count' => count($missingFields),
'status' => $this->getCompletionStatus($completion),
];
}
/**
* Get completion status message
*/
private function getCompletionStatus(int $completion): string
{
if ($completion >= 90) {
return 'excellent';
} elseif ($completion >= 70) {
return 'good';
} elseif ($completion >= 50) {
return 'fair';
} else {
return 'incomplete';
}
}
}

View File

@@ -0,0 +1,380 @@
<?php
/**
* Resume Service
* Professional Resume Builder - Resume Management Service
*
* @author David Valera Melendez <david@valera-melendez.de>
* @created 2025-08-08
* @location Made in Germany 🇩🇪
*/
namespace App\Services;
use App\Models\Resume;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Response;
use Barryvdh\DomPDF\Facade\Pdf;
/**
* Resume Service
* Handles resume creation, management, and operations
*/
class ResumeService
{
/**
* Available resume templates
*/
protected array $templates = [
'professional' => [
'id' => 'professional',
'name' => 'Professional Classic',
'description' => 'Clean and professional design perfect for corporate environments',
'preview' => 'templates/professional-preview.jpg',
'category' => 'Professional'
],
'modern' => [
'id' => 'modern',
'name' => 'Modern Creative',
'description' => 'Contemporary design with creative elements for modern companies',
'preview' => 'templates/modern-preview.jpg',
'category' => 'Creative'
],
'executive' => [
'id' => 'executive',
'name' => 'Executive',
'description' => 'Sophisticated layout for senior-level positions',
'preview' => 'templates/executive-preview.jpg',
'category' => 'Executive'
],
'minimal' => [
'id' => 'minimal',
'name' => 'Minimal Clean',
'description' => 'Simple and clean design focusing on content',
'preview' => 'templates/minimal-preview.jpg',
'category' => 'Minimal'
],
'technical' => [
'id' => 'technical',
'name' => 'Technical',
'description' => 'Optimized for technical and engineering roles',
'preview' => 'templates/technical-preview.jpg',
'category' => 'Technical'
]
];
/**
* Get all resumes for a specific user
*/
public function getUserResumes(int $userId): Collection
{
return Resume::where('user_id', $userId)
->orderBy('updated_at', 'desc')
->get();
}
/**
* Create a new resume
*/
public function createResume(array $resumeData): Resume
{
// Set default values
$resumeData['is_active'] = true;
$resumeData['is_completed'] = false;
$resumeData['is_public'] = false;
// Initialize empty sections
$resumeData['personal_info'] = $resumeData['personal_info'] ?? [];
$resumeData['work_experiences'] = $resumeData['work_experiences'] ?? [];
$resumeData['education'] = $resumeData['education'] ?? [];
$resumeData['skills'] = $resumeData['skills'] ?? [];
$resumeData['languages'] = $resumeData['languages'] ?? [];
$resumeData['certifications'] = $resumeData['certifications'] ?? [];
$resumeData['projects'] = $resumeData['projects'] ?? [];
$resumeData['references'] = $resumeData['references'] ?? [];
$resumeData['custom_sections'] = $resumeData['custom_sections'] ?? [];
$resumeData['settings'] = $resumeData['settings'] ?? $this->getDefaultSettings();
$resume = Resume::create($resumeData);
logger()->info('Resume created', [
'resume_id' => $resume->id,
'user_id' => $resume->user_id,
'title' => $resume->title,
]);
return $resume;
}
/**
* Update an existing resume
*/
public function updateResume(Resume $resume, array $resumeData): Resume
{
// Update completion status based on content
if ($this->shouldMarkAsCompleted($resumeData)) {
$resumeData['is_completed'] = true;
}
$resume->update($resumeData);
logger()->info('Resume updated', [
'resume_id' => $resume->id,
'user_id' => $resume->user_id,
'completion' => $resume->completion_percentage,
]);
return $resume;
}
/**
* Delete a resume
*/
public function deleteResume(Resume $resume): bool
{
$resumeId = $resume->id;
$userId = $resume->user_id;
$deleted = $resume->delete();
if ($deleted) {
logger()->info('Resume deleted', [
'resume_id' => $resumeId,
'user_id' => $userId,
]);
}
return $deleted;
}
/**
* Duplicate a resume
*/
public function duplicateResume(Resume $resume, string $newTitle): Resume
{
$resumeData = $resume->toArray();
// Remove unique identifiers
unset($resumeData['id'], $resumeData['slug'], $resumeData['public_url']);
// Set new title and reset status
$resumeData['title'] = $newTitle;
$resumeData['is_public'] = false;
$resumeData['created_at'] = now();
$resumeData['updated_at'] = now();
$newResume = Resume::create($resumeData);
logger()->info('Resume duplicated', [
'original_resume_id' => $resume->id,
'new_resume_id' => $newResume->id,
'user_id' => $resume->user_id,
]);
return $newResume;
}
/**
* Generate PDF for a resume
*/
public function generatePdf(Resume $resume): Response
{
$template = $this->getTemplate($resume->template_id ?? 'professional');
$pdf = Pdf::loadView('pdf.resume.' . $template['id'], [
'resume' => $resume,
'template' => $template
]);
$fileName = str()->slug($resume->title) . '-resume.pdf';
logger()->info('PDF generated', [
'resume_id' => $resume->id,
'user_id' => $resume->user_id,
'template' => $template['id'],
]);
return $pdf->download($fileName);
}
/**
* Make resume public
*/
public function makePublic(Resume $resume): Resume
{
if (!$resume->public_url) {
$resume->public_url = $resume->generatePublicUrl();
}
$resume->update(['is_public' => true]);
logger()->info('Resume made public', [
'resume_id' => $resume->id,
'user_id' => $resume->user_id,
'public_url' => $resume->public_url,
]);
return $resume;
}
/**
* Make resume private
*/
public function makePrivate(Resume $resume): Resume
{
$resume->update(['is_public' => false]);
logger()->info('Resume made private', [
'resume_id' => $resume->id,
'user_id' => $resume->user_id,
]);
return $resume;
}
/**
* Get available templates
*/
public function getAvailableTemplates(): array
{
return $this->templates;
}
/**
* Get specific template
*/
public function getTemplate(string $templateId): array
{
return $this->templates[$templateId] ?? $this->templates['professional'];
}
/**
* Get resume analytics
*/
public function getResumeAnalytics(Resume $resume): array
{
return [
'completion_percentage' => $resume->completion_percentage,
'sections_completed' => $this->getCompletedSections($resume),
'total_sections' => 7,
'word_count' => $this->calculateWordCount($resume),
'last_updated' => $resume->updated_at,
'views' => 0, // Implement view tracking if needed
];
}
/**
* Get completed sections count
*/
protected function getCompletedSections(Resume $resume): int
{
$sections = ['personal_info', 'professional_summary', 'work_experiences',
'education', 'skills', 'languages', 'certifications'];
$completed = 0;
foreach ($sections as $section) {
if ($resume->isSectionCompleted($section)) {
$completed++;
}
}
return $completed;
}
/**
* Calculate total word count in resume
*/
protected function calculateWordCount(Resume $resume): int
{
$wordCount = 0;
// Professional summary
if ($resume->professional_summary) {
$wordCount += str_word_count(strip_tags($resume->professional_summary));
}
// Work experiences
if ($resume->work_experiences) {
foreach ($resume->work_experiences as $experience) {
$wordCount += str_word_count(strip_tags($experience['description'] ?? ''));
}
}
// Education
if ($resume->education) {
foreach ($resume->education as $education) {
$wordCount += str_word_count(strip_tags($education['description'] ?? ''));
}
}
// Projects
if ($resume->projects) {
foreach ($resume->projects as $project) {
$wordCount += str_word_count(strip_tags($project['description'] ?? ''));
}
}
return $wordCount;
}
/**
* Check if resume should be marked as completed
*/
protected function shouldMarkAsCompleted(array $resumeData): bool
{
$requiredSections = ['personal_info', 'work_experiences', 'education'];
foreach ($requiredSections as $section) {
if (empty($resumeData[$section])) {
return false;
}
}
return true;
}
/**
* Get default resume settings
*/
protected function getDefaultSettings(): array
{
return [
'theme' => 'light',
'font_family' => 'Roboto',
'font_size' => 14,
'color_scheme' => 'blue',
'show_photo' => false,
'show_references' => true,
'show_certifications' => true,
'show_projects' => true,
'section_order' => [
'personal_info',
'professional_summary',
'work_experiences',
'education',
'skills',
'languages',
'certifications',
'projects',
'references'
]
];
}
/**
* Get resume statistics for user
*/
public function getUserResumeStats(int $userId): array
{
$resumes = $this->getUserResumes($userId);
return [
'total_resumes' => $resumes->count(),
'completed_resumes' => $resumes->where('is_completed', true)->count(),
'public_resumes' => $resumes->where('is_public', true)->count(),
'avg_completion' => $resumes->avg('completion_percentage') ?? 0,
'last_updated' => $resumes->max('updated_at'),
];
}
}

View File

@@ -0,0 +1,400 @@
<?php
/**
* Security Service
* Professional Resume Builder - Enterprise Security Service
*
* @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\Support\Facades\Log;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon;
use Exception;
/**
* Security Service
* Handles security-related operations and monitoring
*/
class SecurityService
{
/**
* User repository instance
*/
private UserRepositoryInterface $userRepository;
/**
* Maximum failed attempts before account lockout
*/
private const MAX_FAILED_ATTEMPTS = 5;
/**
* Account lockout duration in minutes
*/
private const LOCKOUT_DURATION = 15;
/**
* Password reset token expiry in hours
*/
private const PASSWORD_RESET_EXPIRY = 24;
/**
* Create a new service instance
*/
public function __construct(UserRepositoryInterface $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* Generate secure password reset token
*
* @param User $user
* @return string
*/
public function generatePasswordResetToken(User $user): string
{
$token = bin2hex(random_bytes(32));
$expiry = now()->addHours(self::PASSWORD_RESET_EXPIRY);
// Store token in cache with expiry
Cache::put(
"password_reset:{$user->id}:{$token}",
[
'user_id' => $user->id,
'email' => $user->email,
'expires_at' => $expiry,
'created_at' => now()
],
$expiry
);
Log::info('Password reset token generated', [
'user_id' => $user->id,
'email' => $user->email,
'expires_at' => $expiry,
'ip_address' => request()->ip()
]);
return $token;
}
/**
* Validate password reset token
*
* @param string $token
* @param int $userId
* @return bool
*/
public function validatePasswordResetToken(string $token, int $userId): bool
{
$cacheKey = "password_reset:{$userId}:{$token}";
$tokenData = Cache::get($cacheKey);
if (!$tokenData) {
Log::warning('Invalid password reset token used', [
'user_id' => $userId,
'token' => substr($token, 0, 8) . '...',
'ip_address' => request()->ip()
]);
return false;
}
if ($tokenData['user_id'] !== $userId) {
Log::warning('Password reset token user mismatch', [
'expected_user_id' => $userId,
'token_user_id' => $tokenData['user_id'],
'ip_address' => request()->ip()
]);
return false;
}
return true;
}
/**
* Reset user password using token
*
* @param string $token
* @param int $userId
* @param string $newPassword
* @return bool
* @throws Exception
*/
public function resetPassword(string $token, int $userId, string $newPassword): bool
{
try {
if (!$this->validatePasswordResetToken($token, $userId)) {
throw new Exception('Invalid or expired password reset token');
}
$user = $this->userRepository->findById($userId);
if (!$user) {
throw new Exception('User not found');
}
// Update password
$hashedPassword = Hash::make($newPassword);
$updateData = [
'password' => $hashedPassword,
'password_updated_at' => now(),
'login_attempts' => 0,
'account_locked_until' => null
];
$success = $this->userRepository->update($userId, $updateData);
if ($success) {
// Remove used token
$cacheKey = "password_reset:{$userId}:{$token}";
Cache::forget($cacheKey);
Log::info('Password reset completed successfully', [
'user_id' => $userId,
'email' => $user->email,
'ip_address' => request()->ip()
]);
}
return $success;
} catch (Exception $e) {
Log::error('Password reset failed', [
'error_message' => $e->getMessage(),
'user_id' => $userId,
'ip_address' => request()->ip()
]);
throw $e;
}
}
/**
* Check if user account is secure
*
* @param User $user
* @return array
*/
public function checkAccountSecurity(User $user): array
{
$securityChecks = [
'password_strength' => $this->checkPasswordStrength($user),
'two_factor_enabled' => $user->two_factor_secret !== null,
'email_verified' => $user->email_verified_at !== null,
'recent_password_change' => $this->checkRecentPasswordChange($user),
'suspicious_activity' => $this->checkSuspiciousActivity($user),
'account_locked' => $this->userRepository->isAccountLocked($user->email)
];
$securityScore = $this->calculateSecurityScore($securityChecks);
return [
'security_score' => $securityScore,
'checks' => $securityChecks,
'recommendations' => $this->getSecurityRecommendations($securityChecks)
];
}
/**
* Log security event
*
* @param string $event
* @param User $user
* @param array $details
*/
public function logSecurityEvent(string $event, User $user, array $details = []): void
{
$logData = array_merge([
'event' => $event,
'user_id' => $user->id,
'email' => $user->email,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'timestamp' => now()->toISOString()
], $details);
Log::warning("Security Event: {$event}", $logData);
// Store in security events table (implement if needed)
// SecurityEvent::create($logData);
}
/**
* Generate two-factor authentication secret
*
* @param User $user
* @return string
*/
public function generateTwoFactorSecret(User $user): string
{
$secret = bin2hex(random_bytes(16));
$this->userRepository->update($user->id, [
'two_factor_secret' => $secret,
'two_factor_recovery_codes' => $this->generateRecoveryCodes()
]);
Log::info('Two-factor authentication enabled', [
'user_id' => $user->id,
'email' => $user->email,
'ip_address' => request()->ip()
]);
return $secret;
}
/**
* Verify two-factor authentication code
*
* @param User $user
* @param string $code
* @return bool
*/
public function verifyTwoFactorCode(User $user, string $code): bool
{
if (!$user->two_factor_secret) {
return false;
}
// Implement TOTP verification logic here
// This is a simplified version
$isValid = $this->validateTOTP($user->two_factor_secret, $code);
if ($isValid) {
Log::info('Two-factor authentication verified', [
'user_id' => $user->id,
'email' => $user->email,
'ip_address' => request()->ip()
]);
} else {
Log::warning('Invalid two-factor authentication code', [
'user_id' => $user->id,
'email' => $user->email,
'ip_address' => request()->ip()
]);
}
return $isValid;
}
/**
* Check password strength
*
* @param User $user
* @return bool
*/
private function checkPasswordStrength(User $user): bool
{
// This is a simplified check - implement proper password strength validation
return $user->password_updated_at &&
$user->password_updated_at->gt(now()->subMonths(6));
}
/**
* Check if password was changed recently
*
* @param User $user
* @return bool
*/
private function checkRecentPasswordChange(User $user): bool
{
return $user->password_updated_at &&
$user->password_updated_at->gt(now()->subMonths(3));
}
/**
* Check for suspicious activity
*
* @param User $user
* @return bool
*/
private function checkSuspiciousActivity(User $user): bool
{
// Check failed login attempts
return $user->login_attempts > 2;
}
/**
* Calculate security score
*
* @param array $checks
* @return int
*/
private function calculateSecurityScore(array $checks): int
{
$score = 0;
$totalChecks = count($checks);
foreach ($checks as $check) {
if ($check === true) {
$score++;
}
}
return (int) (($score / $totalChecks) * 100);
}
/**
* Get security recommendations
*
* @param array $checks
* @return array
*/
private function getSecurityRecommendations(array $checks): array
{
$recommendations = [];
if (!$checks['two_factor_enabled']) {
$recommendations[] = 'Enable two-factor authentication for enhanced security';
}
if (!$checks['email_verified']) {
$recommendations[] = 'Verify your email address';
}
if (!$checks['recent_password_change']) {
$recommendations[] = 'Consider changing your password regularly';
}
if ($checks['suspicious_activity']) {
$recommendations[] = 'Review recent login activity for suspicious behavior';
}
return $recommendations;
}
/**
* Generate recovery codes
*
* @return array
*/
private function generateRecoveryCodes(): array
{
$codes = [];
for ($i = 0; $i < 10; $i++) {
$codes[] = strtoupper(bin2hex(random_bytes(4)));
}
return $codes;
}
/**
* Validate TOTP code (simplified implementation)
*
* @param string $secret
* @param string $code
* @return bool
*/
private function validateTOTP(string $secret, string $code): bool
{
// This is a placeholder - implement proper TOTP validation
// You would typically use a library like spomky-labs/otphp
return strlen($code) === 6 && is_numeric($code);
}
}