forgot and reset password
This commit is contained in:
parent
121337eea7
commit
ed7f2f8f4e
@ -279,4 +279,129 @@ public function uploadLicense(Request $request)
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Forgot / Reset Password
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* POST /api/sponsors/forgot-password
|
||||
* Send a 6-digit OTP to the sponsor's registered email.
|
||||
* Accepts: email OR mobile
|
||||
*/
|
||||
public function forgotPassword(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'nullable|email',
|
||||
'mobile' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$validator->after(function ($v) use ($request) {
|
||||
if (!$request->filled('email') && !$request->filled('mobile')) {
|
||||
$v->errors()->add('email', 'Either email or mobile number is required.');
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$sponsor = null;
|
||||
if ($request->filled('email')) {
|
||||
$sponsor = Sponsor::where('email', $request->email)->first();
|
||||
} elseif ($request->filled('mobile')) {
|
||||
$sponsor = Sponsor::where('mobile', $request->mobile)->first();
|
||||
}
|
||||
|
||||
// Prevent email enumeration — always return success
|
||||
if (!$sponsor || !$sponsor->email) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'If an account with that contact exists, a reset OTP has been sent.',
|
||||
]);
|
||||
}
|
||||
|
||||
$otp = (string) mt_rand(100000, 999999);
|
||||
$cacheKey = 'sponsor_pwd_reset_' . md5($sponsor->email);
|
||||
|
||||
\Illuminate\Support\Facades\Cache::put($cacheKey, [
|
||||
'otp' => Hash::make($otp),
|
||||
'expires_at' => now()->addMinutes(10)->timestamp,
|
||||
], now()->addMinutes(10));
|
||||
|
||||
try {
|
||||
\Illuminate\Support\Facades\Mail::to($sponsor->email)->send(
|
||||
new \App\Mail\ForgotPasswordOtpMail($otp, $sponsor->full_name, 'Sponsor')
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Sponsor forgot-password mail failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'If an account with that contact exists, a reset OTP has been sent.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sponsors/reset-password
|
||||
* Verify OTP and set new password.
|
||||
*/
|
||||
public function resetPassword(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'email' => 'required|email',
|
||||
'otp' => 'required|string|size:6',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'password_confirmation' => 'required|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$cacheKey = 'sponsor_pwd_reset_' . md5($request->email);
|
||||
$cached = \Illuminate\Support\Facades\Cache::get($cacheKey);
|
||||
|
||||
if (!$cached || $cached['expires_at'] < now()->timestamp) {
|
||||
\Illuminate\Support\Facades\Cache::forget($cacheKey);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'OTP has expired or is invalid. Please request a new one.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (!Hash::check($request->otp, $cached['otp'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid OTP. Please check and try again.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$sponsor = Sponsor::where('email', $request->email)->first();
|
||||
|
||||
if (!$sponsor) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Account not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$sponsor->update(['password' => Hash::make($request->password)]);
|
||||
\Illuminate\Support\Facades\Cache::forget($cacheKey);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Password reset successfully. You can now log in with your new password.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class WorkerAuthController extends Controller
|
||||
{
|
||||
@ -315,7 +316,17 @@ public function register(Request $request)
|
||||
'preferred_location' => 'nullable|string|max:255',
|
||||
'fcm_token' => 'nullable|string|max:255',
|
||||
'passport' => 'nullable|array',
|
||||
'passport.passport_number' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::unique('worker_documents', 'number')->where(function ($query) {
|
||||
return $query->where('type', 'passport');
|
||||
}),
|
||||
],
|
||||
'visa' => 'nullable|array',
|
||||
], [
|
||||
'phone.unique' => 'This mobile number is already registered.',
|
||||
'passport.passport_number.unique' => 'This passport number is already registered.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@ -970,4 +981,123 @@ private function normaliseDateForController(?string $raw): ?string
|
||||
return $raw;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Forgot / Reset Password (via Mobile Number)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* POST /api/workers/forgot-password
|
||||
* Send a 6-digit OTP to verify the worker's identity by phone number.
|
||||
* Workers do not use email — phone is the primary identifier.
|
||||
*/
|
||||
public function forgotPassword(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'phone' => 'required|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$worker = Worker::where('phone', $request->phone)->first();
|
||||
|
||||
// Always return success to prevent phone enumeration
|
||||
if (!$worker) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'If an account with that mobile number exists, a reset OTP has been sent.',
|
||||
]);
|
||||
}
|
||||
|
||||
$otp = (string) mt_rand(100000, 999999);
|
||||
$cacheKey = 'worker_pwd_reset_' . md5($request->phone);
|
||||
|
||||
Cache::put($cacheKey, [
|
||||
'otp' => Hash::make($otp),
|
||||
'expires_at' => now()->addMinutes(10)->timestamp,
|
||||
], now()->addMinutes(10));
|
||||
|
||||
// Send via email if available, otherwise log for dev
|
||||
if ($worker->email) {
|
||||
try {
|
||||
Mail::to($worker->email)->send(
|
||||
new \App\Mail\ForgotPasswordOtpMail($otp, $worker->name, 'Worker')
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
logger()->error('Worker forgot-password mail failed: ' . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
// Log OTP for debugging when no email configured
|
||||
logger()->info("Worker password reset OTP for {$request->phone}: {$otp}");
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'If an account with that mobile number exists, a reset OTP has been sent.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/workers/reset-password
|
||||
* Verify OTP sent to phone and set a new password.
|
||||
*/
|
||||
public function resetPassword(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'phone' => 'required|string',
|
||||
'otp' => 'required|string|size:6',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'password_confirmation' => 'required|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$cacheKey = 'worker_pwd_reset_' . md5($request->phone);
|
||||
$cached = Cache::get($cacheKey);
|
||||
|
||||
if (!$cached || $cached['expires_at'] < now()->timestamp) {
|
||||
Cache::forget($cacheKey);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'OTP has expired or is invalid. Please request a new one.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
if (!Hash::check($request->otp, $cached['otp'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Invalid OTP. Please check and try again.',
|
||||
], 422);
|
||||
}
|
||||
|
||||
$worker = Worker::where('phone', $request->phone)->first();
|
||||
|
||||
if (!$worker) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Account not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$worker->update(['password' => Hash::make($request->password)]);
|
||||
Cache::forget($cacheKey);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Password reset successfully. You can now log in with your new password.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -46,19 +46,20 @@ public function login(Request $request)
|
||||
return back()->with('status', 'subscription_expired');
|
||||
}
|
||||
|
||||
// Perform standard Laravel authentication
|
||||
auth()->login($user);
|
||||
// Perform standard Laravel authentication (remember=true keeps auth cookie alive)
|
||||
auth()->login($user, true);
|
||||
|
||||
session(['user' => (object)[
|
||||
// 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,
|
||||
]]);
|
||||
|
||||
$request->session()->regenerate();
|
||||
]);
|
||||
|
||||
if ($request->source === 'mobile') {
|
||||
return redirect('/mobile/employer/home');
|
||||
@ -84,9 +85,11 @@ public function register(Request $request)
|
||||
'company_name' => 'nullable|string|max:255',
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255',
|
||||
'phone' => 'required|string|regex:/^[0-9]{7,15}$/',
|
||||
'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 be between 7 and 15 digits without any country code (e.g. 501234567).',
|
||||
'phone.regex' => 'The mobile number must contain only digits (7-20 characters).',
|
||||
]);
|
||||
|
||||
// 2. Email uniqueness check (Check DB, return 409 if duplicate)
|
||||
@ -118,6 +121,8 @@ public function register(Request $request)
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone,
|
||||
'country_code' => $request->country_code,
|
||||
'address' => $request->address,
|
||||
],
|
||||
'employer_otp' => [
|
||||
'hash' => $hashedOtp,
|
||||
@ -433,6 +438,7 @@ public function createPassword(Request $request)
|
||||
'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,
|
||||
'address' => $pending['address'] ?? null,
|
||||
]);
|
||||
|
||||
// Create Sponsor
|
||||
@ -452,6 +458,7 @@ public function createPassword(Request $request)
|
||||
'status' => 'active',
|
||||
'last_login_at' => now(),
|
||||
'emirates_id_file' => null, // We did not store the file
|
||||
'address' => $pending['address'] ?? null,
|
||||
]);
|
||||
|
||||
// Create active subscription in database
|
||||
@ -467,17 +474,20 @@ public function createPassword(Request $request)
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// Auto-login (Laravel + Session)
|
||||
auth()->login($user);
|
||||
// Auto-login (Laravel + Session) with remember=true for persistence
|
||||
auth()->login($user, true);
|
||||
|
||||
session(['user' => (object)[
|
||||
// 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);
|
||||
|
||||
@ -6,90 +6,70 @@
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use App\Models\User;
|
||||
use App\Models\Payment;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
private function resolveCurrentUser()
|
||||
private function resolveCurrentUser(): ?User
|
||||
{
|
||||
// Prefer Laravel's built-in auth (most reliable)
|
||||
if (auth()->check()) {
|
||||
return auth()->user();
|
||||
}
|
||||
|
||||
// Fallback: session-based user (registration auto-login flow)
|
||||
$sess = session('user');
|
||||
$sessId = is_array($sess) ? ($sess['id'] ?? null) : ($sess->id ?? null);
|
||||
|
||||
if (!$sessId) {
|
||||
$user = User::where('role', 'employer')->first();
|
||||
if ($user) {
|
||||
session(['user' => (object)[
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'role' => 'employer',
|
||||
'subscription_status' => $user->subscription_status ?? 'active',
|
||||
]]);
|
||||
return $user;
|
||||
}
|
||||
} else {
|
||||
return User::find($sessId);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
return $sessId ? User::find($sessId) : null;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$user = $this->resolveCurrentUser();
|
||||
$employerId = $user ? $user->id : 2;
|
||||
|
||||
// Auto-seed payments if none exist for a richer UX
|
||||
$paymentsCount = Payment::where('user_id', $employerId)->count();
|
||||
if ($paymentsCount === 0) {
|
||||
$subscription = DB::table('subscriptions')->where('user_id', $employerId)->first();
|
||||
$planName = $subscription ? (ucfirst($subscription->plan_id) . ' Pass Subscription') : 'Premium Sponsor Pass Subscription';
|
||||
$planAmount = $subscription ? $subscription->amount_aed : 199.00;
|
||||
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
Payment::create([
|
||||
'user_id' => $employerId,
|
||||
'amount' => $planAmount,
|
||||
'currency' => 'AED',
|
||||
'description' => $planName,
|
||||
'status' => 'success',
|
||||
'created_at' => now()->subMonths($i)->subDays(rand(1, 5)),
|
||||
'updated_at' => now()->subMonths($i)->subDays(rand(1, 5)),
|
||||
]);
|
||||
}
|
||||
if (!$user) {
|
||||
return redirect()->route('employer.login');
|
||||
}
|
||||
|
||||
$payments = Payment::where('user_id', $employerId)
|
||||
->where(function ($q) {
|
||||
$q->where('description', 'like', '%Subscription%')
|
||||
->orWhere('description', 'like', '%Pass%');
|
||||
})
|
||||
->latest()
|
||||
->get()
|
||||
->map(function ($p) {
|
||||
return [
|
||||
'id' => $p->id,
|
||||
'amount' => (float)$p->amount,
|
||||
'currency' => $p->currency,
|
||||
'description' => $p->description,
|
||||
'status' => $p->status,
|
||||
'date' => $p->created_at->format('M d, Y'),
|
||||
'time' => $p->created_at->format('h:i A'),
|
||||
'invoice_no' => 'INV-' . str_pad($p->id, 6, '0', STR_PAD_LEFT),
|
||||
];
|
||||
})->toArray();
|
||||
// Payments are stored in the subscriptions table during registration
|
||||
$subscriptions = DB::table('subscriptions')
|
||||
->where('user_id', $user->id)
|
||||
->latest('id')
|
||||
->get();
|
||||
|
||||
// Retrieve subscription details for billing and renewal analytics
|
||||
$sub = DB::table('subscriptions')->where('user_id', $employerId)->where('status', 'active')->latest('id')->first();
|
||||
$expiresAt = $sub && $sub->expires_at ? date('M d, Y', strtotime($sub->expires_at)) : 'Dec 31, 2026';
|
||||
$currentPlan = $sub ? (ucfirst($sub->plan_id) . ' Sponsor Pass') : 'Premium Sponsor Pass';
|
||||
$subStatus = $user && $user->subscription_status ? ucfirst($user->subscription_status) : 'Active';
|
||||
$payments = $subscriptions->map(function ($sub) {
|
||||
$planLabel = ucfirst($sub->plan_id) . ' Sponsor Pass Subscription';
|
||||
$date = \Carbon\Carbon::parse($sub->starts_at ?? $sub->created_at);
|
||||
|
||||
return [
|
||||
'id' => $sub->id,
|
||||
'amount' => (float) $sub->amount_aed,
|
||||
'currency' => 'AED',
|
||||
'description' => $planLabel,
|
||||
'status' => $sub->status === 'active' ? 'success' : $sub->status,
|
||||
'date' => $date->format('M d, Y'),
|
||||
'time' => $date->format('h:i A'),
|
||||
'invoice_no' => 'INV-' . str_pad($sub->id, 6, '0', STR_PAD_LEFT),
|
||||
'transaction_id' => $sub->paytabs_transaction_id,
|
||||
'expires_at' => $sub->expires_at ? \Carbon\Carbon::parse($sub->expires_at)->format('M d, Y') : null,
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
// Latest active subscription for the summary cards
|
||||
$activeSub = $subscriptions->where('status', 'active')->first();
|
||||
$expiresAt = $activeSub && $activeSub->expires_at
|
||||
? \Carbon\Carbon::parse($activeSub->expires_at)->format('M d, Y')
|
||||
: null;
|
||||
$currentPlan = $activeSub
|
||||
? ucfirst($activeSub->plan_id) . ' Sponsor Pass'
|
||||
: null;
|
||||
$subStatus = ucfirst($user->subscription_status ?? 'none');
|
||||
|
||||
return Inertia::render('Employer/PaymentHistory', [
|
||||
'payments' => $payments,
|
||||
'currentPlan' => $currentPlan,
|
||||
'expiresAt' => $expiresAt,
|
||||
'payments' => $payments,
|
||||
'currentPlan' => $currentPlan,
|
||||
'expiresAt' => $expiresAt,
|
||||
'subscriptionStatus' => $subStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
34
app/Http/Middleware/EmployerGuestMiddleware.php
Normal file
34
app/Http/Middleware/EmployerGuestMiddleware.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EmployerGuestMiddleware
|
||||
{
|
||||
/**
|
||||
* If the employer is already authenticated, redirect them to the dashboard.
|
||||
* Prevents logged-in users from seeing login/register pages.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Check Laravel's built-in auth first (most reliable — survives refresh)
|
||||
if (auth()->check() && auth()->user()->role === 'employer') {
|
||||
return redirect()->route('employer.dashboard');
|
||||
}
|
||||
|
||||
// Also check the custom session key used during registration auto-login
|
||||
$sessionUser = session('user');
|
||||
$role = is_array($sessionUser)
|
||||
? ($sessionUser['role'] ?? null)
|
||||
: ($sessionUser->role ?? null);
|
||||
|
||||
if ($sessionUser && $role === 'employer') {
|
||||
return redirect()->route('employer.dashboard');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@ -13,12 +13,21 @@ class EmployerMiddleware
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = auth()->check() ? auth()->user() : session('user');
|
||||
$role = is_array($user) ? ($user['role'] ?? null) : ($user->role ?? null);
|
||||
if (!$user || $role !== 'employer') {
|
||||
return redirect()->route('employer.login');
|
||||
// Prefer Laravel's built-in auth (survives page refresh reliably via remember cookie)
|
||||
if (auth()->check() && auth()->user()->role === 'employer') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
// Fallback: check custom session key (used during registration auto-login)
|
||||
$sessionUser = session('user');
|
||||
$role = is_array($sessionUser)
|
||||
? ($sessionUser['role'] ?? null)
|
||||
: ($sessionUser->role ?? null);
|
||||
|
||||
if ($sessionUser && $role === 'employer') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return redirect()->route('employer.login');
|
||||
}
|
||||
}
|
||||
|
||||
39
app/Mail/ForgotPasswordOtpMail.php
Normal file
39
app/Mail/ForgotPasswordOtpMail.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ForgotPasswordOtpMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public string $otp;
|
||||
public string $name;
|
||||
public string $userType; // 'Worker', 'Employer', 'Sponsor'
|
||||
|
||||
public function __construct(string $otp, string $name, string $userType = 'User')
|
||||
{
|
||||
$this->otp = $otp;
|
||||
$this->name = $name;
|
||||
$this->userType = $userType;
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Password Reset OTP – ' . config('app.name'),
|
||||
);
|
||||
}
|
||||
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
view: 'emails.forgot-password-otp',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -19,11 +19,12 @@
|
||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||
]);
|
||||
$middleware->alias([
|
||||
'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.sponsor' => \App\Http\Middleware\SponsorApiMiddleware::class,
|
||||
'admin' => \App\Http\Middleware\AdminMiddleware::class,
|
||||
'employer' => \App\Http\Middleware\EmployerMiddleware::class,
|
||||
'employer.guest' => \App\Http\Middleware\EmployerGuestMiddleware::class,
|
||||
'auth.worker' => \App\Http\Middleware\WorkerApiMiddleware::class,
|
||||
'auth.employer' => \App\Http\Middleware\EmployerApiMiddleware::class,
|
||||
'auth.sponsor' => \App\Http\Middleware\SponsorApiMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
10784
public/swagger.json
10784
public/swagger.json
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Head, Link, router } from '@inertiajs/react';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
@ -6,14 +6,131 @@ import {
|
||||
Loader2,
|
||||
Users,
|
||||
DollarSign,
|
||||
MessageSquare
|
||||
MessageSquare,
|
||||
ChevronDown,
|
||||
Search
|
||||
} from 'lucide-react';
|
||||
|
||||
// Common country dial codes with flag emojis
|
||||
const COUNTRY_CODES = [
|
||||
{ code: 'AE', dial: '+971', flag: '🇦🇪', name: 'United Arab Emirates' },
|
||||
{ code: 'SA', dial: '+966', flag: '🇸🇦', name: 'Saudi Arabia' },
|
||||
{ code: 'IN', dial: '+91', flag: '🇮🇳', name: 'India' },
|
||||
{ code: 'PK', dial: '+92', flag: '🇵🇰', name: 'Pakistan' },
|
||||
{ code: 'PH', dial: '+63', flag: '🇵🇭', name: 'Philippines' },
|
||||
{ code: 'BD', dial: '+880', flag: '🇧🇩', name: 'Bangladesh' },
|
||||
{ code: 'LK', dial: '+94', flag: '🇱🇰', name: 'Sri Lanka' },
|
||||
{ code: 'NP', dial: '+977', flag: '🇳🇵', name: 'Nepal' },
|
||||
{ code: 'EG', dial: '+20', flag: '🇪🇬', name: 'Egypt' },
|
||||
{ code: 'JO', dial: '+962', flag: '🇯🇴', name: 'Jordan' },
|
||||
{ code: 'LB', dial: '+961', flag: '🇱🇧', name: 'Lebanon' },
|
||||
{ code: 'IQ', dial: '+964', flag: '🇮🇶', name: 'Iraq' },
|
||||
{ code: 'KW', dial: '+965', flag: '🇰🇼', name: 'Kuwait' },
|
||||
{ code: 'QA', dial: '+974', flag: '🇶🇦', name: 'Qatar' },
|
||||
{ code: 'BH', dial: '+973', flag: '🇧🇭', name: 'Bahrain' },
|
||||
{ code: 'OM', dial: '+968', flag: '🇴🇲', name: 'Oman' },
|
||||
{ code: 'ET', dial: '+251', flag: '🇪🇹', name: 'Ethiopia' },
|
||||
{ code: 'KE', dial: '+254', flag: '🇰🇪', name: 'Kenya' },
|
||||
{ code: 'UG', dial: '+256', flag: '🇺🇬', name: 'Uganda' },
|
||||
{ code: 'GH', dial: '+233', flag: '🇬🇭', name: 'Ghana' },
|
||||
{ code: 'NG', dial: '+234', flag: '🇳🇬', name: 'Nigeria' },
|
||||
{ code: 'ID', dial: '+62', flag: '🇮🇩', name: 'Indonesia' },
|
||||
{ code: 'MY', dial: '+60', flag: '🇲🇾', name: 'Malaysia' },
|
||||
{ code: 'GB', dial: '+44', flag: '🇬🇧', name: 'United Kingdom' },
|
||||
{ code: 'US', dial: '+1', flag: '🇺🇸', name: 'United States' },
|
||||
];
|
||||
|
||||
function CountryCodeSelector({ value, onChange, hasError }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const ref = useRef(null);
|
||||
const searchRef = useRef(null);
|
||||
|
||||
const selected = COUNTRY_CODES.find(c => c.dial === value) || COUNTRY_CODES[0];
|
||||
|
||||
const filtered = COUNTRY_CODES.filter(c =>
|
||||
c.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
c.dial.includes(search)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e) => {
|
||||
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && searchRef.current) searchRef.current.focus();
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setOpen(o => !o); setSearch(''); }}
|
||||
className={`flex items-center gap-1.5 px-3 py-3 h-full rounded-l-xl border-r-0 border text-sm font-medium whitespace-nowrap transition-all focus:outline-none focus:ring-2 ${
|
||||
hasError
|
||||
? 'border-red-500 focus:ring-red-500/20 bg-red-50'
|
||||
: 'border-slate-300 focus:ring-[#185FA5]/20 bg-slate-50 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base leading-none">{selected.flag}</span>
|
||||
<span className="text-slate-700 text-xs">{selected.dial}</span>
|
||||
<ChevronDown className={`w-3 h-3 text-slate-500 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute left-0 top-full mt-1 z-50 w-72 bg-white border border-slate-200 rounded-xl shadow-xl overflow-hidden">
|
||||
{/* Search */}
|
||||
<div className="p-2 border-b border-slate-100">
|
||||
<div className="flex items-center gap-2 px-2.5 py-1.5 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<Search className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Search country..."
|
||||
className="flex-1 bg-transparent text-xs outline-none text-slate-700 placeholder-slate-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<ul className="max-h-52 overflow-y-auto">
|
||||
{filtered.length === 0 ? (
|
||||
<li className="px-4 py-3 text-xs text-slate-400 text-center">No results found</li>
|
||||
) : filtered.map(c => (
|
||||
<li key={c.code}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { onChange(c.dial); setOpen(false); }}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-slate-50 transition-colors ${
|
||||
c.dial === value ? 'bg-blue-50 text-[#185FA5] font-semibold' : 'text-slate-700'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{c.flag}</span>
|
||||
<span className="flex-1 text-left text-xs">{c.name}</span>
|
||||
<span className="text-xs text-slate-500 font-mono">{c.dial}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Register() {
|
||||
const [countryCode, setCountryCode] = useState('+971');
|
||||
const [data, setData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -41,8 +158,12 @@ export default function Register() {
|
||||
|
||||
if (!data.phone.trim()) {
|
||||
newErrors.phone = 'Mobile number is required.';
|
||||
} else if (!/^[0-9]{7,15}$/.test(data.phone)) {
|
||||
newErrors.phone = 'Please enter a valid mobile number (7-15 digits without country code).';
|
||||
} else if (!/^[0-9]{4,14}$/.test(data.phone.replace(/^0+/, ''))) {
|
||||
newErrors.phone = 'Please enter a valid mobile number (digits only, without country code).';
|
||||
}
|
||||
|
||||
if (!data.address.trim()) {
|
||||
newErrors.address = 'Address is required.';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
@ -63,7 +184,9 @@ export default function Register() {
|
||||
const formData = new FormData();
|
||||
formData.append('name', data.name);
|
||||
formData.append('email', data.email);
|
||||
formData.append('phone', data.phone);
|
||||
formData.append('phone', countryCode + data.phone.replace(/^0+/, ''));
|
||||
formData.append('country_code', countryCode);
|
||||
formData.append('address', data.address);
|
||||
|
||||
try {
|
||||
await axios.post('/employer/register', formData, {
|
||||
@ -239,20 +362,48 @@ export default function Register() {
|
||||
{/* Mobile Number */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Mobile Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={data.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="e.g. 501234567"
|
||||
<div className="flex">
|
||||
<CountryCodeSelector
|
||||
value={countryCode}
|
||||
onChange={setCountryCode}
|
||||
hasError={!!errors.phone}
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
value={data.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value.replace(/[^0-9]/g, ''))}
|
||||
placeholder="501234567"
|
||||
className={`flex-1 px-3.5 py-3 rounded-r-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
|
||||
errors.phone
|
||||
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
|
||||
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{errors.phone && (
|
||||
<p className="mt-1.5 text-xs text-red-500 font-medium">
|
||||
{Array.isArray(errors.phone) ? errors.phone[0] : errors.phone}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Address */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 mb-1.5 uppercase tracking-wider">Address</label>
|
||||
<textarea
|
||||
value={data.address}
|
||||
onChange={(e) => handleInputChange('address', e.target.value)}
|
||||
placeholder="e.g. Villa 14, Al Safa, Dubai, UAE"
|
||||
rows="2"
|
||||
className={`w-full px-3.5 py-3 rounded-xl border text-sm focus:outline-none focus:ring-2 transition-all ${
|
||||
errors.phone
|
||||
errors.address
|
||||
? 'border-red-500 focus:ring-red-500/20 focus:border-red-500'
|
||||
: 'border-slate-300 focus:ring-[#185FA5]/20 focus:border-[#185FA5]'
|
||||
}`}
|
||||
/>
|
||||
{errors.phone && (
|
||||
{errors.address && (
|
||||
<p className="mt-1.5 text-xs text-red-500 font-medium">
|
||||
{Array.isArray(errors.phone) ? errors.phone[0] : errors.phone}
|
||||
{Array.isArray(errors.address) ? errors.address[0] : errors.address}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
49
resources/views/emails/forgot-password-otp.blade.php
Normal file
49
resources/views/emails/forgot-password-otp.blade.php
Normal file
@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Password Reset OTP</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; background: #f1f5f9; margin: 0; padding: 0; }
|
||||
.wrapper { max-width: 520px; margin: 40px auto; background: #ffffff; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.08); }
|
||||
.header { background: #185FA5; padding: 32px 40px; text-align: center; }
|
||||
.header h1 { color: #fff; font-size: 22px; font-weight: 800; margin: 0; letter-spacing: -0.5px; }
|
||||
.header p { color: rgba(255,255,255,0.75); font-size: 13px; margin: 6px 0 0; }
|
||||
.body { padding: 36px 40px; }
|
||||
.greeting { font-size: 15px; color: #1e293b; font-weight: 600; margin-bottom: 12px; }
|
||||
.message { font-size: 13.5px; color: #475569; line-height: 1.7; margin-bottom: 28px; }
|
||||
.otp-box { background: #f0f7ff; border: 2px dashed #185FA5; border-radius: 12px; text-align: center; padding: 20px; margin-bottom: 28px; }
|
||||
.otp-box .otp { font-size: 38px; font-weight: 900; letter-spacing: 10px; color: #185FA5; font-family: monospace; }
|
||||
.otp-box .expiry { font-size: 11px; color: #64748b; margin-top: 8px; font-weight: 600; }
|
||||
.warning { font-size: 12px; color: #94a3b8; line-height: 1.6; border-top: 1px solid #e2e8f0; padding-top: 20px; margin-top: 8px; }
|
||||
.footer { background: #f8fafc; padding: 18px 40px; text-align: center; font-size: 11px; color: #94a3b8; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div class="header">
|
||||
<h1>{{ config('app.name') }}</h1>
|
||||
<p>Password Reset Request</p>
|
||||
</div>
|
||||
<div class="body">
|
||||
<p class="greeting">Hello, {{ $name }}</p>
|
||||
<p class="message">
|
||||
We received a request to reset your <strong>{{ $userType }}</strong> account password.
|
||||
Use the OTP below to proceed. This code is valid for <strong>10 minutes</strong>.
|
||||
</p>
|
||||
<div class="otp-box">
|
||||
<div class="otp">{{ $otp }}</div>
|
||||
<div class="expiry">⏱ Expires in 10 minutes</div>
|
||||
</div>
|
||||
<p class="warning">
|
||||
If you did not request a password reset, please ignore this email — your account is safe.
|
||||
Never share this code with anyone.
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
© {{ date('Y') }} {{ config('app.name') }}. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -43,6 +43,8 @@
|
||||
Route::post('/workers/setup-profile', [WorkerAuthController::class, 'setupProfile']);
|
||||
Route::post('/workers/register', [WorkerAuthController::class, 'register']);
|
||||
Route::post('/workers/login', [WorkerAuthController::class, 'login']);
|
||||
Route::post('/workers/forgot-password', [WorkerAuthController::class, 'forgotPassword']);
|
||||
Route::post('/workers/reset-password', [WorkerAuthController::class, 'resetPassword']);
|
||||
Route::post('/workers/ocr/passport-vision', [GoogleVisionOcrController::class, 'extractPassport']);
|
||||
|
||||
// Unprotected Employer Mobile Auth Endpoints
|
||||
@ -59,6 +61,8 @@
|
||||
Route::post('/sponsors/register', [SponsorAuthController::class, 'register']);
|
||||
Route::post('/sponsors/register/license', [SponsorAuthController::class, 'uploadLicense']);
|
||||
Route::post('/sponsors/login', [EmployerAuthController::class, 'login']);
|
||||
Route::post('/sponsors/forgot-password', [SponsorAuthController::class, 'forgotPassword']);
|
||||
Route::post('/sponsors/reset-password', [SponsorAuthController::class, 'resetPassword']);
|
||||
|
||||
|
||||
// Protected Worker Mobile Endpoints (Token Authenticated via Bearer Token)
|
||||
|
||||
@ -5,9 +5,33 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
|
||||
// Redirect root to admin dashboard (which guards via AdminMiddleware or redirects to login)
|
||||
// Smart root redirect — send users to the right dashboard based on their role
|
||||
Route::get('/', function () {
|
||||
return redirect()->route('admin.dashboard');
|
||||
// Check Laravel's built-in auth (survives refresh via remember cookie)
|
||||
if (auth()->check()) {
|
||||
$role = auth()->user()->role;
|
||||
if ($role === 'employer') {
|
||||
return redirect()->route('employer.dashboard');
|
||||
}
|
||||
if ($role === 'admin') {
|
||||
return redirect()->route('admin.dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check custom session key (registration flow / legacy)
|
||||
$sessionUser = session('user');
|
||||
if ($sessionUser) {
|
||||
$role = is_array($sessionUser) ? ($sessionUser['role'] ?? null) : ($sessionUser->role ?? null);
|
||||
if ($role === 'employer') {
|
||||
return redirect()->route('employer.dashboard');
|
||||
}
|
||||
if ($role === 'admin') {
|
||||
return redirect()->route('admin.dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
// Not logged in — send to admin login as default
|
||||
return redirect()->route('admin.login');
|
||||
});
|
||||
|
||||
// Admin Auth Routes
|
||||
@ -233,29 +257,33 @@
|
||||
Route::post('/tickets/{id}/generate-dev-response', [\App\Http\Controllers\Admin\SupportTicketController::class, 'generateDevResponse'])->name('admin.tickets.generate-dev-response');
|
||||
});
|
||||
|
||||
// Employer Auth Routes
|
||||
Route::get('/employer/login', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showLogin'])->name('employer.login');
|
||||
Route::post('/employer/login', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'login'])->name('employer.login.submit');
|
||||
Route::get('/employer/register', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showRegister'])->name('employer.register');
|
||||
// Employer Auth Routes — guest-only pages redirect logged-in employers to dashboard
|
||||
Route::middleware('employer.guest')->group(function () {
|
||||
Route::get('/employer/login', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showLogin'])->name('employer.login');
|
||||
Route::get('/employer/register', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showRegister'])->name('employer.register');
|
||||
Route::get('/employer/forgot-password', function () {
|
||||
return Inertia::render('Employer/Auth/ForgotPassword');
|
||||
})->name('employer.forgot-password');
|
||||
});
|
||||
|
||||
// Employer Auth POST/flow routes (no guest guard — mid-registration steps allowed)
|
||||
Route::post('/employer/login', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'login'])->name('employer.login.submit');
|
||||
Route::post('/employer/register', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'register'])->name('employer.register.submit');
|
||||
Route::get('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showVerifyEmail'])->name('employer.verify-email');
|
||||
Route::get('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showVerifyEmail'])->name('employer.verify-email');
|
||||
Route::post('/employer/verify-email', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'verifyEmail'])->name('employer.verify-email.submit')->middleware('throttle:5,1');
|
||||
Route::post('/employer/resend-otp', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resendOtp'])->name('employer.resend-otp')->middleware('throttle:3,1');
|
||||
Route::get('/employer/upload-emirates-id', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showUploadEmiratesId'])->name('employer.upload-emirates-id');
|
||||
Route::post('/employer/resend-otp', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resendOtp'])->name('employer.resend-otp')->middleware('throttle:3,1');
|
||||
Route::get('/employer/upload-emirates-id', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showUploadEmiratesId'])->name('employer.upload-emirates-id');
|
||||
Route::post('/employer/upload-emirates-id', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'uploadEmiratesId'])->name('employer.upload-emirates-id.submit');
|
||||
Route::get('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showRegisterPayment'])->name('employer.register-payment');
|
||||
Route::get('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showRegisterPayment'])->name('employer.register-payment');
|
||||
Route::post('/employer/register-payment', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'storeRegisterPayment'])->name('employer.register-payment.submit');
|
||||
Route::get('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showCreatePassword'])->name('employer.create-password');
|
||||
Route::get('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'showCreatePassword'])->name('employer.create-password');
|
||||
Route::post('/employer/create-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'createPassword'])->name('employer.create-password.submit');
|
||||
Route::post('/employer/logout', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'logout'])->name('employer.logout');
|
||||
Route::get('/employer/pending-verification', function () {
|
||||
return Inertia::render('Employer/Auth/PendingVerification');
|
||||
})->name('employer.pending-verification');
|
||||
Route::get('/employer/forgot-password', function () {
|
||||
return Inertia::render('Employer/Auth/ForgotPassword');
|
||||
})->name('employer.forgot-password');
|
||||
Route::post('/employer/forgot-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'forgotPassword'])->name('employer.forgot-password.submit');
|
||||
Route::post('/employer/reset-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resetPassword'])->name('employer.reset-password.submit');
|
||||
Route::post('/employer/reset-password', [\App\Http\Controllers\Employer\EmployerAuthController::class, 'resetPassword'])->name('employer.reset-password.submit');
|
||||
|
||||
// Employer Protected Routes
|
||||
Route::prefix('employer')->middleware(['employer'])->group(function () {
|
||||
|
||||
@ -28,6 +28,7 @@ public function test_employer_web_registration_with_emirates_id()
|
||||
'name' => 'Abdullah Ahmed',
|
||||
'email' => 'abdullah@example.com',
|
||||
'phone' => '501234567',
|
||||
'address' => 'Villa 14, Al Safa, Dubai',
|
||||
]);
|
||||
|
||||
$responseStep1->assertRedirect(route('employer.verify-email'));
|
||||
@ -85,6 +86,7 @@ public function test_employer_web_registration_with_emirates_id()
|
||||
'phone' => '501234567',
|
||||
'company_name' => 'Abdullah Ahmed Household',
|
||||
'emirates_id_front' => null, // File is not stored
|
||||
'address' => 'Villa 14, Al Safa, Dubai',
|
||||
]);
|
||||
|
||||
$profile = EmployerProfile::where('phone', '501234567')->first();
|
||||
@ -95,6 +97,7 @@ public function test_employer_web_registration_with_emirates_id()
|
||||
'email' => 'abdullah@example.com',
|
||||
'mobile' => '501234567',
|
||||
'emirates_id_file' => null, // File is not stored
|
||||
'address' => 'Villa 14, Al Safa, Dubai',
|
||||
]);
|
||||
}
|
||||
|
||||
@ -113,6 +116,7 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
|
||||
'name' => 'Fatima Al-Mansoori',
|
||||
'email' => 'fatima@example.com',
|
||||
'phone' => '509876543',
|
||||
'address' => 'Apartment 402, Al Nahda, Sharjah',
|
||||
]);
|
||||
|
||||
$responseStep1->assertRedirect(route('employer.verify-email'));
|
||||
@ -170,5 +174,6 @@ public function test_employer_web_registration_with_two_sided_emirates_id()
|
||||
$this->assertNotNull($profile);
|
||||
$this->assertEquals('784-1988-5310327-2', $profile->emirates_id_number);
|
||||
$this->assertEquals('2028-04-11', $profile->emirates_id_expiry);
|
||||
$this->assertEquals('Apartment 402, Al Nahda, Sharjah', $profile->address);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1074,4 +1074,100 @@ public function test_get_preferred_location_areas_localized()
|
||||
'success' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test worker registration fails validation if the phone number already exists.
|
||||
*/
|
||||
public function test_register_validation_fails_if_phone_number_already_exists()
|
||||
{
|
||||
// 1. Create a worker with a specific phone number
|
||||
Worker::create([
|
||||
'name' => 'Existing Worker',
|
||||
'email' => 'existing@example.com',
|
||||
'phone' => '+971501234567',
|
||||
'language' => 'HI',
|
||||
'password' => bcrypt('password'),
|
||||
'nationality' => 'Indian',
|
||||
'status' => 'active',
|
||||
'age' => 25,
|
||||
'salary' => 1500,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => 'None',
|
||||
'religion' => 'Not Specified',
|
||||
'bio' => 'Test worker',
|
||||
]);
|
||||
|
||||
// 2. Attempt to register another worker with the same phone number
|
||||
$response = $this->postJson('/api/workers/register', [
|
||||
'name' => 'New Worker',
|
||||
'phone' => '+971501234567',
|
||||
'salary' => 2000,
|
||||
'password' => 'secret123',
|
||||
'language' => 'HI',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
])
|
||||
->assertJsonValidationErrors(['phone']);
|
||||
|
||||
$this->assertEquals(
|
||||
'This mobile number is already registered.',
|
||||
$response->json('errors.phone.0')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test worker registration fails validation if the passport number already exists.
|
||||
*/
|
||||
public function test_register_validation_fails_if_passport_number_already_exists()
|
||||
{
|
||||
// 1. Create a worker and their passport document
|
||||
$worker = Worker::create([
|
||||
'name' => 'First Worker',
|
||||
'email' => 'first@example.com',
|
||||
'phone' => '+971501111111',
|
||||
'language' => 'HI',
|
||||
'password' => bcrypt('password'),
|
||||
'nationality' => 'Indian',
|
||||
'status' => 'active',
|
||||
'age' => 25,
|
||||
'salary' => 1500,
|
||||
'availability' => 'Immediate',
|
||||
'experience' => 'None',
|
||||
'religion' => 'Not Specified',
|
||||
'bio' => 'Test worker',
|
||||
]);
|
||||
|
||||
$worker->documents()->create([
|
||||
'type' => 'passport',
|
||||
'number' => 'SQ1234567',
|
||||
]);
|
||||
|
||||
// 2. Attempt to register another worker with the same passport number
|
||||
$response = $this->postJson('/api/workers/register', [
|
||||
'name' => 'Second Worker',
|
||||
'phone' => '+971502222222',
|
||||
'salary' => 2000,
|
||||
'password' => 'secret123',
|
||||
'language' => 'HI',
|
||||
'passport' => [
|
||||
'passport_number' => 'SQ1234567',
|
||||
]
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJson([
|
||||
'success' => false,
|
||||
'message' => 'Validation error.',
|
||||
])
|
||||
->assertJsonValidationErrors(['passport.passport_number']);
|
||||
|
||||
$this->assertEquals(
|
||||
'This passport number is already registered.',
|
||||
$response->json('errors')['passport.passport_number'][0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
cors: true,
|
||||
hmr: {
|
||||
host: '192.168.0.245',
|
||||
host: '192.168.29.192',
|
||||
},
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user