validate([ 'email' => ['required', 'email'], 'password' => ['required'], ]); // Attempt actual database login $user = User::where('email', $credentials['email'])->first(); if ($user && Hash::check($credentials['password'], $user->password) && $user->role === 'employer') { $profile = EmployerProfile::where('user_id', $user->id)->first(); $verification_status = $profile ? $profile->verification_status : 'approved'; if ($verification_status === 'pending') { return back()->with('status', 'pending_verification'); } if ($verification_status === 'rejected') { return back()->with([ 'status' => 'rejected', 'reason' => ($profile && $profile->rejection_reason) ? $profile->rejection_reason : 'Emirates ID scan was blurry or illegible. Please provide a high-resolution color scan.', ]); } if ($user->subscription_status === 'expired') { return back()->with('status', 'subscription_expired'); } // Perform standard Laravel authentication (remember=true keeps auth cookie alive) auth()->login($user, true); // Regenerate BEFORE writing session data to avoid losing data on ID change $request->session()->regenerate(); $request->session()->put('user', (object)[ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'role' => $user->role, 'subscription_status' => $user->subscription_status ?? 'active', 'verification_status' => $verification_status, ]); if ($request->source === 'mobile') { return redirect('/mobile/employer/home'); } return redirect()->intended('/employer/dashboard'); } return back()->withErrors([ 'email' => 'These credentials do not match our records.', ]); } public function showRegister() { return Inertia::render('Employer/Auth/Register'); } public function register(Request $request) { // 1. Validation $request->validate([ 'company_name' => 'nullable|string|max:255', 'name' => 'required|string|max:255', 'email' => 'required|string|email|max:255', 'phone' => ['required', 'string', 'regex:/^\+?[0-9]{7,20}$/'], 'country_code' => 'nullable|string|max:10', 'address' => 'required|string|max:255', ], [ 'phone.regex' => 'The mobile number must contain only digits (7-20 characters).', ]); // 2. Email uniqueness check (Check DB, return 409 if duplicate) if (User::where('email', $request->email)->exists() || \App\Models\Sponsor::where('email', $request->email)->exists()) { return response()->json([ 'errors' => [ 'email' => 'This email address is already registered. Please login or use a different email.' ] ], 409); } // 2b. Phone uniqueness check $phoneClean = preg_replace('/[^0-9]/', '', $request->phone); if (\App\Models\EmployerProfile::where('phone', 'like', '%' . $phoneClean . '%')->exists() || \App\Models\Sponsor::where('mobile', 'like', '%' . $phoneClean . '%')->exists()) { return response()->json([ 'errors' => [ 'phone' => 'This mobile number is already registered. Please login or use a different number.' ] ], 409); } // 3. Generate 6-digit OTP, hash & store with 10-min expiry $otp = (string) mt_rand(100000, 999999); $hashedOtp = Hash::make($otp); session([ 'pending_employer_registration' => [ 'company_name' => $request->company_name ?: ($request->name . ' Household'), 'name' => $request->name, 'email' => $request->email, 'phone' => $request->phone, 'country_code' => $request->country_code, 'address' => $request->address, ], 'employer_otp' => [ 'hash' => $hashedOtp, 'expires_at' => now()->addMinutes(10), 'attempts' => 0, 'last_sent_at' => now(), ] ]); // 4. Send professional OTP email try { Mail::to($request->email)->send(new EmployerOtpMail( $otp, $request->company_name, $request->name )); } catch (\Exception $e) { // Log error but proceed for local testing/robustness logger()->error('Failed to send registration OTP email: ' . $e->getMessage()); } return redirect()->route('employer.verify-email') ->with('success', 'Verification code sent to your email.'); } public function showVerifyEmail() { if (!session()->has('pending_employer_registration')) { return redirect()->route('employer.register') ->with('error', 'Please submit the registration form first.'); } return Inertia::render('Employer/Auth/VerifyEmail', [ 'email' => session('pending_employer_registration.email'), ]); } public function verifyEmail(Request $request) { $request->validate([ 'otp' => 'required|string|size:6', ]); if (!session()->has('pending_employer_registration') || !session()->has('employer_otp')) { return redirect()->route('employer.register') ->with('error', 'Registration session expired. Please start over.'); } $otpSession = session('employer_otp'); // Check attempts (max 3) if ($otpSession['attempts'] >= 3) { session()->forget(['pending_employer_registration', 'employer_otp']); return redirect()->route('employer.register') ->with('error', 'Too many failed verification attempts. Please register again.'); } // Check expiry (10 mins) if (now()->greaterThan($otpSession['expires_at'])) { return back()->withErrors([ 'otp' => 'The verification code has expired. Please request a new one.' ]); } // Hash comparison (allow 000000 or 111111 in local/testing environment for automated testing) $isLocalDebug = (app()->environment('local') || app()->environment('testing')) && ($request->otp === '000000' || $request->otp === '111111'); if (!$isLocalDebug && !Hash::check($request->otp, $otpSession['hash'])) { $otpSession['attempts']++; session(['employer_otp' => $otpSession]); $attemptsLeft = 3 - $otpSession['attempts']; if ($attemptsLeft <= 0) { session()->forget(['pending_employer_registration', 'employer_otp']); return redirect()->route('employer.register') ->with('error', 'Too many failed verification attempts. Please register again.'); } return back()->withErrors([ 'otp' => "Invalid verification code. You have {$attemptsLeft} attempts remaining." ]); } // Success - mark verified session(['employer_email_verified' => true]); return redirect()->route('employer.upload-emirates-id') ->with('success', 'Email verified successfully! Please upload your Emirates ID.'); } public function resendOtp(Request $request) { if (!session()->has('pending_employer_registration')) { return response()->json([ 'message' => 'No pending registration session found.' ], 400); } $pending = session('pending_employer_registration'); $otpSession = session('employer_otp'); // Cooldown timer check (60s) if ($otpSession && now()->diffInSeconds($otpSession['last_sent_at']) < 60) { $secondsRemaining = 60 - now()->diffInSeconds($otpSession['last_sent_at']); return response()->json([ 'message' => "Please wait {$secondsRemaining} seconds before requesting a new code." ], 429); } $otp = (string) mt_rand(100000, 999999); $hashedOtp = Hash::make($otp); session([ 'employer_otp' => [ 'hash' => $hashedOtp, 'expires_at' => now()->addMinutes(10), 'attempts' => 0, 'last_sent_at' => now(), ] ]); try { Mail::to($pending['email'])->send(new EmployerOtpMail( $otp, $pending['company_name'], $pending['name'] )); } catch (\Exception $e) { logger()->error('Failed to resend registration OTP email: ' . $e->getMessage()); } return response()->json([ 'message' => 'A fresh verification code has been sent to your email.' ]); } public function showUploadEmiratesId() { if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) { return redirect()->route('employer.register') ->with('error', 'Please complete email verification first.'); } return Inertia::render('Employer/Auth/UploadEmiratesId', [ 'email' => session('pending_employer_registration.email'), ]); } public function uploadEmiratesId(Request $request) { if (!session()->has('pending_employer_registration') || !session('employer_email_verified')) { return redirect()->route('employer.register') ->with('error', 'Registration session expired. Please start over.'); } $request->validate([ 'emirates_id_front' => 'required_without:emirates_id_file|file|mimes:jpeg,png,jpg,pdf|max:10240', 'emirates_id_back' => 'required_without:emirates_id_file|file|mimes:jpeg,png,jpg,pdf|max:10240', 'emirates_id_file' => 'required_without_all:emirates_id_front,emirates_id_back|file|mimes:jpeg,png,jpg,pdf|max:10240', ], [ 'emirates_id_front.required_without' => 'Please upload the front side of your Emirates ID.', 'emirates_id_back.required_without' => 'Please upload the back side of your Emirates ID.', 'emirates_id_front.mimes' => 'The Emirates ID front must be an image or a PDF.', 'emirates_id_back.mimes' => 'The Emirates ID back must be an image or a PDF.', ]); $extractedIdNumber = null; $extractedExpiry = null; $extractedName = null; $extractedDob = null; $extractedNationality = null; $extractedIssueDate = null; $extractedEmployer = null; $extractedIssuePlace = null; $extractedOccupation = null; $extractedCardNumber = null; $extractedGender = null; if ($request->hasFile('emirates_id_front')) { $frontFile = $request->file('emirates_id_front'); $frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($frontFile); $extractedIdNumber = $frontOcr['emirates_id_number']; $extractedName = $frontOcr['name'] ?? null; $extractedDob = $frontOcr['date_of_birth'] ?? null; $extractedNationality = $frontOcr['nationality'] ?? null; $extractedCardNumber = $frontOcr['card_number'] ?? null; $extractedGender = $frontOcr['gender'] ?? null; $extractedOccupation = $frontOcr['occupation'] ?? null; $extractedEmployer = $frontOcr['employer'] ?? null; $extractedIssuePlace = $frontOcr['issue_place'] ?? null; } if ($request->hasFile('emirates_id_back')) { $backFile = $request->file('emirates_id_back'); $backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($backFile); $extractedExpiry = $backOcr['expiry_date']; $extractedIssueDate = $backOcr['issue_date'] ?? null; } if ($request->hasFile('emirates_id_file')) { $file = $request->file('emirates_id_file'); $frontOcr = \App\Services\OcrDocumentService::extractEmiratesIdFrontData($file); $backOcr = \App\Services\OcrDocumentService::extractEmiratesIdBackData($file); $extractedIdNumber = $frontOcr['emirates_id_number']; $extractedExpiry = $backOcr['expiry_date']; $extractedName = $frontOcr['name'] ?? null; $extractedDob = $frontOcr['date_of_birth'] ?? null; $extractedNationality = $frontOcr['nationality'] ?? null; $extractedCardNumber = $frontOcr['card_number'] ?? null; $extractedGender = $frontOcr['gender'] ?? null; $extractedOccupation = $frontOcr['occupation'] ?? null; $extractedEmployer = $frontOcr['employer'] ?? null; $extractedIssuePlace = $frontOcr['issue_place'] ?? null; $extractedIssueDate = $backOcr['issue_date'] ?? null; } $pending = session('pending_employer_registration'); $pending['emirates_id_file'] = null; $pending['emirates_id_number'] = $extractedIdNumber; $pending['emirates_id_expiry'] = $extractedExpiry; $pending['emirates_id_name'] = $extractedName; $pending['emirates_id_dob'] = $extractedDob; $pending['emirates_id_nationality'] = $extractedNationality; $pending['emirates_id_card_number'] = $extractedCardNumber; $pending['emirates_id_gender'] = $extractedGender; $pending['emirates_id_occupation'] = $extractedOccupation; $pending['emirates_id_employer'] = $extractedEmployer; $pending['emirates_id_issue_place'] = $extractedIssuePlace; $pending['emirates_id_issue_date'] = $extractedIssueDate; session(['pending_employer_registration' => $pending]); session(['employer_emirates_id_uploaded' => true]); return redirect()->route('employer.register-payment') ->with('success', 'Emirates ID uploaded and verified successfully.'); } public function showRegisterPayment() { if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded')) { return redirect()->route('employer.register') ->with('error', 'Please complete Emirates ID upload first.'); } return Inertia::render('Employer/Auth/RegisterPayment', [ 'email' => session('pending_employer_registration.email'), 'plans' => [ [ 'id' => 'basic', 'name' => 'Basic Search Pass', 'price' => '99 AED', 'period' => 'month', 'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR verification'], 'popular' => false, ], [ 'id' => 'premium', 'name' => 'Premium Sponsor Pass', 'price' => '199 AED', 'period' => 'month', 'features' => ['Unlimited shortlisting', 'Direct candidate messaging', 'Priority interview scheduling', 'Dedicated support'], 'popular' => true, ], [ 'id' => 'enterprise', 'name' => 'VIP Concierge Pass', 'price' => '499 AED', 'period' => 'month', 'features' => ['All Premium features', 'Assigned recruitment manager', 'Background medical verification guarantee', 'Free replacement within 30 days'], 'popular' => false, ], ] ]); } public function storeRegisterPayment(Request $request) { $request->validate([ 'plan_id' => 'required|string', 'amount_aed' => 'required|numeric', 'paytabs_transaction_id' => 'required|string', ]); session([ 'pending_employer_payment' => [ 'plan_id' => $request->plan_id, 'amount_aed' => $request->amount_aed, 'paytabs_transaction_id' => $request->paytabs_transaction_id, ] ]); return response()->json([ 'message' => 'Payment processed successfully.' ]); } public function showCreatePassword() { if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded') || !session()->has('pending_employer_payment')) { return redirect()->route('employer.register') ->with('error', 'Please complete payment step first.'); } return Inertia::render('Employer/Auth/CreatePassword'); } public function createPassword(Request $request) { $request->validate([ 'password' => [ 'required', 'string', 'min:8', 'confirmed', 'regex:/[a-z]/', // 1 lowercase 'regex:/[A-Z]/', // 1 uppercase 'regex:/[0-9]/', // 1 number 'regex:/[^a-zA-Z0-9]/', // 1 special character ] ], [ 'password.regex' => 'The password must contain at least 8 characters, including 1 uppercase, 1 lowercase, 1 number, and 1 special character.', ]); if (!session()->has('pending_employer_registration') || !session('employer_email_verified') || !session('employer_emirates_id_uploaded') || !session()->has('pending_employer_payment')) { return redirect()->route('employer.register') ->with('error', 'Registration session expired. Please start over.'); } $pending = session('pending_employer_registration'); $payment = session('pending_employer_payment'); // Create user $user = User::create([ 'name' => $pending['name'], 'email' => $pending['email'], 'password' => Hash::make($request->password), 'role' => 'employer', 'subscription_status' => 'active', 'subscription_expires_at' => now()->addDays(30), ]); // Create profile EmployerProfile::create([ 'user_id' => $user->id, 'company_name' => $pending['company_name'], 'phone' => $pending['phone'], 'country' => 'United Arab Emirates', // Default country since dropdown was removed 'verification_status' => 'approved', 'language' => 'English', 'notifications' => true, 'emirates_id_front' => null, // We did not store the file 'emirates_id_number' => $pending['emirates_id_number'] ?? null, 'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null, 'emirates_id_name' => $pending['emirates_id_name'] ?? null, 'emirates_id_dob' => $pending['emirates_id_dob'] ?? null, 'emirates_id_issue_date' => $pending['emirates_id_issue_date'] ?? null, 'emirates_id_employer' => $pending['emirates_id_employer'] ?? null, 'emirates_id_issue_place' => $pending['emirates_id_issue_place'] ?? null, 'emirates_id_occupation' => $pending['emirates_id_occupation'] ?? null, 'emirates_id_card_number' => $pending['emirates_id_card_number'] ?? null, 'emirates_id_gender' => $pending['emirates_id_gender'] ?? null, 'nationality' => $pending['emirates_id_nationality'] ?? null, 'address' => $pending['address'] ?? null, ]); // Create Sponsor \App\Models\Sponsor::create([ 'full_name' => $pending['name'], 'email' => $pending['email'], 'mobile' => $pending['phone'], 'password' => Hash::make($request->password), 'country_code' => 'AE', 'subscription_status' => 'active', 'subscription_plan' => $payment['plan_id'], 'subscription_start_date' => now(), 'subscription_end_date' => now()->addDays(30), 'payment_status' => 'paid', 'is_verified' => true, 'otp_verified_at' => now(), 'status' => 'active', 'last_login_at' => now(), 'emirates_id_file' => null, // We did not store the file 'address' => $pending['address'] ?? null, 'emirates_id' => $pending['emirates_id_number'] ?? null, 'emirates_id_name' => $pending['emirates_id_name'] ?? null, 'emirates_id_dob' => $pending['emirates_id_dob'] ?? null, 'emirates_id_issue_date' => $pending['emirates_id_issue_date'] ?? null, 'emirates_id_expiry' => $pending['emirates_id_expiry'] ?? null, 'emirates_id_employer' => $pending['emirates_id_employer'] ?? null, 'emirates_id_issue_place' => $pending['emirates_id_issue_place'] ?? null, 'emirates_id_occupation' => $pending['emirates_id_occupation'] ?? null, 'emirates_id_card_number' => $pending['emirates_id_card_number'] ?? null, 'emirates_id_gender' => $pending['emirates_id_gender'] ?? null, 'nationality' => $pending['emirates_id_nationality'] ?? null, ]); // Create active subscription in database \Illuminate\Support\Facades\DB::table('subscriptions')->insert([ 'user_id' => $user->id, 'plan_id' => $payment['plan_id'], 'amount_aed' => $payment['amount_aed'], 'starts_at' => now(), 'expires_at' => now()->addDays(30), 'paytabs_transaction_id' => $payment['paytabs_transaction_id'], 'status' => 'active', 'created_at' => now(), 'updated_at' => now(), ]); // Auto-login (Laravel + Session) with remember=true for persistence auth()->login($user, true); // Regenerate session ID BEFORE writing data request()->session()->regenerate(); request()->session()->put('user', (object)[ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'role' => 'employer', 'subscription_status' => 'active', 'verification_status' => 'approved', ]); // Flash first_login toast flag session()->flash('first_login', true); // Clear registration session keys session()->forget([ 'pending_employer_registration', 'employer_otp', 'employer_email_verified', 'employer_emirates_id_uploaded', 'pending_employer_payment' ]); return redirect()->route('employer.dashboard') ->with('success', 'Welcome aboard! Your registration is complete.'); } public function logout(Request $request) { session()->forget('user'); auth()->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('employer.login') ->with('success', 'Logged out successfully.'); } public function forgotPassword(Request $request) { $request->validate([ 'email' => 'required|email', ]); $user = User::where('email', $request->email)->where('role', 'employer')->first(); if (!$user) { return response()->json([ 'message' => 'No employer account found with this email address.' ], 404); } $otp = '111111'; // default development OTP if (app()->environment('production')) { $otp = strval(rand(100000, 999999)); } \Illuminate\Support\Facades\Cache::put('employer_reset_otp_' . $request->email, [ 'code' => $otp, 'expires_at' => now()->addMinutes(10)->timestamp ], 600); // Send reset OTP email try { Mail::to($request->email)->send(new \App\Mail\EmployerResetOtpMail( $otp, $user->name )); } catch (\Exception $e) { logger()->error('Web Employer Forgot Password Mail Failure: ' . $e->getMessage()); } return response()->json([ 'success' => true, 'message' => 'Verification code sent to your email.' ]); } public function resetPassword(Request $request) { $request->validate([ 'email' => 'required|email', 'otp' => 'required|string|size:6', 'password' => [ 'required', 'string', 'min:8', 'confirmed', 'regex:/[a-z]/', // 1 lowercase 'regex:/[A-Z]/', // 1 uppercase 'regex:/[0-9]/', // 1 number 'regex:/[^a-zA-Z0-9]/', // 1 special character ] ], [ 'password.regex' => 'The password must contain at least 8 characters, including 1 uppercase, 1 lowercase, 1 number, and 1 special character.', ]); $cachedData = \Illuminate\Support\Facades\Cache::get('employer_reset_otp_' . $request->email); if (!$cachedData || !isset($cachedData['code']) || !isset($cachedData['expires_at'])) { return response()->json([ 'message' => 'Invalid or expired verification code.' ], 400); } if ($cachedData['code'] !== $request->otp || now()->timestamp > $cachedData['expires_at']) { return response()->json([ 'message' => 'Invalid or expired verification code.' ], 400); } $user = User::where('email', $request->email)->where('role', 'employer')->first(); $sponsor = \App\Models\Sponsor::where('email', $request->email)->first(); if (!$user || !$sponsor) { return response()->json([ 'message' => 'User account not found.' ], 404); } \Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor) { $hashedPassword = Hash::make($request->password); $user->update([ 'password' => $hashedPassword, ]); $sponsor->update([ 'password' => $hashedPassword, ]); }); \Illuminate\Support\Facades\Cache::forget('employer_reset_otp_' . $request->email); return response()->json([ 'success' => true, 'message' => 'Password reset successfully.' ]); } }