migrant-web/app/Http/Controllers/Api/WorkerAuthController.php
2026-05-29 10:28:11 +05:30

450 lines
18 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Worker;
use App\Models\WorkerCategory;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class WorkerAuthController extends Controller
{
/**
* Get system configuration and supported languages for worker onboarding.
* Aligns with Step 2 "Select Language (HI / SW / TL / TA)".
*/
public function config()
{
return response()->json([
'success' => true,
'languages' => [
['code' => 'HI', 'name' => 'Hindi (हिन्दी)'],
['code' => 'SW', 'name' => 'Swahili (Kiswahili)'],
['code' => 'TL', 'name' => 'Tagalog (Wikang Tagalog)'],
['code' => 'TA', 'name' => 'Tamil (தமிழ்)'],
]
], 200);
}
/**
* Step 1: Send OTP to worker's mobile number or email.
* Accepts: phone OR email (one is required).
*/
public function sendOtp(Request $request)
{
$validator = Validator::make($request->all(), [
'phone' => 'required_without:email|nullable|string|max:50',
'email' => 'required_without:phone|nullable|email|max:255',
], [
'phone.required_without' => 'Please provide a mobile number or email address.',
'email.required_without' => 'Please provide a mobile number or email address.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
// Determine identifier (phone takes priority)
$identifier = $request->phone ?? $request->email;
$isPhone = !empty($request->phone);
// Generate OTP (always 111111 in local/dev, random in production)
$otp = app()->environment('production') ? strval(rand(100000, 999999)) : '111111';
// Cache OTP for 10 minutes
Cache::put('worker_otp_' . $identifier, [
'code' => $otp,
'expires_at' => now()->addMinutes(10)->timestamp,
], 600);
// Send OTP via email if email provided (phone SMS would be handled by SMS gateway)
if (!$isPhone && $request->email) {
try {
Mail::raw("Your Migrant Worker verification code is: {$otp}\n\nThis code expires in 10 minutes.", function ($m) use ($request, $otp) {
$m->to($request->email)
->subject('Your Migrant Worker Verification Code');
});
} catch (\Exception $mailEx) {
logger()->error('Worker OTP Mail Failure: ' . $mailEx->getMessage());
}
}
return response()->json([
'success' => true,
'message' => $isPhone
? 'OTP sent to your mobile number.'
: 'OTP sent to your email address.',
'identifier' => $identifier,
'otp' => app()->environment('production') ? null : $otp, // Return OTP in response in non-prod for testing convenience
], 200);
}
/**
* Step 2: Verify OTP sent to mobile/email.
* Accepts: phone OR email, otp.
*/
public function verifyOtp(Request $request)
{
$validator = Validator::make($request->all(), [
'phone' => 'required_without:email|nullable|string|max:50',
'email' => 'required_without:phone|nullable|email|max:255',
'otp' => 'required|string|size:6',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
$identifier = $request->phone ?? $request->email;
$cachedData = Cache::get('worker_otp_' . $identifier);
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) {
Cache::forget('worker_otp_' . $identifier);
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);
}
// Mark OTP as verified in cache (used as gate for profile setup step)
Cache::put('worker_otp_verified_' . $identifier, true, 3600); // 1 hour grace
Cache::forget('worker_otp_' . $identifier);
return response()->json([
'success' => true,
'message' => 'Mobile number verified successfully. Please complete your profile.',
'identifier' => $identifier,
], 200);
}
/**
* Step 3: Complete basic profile setup after OTP verification.
* Accepts: phone OR email, name, nationality, skills (array of skill IDs), language.
*/
public function setupProfile(Request $request)
{
$validator = Validator::make($request->all(), [
'phone' => 'required_without:email|nullable|string|max:50',
'email' => 'required_without:phone|nullable|email|max:255',
'name' => 'required|string|max:255',
'nationality' => 'required|string|max:100',
'language' => 'nullable|string|in:HI,SW,TL,TA',
'skills' => 'nullable|array',
'skills.*' => 'integer|exists:skills,id',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
$identifier = $request->phone ?? $request->email;
// Ensure OTP was verified in this session
if (!Cache::get('worker_otp_verified_' . $identifier)) {
return response()->json([
'success' => false,
'message' => 'OTP verification is required before profile setup.'
], 403);
}
// Check if already registered
if ($request->phone && Worker::where('phone', $request->phone)->exists()) {
return response()->json([
'success' => false,
'message' => 'This mobile number is already registered.'
], 409);
}
if ($request->email && Worker::where('email', $request->email)->exists()) {
return response()->json([
'success' => false,
'message' => 'This email address is already registered.'
], 409);
}
try {
// Build email for workers who registered by phone
$email = $request->email;
if (!$email && $request->phone) {
$phoneClean = preg_replace('/[^0-9]/', '', $request->phone);
$email = "worker.{$phoneClean}@migrant.ae";
if (Worker::where('email', $email)->exists()) {
$email = "worker.{$phoneClean}." . rand(10, 99) . "@migrant.ae";
}
}
$apiToken = Str::random(80);
$worker = DB::transaction(function () use ($request, $email, $apiToken) {
$worker = Worker::create([
'name' => $request->name,
'email' => $email,
'phone' => $request->phone ?? null,
'language' => $request->language ?? 'HI', // Default to Hindi if not selected
'password' => Hash::make(Str::random(16)), // No password at this stage
'nationality' => $request->nationality,
'age' => 25, // Default — updated later in full profile
'salary' => 1500, // Default AED — updated later
'availability'=> 'Immediate',
'experience' => 'Not Specified',
'religion' => 'Not Specified',
'bio' => 'New worker on Migrant platform.',
'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? 1,
'verified' => false,
'status' => 'active',
'api_token' => $apiToken,
]);
// Sync skills if provided
if (!empty($request->skills)) {
$worker->skills()->sync($request->skills);
}
return $worker;
});
// Clear the OTP verification gate
Cache::forget('worker_otp_verified_' . $identifier);
$worker->load(['category', 'skills']);
return response()->json([
'success' => true,
'message' => 'Registration complete! Welcome to Migrant.',
'data' => [
'worker' => $worker,
'token' => $apiToken,
]
], 201);
} catch (\Exception $e) {
logger()->error('Worker Profile Setup Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'Registration failed. Please try again.',
'error' => app()->environment('local', 'testing') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* Unified registration — supports backward compatibility and multipart file registration.
*/
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'phone' => 'required|string|max:50|unique:workers,phone',
'salary' => 'required|numeric|min:0',
'password' => 'required|string|min:6',
'passport_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
'skills' => 'nullable',
'visa_file' => 'nullable|file|mimes:jpeg,png,jpg,pdf|max:10240',
'language' => 'nullable|string|in:HI,SW,TL,TA',
'nationality' => 'nullable|string|max:100',
'age' => 'nullable|integer',
'experience' => 'nullable|string|max:100',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
$phoneClean = preg_replace('/[^0-9]/', '', $request->phone);
$email = "worker.{$phoneClean}@migrant.ae";
if (Worker::where('email', $email)->exists()) {
$email = "worker.{$phoneClean}." . rand(10, 99) . "@migrant.ae";
}
$skillsArray = [];
if ($request->has('skills')) {
$skillsInput = $request->skills;
if (is_array($skillsInput)) {
$skillsArray = $skillsInput;
} elseif (is_string($skillsInput)) {
$decoded = json_decode($skillsInput, true);
$skillsArray = (json_last_error() === JSON_ERROR_NONE && is_array($decoded))
? $decoded
: array_filter(array_map('trim', explode(',', $skillsInput)));
}
}
$destinationPath = public_path('uploads/documents');
if (!file_exists($destinationPath)) {
mkdir($destinationPath, 0755, true);
}
$passportPath = null;
$visaPath = null;
if ($request->hasFile('passport_file')) {
$file = $request->file('passport_file');
$fileName = time() . '_passport_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
$file->move($destinationPath, $fileName);
$passportPath = 'uploads/documents/' . $fileName;
}
if ($request->hasFile('visa_file')) {
$file = $request->file('visa_file');
$fileName = time() . '_visa_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $file->getClientOriginalName());
$file->move($destinationPath, $fileName);
$visaPath = 'uploads/documents/' . $fileName;
}
$apiToken = Str::random(80);
$result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken) {
$worker = Worker::create([
'name' => $request->name,
'email' => $email,
'phone' => $request->phone,
'language' => $request->language ?? 'HI',
'password' => Hash::make($request->password),
'nationality' => $request->nationality ?? 'Not Specified',
'age' => $request->age ?? 25,
'salary' => $request->salary,
'availability'=> 'Immediate',
'experience' => $request->experience ?? 'Not Specified',
'religion' => 'Not Specified',
'bio' => 'Hardworking and reliable General Helper available for immediate hire.',
'category_id' => \App\Models\WorkerCategory::where('name', 'General Helper')->value('id') ?? 1,
'verified' => false,
'status' => 'active',
'api_token' => $apiToken,
]);
if (!empty($skillsArray)) {
$validSkillIds = DB::table('skills')->whereIn('id', $skillsArray)->pluck('id')->toArray();
if (!empty($validSkillIds)) {
$worker->skills()->sync($validSkillIds);
}
}
$worker->documents()->create([
'type' => 'passport',
'number' => 'P' . rand(100000, 999999),
'issue_date' => now()->subYears(2)->toDateString(),
'expiry_date' => now()->addYears(8)->toDateString(),
'ocr_accuracy'=> 98.5,
'file_path' => $passportPath,
]);
$worker->documents()->create([
'type' => 'visa',
'number' => 'V' . rand(100000, 999999),
'issue_date' => now()->subYear()->toDateString(),
'expiry_date' => now()->addYears(2)->toDateString(),
'ocr_accuracy'=> $visaPath ? 98.5 : 0.0,
'file_path' => $visaPath,
]);
return $worker;
});
$result->load(['category', 'skills', 'documents']);
return response()->json([
'success' => true,
'message' => 'Worker registered and authenticated successfully.',
'data' => ['worker' => $result, 'token' => $apiToken]
], 201);
} catch (\Exception $e) {
logger()->error('Worker Registration Failure: ' . $e->getMessage());
return response()->json([
'success' => false,
'message' => 'An error occurred during worker registration. Please try again.',
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
], 500);
}
}
/**
* Authenticate a worker and return a secure Bearer token.
*/
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'phone' => 'required_without:email|nullable|string',
'email' => 'required_without:phone|nullable|email',
'password' => 'required|string',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation error.',
'errors' => $validator->errors()
], 422);
}
try {
$worker = $request->phone
? Worker::where('phone', $request->phone)->first()
: Worker::where('email', $request->email)->first();
if (!$worker || !Hash::check($request->password, $worker->password)) {
return response()->json([
'success' => false,
'message' => 'Invalid credentials.'
], 401);
}
$apiToken = Str::random(80);
$worker->update(['api_token' => $apiToken]);
return response()->json([
'success' => true,
'message' => 'Worker logged in successfully.',
'data' => [
'worker' => $worker->load(['category', 'skills', 'documents']),
'token' => $apiToken
]
], 200);
} catch (\Exception $e) {
logger()->error('Worker 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);
}
}
}