mohan #5
@ -42,7 +42,6 @@ public function index()
|
||||
'country' => $worker->country,
|
||||
'city' => $worker->city,
|
||||
'area' => $worker->area,
|
||||
'preferred_location' => $worker->preferred_location,
|
||||
'live_in_out' => $worker->live_in_out ?? 'Live-in',
|
||||
'category' => $worker->category ? $worker->category->name : 'N/A',
|
||||
'experience' => $worker->experience,
|
||||
@ -126,7 +125,6 @@ public function updateProfile(Request $request, $id)
|
||||
'country' => 'nullable|string',
|
||||
'city' => 'nullable|string',
|
||||
'area' => 'nullable|string',
|
||||
'preferred_location' => 'nullable|string',
|
||||
'live_in_out' => 'nullable|string',
|
||||
'category' => 'required|string',
|
||||
'experience' => 'required|string',
|
||||
@ -148,7 +146,6 @@ public function updateProfile(Request $request, $id)
|
||||
$worker->country = $validated['country'] ?? $worker->country;
|
||||
$worker->city = $validated['city'] ?? $worker->city;
|
||||
$worker->area = $validated['area'] ?? $worker->area;
|
||||
$worker->preferred_location = $validated['preferred_location'] ?? $worker->preferred_location;
|
||||
$worker->live_in_out = $validated['live_in_out'] ?? $worker->live_in_out;
|
||||
$worker->experience = $validated['experience'];
|
||||
$worker->salary = $validated['salary'] ?? $worker->salary;
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
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;
|
||||
@ -21,10 +22,17 @@ class EmployerAuthController extends Controller
|
||||
public function login(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'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,
|
||||
@ -34,15 +42,49 @@ public function login(Request $request)
|
||||
}
|
||||
|
||||
try {
|
||||
$user = User::where('email', $request->email)->where('role', 'employer')->first();
|
||||
$loginValue = $request->input('mobile') ?? $request->input('email');
|
||||
|
||||
if (!$user || !Hash::check($request->password, $user->password)) {
|
||||
$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 email or password.'
|
||||
'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';
|
||||
|
||||
@ -61,21 +103,43 @@ public function login(Request $request)
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Generate and assign a fresh API token
|
||||
$apiToken = Str::random(80);
|
||||
$user->update(['api_token' => $apiToken]);
|
||||
if ($sponsor) {
|
||||
$sponsor->update([
|
||||
'api_token' => $apiToken,
|
||||
'last_login_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Employer logged in successfully.',
|
||||
'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 Employer Login Failure: ' . $e->getMessage());
|
||||
logger()->error('Mobile Login Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
|
||||
162
app/Http/Controllers/Api/SponsorAuthController.php
Normal file
162
app/Http/Controllers/Api/SponsorAuthController.php
Normal file
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sponsor;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SponsorAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* Register a new Sponsor account.
|
||||
*
|
||||
* Required fields: full_name, mobile, password, license_file
|
||||
* Optional: organization_name, email, nationality, city, address, country_code
|
||||
*
|
||||
* POST /api/sponsors/register
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'full_name' => 'required|string|max:255',
|
||||
'mobile' => 'required|string|max:50|unique:sponsors,mobile',
|
||||
'password' => 'required|string|min:6',
|
||||
'license_file' => 'required|file|mimes:jpeg,png,jpg,pdf|max:10240',
|
||||
'organization_name' => 'nullable|string|max:255',
|
||||
'email' => 'nullable|email|max:255|unique:sponsors,email',
|
||||
'nationality' => 'nullable|string|max:100',
|
||||
'city' => 'nullable|string|max:100',
|
||||
'address' => 'nullable|string|max:255',
|
||||
'country_code' => 'nullable|string|max:10',
|
||||
], [
|
||||
'mobile.unique' => 'This mobile number is already registered.',
|
||||
'email.unique' => 'This email address is already registered.',
|
||||
'license_file.required' => 'Please upload your organization or trade license.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
// Auto-generate email if not provided
|
||||
$mobileClean = preg_replace('/[^0-9]/', '', $request->mobile);
|
||||
$email = $request->email ?? "sponsor.{$mobileClean}@migrant.ae";
|
||||
if (!$request->email && Sponsor::where('email', $email)->exists()) {
|
||||
$email = "sponsor.{$mobileClean}." . rand(10, 99) . "@migrant.ae";
|
||||
}
|
||||
|
||||
// Store license file
|
||||
$destinationPath = public_path('uploads/licenses');
|
||||
if (!file_exists($destinationPath)) {
|
||||
mkdir($destinationPath, 0755, true);
|
||||
}
|
||||
|
||||
$licenseFile = $request->file('license_file');
|
||||
$licenseFileName = time() . '_license_' . preg_replace('/[^a-zA-Z0-9_.-]/', '', $licenseFile->getClientOriginalName());
|
||||
$licenseFile->move($destinationPath, $licenseFileName);
|
||||
$licensePath = 'uploads/licenses/' . $licenseFileName;
|
||||
|
||||
$apiToken = Str::random(80);
|
||||
|
||||
$sponsor = DB::transaction(function () use ($request, $email, $licensePath, $apiToken) {
|
||||
return Sponsor::create([
|
||||
'full_name' => $request->full_name,
|
||||
'organization_name' => $request->organization_name,
|
||||
'email' => $email,
|
||||
'mobile' => $request->mobile,
|
||||
'password' => Hash::make($request->password),
|
||||
'country_code' => $request->country_code,
|
||||
'nationality' => $request->nationality,
|
||||
'city' => $request->city,
|
||||
'address' => $request->address,
|
||||
'license_file' => $licensePath,
|
||||
'status' => 'active',
|
||||
'is_verified' => false,
|
||||
'subscription_status' => 'none',
|
||||
'api_token' => $apiToken,
|
||||
]);
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Sponsor account registered successfully. Your license is pending admin review.',
|
||||
'data' => [
|
||||
'sponsor' => $sponsor->makeHidden(['password', 'api_token']),
|
||||
'token' => $apiToken,
|
||||
]
|
||||
], 201);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Sponsor Registration Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred during registration. Please try again.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a Sponsor and return their bearer token.
|
||||
*
|
||||
* POST /api/sponsors/login
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'mobile' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors()
|
||||
], 422);
|
||||
}
|
||||
|
||||
$sponsor = Sponsor::where('mobile', $request->mobile)->first();
|
||||
|
||||
if (!$sponsor || !Hash::check($request->password, $sponsor->password)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid mobile number or password.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
if ($sponsor->status === 'suspended') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Your account has been suspended. Please contact support.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Rotate token on each login for security
|
||||
$apiToken = Str::random(80);
|
||||
$sponsor->update([
|
||||
'api_token' => $apiToken,
|
||||
'last_login_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Login successful.',
|
||||
'data' => [
|
||||
'sponsor' => $sponsor->makeHidden(['password', 'api_token']),
|
||||
'token' => $apiToken,
|
||||
]
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
160
app/Http/Controllers/Api/SponsorController.php
Normal file
160
app/Http/Controllers/Api/SponsorController.php
Normal file
@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Announcement;
|
||||
use App\Models\Sponsor;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SponsorController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /api/sponsors/dashboard
|
||||
*
|
||||
* Returns the sponsor's profile summary and unread charity event count.
|
||||
*/
|
||||
public function getDashboard(Request $request)
|
||||
{
|
||||
/** @var Sponsor $sponsor */
|
||||
$sponsor = $request->attributes->get('sponsor');
|
||||
|
||||
try {
|
||||
// Recent charity/update announcements for the dashboard preview
|
||||
$recentEvents = Announcement::latest()
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(function ($event) {
|
||||
return [
|
||||
'id' => $event->id,
|
||||
'title' => $event->title,
|
||||
'body' => $event->body,
|
||||
'type' => $event->type,
|
||||
'created_at' => $event->created_at->toIso8601String(),
|
||||
'time_ago' => $event->created_at->diffForHumans(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'sponsor' => [
|
||||
'id' => $sponsor->id,
|
||||
'full_name' => $sponsor->full_name,
|
||||
'organization_name' => $sponsor->organization_name,
|
||||
'mobile' => $sponsor->mobile,
|
||||
'email' => $sponsor->email,
|
||||
'city' => $sponsor->city,
|
||||
'nationality' => $sponsor->nationality,
|
||||
'is_verified' => $sponsor->is_verified,
|
||||
'status' => $sponsor->status,
|
||||
'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null,
|
||||
'joined_at' => $sponsor->created_at->toIso8601String(),
|
||||
],
|
||||
'recent_charity_events' => $recentEvents,
|
||||
'total_events' => Announcement::count(),
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Sponsor Dashboard API Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while loading your dashboard.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sponsors/charity-events
|
||||
*
|
||||
* Returns a paginated list of all charity/announcement events.
|
||||
*/
|
||||
public function getCharityEvents(Request $request)
|
||||
{
|
||||
try {
|
||||
$page = (int) $request->input('page', 1);
|
||||
$perPage = (int) $request->input('per_page', 15);
|
||||
$type = $request->input('type'); // optional filter: 'charity', 'update', etc.
|
||||
|
||||
$query = Announcement::with('employer.employerProfile')->latest();
|
||||
|
||||
if ($type) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$events = $query->skip($offset)->take($perPage)->get()
|
||||
->map(function ($event) {
|
||||
return [
|
||||
'id' => $event->id,
|
||||
'title' => $event->title,
|
||||
'body' => $event->body,
|
||||
'type' => $event->type,
|
||||
'posted_by' => $event->employer->name ?? 'System',
|
||||
'organization' => optional($event->employer)->employerProfile->company_name ?? 'Migrant Support',
|
||||
'created_at' => $event->created_at->toIso8601String(),
|
||||
'time_ago' => $event->created_at->diffForHumans(),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'events' => $events,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'per_page' => $perPage,
|
||||
'current_page' => $page,
|
||||
'last_page' => max(1, (int) ceil($total / $perPage)),
|
||||
],
|
||||
]
|
||||
], 200);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Sponsor Charity Events API Failure: ' . $e->getMessage());
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'An error occurred while fetching charity events.',
|
||||
'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sponsors/profile
|
||||
*
|
||||
* Returns the authenticated sponsor's full profile.
|
||||
*/
|
||||
public function getProfile(Request $request)
|
||||
{
|
||||
/** @var Sponsor $sponsor */
|
||||
$sponsor = $request->attributes->get('sponsor');
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'sponsor' => [
|
||||
'id' => $sponsor->id,
|
||||
'full_name' => $sponsor->full_name,
|
||||
'organization_name' => $sponsor->organization_name,
|
||||
'mobile' => $sponsor->mobile,
|
||||
'email' => $sponsor->email,
|
||||
'nationality' => $sponsor->nationality,
|
||||
'city' => $sponsor->city,
|
||||
'address' => $sponsor->address,
|
||||
'country_code' => $sponsor->country_code,
|
||||
'is_verified' => $sponsor->is_verified,
|
||||
'status' => $sponsor->status,
|
||||
'license_file' => $sponsor->license_file ? asset($sponsor->license_file) : null,
|
||||
'joined_at' => $sponsor->created_at->toIso8601String(),
|
||||
]
|
||||
]
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
@ -292,6 +292,13 @@ public function register(Request $request)
|
||||
'nationality' => 'nullable|string|max:100',
|
||||
'age' => 'nullable|integer',
|
||||
'experience' => 'nullable|string|max:100',
|
||||
'in_country' => 'nullable',
|
||||
'visa_status' => 'nullable|string|max:100',
|
||||
'preferred_job_type' => 'nullable|string|max:100',
|
||||
'live_in_out' => 'nullable|string|max:100',
|
||||
'country' => 'nullable|string|max:100',
|
||||
'city' => 'nullable|string|max:100',
|
||||
'area' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@ -344,9 +351,10 @@ public function register(Request $request)
|
||||
$visaPath = 'uploads/documents/' . $fileName;
|
||||
}
|
||||
|
||||
$inCountry = filter_var($request->input('in_country', true), FILTER_VALIDATE_BOOLEAN);
|
||||
$apiToken = Str::random(80);
|
||||
|
||||
$result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken) {
|
||||
$result = DB::transaction(function () use ($request, $email, $skillsArray, $passportPath, $visaPath, $apiToken, $inCountry) {
|
||||
$worker = Worker::create([
|
||||
'name' => $request->name,
|
||||
'email' => $email,
|
||||
@ -364,6 +372,13 @@ public function register(Request $request)
|
||||
'verified' => false,
|
||||
'status' => 'active',
|
||||
'api_token' => $apiToken,
|
||||
'in_country' => $inCountry,
|
||||
'visa_status' => $inCountry ? $request->visa_status : null,
|
||||
'preferred_job_type' => $request->preferred_job_type,
|
||||
'live_in_out' => $request->live_in_out,
|
||||
'country' => $request->country,
|
||||
'city' => $request->city,
|
||||
'area' => $request->area,
|
||||
]);
|
||||
|
||||
if (!empty($skillsArray)) {
|
||||
|
||||
@ -61,6 +61,13 @@ public function updateProfile(Request $request)
|
||||
'category_id' => 'nullable|exists:worker_categories,id',
|
||||
'skills' => 'nullable|array',
|
||||
'skills.*' => 'exists:skills,id',
|
||||
'in_country' => 'nullable',
|
||||
'visa_status' => 'nullable|string|max:100',
|
||||
'preferred_job_type' => 'nullable|string|max:100',
|
||||
'live_in_out' => 'nullable|string|max:100',
|
||||
'country' => 'nullable|string|max:100',
|
||||
'city' => 'nullable|string|max:100',
|
||||
'area' => 'nullable|string|max:100',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@ -84,7 +91,13 @@ public function updateProfile(Request $request)
|
||||
'experience',
|
||||
'religion',
|
||||
'bio',
|
||||
'category_id'
|
||||
'category_id',
|
||||
'visa_status',
|
||||
'preferred_job_type',
|
||||
'live_in_out',
|
||||
'country',
|
||||
'city',
|
||||
'area'
|
||||
]);
|
||||
|
||||
// Filter out null inputs if we want to support partial updates
|
||||
@ -92,6 +105,10 @@ public function updateProfile(Request $request)
|
||||
return !is_null($value);
|
||||
});
|
||||
|
||||
if ($request->has('in_country')) {
|
||||
$updateData['in_country'] = filter_var($request->input('in_country'), FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
$worker->update($updateData);
|
||||
|
||||
// Sync skills if provided
|
||||
|
||||
47
app/Http/Middleware/SponsorApiMiddleware.php
Normal file
47
app/Http/Middleware/SponsorApiMiddleware.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Sponsor;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SponsorApiMiddleware
|
||||
{
|
||||
/**
|
||||
* Authenticate incoming sponsor API requests via Bearer token.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$authHeader = $request->header('Authorization');
|
||||
|
||||
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Authentication token required.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
$token = substr($authHeader, 7);
|
||||
$sponsor = Sponsor::where('api_token', $token)->first();
|
||||
|
||||
if (!$sponsor) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid or expired authentication token.'
|
||||
], 401);
|
||||
}
|
||||
|
||||
if ($sponsor->status === 'suspended') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Your account has been suspended. Please contact support.'
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Attach sponsor to request for use in controllers
|
||||
$request->attributes->set('sponsor', $sponsor);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@ -12,6 +12,7 @@ class Sponsor extends Authenticatable
|
||||
|
||||
protected $fillable = [
|
||||
'full_name',
|
||||
'organization_name',
|
||||
'email',
|
||||
'mobile',
|
||||
'password',
|
||||
@ -29,10 +30,13 @@ class Sponsor extends Authenticatable
|
||||
'nationality',
|
||||
'status',
|
||||
'last_login_at',
|
||||
'api_token',
|
||||
'license_file',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'api_token',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@ -20,7 +20,6 @@ class Worker extends Model
|
||||
'country',
|
||||
'city',
|
||||
'area',
|
||||
'preferred_location',
|
||||
'live_in_out',
|
||||
'age',
|
||||
'gender',
|
||||
@ -33,6 +32,9 @@ class Worker extends Model
|
||||
'verified',
|
||||
'status',
|
||||
'api_token',
|
||||
'in_country',
|
||||
'visa_status',
|
||||
'preferred_job_type',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@ -42,6 +44,7 @@ class Worker extends Model
|
||||
|
||||
protected $casts = [
|
||||
'verified' => 'boolean',
|
||||
'in_country' => 'boolean',
|
||||
'salary' => 'decimal:2',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
@ -63,6 +66,9 @@ public function getPassportStatusAttribute()
|
||||
|
||||
public function getVisaStatusAttribute()
|
||||
{
|
||||
if ($this->attributes['visa_status'] ?? null) {
|
||||
return $this->attributes['visa_status'];
|
||||
}
|
||||
$visa = $this->documents->where('type', 'visa')->first();
|
||||
if (!$visa) {
|
||||
return 'Visa Pending';
|
||||
|
||||
@ -20,7 +20,8 @@
|
||||
'admin' => \App\Http\Middleware\AdminMiddleware::class,
|
||||
'employer' => \App\Http\Middleware\EmployerMiddleware::class,
|
||||
'auth.worker' => \App\Http\Middleware\WorkerApiMiddleware::class,
|
||||
'auth.employer' => \App\Http\Middleware\EmployerApiMiddleware::class,
|
||||
'auth.employer'=> \App\Http\Middleware\EmployerApiMiddleware::class,
|
||||
'auth.sponsor' => \App\Http\Middleware\SponsorApiMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('workers', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('workers', 'in_country')) {
|
||||
$table->boolean('in_country')->default(true)->after('nationality');
|
||||
}
|
||||
if (!Schema::hasColumn('workers', 'visa_status')) {
|
||||
$table->string('visa_status')->nullable()->after('in_country');
|
||||
}
|
||||
if (!Schema::hasColumn('workers', 'preferred_job_type')) {
|
||||
$table->string('preferred_job_type')->nullable()->after('live_in_out');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('workers', function (Blueprint $table) {
|
||||
$table->dropColumn(['in_country', 'visa_status', 'preferred_job_type']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('sponsors', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('sponsors', 'api_token')) {
|
||||
$table->string('api_token', 100)->nullable()->unique()->after('status');
|
||||
}
|
||||
if (!Schema::hasColumn('sponsors', 'license_file')) {
|
||||
$table->string('license_file')->nullable()->after('api_token');
|
||||
}
|
||||
if (!Schema::hasColumn('sponsors', 'organization_name')) {
|
||||
$table->string('organization_name')->nullable()->after('full_name');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('sponsors', function (Blueprint $table) {
|
||||
$table->dropColumn(['api_token', 'license_file', 'organization_name']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -17,6 +17,216 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/sponsors/register": {
|
||||
"post": {
|
||||
"tags": ["Sponsor/Auth"],
|
||||
"summary": "Register Sponsor Account",
|
||||
"description": "Registers a new sponsor with basic information and uploads their organization/trade license. Sponsors have restricted access — dashboard and charity events only. No payment required.",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["full_name", "mobile", "password", "license_file"],
|
||||
"properties": {
|
||||
"full_name": {
|
||||
"type": "string",
|
||||
"example": "Mohammed Al-Rashidi",
|
||||
"description": "Full legal name of the sponsor. (REQUIRED)"
|
||||
},
|
||||
"mobile": {
|
||||
"type": "string",
|
||||
"example": "+971501112233",
|
||||
"description": "Mobile phone number — must be unique. (REQUIRED)"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"example": "securepass123",
|
||||
"description": "Login password (min 6 characters). (REQUIRED)"
|
||||
},
|
||||
"license_file": {
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
"description": "Organization or trade license document (jpg/png/pdf, max 10MB). (REQUIRED)"
|
||||
},
|
||||
"organization_name": {
|
||||
"type": "string",
|
||||
"example": "Al-Rashidi Charitable Foundation",
|
||||
"description": "Name of the sponsoring organization."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email",
|
||||
"example": "info@alrashidi.ae",
|
||||
"description": "Email address (optional — auto-generated if not provided)."
|
||||
},
|
||||
"nationality": {
|
||||
"type": "string",
|
||||
"example": "UAE",
|
||||
"description": "Nationality of the sponsor."
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"example": "Dubai",
|
||||
"description": "City of residence."
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"example": "Villa 12, Jumeirah 2",
|
||||
"description": "Physical address."
|
||||
},
|
||||
"country_code": {
|
||||
"type": "string",
|
||||
"example": "+971",
|
||||
"description": "Country dial code."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Sponsor registered successfully.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {"type": "boolean", "example": true},
|
||||
"message": {"type": "string", "example": "Sponsor account registered successfully. Your license is pending admin review."},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sponsor": {"$ref": "#/components/schemas/Sponsor"},
|
||||
"token": {"type": "string", "example": "abc123...bearerToken..."}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {"description": "Validation error (duplicate mobile/email, missing required fields)."}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/login": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Sponsor/Auth"
|
||||
],
|
||||
"summary": "Authenticate Sponsor or Employer",
|
||||
"description": "Unified authentication endpoint. Validates email/mobile and password, then returns a stateless Bearer token and the user's role (employer or sponsor). Same behavior as /employers/login.",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com",
|
||||
"description": "Email address (required if mobile is not provided)"
|
||||
},
|
||||
"mobile": {
|
||||
"type": "string",
|
||||
"example": "+971509990001",
|
||||
"description": "Mobile number (required if email is not provided)"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"example": "Password@123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Login successful. Returns Bearer token and role.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Login successful."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"example": "sponsor",
|
||||
"enum": ["employer", "sponsor"]
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"example": "api_token_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/dashboard": {
|
||||
"get": {
|
||||
"tags": ["Sponsor"],
|
||||
"summary": "Sponsor Dashboard",
|
||||
"description": "Returns the sponsor profile summary and a preview of the latest charity events.",
|
||||
"responses": {
|
||||
"200": {"description": "Dashboard data retrieved successfully."},
|
||||
"401": {"description": "Unauthenticated."}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/charity-events": {
|
||||
"get": {
|
||||
"tags": ["Sponsor"],
|
||||
"summary": "List Charity Events",
|
||||
"description": "Returns a paginated list of all charity and announcement events. Optionally filter by type.",
|
||||
"parameters": [
|
||||
{"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}},
|
||||
{"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 15}},
|
||||
{"name": "type", "in": "query", "schema": {"type": "string"}, "description": "Optional event type filter (e.g. 'charity', 'update')."}
|
||||
],
|
||||
"responses": {
|
||||
"200": {"description": "Events retrieved successfully."},
|
||||
"401": {"description": "Unauthenticated."}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/profile": {
|
||||
"get": {
|
||||
"tags": ["Sponsor"],
|
||||
"summary": "Get Sponsor Profile",
|
||||
"description": "Returns the full profile of the authenticated sponsor.",
|
||||
"responses": {
|
||||
"200": {"description": "Profile retrieved successfully."},
|
||||
"401": {"description": "Unauthenticated."}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/workers/register": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -101,6 +311,41 @@
|
||||
"type": "string",
|
||||
"example": "4 Years",
|
||||
"description": "Years of experience."
|
||||
},
|
||||
"in_country": {
|
||||
"type": "boolean",
|
||||
"example": true,
|
||||
"description": "Whether the worker is currently in-country (true) or out-country (false)."
|
||||
},
|
||||
"visa_status": {
|
||||
"type": "string",
|
||||
"example": "Tourist Visa",
|
||||
"description": "Visa status (only required/applicable if in_country is true)."
|
||||
},
|
||||
"preferred_job_type": {
|
||||
"type": "string",
|
||||
"example": "full-time",
|
||||
"description": "Preferred job type."
|
||||
},
|
||||
"live_in_out": {
|
||||
"type": "string",
|
||||
"example": "Live-out",
|
||||
"description": "Accommodation preference (e.g. 'Live-in', 'Live-out')."
|
||||
},
|
||||
"country": {
|
||||
"type": "string",
|
||||
"example": "UAE",
|
||||
"description": "Preferred location country."
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"example": "Dubai",
|
||||
"description": "Preferred location city."
|
||||
},
|
||||
"area": {
|
||||
"type": "string",
|
||||
"example": "Marina",
|
||||
"description": "Preferred location area."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1277,53 +1522,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/register": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Sponsor/Auth"
|
||||
],
|
||||
"summary": "Register Sponsor",
|
||||
"description": "Registers a new sponsor profile with basic info (Name, Email, Phone).",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"email",
|
||||
"phone"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "Ahmad Bin Ahmed",
|
||||
"description": "Sponsor's full name"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com",
|
||||
"description": "Sponsor's contact email address"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "+971509990001",
|
||||
"description": "Sponsor's mobile phone number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Sponsor registered successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/verify": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -1364,46 +1562,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/verify": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Sponsor/Auth"
|
||||
],
|
||||
"summary": "Verify Sponsor Email OTP (Sponsor Prefix)",
|
||||
"description": "Verifies the email address with the OTP verification code (Step 2).",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"otp"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
},
|
||||
"otp": {
|
||||
"type": "string",
|
||||
"example": "111111",
|
||||
"description": "6-digit OTP code"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Email verified successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/payment": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -1453,55 +1611,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/payment": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Sponsor/Auth"
|
||||
],
|
||||
"summary": "Sponsor Subscription Payment (Sponsor Prefix)",
|
||||
"description": "Submits a successful plan selection and PayTabs payment confirmation (Step 3).",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"plan_id",
|
||||
"amount_aed",
|
||||
"paytabs_transaction_id"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
},
|
||||
"plan_id": {
|
||||
"type": "string",
|
||||
"example": "premium"
|
||||
},
|
||||
"amount_aed": {
|
||||
"type": "number",
|
||||
"example": 199
|
||||
},
|
||||
"paytabs_transaction_id": {
|
||||
"type": "string",
|
||||
"example": "TXN998877"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Subscription payment confirmed."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/password": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@ -1548,52 +1657,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/password": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Sponsor/Auth"
|
||||
],
|
||||
"summary": "Create Sponsor Account Password (Sponsor Prefix)",
|
||||
"description": "Configures and finalizes the portal login password to complete registration (Step 4). Returns bearer token.",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password",
|
||||
"password_confirmation"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"example": "Password@123"
|
||||
},
|
||||
"password_confirmation": {
|
||||
"type": "string",
|
||||
"format": "password",
|
||||
"example": "Password@123"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Password set and registration finalized successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/plans": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@ -1609,28 +1672,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/sponsors/plans": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Sponsor/Auth"
|
||||
],
|
||||
"summary": "Get Available Subscription Plans (Sponsor Prefix)",
|
||||
"description": "Returns details (price, features) of subscription packages available to sponsors.",
|
||||
"security": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Subscription plans list retrieved successfully."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/employers/login": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Employer/Auth"
|
||||
],
|
||||
"summary": "Authenticate Employer",
|
||||
"description": "Validates employer email and password and returns a stateless Bearer token.",
|
||||
"summary": "Authenticate Employer or Sponsor",
|
||||
"description": "Unified authentication endpoint. Validates email/mobile and password, then returns a stateless Bearer token and the user's role (employer or sponsor).",
|
||||
"security": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@ -1639,13 +1687,18 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "ahmad@example.com"
|
||||
"example": "ahmad@example.com",
|
||||
"description": "Email address (required if mobile is not provided)"
|
||||
},
|
||||
"mobile": {
|
||||
"type": "string",
|
||||
"example": "+971509990001",
|
||||
"description": "Mobile number (required if email is not provided)"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
@ -1658,7 +1711,38 @@
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Employer logged in successfully."
|
||||
"description": "Login successful. Returns Bearer token and role.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Login successful."
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"example": "employer",
|
||||
"enum": ["employer", "sponsor"]
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"example": "api_token_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3086,6 +3170,24 @@
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Sponsor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer", "example": 1},
|
||||
"full_name": {"type": "string", "example": "Mohammed Al-Rashidi"},
|
||||
"organization_name": {"type": "string", "example": "Al-Rashidi Charitable Foundation"},
|
||||
"email": {"type": "string", "example": "sponsor.971501112233@migrant.ae"},
|
||||
"mobile": {"type": "string", "example": "+971501112233"},
|
||||
"nationality": {"type": "string", "example": "UAE"},
|
||||
"city": {"type": "string", "example": "Dubai"},
|
||||
"address": {"type": "string", "example": "Villa 12, Jumeirah 2"},
|
||||
"country_code": {"type": "string", "example": "+971"},
|
||||
"is_verified": {"type": "boolean", "example": false},
|
||||
"status": {"type": "string", "example": "active"},
|
||||
"license_file": {"type": "string", "example": "uploads/licenses/1234567890_license_trade.pdf"},
|
||||
"created_at": {"type": "string", "format": "date-time"}
|
||||
}
|
||||
},
|
||||
"Worker": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -3145,6 +3247,34 @@
|
||||
"type": "string",
|
||||
"example": "active"
|
||||
},
|
||||
"in_country": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"visa_status": {
|
||||
"type": "string",
|
||||
"example": "Tourist Visa"
|
||||
},
|
||||
"preferred_job_type": {
|
||||
"type": "string",
|
||||
"example": "full-time"
|
||||
},
|
||||
"live_in_out": {
|
||||
"type": "string",
|
||||
"example": "Live-out"
|
||||
},
|
||||
"country": {
|
||||
"type": "string",
|
||||
"example": "UAE"
|
||||
},
|
||||
"city": {
|
||||
"type": "string",
|
||||
"example": "Dubai"
|
||||
},
|
||||
"area": {
|
||||
"type": "string",
|
||||
"example": "Marina"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
|
||||
BIN
public/uploads/documents/1780653554_passport_1000100671.jpg
Normal file
BIN
public/uploads/documents/1780653554_passport_1000100671.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
BIN
public/uploads/documents/1780653554_visa_1000100465.jpg
Normal file
BIN
public/uploads/documents/1780653554_visa_1000100465.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
public/uploads/documents/1780655814_passport_mds.jpeg
Normal file
BIN
public/uploads/documents/1780655814_passport_mds.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
public/uploads/documents/1780655814_visa_mds12.png
Normal file
BIN
public/uploads/documents/1780655814_visa_mds12.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@ -10,6 +10,8 @@
|
||||
use App\Http\Controllers\Api\EmployerAnnouncementController;
|
||||
use App\Http\Controllers\Api\EmployerProfileController;
|
||||
use App\Http\Controllers\Api\EmployerReviewController;
|
||||
use App\Http\Controllers\Api\SponsorAuthController;
|
||||
use App\Http\Controllers\Api\SponsorController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@ -33,20 +35,17 @@
|
||||
|
||||
// Unprotected Employer Mobile Auth Endpoints
|
||||
Route::post('/employers/register', [EmployerAuthController::class, 'register']);
|
||||
Route::post('/sponsors/register', [EmployerAuthController::class, 'register']);
|
||||
Route::post('/employers/verify', [EmployerAuthController::class, 'verify']);
|
||||
Route::post('/sponsors/verify', [EmployerAuthController::class, 'verify']);
|
||||
Route::post('/employers/payment', [EmployerAuthController::class, 'payment']);
|
||||
Route::post('/sponsors/payment', [EmployerAuthController::class, 'payment']);
|
||||
Route::post('/employers/password', [EmployerAuthController::class, 'password']);
|
||||
Route::post('/sponsors/password', [EmployerAuthController::class, 'password']);
|
||||
Route::get('/employers/plans', [EmployerAuthController::class, 'plans']);
|
||||
Route::get('/sponsors/plans', [EmployerAuthController::class, 'plans']);
|
||||
Route::post('/employers/login', [EmployerAuthController::class, 'login']);
|
||||
Route::post('/employers/forgot-password', [EmployerAuthController::class, 'forgotPassword']);
|
||||
Route::post('/sponsors/forgot-password', [EmployerAuthController::class, 'forgotPassword']);
|
||||
Route::post('/employers/reset-password', [EmployerAuthController::class, 'resetPassword']);
|
||||
Route::post('/sponsors/reset-password', [EmployerAuthController::class, 'resetPassword']);
|
||||
|
||||
// Sponsor Mobile Auth Endpoints (unprotected)
|
||||
Route::post('/sponsors/register', [SponsorAuthController::class, 'register']);
|
||||
Route::post('/sponsors/login', [EmployerAuthController::class, 'login']);
|
||||
|
||||
|
||||
// Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||
@ -119,3 +118,11 @@
|
||||
Route::post('/employers/report', [\App\Http\Controllers\Api\ReportController::class, 'reportFromEmployer']);
|
||||
Route::get('/employers/report-reasons', [\App\Http\Controllers\Api\ReportController::class, 'getReasonsForEmployer']);
|
||||
});
|
||||
|
||||
// Protected Sponsor Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||
// Sponsors can ONLY access: dashboard and charity events. No payments, no worker browsing.
|
||||
Route::middleware(['auth.sponsor'])->group(function () {
|
||||
Route::get('/sponsors/profile', [SponsorController::class, 'getProfile']);
|
||||
Route::get('/sponsors/dashboard', [SponsorController::class, 'getDashboard']);
|
||||
Route::get('/sponsors/charity-events', [SponsorController::class, 'getCharityEvents']);
|
||||
});
|
||||
|
||||
168
tests/Feature/SponsorAuthApiTest.php
Normal file
168
tests/Feature/SponsorAuthApiTest.php
Normal file
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Sponsor;
|
||||
use App\Models\User;
|
||||
use App\Models\EmployerProfile;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SponsorAuthApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* Test a pure Sponsor can register with simple info and a license file upload.
|
||||
*/
|
||||
public function test_sponsor_can_register_with_license()
|
||||
{
|
||||
$licenseFile = UploadedFile::fake()->create('license.pdf', 500, 'application/pdf');
|
||||
|
||||
$response = $this->postJson('/api/sponsors/register', [
|
||||
'full_name' => 'Test Sponsor Organization',
|
||||
'mobile' => '+971509990001',
|
||||
'password' => 'securepassword',
|
||||
'license_file' => $licenseFile,
|
||||
'organization_name' => 'Charity Co',
|
||||
'nationality' => 'Emirati',
|
||||
'city' => 'Abu Dhabi',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJsonStructure([
|
||||
'success',
|
||||
'message',
|
||||
'data' => [
|
||||
'sponsor' => [
|
||||
'id',
|
||||
'full_name',
|
||||
'mobile',
|
||||
'organization_name',
|
||||
'nationality',
|
||||
'city',
|
||||
'license_file',
|
||||
],
|
||||
'token',
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('sponsors', [
|
||||
'mobile' => '+971509990001',
|
||||
'full_name' => 'Test Sponsor Organization',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test both Sponsor and Employer can log in using the unified login endpoint.
|
||||
*/
|
||||
public function test_unified_login_returns_correct_role_and_data()
|
||||
{
|
||||
// 1. Create a pure Sponsor (no User record)
|
||||
$sponsorUser = Sponsor::create([
|
||||
'full_name' => 'Sponsor User',
|
||||
'email' => 'sponsor@example.com',
|
||||
'mobile' => '+971509991111',
|
||||
'password' => Hash::make('password123'),
|
||||
'license_file' => 'uploads/licenses/fake.pdf',
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
// 2. Create an Employer (has both User and Sponsor records, legacy setup)
|
||||
$employerUser = User::create([
|
||||
'name' => 'Employer User',
|
||||
'email' => 'employer@example.com',
|
||||
'password' => Hash::make('password123'),
|
||||
'role' => 'employer',
|
||||
'api_token' => 'old_token',
|
||||
]);
|
||||
|
||||
EmployerProfile::create([
|
||||
'user_id' => $employerUser->id,
|
||||
'company_name' => 'Employer Corp',
|
||||
'phone' => '+971509992222',
|
||||
'country' => 'United Arab Emirates',
|
||||
'verification_status' => 'approved',
|
||||
]);
|
||||
|
||||
Sponsor::create([
|
||||
'full_name' => 'Employer User',
|
||||
'email' => 'employer@example.com',
|
||||
'mobile' => '+971509992222',
|
||||
'password' => $employerUser->password,
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
// --- Test Sponsor Login via /api/employers/login ---
|
||||
$responseSponsor = $this->postJson('/api/employers/login', [
|
||||
'mobile' => '+971509991111',
|
||||
'password' => 'password123',
|
||||
]);
|
||||
|
||||
$responseSponsor->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'role' => 'sponsor',
|
||||
])
|
||||
->assertJsonStructure([
|
||||
'data' => [
|
||||
'sponsor',
|
||||
'token',
|
||||
]
|
||||
]);
|
||||
|
||||
// --- Test Employer Login via /api/employers/login ---
|
||||
$responseEmployer = $this->postJson('/api/employers/login', [
|
||||
'mobile' => '+971509992222',
|
||||
'password' => 'password123',
|
||||
]);
|
||||
|
||||
$responseEmployer->assertStatus(200)
|
||||
->assertJson([
|
||||
'success' => true,
|
||||
'role' => 'employer',
|
||||
])
|
||||
->assertJsonStructure([
|
||||
'data' => [
|
||||
'employer' => [
|
||||
'id',
|
||||
'name',
|
||||
'email',
|
||||
'employer_profile',
|
||||
],
|
||||
'token',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that pure sponsors can only access their limited routes.
|
||||
*/
|
||||
public function test_sponsor_restricted_access()
|
||||
{
|
||||
$sponsor = Sponsor::create([
|
||||
'full_name' => 'Pure Sponsor',
|
||||
'email' => 'pure_sponsor@example.com',
|
||||
'mobile' => '+971509993333',
|
||||
'password' => Hash::make('password123'),
|
||||
'api_token' => 'sponsor_token_abc',
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
// Access dashboard successfully
|
||||
$responseDash = $this->withHeaders([
|
||||
'Authorization' => 'Bearer sponsor_token_abc',
|
||||
])->getJson('/api/sponsors/dashboard');
|
||||
|
||||
$responseDash->assertStatus(200);
|
||||
|
||||
// Try to access employer protected route (should get blocked as they don't have employer token/role)
|
||||
$responseEmployerRoute = $this->withHeaders([
|
||||
'Authorization' => 'Bearer sponsor_token_abc',
|
||||
])->getJson('/api/employers/profile');
|
||||
|
||||
$responseEmployerRoute->assertStatus(401);
|
||||
}
|
||||
}
|
||||
@ -407,4 +407,95 @@ public function test_s6_outcome_mark_hired()
|
||||
'status' => 'Hired',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test registration with new API fields.
|
||||
*/
|
||||
public function test_register_with_new_api_fields()
|
||||
{
|
||||
$fakePassport = UploadedFile::fake()->create('passport.pdf', 500, 'application/pdf');
|
||||
|
||||
$response = $this->postJson('/api/workers/register', [
|
||||
'name' => 'John New Worker',
|
||||
'phone' => '+971501111112',
|
||||
'salary' => 2200.50,
|
||||
'password' => 'secret123',
|
||||
'passport_file' => $fakePassport,
|
||||
'language' => 'HI, SW',
|
||||
'nationality' => 'Kenya',
|
||||
'age' => 30,
|
||||
'experience' => '5 Years',
|
||||
'in_country' => 'true',
|
||||
'visa_status' => 'Tourist Visa',
|
||||
'preferred_job_type' => 'full-time',
|
||||
'live_in_out' => 'Live-out (External housing)',
|
||||
'country' => 'UAE',
|
||||
'city' => 'Dubai',
|
||||
'area' => 'Marina',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
|
||||
$this->assertDatabaseHas('workers', [
|
||||
'name' => 'John New Worker',
|
||||
'phone' => '+971501111112',
|
||||
'in_country' => true,
|
||||
'visa_status' => 'Tourist Visa',
|
||||
'preferred_job_type' => 'full-time',
|
||||
'live_in_out' => 'Live-out (External housing)',
|
||||
'country' => 'UAE',
|
||||
'city' => 'Dubai',
|
||||
'area' => 'Marina',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating profile with new API fields.
|
||||
*/
|
||||
public function test_update_profile_with_new_api_fields()
|
||||
{
|
||||
$worker = Worker::create([
|
||||
'name' => 'Original Worker',
|
||||
'email' => 'orig@example.com',
|
||||
'phone' => '+971502222223',
|
||||
'language' => 'SW',
|
||||
'password' => bcrypt('password'),
|
||||
'nationality' => 'Kenya',
|
||||
'age' => 22,
|
||||
'salary' => 1200,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => 'None',
|
||||
'religion' => 'Christian',
|
||||
'bio' => 'New helper',
|
||||
'category_id' => $this->category->id,
|
||||
'verified' => false,
|
||||
'status' => 'active',
|
||||
'api_token' => 'bearer-update-token',
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer bearer-update-token',
|
||||
])->postJson('/api/workers/profile/update', [
|
||||
'in_country' => false,
|
||||
'visa_status' => null,
|
||||
'preferred_job_type' => 'part-time',
|
||||
'live_in_out' => 'Live-in (Stay with family)',
|
||||
'country' => 'UAE',
|
||||
'city' => 'Abu Dhabi',
|
||||
'area' => 'Khalifa City',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200);
|
||||
|
||||
$this->assertDatabaseHas('workers', [
|
||||
'id' => $worker->id,
|
||||
'in_country' => false,
|
||||
'visa_status' => null,
|
||||
'preferred_job_type' => 'part-time',
|
||||
'live_in_out' => 'Live-in (Stay with family)',
|
||||
'country' => 'UAE',
|
||||
'city' => 'Abu Dhabi',
|
||||
'area' => 'Khalifa City',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user