init commit
This commit is contained in:
117
app/Http/Controllers/Api/AuthController.php
Normal file
117
app/Http/Controllers/Api/AuthController.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* API Authentication Controller
|
||||
* Handles API authentication endpoints
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-09
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Handle API login
|
||||
*/
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API login not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API registration
|
||||
*/
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API registration not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle forgot password
|
||||
*/
|
||||
public function forgotPassword(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API forgot password not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle password reset
|
||||
*/
|
||||
public function resetPassword(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API password reset not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh token
|
||||
*/
|
||||
public function refresh(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API token refresh not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle logout
|
||||
*/
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API logout not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user
|
||||
*/
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API user profile not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
*/
|
||||
public function updateProfile(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API profile update not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update password
|
||||
*/
|
||||
public function updatePassword(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'message' => 'API password update not implemented yet',
|
||||
'status' => 'error'
|
||||
], 501);
|
||||
}
|
||||
}
|
||||
55
app/Http/Controllers/Api/V1/ApiController.php
Normal file
55
app/Http/Controllers/Api/V1/ApiController.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* Base API Controller for V1
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-08
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
abstract class ApiController extends Controller
|
||||
{
|
||||
/**
|
||||
* Success response
|
||||
*/
|
||||
protected function successResponse($data = null, string $message = 'Success', int $code = 200): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
], $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response
|
||||
*/
|
||||
protected function errorResponse(string $message = 'Error', int $code = 400, $errors = null): JsonResponse
|
||||
{
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if ($errors) {
|
||||
$response['errors'] = $errors;
|
||||
}
|
||||
|
||||
return response()->json($response, $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation error response
|
||||
*/
|
||||
protected function validationErrorResponse($errors): JsonResponse
|
||||
{
|
||||
return $this->errorResponse('Validation failed', 422, $errors);
|
||||
}
|
||||
}
|
||||
586
app/Http/Controllers/Auth/AuthController.php
Normal file
586
app/Http/Controllers/Auth/AuthController.php
Normal file
@@ -0,0 +1,586 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Authentication Controller
|
||||
* Professional Resume Builder - Enterprise Auth System
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-08
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Auth\LoginRequest;
|
||||
use App\Http\Requests\Auth\RegisterRequest;
|
||||
use App\Http\Requests\Auth\UpdateProfileRequest;
|
||||
use App\Interfaces\UserRepositoryInterface;
|
||||
use App\Services\AuthService;
|
||||
use App\Services\ProfileCompletionService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Authentication Controller
|
||||
* Handles user authentication, registration, and session management with repository pattern
|
||||
*/
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* User repository instance
|
||||
*/
|
||||
private UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* AuthService instance
|
||||
*/
|
||||
private AuthService $authService;
|
||||
|
||||
/**
|
||||
* Profile completion service instance
|
||||
*/
|
||||
private ProfileCompletionService $profileCompletionService;
|
||||
|
||||
/**
|
||||
* Maximum login attempts before lockout
|
||||
*/
|
||||
private const MAX_LOGIN_ATTEMPTS = 5;
|
||||
|
||||
/**
|
||||
* Account lockout duration in minutes
|
||||
*/
|
||||
private const LOCKOUT_DURATION = 15;
|
||||
|
||||
/**
|
||||
* Create a new controller instance with dependency injection
|
||||
*/
|
||||
public function __construct(
|
||||
UserRepositoryInterface $userRepository,
|
||||
AuthService $authService,
|
||||
ProfileCompletionService $profileCompletionService
|
||||
) {
|
||||
$this->userRepository = $userRepository;
|
||||
$this->authService = $authService;
|
||||
$this->profileCompletionService = $profileCompletionService;
|
||||
$this->middleware('guest')->except(['logout', 'profile', 'updateProfile', 'activity']);
|
||||
$this->middleware('auth')->only(['logout', 'profile', 'updateProfile', 'activity']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the login form
|
||||
*/
|
||||
public function showLoginForm(): View
|
||||
{
|
||||
return view('auth.login', [
|
||||
'title' => 'Sign In - Professional Resume Builder',
|
||||
'description' => 'Access your professional resume builder account'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a login request with enhanced maintainability
|
||||
*
|
||||
* @param LoginRequest $request
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function login(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$credentials = $request->validated();
|
||||
$loginResult = $this->authService->attemptLogin($credentials);
|
||||
|
||||
return $this->handleLoginResult($loginResult, $request);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return $this->handleLoginValidationError($e, $request);
|
||||
} catch (Exception $e) {
|
||||
return $this->handleLoginSystemError($e, $request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the result of a login attempt
|
||||
*
|
||||
* @param array $loginResult
|
||||
* @param LoginRequest $request
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
private function handleLoginResult(array $loginResult, LoginRequest $request): RedirectResponse
|
||||
{
|
||||
if ($loginResult['success']) {
|
||||
return $this->handleSuccessfulLogin($loginResult, $request);
|
||||
}
|
||||
|
||||
return $this->handleFailedLogin($loginResult, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful login response
|
||||
*
|
||||
* @param array $loginResult
|
||||
* @param LoginRequest $request
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
private function handleSuccessfulLogin(array $loginResult, LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$request->session()->regenerate();
|
||||
|
||||
Log::info('User logged in successfully', [
|
||||
'user_id' => $loginResult['user']->id,
|
||||
'email' => $loginResult['user']->email,
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'login_method' => 'standard'
|
||||
]);
|
||||
|
||||
$welcomeMessage = $this->getWelcomeMessage($loginResult['user']);
|
||||
$redirectUrl = $this->getPostLoginRedirectUrl($loginResult['user']);
|
||||
|
||||
return redirect()->intended($redirectUrl)->with('success', $welcomeMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failed login response
|
||||
*
|
||||
* @param array $loginResult
|
||||
* @param LoginRequest $request
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
private function handleFailedLogin(array $loginResult, LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$errorMessage = $this->getLoginErrorMessage($loginResult['reason']);
|
||||
|
||||
Log::warning('Login attempt failed', [
|
||||
'email' => $request->input('email'),
|
||||
'reason' => $loginResult['reason'],
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent()
|
||||
]);
|
||||
|
||||
return back()
|
||||
->withErrors(['email' => $errorMessage])
|
||||
->onlyInput('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle validation errors during login
|
||||
*
|
||||
* @param ValidationException $e
|
||||
* @param LoginRequest $request
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
private function handleLoginValidationError(ValidationException $e, LoginRequest $request): RedirectResponse
|
||||
{
|
||||
Log::warning('Login validation error', [
|
||||
'email' => $request->input('email'),
|
||||
'errors' => $e->errors(),
|
||||
'ip_address' => $request->ip()
|
||||
]);
|
||||
|
||||
return back()
|
||||
->withErrors($e->errors())
|
||||
->onlyInput('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle system errors during login
|
||||
*
|
||||
* @param Exception $e
|
||||
* @param LoginRequest $request
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
private function handleLoginSystemError(Exception $e, LoginRequest $request): RedirectResponse
|
||||
{
|
||||
Log::error('Login system error', [
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
'email' => $request->input('email'),
|
||||
'ip_address' => $request->ip(),
|
||||
'timestamp' => now()
|
||||
]);
|
||||
|
||||
return back()
|
||||
->withErrors(['email' => 'A system error occurred. Please try again or contact support.'])
|
||||
->onlyInput('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get personalized welcome message
|
||||
*
|
||||
* @param User $user
|
||||
* @return string
|
||||
*/
|
||||
private function getWelcomeMessage(User $user): string
|
||||
{
|
||||
$timeOfDay = $this->getTimeOfDay();
|
||||
$firstName = $user->first_name ?? 'there';
|
||||
|
||||
return "Good {$timeOfDay}, {$firstName}! Welcome back to your professional resume builder.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post-login redirect URL based on user profile and activity state
|
||||
*
|
||||
* Intelligently determines the most appropriate landing page based on:
|
||||
* - Profile completion percentage (incomplete profiles → profile page)
|
||||
* - Resume creation status (no resumes → dashboard for guidance)
|
||||
* - Default → main dashboard
|
||||
*
|
||||
* @param User $user The authenticated user
|
||||
* @return string The route name for redirection
|
||||
*/
|
||||
private function getPostLoginRedirectUrl(User $user): string
|
||||
{
|
||||
$profileCompletion = $this->profileCompletionService->calculateCompletion($user);
|
||||
|
||||
if ($profileCompletion < 50) {
|
||||
return route('profile');
|
||||
}
|
||||
|
||||
$userStats = $this->userRepository->getUserStatistics($user->id);
|
||||
if (($userStats['resumes_count'] ?? 0) === 0) {
|
||||
return route('dashboard');
|
||||
}
|
||||
|
||||
return route('dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate error message based on failure reason
|
||||
*
|
||||
* @param string $reason
|
||||
* @return string
|
||||
*/
|
||||
private function getLoginErrorMessage(string $reason): string
|
||||
{
|
||||
switch ($reason) {
|
||||
case 'account_locked':
|
||||
return 'Account is temporarily locked due to too many failed attempts. Please try again later.';
|
||||
case 'account_inactive':
|
||||
return 'Your account has been deactivated. Please contact support.';
|
||||
case 'invalid_credentials':
|
||||
return 'The provided credentials do not match our records.';
|
||||
case 'too_many_attempts':
|
||||
return 'Too many failed attempts. Account has been locked for security.';
|
||||
case 'rate_limited':
|
||||
return 'Too many login attempts. Please wait before trying again.';
|
||||
default:
|
||||
return 'Login failed. Please check your credentials and try again.';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time of day greeting
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getTimeOfDay(): string
|
||||
{
|
||||
$hour = now()->hour;
|
||||
|
||||
if ($hour < 12) {
|
||||
return 'morning';
|
||||
} elseif ($hour < 17) {
|
||||
return 'afternoon';
|
||||
} else {
|
||||
return 'evening';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the registration form
|
||||
*/
|
||||
public function showRegistrationForm(): View
|
||||
{
|
||||
return view('auth.register', [
|
||||
'title' => 'Create Account - Professional Resume Builder',
|
||||
'description' => 'Join thousands of professionals creating outstanding resumes'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a registration request
|
||||
*/
|
||||
public function register(RegisterRequest $request): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$userData = $request->validated();
|
||||
|
||||
$user = $this->authService->createUser($userData);
|
||||
|
||||
if ($user) {
|
||||
Auth::login($user);
|
||||
|
||||
// Log successful registration
|
||||
logger()->info('User registered successfully', [
|
||||
'user_id' => $user->id,
|
||||
'email' => $user->email,
|
||||
'ip' => $request->ip(),
|
||||
'user_agent' => $request->userAgent()
|
||||
]);
|
||||
|
||||
return redirect()->route('dashboard')
|
||||
->with('success', 'Welcome! Your account has been created successfully.');
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'Unable to create account. Please try again.',
|
||||
])->withInput();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Registration error', [
|
||||
'error' => $e->getMessage(),
|
||||
'email' => $request->input('email'),
|
||||
'ip' => $request->ip()
|
||||
]);
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => 'An error occurred during registration. Please try again.',
|
||||
])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle logout request
|
||||
*/
|
||||
public function logout(Request $request): RedirectResponse
|
||||
{
|
||||
// Log the logout
|
||||
logger()->info('User logged out', [
|
||||
'user_id' => Auth::id(),
|
||||
'ip' => $request->ip()
|
||||
]);
|
||||
|
||||
Auth::logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login')
|
||||
->with('success', 'You have been successfully logged out.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show user profile with comprehensive analytics
|
||||
*/
|
||||
public function profile(): View
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();
|
||||
|
||||
// Get user statistics from repository
|
||||
$userStats = $this->userRepository->getUserStatistics($user->id);
|
||||
|
||||
// Get profile completion analysis from service
|
||||
$profileCompletion = $this->profileCompletionService->calculateCompletion($user);
|
||||
$profileAnalysis = $this->profileCompletionService->getDetailedAnalysis($user);
|
||||
|
||||
// Prepare view data
|
||||
$viewData = [
|
||||
'title' => 'Profile Settings - Professional Resume Builder',
|
||||
'user' => $user,
|
||||
'userStats' => $userStats,
|
||||
'profileCompletion' => $profileCompletion,
|
||||
'profileAnalysis' => $profileAnalysis,
|
||||
'accountSecurity' => [
|
||||
'two_factor_enabled' => $user->two_factor_secret !== null,
|
||||
'email_verified' => $user->email_verified_at !== null,
|
||||
'is_locked' => $this->userRepository->isAccountLocked($user->email)
|
||||
]
|
||||
];
|
||||
|
||||
// Log profile view for analytics
|
||||
Log::info('User profile viewed', [
|
||||
'user_id' => $user->id,
|
||||
'profile_completion' => $profileCompletion,
|
||||
'ip_address' => request()->ip(),
|
||||
'timestamp' => now()
|
||||
]);
|
||||
|
||||
return view('auth.profile', $viewData);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Error loading user profile', [
|
||||
'error_message' => $e->getMessage(),
|
||||
'user_id' => Auth::id(),
|
||||
'ip_address' => request()->ip(),
|
||||
'timestamp' => now()
|
||||
]);
|
||||
|
||||
// Fallback with minimal data
|
||||
return view('auth.profile', [
|
||||
'title' => 'Profile Settings',
|
||||
'user' => Auth::user(),
|
||||
'userStats' => [],
|
||||
'profileCompletion' => 0,
|
||||
'error' => 'Unable to load profile data completely'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile using professional form request validation
|
||||
*
|
||||
* Handles comprehensive profile updates with:
|
||||
* - Repository pattern for data persistence
|
||||
* - Structured logging for security audit trails
|
||||
* - Real-time profile completion calculation
|
||||
* - User-friendly completion status messaging
|
||||
*
|
||||
* @param UpdateProfileRequest $request Validated profile update data
|
||||
* @return RedirectResponse Response with success message or error state
|
||||
* @throws Exception When profile update fails due to system errors
|
||||
*/
|
||||
public function updateProfile(UpdateProfileRequest $request): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();
|
||||
$updateData = $request->validated();
|
||||
|
||||
$updatedUser = $this->userRepository->update($user->id, $updateData);
|
||||
|
||||
Log::info('User profile updated successfully', [
|
||||
'user_id' => $user->id,
|
||||
'updated_fields' => array_keys($updateData),
|
||||
'ip_address' => $request->ip(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'timestamp' => now()
|
||||
]);
|
||||
|
||||
$completionPercentage = $this->profileCompletionService
|
||||
->calculateCompletion($updatedUser);
|
||||
|
||||
$message = "Profile updated successfully! ";
|
||||
if ($completionPercentage < 100) {
|
||||
$message .= "Your profile is {$completionPercentage}% complete.";
|
||||
} else {
|
||||
$message .= "Your profile is now 100% complete!";
|
||||
}
|
||||
|
||||
return back()->with('success', $message);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Profile update failed', [
|
||||
'error_message' => $e->getMessage(),
|
||||
'error_trace' => $e->getTraceAsString(),
|
||||
'user_id' => Auth::id(),
|
||||
'request_data' => $request->except(['password', 'password_confirmation']),
|
||||
'ip_address' => $request->ip(),
|
||||
'timestamp' => now()
|
||||
]);
|
||||
|
||||
return back()
|
||||
->withErrors(['error' => 'Unable to update profile. Please try again.'])
|
||||
->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve comprehensive user activity and analytics data
|
||||
*
|
||||
* Provides detailed activity analytics including:
|
||||
* - User profile and authentication information
|
||||
* - Login activity patterns and security metrics
|
||||
* - Profile completion analysis with detailed breakdown
|
||||
* - Account security status and recommendations
|
||||
* - Usage statistics (resumes, templates, exports)
|
||||
*
|
||||
* @return JsonResponse Structured activity data with metadata
|
||||
* @throws Exception When analytics data retrieval fails
|
||||
*/
|
||||
public function activity(): JsonResponse
|
||||
{
|
||||
try {
|
||||
$user = Auth::user();
|
||||
|
||||
$userStats = $this->userRepository->getUserStatistics($user->id);
|
||||
|
||||
$profileCompletion = $this->profileCompletionService
|
||||
->calculateCompletion($user);
|
||||
|
||||
$profileAnalysis = $this->profileCompletionService
|
||||
->getDetailedAnalysis($user);
|
||||
|
||||
$activityData = [
|
||||
'user_info' => [
|
||||
'id' => $user->id,
|
||||
'name' => $user->first_name . ' ' . $user->last_name,
|
||||
'email' => $user->email,
|
||||
'member_since' => $user->created_at->format('F Y'),
|
||||
'account_status' => $user->status ?? 'active'
|
||||
],
|
||||
'login_activity' => [
|
||||
'last_login' => $user->last_login_at ? $user->last_login_at->format('Y-m-d H:i:s') : null,
|
||||
'last_login_ip' => $user->last_login_ip,
|
||||
'total_logins' => $userStats['total_logins'] ?? 0,
|
||||
'failed_attempts' => $user->login_attempts ?? 0
|
||||
],
|
||||
'profile_metrics' => [
|
||||
'completion_percentage' => $profileCompletion,
|
||||
'completed_fields' => $profileAnalysis['completed_fields'],
|
||||
'missing_fields' => $profileAnalysis['missing_fields'],
|
||||
'completion_score' => $profileAnalysis['weighted_score']
|
||||
],
|
||||
'account_security' => [
|
||||
'two_factor_enabled' => $user->two_factor_secret !== null,
|
||||
'email_verified' => $user->email_verified_at !== null,
|
||||
'password_updated' => $user->password_updated_at ? $user->password_updated_at->format('Y-m-d') : null,
|
||||
'is_locked' => $this->userRepository->isAccountLocked($user->email)
|
||||
],
|
||||
'activity_summary' => [
|
||||
'resumes_created' => $userStats['resumes_count'] ?? 0,
|
||||
'templates_used' => $userStats['templates_used'] ?? 0,
|
||||
'exports_generated' => $userStats['exports_count'] ?? 0,
|
||||
'profile_views' => $userStats['profile_views'] ?? 0
|
||||
]
|
||||
];
|
||||
|
||||
Log::info('User activity data requested', [
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => request()->ip(),
|
||||
'timestamp' => now()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $activityData,
|
||||
'meta' => [
|
||||
'generated_at' => now()->toISOString(),
|
||||
'version' => '1.0'
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('Failed to retrieve user activity data', [
|
||||
'error_message' => $e->getMessage(),
|
||||
'user_id' => Auth::id(),
|
||||
'ip_address' => request()->ip(),
|
||||
'timestamp' => now()
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Unable to retrieve activity data',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal server error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show forgot password form
|
||||
*/
|
||||
public function showForgotPasswordForm(): View
|
||||
{
|
||||
return view('auth.forgot-password', [
|
||||
'title' => 'Reset Password - Professional Resume Builder',
|
||||
'description' => 'Enter your email to receive password reset instructions'
|
||||
]);
|
||||
}
|
||||
}
|
||||
25
app/Http/Controllers/Controller.php
Normal file
25
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Base Controller Class
|
||||
* Professional Resume Builder - Laravel Application
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-08
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
/**
|
||||
* Base Controller
|
||||
* Parent class for all application controllers
|
||||
*/
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests, ValidatesRequests;
|
||||
}
|
||||
154
app/Http/Controllers/DashboardController.php
Normal file
154
app/Http/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Dashboard Controller
|
||||
* Professional Resume Builder - Main Dashboard
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-08
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Interfaces\UserRepositoryInterface;
|
||||
use App\Interfaces\ResumeRepositoryInterface;
|
||||
use App\Services\ResumeService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Dashboard Controller
|
||||
* Handles the main dashboard functionality with repository pattern
|
||||
*/
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* User repository instance
|
||||
*/
|
||||
protected UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* Resume repository instance
|
||||
*/
|
||||
protected ResumeRepositoryInterface $resumeRepository;
|
||||
|
||||
/**
|
||||
* Resume service instance
|
||||
*/
|
||||
protected ResumeService $resumeService;
|
||||
|
||||
/**
|
||||
* Create a new controller instance with dependency injection
|
||||
*/
|
||||
public function __construct(
|
||||
UserRepositoryInterface $userRepository,
|
||||
ResumeRepositoryInterface $resumeRepository,
|
||||
ResumeService $resumeService
|
||||
) {
|
||||
$this->userRepository = $userRepository;
|
||||
$this->resumeRepository = $resumeRepository;
|
||||
$this->resumeService = $resumeService;
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application dashboard
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Get user's resumes using repository
|
||||
$resumes = $this->resumeRepository->getUserResumes($user->id);
|
||||
|
||||
// Get resume statistics using repository
|
||||
$resumeStats = $this->resumeRepository->getResumeStatistics($user->id);
|
||||
|
||||
// Get recent resumes (last 5)
|
||||
$recentResumes = $this->resumeRepository
|
||||
->orderBy('updated_at', 'desc')
|
||||
->limit(5)
|
||||
->getUserResumes($user->id);
|
||||
|
||||
// Calculate profile completion
|
||||
$profileCompletion = $this->calculateProfileCompletion($user);
|
||||
|
||||
return view('dashboard.index', [
|
||||
'title' => 'Dashboard - Professional Resume Builder',
|
||||
'user' => $user,
|
||||
'resumes' => $resumes,
|
||||
'resumeStats' => $resumeStats,
|
||||
'recentResumes' => $recentResumes,
|
||||
'profileCompletion' => $profileCompletion,
|
||||
'stats' => [
|
||||
'total_resumes' => $resumeStats['total_resumes'],
|
||||
'published_resumes' => $resumeStats['published_resumes'],
|
||||
'draft_resumes' => $resumeStats['draft_resumes'],
|
||||
'completed_resumes' => $resumeStats['completed_resumes'],
|
||||
'avg_completion' => $resumeStats['avg_completion'],
|
||||
'total_views' => $resumeStats['total_views'],
|
||||
'total_downloads' => $resumeStats['total_downloads'],
|
||||
'profile_completion' => $profileCompletion
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate profile completion percentage
|
||||
*/
|
||||
private function calculateProfileCompletion($user): int
|
||||
{
|
||||
$requiredFields = [
|
||||
'first_name', 'last_name', 'email', 'phone',
|
||||
'bio', 'website', 'linkedin'
|
||||
];
|
||||
|
||||
$completed = 0;
|
||||
foreach ($requiredFields as $field) {
|
||||
if (!empty($user->$field)) {
|
||||
$completed++;
|
||||
}
|
||||
}
|
||||
|
||||
return (int) (($completed / count($requiredFields)) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dashboard analytics data
|
||||
*/
|
||||
public function analytics(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$analytics = [
|
||||
'user_stats' => $this->userRepository->getUserStatistics(),
|
||||
'resume_stats' => $this->resumeRepository->getResumeStatistics($user->id),
|
||||
'recent_activity' => $this->getRecentActivity($user->id),
|
||||
];
|
||||
|
||||
return response()->json($analytics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent user activity
|
||||
*/
|
||||
private function getRecentActivity(int $userId): array
|
||||
{
|
||||
$recentResumes = $this->resumeRepository
|
||||
->orderBy('updated_at', 'desc')
|
||||
->limit(10)
|
||||
->getUserResumes($userId);
|
||||
|
||||
return $recentResumes->map(function ($resume) {
|
||||
return [
|
||||
'id' => $resume->id,
|
||||
'title' => $resume->title,
|
||||
'action' => 'updated',
|
||||
'timestamp' => $resume->updated_at,
|
||||
'status' => $resume->status,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
}
|
||||
99
app/Http/Controllers/HomeController.php
Normal file
99
app/Http/Controllers/HomeController.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the homepage.
|
||||
*/
|
||||
public function index(): View|RedirectResponse
|
||||
{
|
||||
// If user is authenticated, redirect to dashboard
|
||||
if (Auth::check()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
// If not authenticated, redirect to login
|
||||
return redirect()->route('login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the features page.
|
||||
*/
|
||||
public function features(): View
|
||||
{
|
||||
return view('home.features');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the pricing page.
|
||||
*/
|
||||
public function pricing(): View
|
||||
{
|
||||
return view('home.pricing');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the help page.
|
||||
*/
|
||||
public function help(): View
|
||||
{
|
||||
return view('home.help');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the contact page.
|
||||
*/
|
||||
public function contact(): View
|
||||
{
|
||||
return view('home.contact');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle contact form submission.
|
||||
*/
|
||||
public function submitContact(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255',
|
||||
'subject' => 'required|string|max:255',
|
||||
'message' => 'required|string',
|
||||
]);
|
||||
|
||||
// Handle contact form logic here
|
||||
// You can send email, save to database, etc.
|
||||
|
||||
return redirect()->route('contact')->with('success', 'Your message has been sent successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the privacy policy page.
|
||||
*/
|
||||
public function privacy(): View
|
||||
{
|
||||
return view('home.privacy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the terms of service page.
|
||||
*/
|
||||
public function terms(): View
|
||||
{
|
||||
return view('home.terms');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the support page.
|
||||
*/
|
||||
public function support(): View
|
||||
{
|
||||
return view('home.support');
|
||||
}
|
||||
}
|
||||
169
app/Http/Controllers/ProfileController.php
Normal file
169
app/Http/Controllers/ProfileController.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the user's profile.
|
||||
*/
|
||||
public function show(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
return view('profile.show', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the user's profile.
|
||||
*/
|
||||
public function edit(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
return view('profile.edit', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255|unique:users,email,' . $user->id,
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'bio' => 'nullable|string|max:1000',
|
||||
]);
|
||||
|
||||
$user->update($request->only(['name', 'email', 'phone', 'bio']));
|
||||
|
||||
return redirect()->route('profile.show')->with('success', 'Profile updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's account.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => 'required|current_password',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
Auth::logout();
|
||||
|
||||
$user->delete();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect('/')->with('success', 'Your account has been deleted.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the settings page.
|
||||
*/
|
||||
public function settings(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
return view('profile.settings', compact('user'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
public function updatePassword(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'current_password' => 'required|current_password',
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
]);
|
||||
|
||||
Auth::user()->update([
|
||||
'password' => Hash::make($request->password),
|
||||
]);
|
||||
|
||||
return redirect()->route('profile.settings')->with('success', 'Password updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user preferences.
|
||||
*/
|
||||
public function updatePreferences(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'theme' => 'required|in:light,dark,auto',
|
||||
'notifications_email' => 'boolean',
|
||||
'notifications_browser' => 'boolean',
|
||||
'language' => 'required|string|max:5',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
// If your User model has a preferences JSON column
|
||||
$preferences = $user->preferences ?? [];
|
||||
$preferences['theme'] = $request->theme;
|
||||
$preferences['notifications_email'] = $request->boolean('notifications_email');
|
||||
$preferences['notifications_browser'] = $request->boolean('notifications_browser');
|
||||
$preferences['language'] = $request->language;
|
||||
|
||||
$user->update(['preferences' => $preferences]);
|
||||
|
||||
return redirect()->route('profile.settings')->with('success', 'Preferences updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user avatar.
|
||||
*/
|
||||
public function updateAvatar(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'avatar' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
// Delete old avatar if exists
|
||||
if ($user->avatar && Storage::exists('public/' . $user->avatar)) {
|
||||
Storage::delete('public/' . $user->avatar);
|
||||
}
|
||||
|
||||
// Store new avatar
|
||||
$avatarPath = $request->file('avatar')->store('avatars', 'public');
|
||||
|
||||
$user->update(['avatar' => $avatarPath]);
|
||||
|
||||
return redirect()->route('profile.settings')->with('success', 'Avatar updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show profile completion status.
|
||||
*/
|
||||
public function completion(): View
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Calculate completion percentage
|
||||
$fields = ['name', 'email', 'phone', 'bio', 'avatar'];
|
||||
$completedFields = 0;
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (!empty($user->$field)) {
|
||||
$completedFields++;
|
||||
}
|
||||
}
|
||||
|
||||
$completionPercentage = round(($completedFields / count($fields)) * 100);
|
||||
|
||||
return view('profile.completion', compact('user', 'completionPercentage'));
|
||||
}
|
||||
}
|
||||
356
app/Http/Controllers/ResumeBuilderController.php
Normal file
356
app/Http/Controllers/ResumeBuilderController.php
Normal file
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Resume Builder Controller
|
||||
* Professional Resume Builder - Resume Creation and Management
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-08
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\Resume\StoreResumeRequest;
|
||||
use App\Http\Requests\Resume\UpdateResumeRequest;
|
||||
use App\Models\Resume;
|
||||
use App\Interfaces\ResumeRepositoryInterface;
|
||||
use App\Interfaces\UserRepositoryInterface;
|
||||
use App\Services\ResumeService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
/**
|
||||
* Resume Builder Controller
|
||||
* Handles resume creation, editing, and management with repository pattern
|
||||
*/
|
||||
class ResumeBuilderController extends Controller
|
||||
{
|
||||
/**
|
||||
* Resume repository instance
|
||||
*/
|
||||
protected ResumeRepositoryInterface $resumeRepository;
|
||||
|
||||
/**
|
||||
* User repository instance
|
||||
*/
|
||||
protected UserRepositoryInterface $userRepository;
|
||||
|
||||
/**
|
||||
* ResumeService instance
|
||||
*/
|
||||
protected ResumeService $resumeService;
|
||||
|
||||
/**
|
||||
* Create a new controller instance with dependency injection
|
||||
*/
|
||||
public function __construct(
|
||||
ResumeRepositoryInterface $resumeRepository,
|
||||
UserRepositoryInterface $userRepository,
|
||||
ResumeService $resumeService
|
||||
) {
|
||||
$this->resumeRepository = $resumeRepository;
|
||||
$this->userRepository = $userRepository;
|
||||
$this->resumeService = $resumeService;
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of user's resumes
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
// Get paginated resumes using repository
|
||||
$resumes = $this->resumeRepository->getPaginatedUserResumes($userId, 12);
|
||||
|
||||
// Get resume statistics using repository
|
||||
$resumeStats = $this->resumeRepository->getResumeStatistics($userId);
|
||||
|
||||
return view('resume-builder.index', [
|
||||
'title' => 'My Resumes - Resume Builder',
|
||||
'resumes' => $resumes,
|
||||
'resumeStats' => $resumeStats
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resume
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$userId = Auth::id();
|
||||
|
||||
// Check if user can create more resumes
|
||||
if (!$this->resumeRepository->canUserCreateMoreResumes($userId, 10)) {
|
||||
return redirect()->route('resume-builder.index')
|
||||
->with('error', 'You have reached the maximum number of resumes allowed.');
|
||||
}
|
||||
|
||||
$templates = $this->resumeService->getAvailableTemplates();
|
||||
|
||||
return view('resume-builder.create', [
|
||||
'title' => 'Create New Resume',
|
||||
'templates' => $templates
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resume
|
||||
*/
|
||||
public function store(StoreResumeRequest $request): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$userId = Auth::id();
|
||||
|
||||
// Check if user can create more resumes
|
||||
if (!$this->resumeRepository->canUserCreateMoreResumes($userId, 10)) {
|
||||
return back()->withErrors([
|
||||
'title' => 'You have reached the maximum number of resumes allowed.'
|
||||
])->withInput();
|
||||
}
|
||||
|
||||
$resumeData = $request->validated();
|
||||
$resumeData['user_id'] = $userId;
|
||||
|
||||
// Create resume using repository
|
||||
$resume = $this->resumeRepository->create($resumeData);
|
||||
|
||||
// Update completion percentage
|
||||
$this->resumeRepository->updateCompletionPercentage($resume->id);
|
||||
|
||||
logger()->info('Resume created', [
|
||||
'user_id' => $userId,
|
||||
'resume_id' => $resume->id,
|
||||
'title' => $resume->title
|
||||
]);
|
||||
|
||||
return redirect()->route('resume-builder.edit', $resume)
|
||||
->with('success', 'Resume created successfully! Start building your professional CV.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Resume creation error', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => Auth::id()
|
||||
]);
|
||||
|
||||
return back()->withErrors([
|
||||
'title' => 'Unable to create resume. Please try again.'
|
||||
])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resume
|
||||
*/
|
||||
public function edit(Resume $resume): View
|
||||
{
|
||||
$this->authorize('update', $resume);
|
||||
|
||||
return view('resume-builder.edit', [
|
||||
'title' => 'Edit Resume - ' . $resume->title,
|
||||
'resume' => $resume,
|
||||
'templates' => $this->resumeService->getAvailableTemplates()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resume
|
||||
*/
|
||||
public function update(UpdateResumeRequest $request, Resume $resume): RedirectResponse
|
||||
{
|
||||
$this->authorize('update', $resume);
|
||||
|
||||
try {
|
||||
$resumeData = $request->validated();
|
||||
|
||||
// Update resume using repository
|
||||
$this->resumeRepository->update($resume->id, $resumeData);
|
||||
|
||||
// Update completion percentage
|
||||
$this->resumeRepository->updateCompletionPercentage($resume->id);
|
||||
|
||||
logger()->info('Resume updated', [
|
||||
'user_id' => Auth::id(),
|
||||
'resume_id' => $resume->id
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Resume updated successfully!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Resume update error', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => Auth::id(),
|
||||
'resume_id' => $resume->id
|
||||
]);
|
||||
|
||||
return back()->withErrors([
|
||||
'title' => 'Unable to update resume. Please try again.'
|
||||
])->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resume preview
|
||||
*/
|
||||
public function preview(Resume $resume): View
|
||||
{
|
||||
$this->authorize('view', $resume);
|
||||
|
||||
// Increment view count using repository
|
||||
$this->resumeRepository->incrementViewCount($resume->id);
|
||||
|
||||
return view('resume-builder.preview', [
|
||||
'title' => 'Preview - ' . $resume->title,
|
||||
'resume' => $resume
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resume
|
||||
*/
|
||||
public function destroy(Resume $resume): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $resume);
|
||||
|
||||
try {
|
||||
// Delete resume using repository
|
||||
$this->resumeRepository->delete($resume->id);
|
||||
|
||||
logger()->info('Resume deleted', [
|
||||
'user_id' => Auth::id(),
|
||||
'resume_id' => $resume->id,
|
||||
'title' => $resume->title
|
||||
]);
|
||||
|
||||
return redirect()->route('resume-builder.index')
|
||||
->with('success', 'Resume deleted successfully.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Resume deletion error', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => Auth::id(),
|
||||
'resume_id' => $resume->id
|
||||
]);
|
||||
|
||||
return back()->withErrors([
|
||||
'error' => 'Unable to delete resume. Please try again.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download resume as PDF
|
||||
*/
|
||||
public function downloadPdf(Resume $resume)
|
||||
{
|
||||
$this->authorize('view', $resume);
|
||||
|
||||
try {
|
||||
// Increment download count using repository
|
||||
$this->resumeRepository->incrementDownloadCount($resume->id);
|
||||
|
||||
return $this->resumeService->generatePdf($resume);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('PDF generation error', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => Auth::id(),
|
||||
'resume_id' => $resume->id
|
||||
]);
|
||||
|
||||
return back()->withErrors([
|
||||
'error' => 'Unable to generate PDF. Please try again.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate an existing resume
|
||||
*/
|
||||
public function duplicate(Resume $resume): RedirectResponse
|
||||
{
|
||||
$this->authorize('view', $resume);
|
||||
|
||||
try {
|
||||
$userId = Auth::id();
|
||||
|
||||
// Check if user can create more resumes
|
||||
if (!$this->resumeRepository->canUserCreateMoreResumes($userId, 10)) {
|
||||
return back()->withErrors([
|
||||
'error' => 'You have reached the maximum number of resumes allowed.'
|
||||
]);
|
||||
}
|
||||
|
||||
$newTitle = $resume->title . ' (Copy)';
|
||||
$duplicatedResume = $this->resumeRepository->duplicateResume($resume->id, $userId, $newTitle);
|
||||
|
||||
logger()->info('Resume duplicated', [
|
||||
'user_id' => $userId,
|
||||
'original_resume_id' => $resume->id,
|
||||
'new_resume_id' => $duplicatedResume->id
|
||||
]);
|
||||
|
||||
return redirect()->route('resume-builder.edit', $duplicatedResume)
|
||||
->with('success', 'Resume duplicated successfully!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Resume duplication error', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => Auth::id(),
|
||||
'resume_id' => $resume->id
|
||||
]);
|
||||
|
||||
return back()->withErrors([
|
||||
'error' => 'Unable to duplicate resume. Please try again.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search user's resumes
|
||||
*/
|
||||
public function search(Request $request): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$query = $request->input('query', '');
|
||||
$userId = Auth::id();
|
||||
|
||||
if (empty($query)) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$resumes = $this->resumeRepository
|
||||
->getUserResumes($userId)
|
||||
->filter(function ($resume) use ($query) {
|
||||
return stripos($resume->title, $query) !== false ||
|
||||
stripos($resume->description, $query) !== false;
|
||||
})
|
||||
->take(10)
|
||||
->values();
|
||||
|
||||
return response()->json($resumes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resume analytics
|
||||
*/
|
||||
public function analytics(Resume $resume): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$this->authorize('view', $resume);
|
||||
|
||||
$analytics = [
|
||||
'view_count' => $resume->view_count,
|
||||
'download_count' => $resume->download_count,
|
||||
'completion_percentage' => $resume->completion_percentage,
|
||||
'last_viewed_at' => $resume->last_viewed_at,
|
||||
'last_downloaded_at' => $resume->last_downloaded_at,
|
||||
'created_at' => $resume->created_at,
|
||||
'updated_at' => $resume->updated_at,
|
||||
];
|
||||
|
||||
return response()->json($analytics);
|
||||
}
|
||||
}
|
||||
87
app/Http/Controllers/TemplateController.php
Normal file
87
app/Http/Controllers/TemplateController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class TemplateController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of available templates.
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
// Get available templates
|
||||
$templates = [
|
||||
// You can replace this with actual template data from database
|
||||
[
|
||||
'id' => 1,
|
||||
'name' => 'Professional',
|
||||
'description' => 'Clean and professional template',
|
||||
'preview_image' => '/images/templates/professional.jpg',
|
||||
'is_premium' => false,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'name' => 'Modern',
|
||||
'description' => 'Modern and stylish template',
|
||||
'preview_image' => '/images/templates/modern.jpg',
|
||||
'is_premium' => true,
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'name' => 'Classic',
|
||||
'description' => 'Traditional classic template',
|
||||
'preview_image' => '/images/templates/classic.jpg',
|
||||
'is_premium' => false,
|
||||
],
|
||||
];
|
||||
|
||||
return view('templates.index', compact('templates'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified template.
|
||||
*/
|
||||
public function show(string $template): View
|
||||
{
|
||||
// Find template by ID or slug
|
||||
// This is a placeholder - replace with actual template fetching logic
|
||||
$templateData = [
|
||||
'id' => 1,
|
||||
'name' => ucfirst($template),
|
||||
'description' => 'Template description',
|
||||
'preview_image' => "/images/templates/{$template}.jpg",
|
||||
'is_premium' => false,
|
||||
];
|
||||
|
||||
return view('templates.show', compact('templateData'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the specified template.
|
||||
*/
|
||||
public function preview(string $template): View
|
||||
{
|
||||
// Generate preview for the template
|
||||
// This would typically include sample data
|
||||
$templateData = [
|
||||
'id' => 1,
|
||||
'name' => ucfirst($template),
|
||||
'description' => 'Template description',
|
||||
'preview_image' => "/images/templates/{$template}.jpg",
|
||||
'is_premium' => false,
|
||||
];
|
||||
|
||||
$sampleData = [
|
||||
'name' => 'John Doe',
|
||||
'title' => 'Software Developer',
|
||||
'email' => 'john.doe@example.com',
|
||||
'phone' => '+1 (555) 123-4567',
|
||||
'summary' => 'Experienced software developer with expertise in web technologies.',
|
||||
];
|
||||
|
||||
return view('templates.preview', compact('templateData', 'sampleData'));
|
||||
}
|
||||
}
|
||||
76
app/Http/Kernel.php
Normal file
76
app/Http/Kernel.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* HTTP Kernel
|
||||
* Professional Resume Builder - Laravel Application
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @created 2025-08-08
|
||||
* @location Made in Germany 🇩🇪
|
||||
*/
|
||||
|
||||
namespace App\Http;
|
||||
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
|
||||
class Kernel extends HttpKernel
|
||||
{
|
||||
/**
|
||||
* The application's global HTTP middleware stack.
|
||||
*
|
||||
* These middleware are run during every request to your application.
|
||||
*
|
||||
* @var array<int, class-string|string>
|
||||
*/
|
||||
protected $middleware = [
|
||||
// \App\Http\Middleware\TrustHosts::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\Illuminate\Http\Middleware\HandleCors::class,
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's route middleware groups.
|
||||
*
|
||||
* @var array<string, array<int, class-string|string>>
|
||||
*/
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* The application's middleware aliases.
|
||||
*
|
||||
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
|
||||
*
|
||||
* @var array<string, class-string|string>
|
||||
*/
|
||||
protected $middlewareAliases = [
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'signed' => \App\Http\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
];
|
||||
}
|
||||
17
app/Http/Middleware/Authenticate.php
Normal file
17
app/Http/Middleware/Authenticate.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Authenticate extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the path the user should be redirected to when they are not authenticated.
|
||||
*/
|
||||
protected function redirectTo(Request $request): ?string
|
||||
{
|
||||
return $request->expectsJson() ? null : route('login');
|
||||
}
|
||||
}
|
||||
17
app/Http/Middleware/EncryptCookies.php
Normal file
17
app/Http/Middleware/EncryptCookies.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
||||
|
||||
class EncryptCookies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the cookies that should not be encrypted.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
17
app/Http/Middleware/PreventRequestsDuringMaintenance.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
|
||||
|
||||
class PreventRequestsDuringMaintenance extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be reachable while maintenance mode is enabled.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
30
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
30
app/Http/Middleware/RedirectIfAuthenticated.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RedirectIfAuthenticated
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, string ...$guards): Response
|
||||
{
|
||||
$guards = empty($guards) ? [null] : $guards;
|
||||
|
||||
foreach ($guards as $guard) {
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
19
app/Http/Middleware/TrimStrings.php
Normal file
19
app/Http/Middleware/TrimStrings.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
||||
|
||||
class TrimStrings extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the attributes that should not be trimmed.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
'current_password',
|
||||
'password',
|
||||
'password_confirmation',
|
||||
];
|
||||
}
|
||||
20
app/Http/Middleware/TrustHosts.php
Normal file
20
app/Http/Middleware/TrustHosts.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
|
||||
class TrustHosts extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the host patterns that should be trusted.
|
||||
*
|
||||
* @return array<int, string|null>
|
||||
*/
|
||||
public function hosts(): array
|
||||
{
|
||||
return [
|
||||
$this->allSubdomainsOfApplicationUrl(),
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Middleware/TrustProxies.php
Normal file
28
app/Http/Middleware/TrustProxies.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The trusted proxies for this application.
|
||||
*
|
||||
* @var array<int, string>|string|null
|
||||
*/
|
||||
protected $proxies;
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $headers =
|
||||
Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
}
|
||||
22
app/Http/Middleware/ValidateSignature.php
Normal file
22
app/Http/Middleware/ValidateSignature.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
|
||||
|
||||
class ValidateSignature extends Middleware
|
||||
{
|
||||
/**
|
||||
* The names of the query string parameters that should be ignored.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
// 'fbclid',
|
||||
// 'utm_campaign',
|
||||
// 'utm_content',
|
||||
// 'utm_medium',
|
||||
// 'utm_source',
|
||||
// 'utm_term',
|
||||
];
|
||||
}
|
||||
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
17
app/Http/Middleware/VerifyCsrfToken.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
||||
|
||||
class VerifyCsrfToken extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
||||
110
app/Http/Requests/Auth/LoginRequest.php
Normal file
110
app/Http/Requests/Auth/LoginRequest.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
/**
|
||||
* Login Form Request
|
||||
*
|
||||
* Validates user login credentials with enterprise security standards.
|
||||
* Implements rate limiting and security validations.
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @since February 2025
|
||||
*/
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email:rfc,dns',
|
||||
'max:255',
|
||||
'exists:users,email'
|
||||
],
|
||||
'password' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:6',
|
||||
'max:255'
|
||||
],
|
||||
'remember' => [
|
||||
'boolean'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages for validation rules.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'email.required' => 'Email address is required.',
|
||||
'email.email' => 'Please enter a valid email address.',
|
||||
'email.exists' => 'No account found with this email address.',
|
||||
'password.required' => 'Password is required.',
|
||||
'password.min' => 'Password must be at least 6 characters long.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'email address',
|
||||
'password' => 'password',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'email' => strtolower(trim($this->email)),
|
||||
'remember' => $this->boolean('remember'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failed validation attempts for security monitoring
|
||||
*
|
||||
* Logs failed login validation attempts with security context
|
||||
* including IP address, user agent, and validation errors.
|
||||
* Essential for detecting potential security threats.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Validation\Validator $validator
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function failedValidation($validator): void
|
||||
{
|
||||
logger()->warning('Login validation failed', [
|
||||
'email' => $this->input('email'),
|
||||
'ip' => $this->ip(),
|
||||
'user_agent' => $this->userAgent(),
|
||||
'errors' => $validator->errors()->toArray(),
|
||||
]);
|
||||
|
||||
parent::failedValidation($validator);
|
||||
}
|
||||
}
|
||||
209
app/Http/Requests/Auth/RegisterRequest.php
Normal file
209
app/Http/Requests/Auth/RegisterRequest.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
/**
|
||||
* Registration Form Request
|
||||
*
|
||||
* Validates user registration data with comprehensive security checks.
|
||||
* Implements password strength validation and data sanitization.
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @since February 2025
|
||||
*/
|
||||
class RegisterRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:2',
|
||||
'max:50',
|
||||
'regex:/^[a-zA-ZÀ-ÿ\s\-\'\.]+$/',
|
||||
],
|
||||
'last_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:2',
|
||||
'max:50',
|
||||
'regex:/^[a-zA-ZÀ-ÿ\s\-\'\.]+$/',
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
'email:rfc,dns',
|
||||
'max:255',
|
||||
'unique:users,email',
|
||||
],
|
||||
'password' => [
|
||||
'required',
|
||||
'string',
|
||||
'confirmed',
|
||||
Password::min(8)
|
||||
->letters()
|
||||
->mixedCase()
|
||||
->numbers()
|
||||
->symbols()
|
||||
->uncompromised(),
|
||||
],
|
||||
'password_confirmation' => [
|
||||
'required',
|
||||
'string',
|
||||
'same:password',
|
||||
],
|
||||
'phone' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'min:10',
|
||||
'max:20',
|
||||
'regex:/^[\+]?[0-9\s\-\(\)]+$/',
|
||||
],
|
||||
'terms' => [
|
||||
'required',
|
||||
'accepted',
|
||||
],
|
||||
'newsletter' => [
|
||||
'boolean',
|
||||
],
|
||||
'marketing' => [
|
||||
'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages for validation rules.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'first_name.required' => 'First name is required.',
|
||||
'first_name.min' => 'First name must be at least 2 characters.',
|
||||
'first_name.regex' => 'First name contains invalid characters.',
|
||||
'last_name.required' => 'Last name is required.',
|
||||
'last_name.min' => 'Last name must be at least 2 characters.',
|
||||
'last_name.regex' => 'Last name contains invalid characters.',
|
||||
'email.required' => 'Email address is required.',
|
||||
'email.email' => 'Please enter a valid email address.',
|
||||
'email.unique' => 'An account with this email address already exists.',
|
||||
'password.required' => 'Password is required.',
|
||||
'password.min' => 'Password must be at least 8 characters long.',
|
||||
'password.confirmed' => 'Password confirmation does not match.',
|
||||
'password_confirmation.required' => 'Password confirmation is required.',
|
||||
'password_confirmation.same' => 'Password confirmation must match the password.',
|
||||
'phone.regex' => 'Please enter a valid phone number.',
|
||||
'terms.required' => 'You must accept the terms and conditions.',
|
||||
'terms.accepted' => 'You must accept the terms and conditions.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => 'first name',
|
||||
'last_name' => 'last name',
|
||||
'email' => 'email address',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password confirmation',
|
||||
'phone' => 'phone number',
|
||||
'terms' => 'terms and conditions',
|
||||
'newsletter' => 'newsletter subscription',
|
||||
'marketing' => 'marketing communications',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'first_name' => ucfirst(strtolower(trim($this->first_name))),
|
||||
'last_name' => ucfirst(strtolower(trim($this->last_name))),
|
||||
'email' => strtolower(trim($this->email)),
|
||||
'phone' => $this->phone ? preg_replace('/[^\+0-9]/', '', $this->phone) : null,
|
||||
'newsletter' => $this->boolean('newsletter'),
|
||||
'marketing' => $this->boolean('marketing'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure advanced validation rules and security checks
|
||||
*
|
||||
* Implements additional validation logic beyond standard rules:
|
||||
* - Name similarity validation
|
||||
* - Suspicious pattern detection
|
||||
* - Data integrity verification
|
||||
*
|
||||
* @param \Illuminate\Validation\Validator $validator
|
||||
* @return void
|
||||
*/
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
if ($this->first_name === $this->last_name) {
|
||||
$validator->errors()->add(
|
||||
'last_name',
|
||||
'Last name should be different from first name.'
|
||||
);
|
||||
}
|
||||
|
||||
$suspiciousPatterns = ['test', 'admin', 'root', 'null', 'undefined'];
|
||||
$fullName = strtolower($this->first_name . ' ' . $this->last_name);
|
||||
|
||||
foreach ($suspiciousPatterns as $pattern) {
|
||||
if (strpos($fullName, $pattern) !== false) {
|
||||
$validator->errors()->add(
|
||||
'first_name',
|
||||
'Please enter your real name.'
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle failed registration validation for security monitoring
|
||||
*
|
||||
* Logs failed registration attempts with comprehensive security context
|
||||
* including user input, IP address, and validation errors.
|
||||
* Critical for detecting registration abuse and attacks.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Validation\Validator $validator
|
||||
* @return void
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function failedValidation($validator): void
|
||||
{
|
||||
logger()->warning('Registration validation failed', [
|
||||
'email' => $this->input('email'),
|
||||
'first_name' => $this->input('first_name'),
|
||||
'last_name' => $this->input('last_name'),
|
||||
'ip' => $this->ip(),
|
||||
'user_agent' => $this->userAgent(),
|
||||
'errors' => $validator->errors()->toArray(),
|
||||
]);
|
||||
|
||||
parent::failedValidation($validator);
|
||||
}
|
||||
}
|
||||
119
app/Http/Requests/Auth/UpdateProfileRequest.php
Normal file
119
app/Http/Requests/Auth/UpdateProfileRequest.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Auth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* Update Profile Form Request
|
||||
*
|
||||
* Validates user profile update data with comprehensive security checks.
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @since February 2025
|
||||
*/
|
||||
class UpdateProfileRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Auth::check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:2',
|
||||
'max:50',
|
||||
'regex:/^[a-zA-ZÀ-ÿ\s\-\'\.]+$/',
|
||||
],
|
||||
'last_name' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:2',
|
||||
'max:50',
|
||||
'regex:/^[a-zA-ZÀ-ÿ\s\-\'\.]+$/',
|
||||
],
|
||||
'phone' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'min:10',
|
||||
'max:20',
|
||||
'regex:/^[\+]?[0-9\s\-\(\)]+$/',
|
||||
],
|
||||
'bio' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:1000',
|
||||
],
|
||||
'website' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
'regex:/^https?:\/\/.+$/',
|
||||
],
|
||||
'linkedin' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
'regex:/^https:\/\/(www\.)?linkedin\.com\/in\/[a-zA-Z0-9\-]+\/?$/',
|
||||
],
|
||||
'github' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
'regex:/^https:\/\/(www\.)?github\.com\/[a-zA-Z0-9\-]+\/?$/',
|
||||
],
|
||||
'twitter' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
'regex:/^https:\/\/(www\.)?twitter\.com\/[a-zA-Z0-9_]+\/?$/',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages for validation rules.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'first_name.required' => 'First name is required.',
|
||||
'first_name.regex' => 'First name contains invalid characters.',
|
||||
'last_name.required' => 'Last name is required.',
|
||||
'last_name.regex' => 'Last name contains invalid characters.',
|
||||
'phone.regex' => 'Please enter a valid phone number.',
|
||||
'website.regex' => 'Website URL must start with http:// or https://',
|
||||
'linkedin.regex' => 'Please enter a valid LinkedIn profile URL.',
|
||||
'github.regex' => 'Please enter a valid GitHub profile URL.',
|
||||
'twitter.regex' => 'Please enter a valid Twitter profile URL.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'first_name' => ucfirst(strtolower(trim($this->first_name ?? ''))),
|
||||
'last_name' => ucfirst(strtolower(trim($this->last_name ?? ''))),
|
||||
'phone' => $this->phone ? preg_replace('/[^\+0-9]/', '', $this->phone) : null,
|
||||
'bio' => $this->bio ? trim($this->bio) : null,
|
||||
'website' => $this->website ? strtolower(trim($this->website)) : null,
|
||||
'linkedin' => $this->linkedin ? strtolower(trim($this->linkedin)) : null,
|
||||
'github' => $this->github ? strtolower(trim($this->github)) : null,
|
||||
'twitter' => $this->twitter ? strtolower(trim($this->twitter)) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
336
app/Http/Requests/Resume/StoreResumeRequest.php
Normal file
336
app/Http/Requests/Resume/StoreResumeRequest.php
Normal file
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Resume;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* Store Resume Form Request
|
||||
*
|
||||
* Validates data for creating a new resume with comprehensive validation rules.
|
||||
* Implements business logic validations and data sanitization.
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @since February 2025
|
||||
*/
|
||||
class StoreResumeRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return Auth::check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => [
|
||||
'required',
|
||||
'string',
|
||||
'min:3',
|
||||
'max:255',
|
||||
'regex:/^[a-zA-Z0-9\s\-\_\.\,\!\?]+$/',
|
||||
],
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:1000',
|
||||
],
|
||||
'template' => [
|
||||
'required',
|
||||
'string',
|
||||
'in:professional,modern,executive,minimal,technical',
|
||||
],
|
||||
'content' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'content.personal_info' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'content.personal_info.full_name' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.title' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.email' => [
|
||||
'nullable',
|
||||
'email',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.phone' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:20',
|
||||
],
|
||||
'content.personal_info.location' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.website' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.linkedin' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.github' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
],
|
||||
'content.summary' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:2000',
|
||||
],
|
||||
'content.experience' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:20',
|
||||
],
|
||||
'content.experience.*.company' => [
|
||||
'required_with:content.experience',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.experience.*.position' => [
|
||||
'required_with:content.experience',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.experience.*.start_date' => [
|
||||
'required_with:content.experience',
|
||||
'date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.experience.*.end_date' => [
|
||||
'nullable',
|
||||
'date',
|
||||
'after:content.experience.*.start_date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.experience.*.current' => [
|
||||
'boolean',
|
||||
],
|
||||
'content.experience.*.description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:2000',
|
||||
],
|
||||
'content.education' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:10',
|
||||
],
|
||||
'content.education.*.institution' => [
|
||||
'required_with:content.education',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.education.*.degree' => [
|
||||
'required_with:content.education',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.education.*.field' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.education.*.start_date' => [
|
||||
'required_with:content.education',
|
||||
'date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.education.*.end_date' => [
|
||||
'nullable',
|
||||
'date',
|
||||
'after:content.education.*.start_date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.skills' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'settings' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'settings.color_scheme' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'in:blue,green,red,purple,orange,gray,black',
|
||||
],
|
||||
'settings.font_family' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'in:Arial,Times New Roman,Calibri,Helvetica,Georgia',
|
||||
],
|
||||
'settings.font_size' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'min:8',
|
||||
'max:16',
|
||||
],
|
||||
'is_public' => [
|
||||
'boolean',
|
||||
],
|
||||
'allow_comments' => [
|
||||
'boolean',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages for validation rules.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => 'Resume title is required.',
|
||||
'title.min' => 'Resume title must be at least 3 characters.',
|
||||
'title.max' => 'Resume title cannot exceed 255 characters.',
|
||||
'title.regex' => 'Resume title contains invalid characters.',
|
||||
'description.max' => 'Description cannot exceed 1000 characters.',
|
||||
'template.required' => 'Please select a resume template.',
|
||||
'template.in' => 'Selected template is not available.',
|
||||
'content.experience.max' => 'You cannot add more than 20 work experiences.',
|
||||
'content.education.max' => 'You cannot add more than 10 education entries.',
|
||||
'content.personal_info.email.email' => 'Please enter a valid email address.',
|
||||
'content.personal_info.website.url' => 'Please enter a valid website URL.',
|
||||
'content.personal_info.linkedin.url' => 'Please enter a valid LinkedIn URL.',
|
||||
'content.personal_info.github.url' => 'Please enter a valid GitHub URL.',
|
||||
'content.experience.*.company.required_with' => 'Company name is required for work experience.',
|
||||
'content.experience.*.position.required_with' => 'Position title is required for work experience.',
|
||||
'content.experience.*.start_date.required_with' => 'Start date is required for work experience.',
|
||||
'content.experience.*.start_date.before_or_equal' => 'Start date cannot be in the future.',
|
||||
'content.experience.*.end_date.after' => 'End date must be after start date.',
|
||||
'content.experience.*.end_date.before_or_equal' => 'End date cannot be in the future.',
|
||||
'content.education.*.institution.required_with' => 'Institution name is required for education.',
|
||||
'content.education.*.degree.required_with' => 'Degree is required for education.',
|
||||
'content.education.*.start_date.required_with' => 'Start date is required for education.',
|
||||
'settings.font_size.min' => 'Font size must be at least 8.',
|
||||
'settings.font_size.max' => 'Font size cannot exceed 16.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'resume title',
|
||||
'description' => 'resume description',
|
||||
'template' => 'resume template',
|
||||
'content.personal_info.full_name' => 'full name',
|
||||
'content.personal_info.title' => 'professional title',
|
||||
'content.personal_info.email' => 'email address',
|
||||
'content.personal_info.phone' => 'phone number',
|
||||
'content.personal_info.location' => 'location',
|
||||
'content.personal_info.website' => 'website',
|
||||
'content.personal_info.linkedin' => 'LinkedIn profile',
|
||||
'content.personal_info.github' => 'GitHub profile',
|
||||
'content.summary' => 'professional summary',
|
||||
'is_public' => 'public visibility',
|
||||
'allow_comments' => 'allow comments',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$this->merge([
|
||||
'title' => trim($this->title),
|
||||
'description' => $this->description ? trim($this->description) : null,
|
||||
'is_public' => $this->boolean('is_public'),
|
||||
'allow_comments' => $this->boolean('allow_comments'),
|
||||
]);
|
||||
|
||||
if ($this->has('content.personal_info')) {
|
||||
$personalInfo = $this->input('content.personal_info', []);
|
||||
|
||||
if (isset($personalInfo['email'])) {
|
||||
$personalInfo['email'] = strtolower(trim($personalInfo['email']));
|
||||
}
|
||||
|
||||
if (isset($personalInfo['full_name'])) {
|
||||
$personalInfo['full_name'] = trim($personalInfo['full_name']);
|
||||
}
|
||||
|
||||
$this->merge(['content' => array_merge($this->input('content', []), [
|
||||
'personal_info' => $personalInfo
|
||||
])]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator instance.
|
||||
*/
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$userId = Auth::id();
|
||||
$existingResume = \App\Models\Resume::where('user_id', $userId)
|
||||
->where('title', $this->title)
|
||||
->first();
|
||||
|
||||
if ($existingResume) {
|
||||
$validator->errors()->add(
|
||||
'title',
|
||||
'You already have a resume with this title. Please choose a different title.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->has('content.experience') && is_array($this->content['experience'])) {
|
||||
foreach ($this->content['experience'] as $index => $experience) {
|
||||
if (!empty($experience['start_date']) && !empty($experience['end_date'])) {
|
||||
$startDate = Carbon::parse($experience['start_date']);
|
||||
$endDate = Carbon::parse($experience['end_date']);
|
||||
|
||||
if ($startDate->greaterThan($endDate)) {
|
||||
$validator->errors()->add(
|
||||
"content.experience.{$index}.end_date",
|
||||
'End date must be after start date.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a failed validation attempt.
|
||||
*/
|
||||
protected function failedValidation($validator): void
|
||||
{
|
||||
logger()->warning('Resume creation validation failed', [
|
||||
'user_id' => Auth::id(),
|
||||
'title' => $this->input('title'),
|
||||
'template' => $this->input('template'),
|
||||
'errors' => $validator->errors()->toArray(),
|
||||
]);
|
||||
|
||||
parent::failedValidation($validator);
|
||||
}
|
||||
}
|
||||
465
app/Http/Requests/Resume/UpdateResumeRequest.php
Normal file
465
app/Http/Requests/Resume/UpdateResumeRequest.php
Normal file
@@ -0,0 +1,465 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Resume;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* Update Resume Form Request
|
||||
*
|
||||
* Validates data for updating an existing resume with comprehensive validation rules.
|
||||
* Implements business logic validations and data sanitization for resume updates.
|
||||
*
|
||||
* @author David Valera Melendez <david@valera-melendez.de>
|
||||
* @since February 2025
|
||||
*/
|
||||
class UpdateResumeRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
$resume = $this->route('resume');
|
||||
return Auth::check() && $resume && $resume->user_id === Auth::id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$resumeId = $this->route('resume')->id ?? null;
|
||||
|
||||
return [
|
||||
'title' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'string',
|
||||
'min:3',
|
||||
'max:255',
|
||||
'regex:/^[a-zA-Z0-9\s\-\_\.\,\!\?]+$/',
|
||||
],
|
||||
'description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:1000',
|
||||
],
|
||||
'template' => [
|
||||
'sometimes',
|
||||
'required',
|
||||
'string',
|
||||
'in:professional,modern,executive,minimal,technical',
|
||||
],
|
||||
'status' => [
|
||||
'sometimes',
|
||||
'string',
|
||||
'in:draft,published,archived',
|
||||
],
|
||||
'content' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'content.personal_info' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'content.personal_info.full_name' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.title' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.email' => [
|
||||
'nullable',
|
||||
'email',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.phone' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:20',
|
||||
'regex:/^[\+]?[0-9\s\-\(\)]+$/',
|
||||
],
|
||||
'content.personal_info.location' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.website' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
],
|
||||
'content.personal_info.linkedin' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
'regex:/^https:\/\/(www\.)?linkedin\.com\/in\/[a-zA-Z0-9\-]+\/?$/',
|
||||
],
|
||||
'content.personal_info.github' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
'regex:/^https:\/\/(www\.)?github\.com\/[a-zA-Z0-9\-]+\/?$/',
|
||||
],
|
||||
'content.summary' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:2000',
|
||||
],
|
||||
'content.experience' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:20',
|
||||
],
|
||||
'content.experience.*.company' => [
|
||||
'required_with:content.experience',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.experience.*.position' => [
|
||||
'required_with:content.experience',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.experience.*.location' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.experience.*.start_date' => [
|
||||
'required_with:content.experience',
|
||||
'date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.experience.*.end_date' => [
|
||||
'nullable',
|
||||
'date',
|
||||
'after:content.experience.*.start_date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.experience.*.current' => [
|
||||
'boolean',
|
||||
],
|
||||
'content.experience.*.description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:2000',
|
||||
],
|
||||
'content.experience.*.achievements' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:10',
|
||||
],
|
||||
'content.experience.*.achievements.*' => [
|
||||
'string',
|
||||
'max:500',
|
||||
],
|
||||
'content.education' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:10',
|
||||
],
|
||||
'content.education.*.institution' => [
|
||||
'required_with:content.education',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.education.*.degree' => [
|
||||
'required_with:content.education',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.education.*.field' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.education.*.start_date' => [
|
||||
'required_with:content.education',
|
||||
'date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.education.*.end_date' => [
|
||||
'nullable',
|
||||
'date',
|
||||
'after:content.education.*.start_date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.education.*.gpa' => [
|
||||
'nullable',
|
||||
'numeric',
|
||||
'min:0',
|
||||
'max:4',
|
||||
],
|
||||
'content.education.*.description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:1000',
|
||||
],
|
||||
'content.skills' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'content.projects' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:15',
|
||||
],
|
||||
'content.projects.*.name' => [
|
||||
'required_with:content.projects',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.projects.*.description' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'max:1000',
|
||||
],
|
||||
'content.projects.*.url' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
],
|
||||
'content.projects.*.technologies' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:20',
|
||||
],
|
||||
'content.certifications' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:20',
|
||||
],
|
||||
'content.certifications.*.name' => [
|
||||
'required_with:content.certifications',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.certifications.*.issuer' => [
|
||||
'required_with:content.certifications',
|
||||
'string',
|
||||
'max:255',
|
||||
],
|
||||
'content.certifications.*.date' => [
|
||||
'required_with:content.certifications',
|
||||
'date',
|
||||
'before_or_equal:today',
|
||||
],
|
||||
'content.certifications.*.url' => [
|
||||
'nullable',
|
||||
'url',
|
||||
'max:255',
|
||||
],
|
||||
'content.languages' => [
|
||||
'nullable',
|
||||
'array',
|
||||
'max:10',
|
||||
],
|
||||
'content.languages.*.language' => [
|
||||
'required_with:content.languages',
|
||||
'string',
|
||||
'max:100',
|
||||
],
|
||||
'content.languages.*.level' => [
|
||||
'required_with:content.languages',
|
||||
'string',
|
||||
'in:Native,Fluent,Advanced,Intermediate,Basic',
|
||||
],
|
||||
'settings' => [
|
||||
'nullable',
|
||||
'array',
|
||||
],
|
||||
'settings.color_scheme' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'in:blue,green,red,purple,orange,gray,black',
|
||||
],
|
||||
'settings.font_family' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'in:Arial,Times New Roman,Calibri,Helvetica,Georgia',
|
||||
],
|
||||
'settings.font_size' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'min:8',
|
||||
'max:16',
|
||||
],
|
||||
'settings.line_spacing' => [
|
||||
'nullable',
|
||||
'numeric',
|
||||
'min:1.0',
|
||||
'max:2.0',
|
||||
],
|
||||
'settings.margin' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
'min:10',
|
||||
'max:30',
|
||||
],
|
||||
'settings.show_photo' => [
|
||||
'boolean',
|
||||
],
|
||||
'is_public' => [
|
||||
'boolean',
|
||||
],
|
||||
'allow_comments' => [
|
||||
'boolean',
|
||||
],
|
||||
'password' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'min:6',
|
||||
'max:50',
|
||||
],
|
||||
'expires_at' => [
|
||||
'nullable',
|
||||
'date',
|
||||
'after:today',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages for validation rules.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => 'Resume title is required.',
|
||||
'title.min' => 'Resume title must be at least 3 characters.',
|
||||
'title.regex' => 'Resume title contains invalid characters.',
|
||||
'template.in' => 'Selected template is not available.',
|
||||
'status.in' => 'Invalid resume status.',
|
||||
'content.personal_info.email.email' => 'Please enter a valid email address.',
|
||||
'content.personal_info.phone.regex' => 'Please enter a valid phone number.',
|
||||
'content.personal_info.linkedin.regex' => 'Please enter a valid LinkedIn profile URL.',
|
||||
'content.personal_info.github.regex' => 'Please enter a valid GitHub profile URL.',
|
||||
'content.experience.max' => 'You cannot have more than 20 work experiences.',
|
||||
'content.education.max' => 'You cannot have more than 10 education entries.',
|
||||
'content.projects.max' => 'You cannot have more than 15 projects.',
|
||||
'content.certifications.max' => 'You cannot have more than 20 certifications.',
|
||||
'content.languages.max' => 'You cannot have more than 10 languages.',
|
||||
'content.experience.*.end_date.after' => 'End date must be after start date.',
|
||||
'content.education.*.gpa.max' => 'GPA cannot exceed 4.0.',
|
||||
'content.languages.*.level.in' => 'Invalid language proficiency level.',
|
||||
'settings.font_size.min' => 'Font size must be at least 8.',
|
||||
'settings.font_size.max' => 'Font size cannot exceed 16.',
|
||||
'settings.line_spacing.min' => 'Line spacing must be at least 1.0.',
|
||||
'settings.line_spacing.max' => 'Line spacing cannot exceed 2.0.',
|
||||
'expires_at.after' => 'Expiration date must be in the future.',
|
||||
'password.min' => 'Password must be at least 6 characters.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
if ($this->has('title')) {
|
||||
$this->merge(['title' => trim($this->title)]);
|
||||
}
|
||||
|
||||
if ($this->has('description')) {
|
||||
$this->merge(['description' => $this->description ? trim($this->description) : null]);
|
||||
}
|
||||
|
||||
$this->merge([
|
||||
'is_public' => $this->boolean('is_public'),
|
||||
'allow_comments' => $this->boolean('allow_comments'),
|
||||
]);
|
||||
|
||||
|
||||
if ($this->has('content.personal_info')) {
|
||||
$personalInfo = $this->input('content.personal_info', []);
|
||||
|
||||
if (isset($personalInfo['email'])) {
|
||||
$personalInfo['email'] = strtolower(trim($personalInfo['email']));
|
||||
}
|
||||
|
||||
$this->merge(['content' => array_merge($this->input('content', []), [
|
||||
'personal_info' => $personalInfo
|
||||
])]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator instance.
|
||||
*/
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$resume = $this->route('resume');
|
||||
|
||||
if ($this->has('title')) {
|
||||
$userId = Auth::id();
|
||||
$existingResume = \App\Models\Resume::where('user_id', $userId)
|
||||
->where('title', $this->title)
|
||||
->where('id', '!=', $resume->id)
|
||||
->first();
|
||||
|
||||
if ($existingResume) {
|
||||
$validator->errors()->add(
|
||||
'title',
|
||||
'You already have a resume with this title. Please choose a different title.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->has('content.experience') && is_array($this->content['experience'])) {
|
||||
foreach ($this->content['experience'] as $index => $experience) {
|
||||
if (!empty($experience['start_date']) && !empty($experience['end_date'])) {
|
||||
$startDate = Carbon::parse($experience['start_date']);
|
||||
$endDate = Carbon::parse($experience['end_date']);
|
||||
|
||||
if ($startDate->greaterThan($endDate)) {
|
||||
$validator->errors()->add(
|
||||
"content.experience.{$index}.end_date",
|
||||
'End date must be after start date.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->has('status') && $this->status === 'published') {
|
||||
$content = $this->input('content', []);
|
||||
$requiredSections = ['personal_info', 'summary'];
|
||||
|
||||
foreach ($requiredSections as $section) {
|
||||
if (empty($content[$section])) {
|
||||
$validator->errors()->add(
|
||||
'status',
|
||||
"Cannot publish resume: {$section} section is required."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a failed validation attempt.
|
||||
*/
|
||||
protected function failedValidation($validator): void
|
||||
{
|
||||
$resume = $this->route('resume');
|
||||
|
||||
logger()->warning('Resume update validation failed', [
|
||||
'user_id' => Auth::id(),
|
||||
'resume_id' => $resume->id ?? null,
|
||||
'title' => $this->input('title'),
|
||||
'errors' => $validator->errors()->toArray(),
|
||||
]);
|
||||
|
||||
parent::failedValidation($validator);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user