migrant-web/app/Http/Controllers/Api/EmployerAuthController.php

737 lines
27 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Models\EmployerProfile;
use App\Models\Sponsor;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class EmployerAuthController extends Controller
{
/**
* Authenticate an employer and return a secure Bearer token.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'nullable|string',
'mobile' => 'nullable|string',
'password' => 'required|string',
]);
$validator->after(function ($validator) use ($request) {
if (!$request->filled('email') && !$request->filled('mobile')) {
$validator->errors()->add('mobile', 'Either mobile number or email is required.');
}
});
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
$loginValue = $request->input('mobile') ?? $request->input('email');
$sponsor = Sponsor::where('mobile', $loginValue)
->orWhere('email', $loginValue)
->first();
if ($sponsor) {
$user = User::where('email', $sponsor->email)
->where('role', 'employer')
->first();
} else {
$user = User::where('email', $loginValue)
->where('role', 'employer')
->first();
}
if (!$sponsor && !$user) {
return response()->json([
'success' => false,
'message' => 'Invalid credentials.'
], 401);
}
$passwordHash = $sponsor ? $sponsor->password : $user->password;
if (!Hash::check($request->password, $passwordHash)) {
return response()->json([
'success' => false,
'message' => 'Invalid credentials.'
], 401);
}
if ($sponsor && $sponsor->status === 'suspended') {
return response()->json([
'success' => false,
'message' => 'Your account has been suspended. Please contact support.'
], 403);
}
$apiToken = Str::random(80);
if ($user) {
// Employer account
$profile = EmployerProfile::where('user_id', $user->id)->first();
$verification_status = $profile ? $profile->verification_status : 'approved';
if ($verification_status === 'pending') {
return response()->json([
'success' => false,
'message' => 'Your account is pending verification.'
], 403);
}
if ($verification_status === 'rejected') {
return response()->json([
'success' => false,
'message' => 'Your account verification has been rejected.',
'reason' => $profile->rejection_reason ?? 'Verification rejected.'
], 403);
}
$user->update(['api_token' => $apiToken]);
if ($sponsor) {
$sponsor->update([
'api_token' => $apiToken,
'last_login_at' => now(),
]);
}
return response()->json([
'success' => true,
'message' => 'Login successful.',
'role' => 'employer',
'data' => [
'employer' => $user->load('employerProfile'),
'token' => $apiToken
]
], 200);
} else {
// Pure Sponsor account
$sponsor->update([
'api_token' => $apiToken,
'last_login_at' => now(),
]);
return response()->json([
'success' => true,
'message' => 'Login successful.',
'role' => 'sponsor',
'data' => [
'sponsor' => $sponsor->makeHidden(['password', 'api_token']),
'token' => $apiToken
]
], 200);
}
} catch (\Exception $e) {
logger()->error('Mobile Login Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred during login. Please try again.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* Register a new employer via API.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users,email',
'phone' => 'required|string|max:50',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
$otp = '111111'; // default development OTP
if (app()->environment('production')) {
$otp = strval(rand(100000, 999999));
}
\Illuminate\Support\Facades\Cache::put('employer_otp_' . $request->email, [
'code' => $otp,
'expires_at' => now()->addMinutes(10)->timestamp
], 600);
// Create inactive User
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make(Str::random(16)), // temporary password
'role' => 'employer',
'subscription_status' => 'none',
'subscription_expires_at' => null,
]);
// Create pending Profile
EmployerProfile::create([
'user_id' => $user->id,
'company_name' => $request->name . ' Household',
'phone' => $request->phone,
'country' => 'United Arab Emirates',
'verification_status' => 'pending',
'language' => 'English',
'notifications' => true,
]);
// Create unverified Sponsor
\App\Models\Sponsor::create([
'full_name' => $request->name,
'email' => $request->email,
'mobile' => $request->phone,
'password' => $user->password,
'country_code' => 'AE',
'subscription_status' => 'none',
'subscription_plan' => null,
'subscription_start_date' => null,
'subscription_end_date' => null,
'payment_status' => 'unpaid',
'is_verified' => false,
'otp_verified_at' => null,
'status' => 'inactive',
]);
// Try sending email
try {
\Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerOtpMail(
$otp,
$request->name . ' Household',
$request->name
));
} catch (\Exception $mailEx) {
logger()->error('API Sponsor Registration Mail Failure: ' . $mailEx->getMessage());
}
return response()->json([
'success' => true,
'message' => 'Verification code sent to email successfully.',
'email' => $request->email
], 201);
} catch (\Exception $e) {
logger()->error('API Sponsor Registration Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Registration failed. Please try again.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
public function verify(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'otp' => 'required|string|size:6',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
$cachedData = \Illuminate\Support\Facades\Cache::get('employer_otp_' . $request->email);
if (!$cachedData || !isset($cachedData['code']) || !isset($cachedData['expires_at'])) {
return response()->json([
'success' => false,
'message' => 'Invalid or expired verification code.'
], 400);
}
// Handle any dirty/legacy cache objects gracefully
if (is_object($cachedData['expires_at']) || $cachedData['expires_at'] instanceof \__PHP_Incomplete_Class) {
\Illuminate\Support\Facades\Cache::forget('employer_otp_' . $request->email);
return response()->json([
'success' => false,
'message' => 'Session expired. Please request a new verification code.'
], 400);
}
if ($cachedData['code'] !== $request->otp || now()->timestamp > $cachedData['expires_at']) {
return response()->json([
'success' => false,
'message' => 'Invalid or expired verification code.'
], 400);
}
try {
// Update Sponsor verification status
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
if ($sponsor) {
$sponsor->update([
'is_verified' => true,
'otp_verified_at' => now(),
]);
}
// Clear Cache
\Illuminate\Support\Facades\Cache::forget('employer_otp_' . $request->email);
return response()->json([
'success' => true,
'message' => 'Email verified successfully. Proceed to payment selection.',
'email' => $request->email
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Verification failed. Please try again.'
], 500);
}
}
/**
* Upload Emirates ID during registration steps (Unauthenticated).
*
* POST /api/employers/upload-emirates-id
*/
public function uploadEmiratesId(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|string|email|max:255',
'emirates_id_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
], [
'email.required' => 'The email address is required.',
'emirates_id_file.required' => 'Please upload a clear scan or image of your Emirates ID.',
'emirates_id_file.mimes' => 'The Emirates ID document must be an image (jpg, jpeg, png) or a PDF file.',
'emirates_id_file.max' => 'The document must not exceed 10MB.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
$user = User::where('email', $request->email)->first();
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'User account not found.'
], 404);
}
$file = $request->file('emirates_id_file');
$destinationPath = public_path('uploads/documents');
if (!file_exists($destinationPath)) {
mkdir($destinationPath, 0755, true);
}
$fileName = time() . '_employer_eid_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
$file->move($destinationPath, $fileName);
$filePath = 'uploads/documents/' . $fileName;
// OCR Simulated extraction
$extractedIdNumber = '784-' . rand(1970, 2005) . '-' . rand(1000000, 9999999) . '-' . rand(1, 9);
$extractedExpiry = now()->addYears(rand(2, 4))->toDateString();
// Update or create EmployerProfile
$profile = EmployerProfile::where('user_id', $user->id)->first();
if (!$profile) {
$profile = EmployerProfile::create([
'user_id' => $user->id,
'company_name' => $user->name . ' Household',
'phone' => $sponsor ? $sponsor->mobile : '+971 50 123 4567',
'country' => 'United Arab Emirates',
'verification_status' => 'pending',
]);
}
$profile->update([
'emirates_id_front' => $filePath,
'emirates_id_number' => $extractedIdNumber,
'emirates_id_expiry' => $extractedExpiry,
]);
// Sync with corresponding sponsor record if found
if ($sponsor) {
$sponsor->update([
'emirates_id_file' => $filePath,
]);
}
return response()->json([
'success' => true,
'message' => 'Emirates ID uploaded successfully.',
'ocr_extracted_data' => [
'emirates_id_number' => $extractedIdNumber,
'expiry_date' => $extractedExpiry,
],
'email' => $request->email,
], 200);
} catch (\Exception $e) {
logger()->error('API Sponsor Registration Emirates ID Upload Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred while uploading the Emirates ID.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
public function payment(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'plan_id' => 'required|string',
'amount_aed' => 'required|numeric',
'paytabs_transaction_id' => 'required|string',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
try {
$user = User::where('email', $request->email)->first();
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
if (!$user || !$sponsor) {
return response()->json([
'success' => false,
'message' => 'User account not found.'
], 404);
}
if (!$sponsor->is_verified) {
return response()->json([
'success' => false,
'message' => 'Please verify your email before initiating payment.'
], 403);
}
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor) {
// Update User subscription
$user->update([
'subscription_status' => 'active',
'subscription_expires_at' => now()->addDays(30),
]);
// Update Sponsor status
$sponsor->update([
'subscription_status' => 'active',
'subscription_plan' => $request->plan_id,
'subscription_start_date' => now(),
'subscription_end_date' => now()->addDays(30),
'payment_status' => 'paid',
]);
// Insert into subscriptions table
\Illuminate\Support\Facades\DB::table('subscriptions')->insert([
'user_id' => $user->id,
'plan_id' => $request->plan_id,
'amount_aed' => $request->amount_aed,
'starts_at' => now(),
'expires_at' => now()->addDays(30),
'paytabs_transaction_id' => $request->paytabs_transaction_id,
'status' => 'active',
'created_at' => now(),
'updated_at' => now(),
]);
});
return response()->json([
'success' => true,
'message' => 'Subscription payment processed and activated successfully.',
'email' => $request->email
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Payment processing failed. Please try again.'
], 500);
}
}
public function password(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
try {
$user = User::where('email', $request->email)->first();
$sponsor = \App\Models\Sponsor::where('email', $request->email)->first();
if (!$user || !$sponsor) {
return response()->json([
'success' => false,
'message' => 'User account not found.'
], 404);
}
if ($sponsor->payment_status !== 'paid') {
return response()->json([
'success' => false,
'message' => 'Please complete subscription payment first.'
], 403);
}
// Generate API Bearer token
$apiToken = Str::random(80);
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor, $apiToken) {
$hashedPassword = Hash::make($request->password);
$user->update([
'password' => $hashedPassword,
'api_token' => $apiToken,
]);
$sponsor->update([
'password' => $hashedPassword,
'status' => 'active',
]);
// Approve profile verification
$profile = EmployerProfile::where('user_id', $user->id)->first();
if ($profile) {
$profile->update([
'verification_status' => 'approved'
]);
}
});
return response()->json([
'success' => true,
'message' => 'Password created successfully. Registration finalized.',
'data' => [
'sponsor' => $user->load('employerProfile'),
'token' => $apiToken
]
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to set password. Please try again.'
], 500);
}
}
public function plans()
{
return response()->json([
'success' => true,
'data' => [
'plans' => [
[
'id' => 'basic',
'name' => 'Basic Search',
'price' => 99.00,
'currency' => 'AED',
'period' => 'month',
'features' => ['Browse 500+ verified workers', 'Shortlist up to 10 candidates', 'Standard OCR verification'],
'popular' => false,
],
[
'id' => 'premium',
'name' => 'Premium Employer Pass',
'price' => 199.00,
'currency' => 'AED',
'period' => 'month',
'features' => ['Unlimited shortlisting', 'Direct candidate messaging', 'Priority interview scheduling', 'Dedicated support'],
'popular' => true,
],
[
'id' => 'enterprise',
'name' => 'VIP Concierge',
'price' => 499.00,
'currency' => 'AED',
'period' => 'month',
'features' => ['All Premium features', 'Assigned recruitment manager', 'Background medical verification guarantee', 'Free replacement within 30 days'],
'popular' => false,
]
]
]
]);
}
public function forgotPassword(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
$user = User::where('email', $request->email)->where('role', 'employer')->first();
if (!$user) {
return response()->json([
'success' => false,
'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 {
\Illuminate\Support\Facades\Mail::to($request->email)->send(new \App\Mail\EmployerResetOtpMail(
$otp,
$user->name
));
} catch (\Exception $mailEx) {
logger()->error('API Employer Forgot Password Mail Failure: ' . $mailEx->getMessage());
}
return response()->json([
'success' => true,
'message' => 'Password reset verification code sent to email successfully.',
'email' => $request->email
], 200);
} catch (\Exception $e) {
logger()->error('API Employer Forgot Password Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred. Please try again.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
public function resetPassword(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'otp' => 'required|string|size:6',
'password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
$cachedData = \Illuminate\Support\Facades\Cache::get('employer_reset_otp_' . $request->email);
if (!$cachedData || !isset($cachedData['code']) || !isset($cachedData['expires_at'])) {
return response()->json([
'success' => false,
'message' => 'Invalid or expired verification code.'
], 400);
}
if ($cachedData['code'] !== $request->otp || now()->timestamp > $cachedData['expires_at']) {
return response()->json([
'success' => false,
'message' => 'Invalid or expired verification code.'
], 400);
}
try {
$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([
'success' => false,
'message' => 'User account not found.'
], 404);
}
$apiToken = Str::random(80);
\Illuminate\Support\Facades\DB::transaction(function () use ($request, $user, $sponsor, $apiToken) {
$hashedPassword = Hash::make($request->password);
$user->update([
'password' => $hashedPassword,
'api_token' => $apiToken,
]);
$sponsor->update([
'password' => $hashedPassword,
]);
});
\Illuminate\Support\Facades\Cache::forget('employer_reset_otp_' . $request->email);
return response()->json([
'success' => true,
'message' => 'Password reset successfully.',
'data' => [
'employer' => $user->load('employerProfile'),
'token' => $apiToken
]
], 200);
} catch (\Exception $e) {
logger()->error('API Employer Reset Password Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Failed to reset password. Please try again.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
}