migrant-web/app/Http/Controllers/Employer/EmployerAuthController.php
2026-06-23 17:56:11 +05:30

676 lines
27 KiB
PHP

<?php
namespace App\Http\Controllers\Employer;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Models\User;
use App\Models\EmployerProfile;
use App\Mail\EmployerOtpMail;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
class EmployerAuthController extends Controller
{
public function showLogin()
{
return Inertia::render('Employer/Auth/Login');
}
public function login(Request $request)
{
$credentials = $request->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.'
]);
}
}