diff --git a/app/Http/Controllers/Api/EmployerProfileController.php b/app/Http/Controllers/Api/EmployerProfileController.php index 322acd0..62ce416 100644 --- a/app/Http/Controllers/Api/EmployerProfileController.php +++ b/app/Http/Controllers/Api/EmployerProfileController.php @@ -396,4 +396,70 @@ public function getDashboard(Request $request) } } + /** + * Change employer's password. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function changePassword(Request $request) + { + /** @var User $employer */ + $employer = $request->attributes->get('employer'); + + $validator = Validator::make($request->all(), [ + 'current_password' => 'required|string', + 'new_password' => 'required|string|min:8|confirmed', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + if (!Hash::check($request->current_password, $employer->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => [ + 'current_password' => ['The current password is incorrect.'] + ] + ], 422); + } + + try { + $hashedPassword = Hash::make($request->new_password); + + // Update user table + $employer->update([ + 'password' => $hashedPassword + ]); + + // Sync with corresponding sponsor record if found + $matchingSponsor = \App\Models\Sponsor::where('email', $employer->email)->first(); + if ($matchingSponsor) { + $matchingSponsor->update([ + 'password' => $hashedPassword + ]); + } + + return response()->json([ + 'success' => true, + 'message' => 'Password changed successfully.' + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Employer Change Password Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while changing password.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + } diff --git a/app/Http/Controllers/Api/SponsorController.php b/app/Http/Controllers/Api/SponsorController.php index 79db18b..cea8fd7 100644 --- a/app/Http/Controllers/Api/SponsorController.php +++ b/app/Http/Controllers/Api/SponsorController.php @@ -326,4 +326,72 @@ public function getProfile(Request $request) ] ], 200); } + + /** + * Change sponsor's password. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function changePassword(Request $request) + { + /** @var Sponsor $sponsor */ + $sponsor = $request->attributes->get('sponsor'); + + $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [ + 'current_password' => 'required|string', + 'new_password' => 'required|string|min:8|confirmed', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + if (!\Illuminate\Support\Facades\Hash::check($request->current_password, $sponsor->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => [ + 'current_password' => ['The current password is incorrect.'] + ] + ], 422); + } + + try { + $hashedPassword = \Illuminate\Support\Facades\Hash::make($request->new_password); + + // Update sponsor table + $sponsor->update([ + 'password' => $hashedPassword + ]); + + // Sync with corresponding employer user record if found + $matchingEmployer = \App\Models\User::where('email', $sponsor->email) + ->where('role', 'employer') + ->first(); + if ($matchingEmployer) { + $matchingEmployer->update([ + 'password' => $hashedPassword + ]); + } + + return response()->json([ + 'success' => true, + 'message' => 'Password changed successfully.' + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Sponsor Change Password Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while changing password.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } } diff --git a/app/Http/Controllers/Api/WorkerProfileController.php b/app/Http/Controllers/Api/WorkerProfileController.php index 3af8bc4..bb006b4 100644 --- a/app/Http/Controllers/Api/WorkerProfileController.php +++ b/app/Http/Controllers/Api/WorkerProfileController.php @@ -669,6 +669,61 @@ public function getDashboard(Request $request) } } + /** + * Change worker's password. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function changePassword(Request $request) + { + /** @var Worker $worker */ + $worker = $request->attributes->get('worker'); + + $validator = Validator::make($request->all(), [ + 'current_password' => 'required|string', + 'new_password' => 'required|string|min:8|confirmed', + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => $validator->errors() + ], 422); + } + + if (!\Illuminate\Support\Facades\Hash::check($request->current_password, $worker->password)) { + return response()->json([ + 'success' => false, + 'message' => 'Validation error.', + 'errors' => [ + 'current_password' => ['The current password is incorrect.'] + ] + ], 422); + } + + try { + $worker->update([ + 'password' => \Illuminate\Support\Facades\Hash::make($request->new_password) + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Password changed successfully.' + ], 200); + + } catch (\Exception $e) { + logger()->error('Mobile Worker Change Password Failure: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'An error occurred while changing password.', + 'error' => app()->environment('local') ? $e->getMessage() : 'Internal Server Error' + ], 500); + } + } + /** * E.g. "14/05/1990" -> "1990-05-14" */ diff --git a/public/swagger.json b/public/swagger.json index eb90e2f..7005276 100644 --- a/public/swagger.json +++ b/public/swagger.json @@ -5793,6 +5793,306 @@ } } } + }, + "/workers/change-password": { + "post": { + "tags": [ + "Worker/Profile" + ], + "summary": "Change Password", + "description": "Securely changes the user's password by validating the current password and confirming the new password.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "current_password", + "new_password", + "new_password_confirmation" + ], + "properties": { + "current_password": { + "type": "string", + "format": "password", + "description": "The current login password. (REQUIRED)", + "example": "OldPassword@123" + }, + "new_password": { + "type": "string", + "format": "password", + "description": "The new secure password (minimum 8 characters). (REQUIRED)", + "example": "NewPassword@123" + }, + "new_password_confirmation": { + "type": "string", + "format": "password", + "description": "Confirmation of the new password. (REQUIRED)", + "example": "NewPassword@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password changed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Password changed successfully." + } + } + } + } + } + }, + "422": { + "description": "Validation error (e.g. invalid current password, password too short, or mismatch).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Validation error." + }, + "errors": { + "type": "object", + "properties": { + "current_password": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "The current password is incorrect." + ] + } + } + } + } + } + } + } + } + } + } + }, + "/employers/change-password": { + "post": { + "tags": [ + "Employer/Profile" + ], + "summary": "Change Password", + "description": "Securely changes the user's password by validating the current password and confirming the new password.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "current_password", + "new_password", + "new_password_confirmation" + ], + "properties": { + "current_password": { + "type": "string", + "format": "password", + "description": "The current login password. (REQUIRED)", + "example": "OldPassword@123" + }, + "new_password": { + "type": "string", + "format": "password", + "description": "The new secure password (minimum 8 characters). (REQUIRED)", + "example": "NewPassword@123" + }, + "new_password_confirmation": { + "type": "string", + "format": "password", + "description": "Confirmation of the new password. (REQUIRED)", + "example": "NewPassword@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password changed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Password changed successfully." + } + } + } + } + } + }, + "422": { + "description": "Validation error (e.g. invalid current password, password too short, or mismatch).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Validation error." + }, + "errors": { + "type": "object", + "properties": { + "current_password": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "The current password is incorrect." + ] + } + } + } + } + } + } + } + } + } + } + }, + "/sponsors/change-password": { + "post": { + "tags": [ + "Sponsor" + ], + "summary": "Change Password", + "description": "Securely changes the user's password by validating the current password and confirming the new password.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "current_password", + "new_password", + "new_password_confirmation" + ], + "properties": { + "current_password": { + "type": "string", + "format": "password", + "description": "The current login password. (REQUIRED)", + "example": "OldPassword@123" + }, + "new_password": { + "type": "string", + "format": "password", + "description": "The new secure password (minimum 8 characters). (REQUIRED)", + "example": "NewPassword@123" + }, + "new_password_confirmation": { + "type": "string", + "format": "password", + "description": "Confirmation of the new password. (REQUIRED)", + "example": "NewPassword@123" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Password changed successfully.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Password changed successfully." + } + } + } + } + } + }, + "422": { + "description": "Validation error (e.g. invalid current password, password too short, or mismatch).", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Validation error." + }, + "errors": { + "type": "object", + "properties": { + "current_password": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "The current password is incorrect." + ] + } + } + } + } + } + } + } + } + } + } } }, "components": { diff --git a/routes/api.php b/routes/api.php index 910551d..c38459d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -75,6 +75,7 @@ Route::post('/workers/profile/mark-hired', [WorkerProfileController::class, 'markHired']); Route::get('/workers/dashboard/views', [WorkerProfileController::class, 'getProfileViews']); Route::get('/workers/dashboard', [WorkerProfileController::class, 'getDashboard']); + Route::post('/workers/change-password', [WorkerProfileController::class, 'changePassword']); @@ -110,6 +111,7 @@ Route::get('/employers/profile', [EmployerProfileController::class, 'getProfile']); Route::post('/employers/profile/update', [EmployerProfileController::class, 'updateProfile']); Route::get('/employers/dashboard', [EmployerProfileController::class, 'getDashboard']); + Route::post('/employers/change-password', [EmployerProfileController::class, 'changePassword']); // Messaging Management Route::get('/employers/conversations', [EmployerMessageController::class, 'getConversations']); @@ -160,4 +162,5 @@ Route::get('/sponsors/dashboard', [SponsorController::class, 'getDashboard']); Route::get('/sponsors/charity-events', [SponsorController::class, 'getCharityEvents']); Route::post('/sponsors/charity-events', [SponsorController::class, 'postCharityEvent']); + Route::post('/sponsors/change-password', [SponsorController::class, 'changePassword']); }); diff --git a/tests/Feature/EmployerProfileApiTest.php b/tests/Feature/EmployerProfileApiTest.php index 01540d5..a7adec4 100644 --- a/tests/Feature/EmployerProfileApiTest.php +++ b/tests/Feature/EmployerProfileApiTest.php @@ -288,4 +288,57 @@ public function test_employer_dashboard_returns_total_workers_active() $response->assertStatus(200); $this->assertEquals(3, $response->json('data.stats.total_workers')); } + + /** + * Test employer can change password successfully. + */ + public function test_employer_can_change_password() + { + // 1. Create a matching sponsor + $sponsor = \App\Models\Sponsor::create([ + 'full_name' => 'Employer One', + 'email' => $this->employer->email, + 'mobile' => '+971509990011', + 'organization_name' => 'Elite Services', + 'address' => 'Dubai', + 'role' => 'sponsor', + 'password' => bcrypt('Password@123'), + ]); + + // Verify initial password hash is set + $this->employer->update([ + 'password' => bcrypt('Password@123') + ]); + + // 2. Try with wrong current password + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/employers/change-password', [ + 'current_password' => 'WrongPassword', + 'new_password' => 'NewPassword@123', + 'new_password_confirmation' => 'NewPassword@123', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['current_password']); + + // 3. Try with correct current password + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $this->token, + ])->postJson('/api/employers/change-password', [ + 'current_password' => 'Password@123', + 'new_password' => 'NewPassword@123', + 'new_password_confirmation' => 'NewPassword@123', + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'message' => 'Password changed successfully.' + ]); + + // Verify password is updated for both User and Sponsor + $this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $this->employer->fresh()->password)); + $this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $sponsor->fresh()->password)); + } } diff --git a/tests/Feature/SponsorAuthApiTest.php b/tests/Feature/SponsorAuthApiTest.php index 2c0ef96..9b28dbb 100644 --- a/tests/Feature/SponsorAuthApiTest.php +++ b/tests/Feature/SponsorAuthApiTest.php @@ -603,4 +603,58 @@ public function test_sponsor_forgot_password_user_not_found() 'message' => 'user not found this email id', ]); } + + /** + * Test sponsor can change password successfully. + */ + public function test_sponsor_can_change_password() + { + $sponsor = Sponsor::create([ + 'full_name' => 'Sponsor Changer', + 'email' => 'changer.sponsor@example.com', + 'mobile' => '+971509997788', + 'password' => Hash::make('Password@123'), + 'api_token' => 'sponsor-changer-token', + 'status' => 'active', + ]); + + // Sync with corresponding employer user record to test sync + $user = User::create([ + 'name' => 'Sponsor Changer', + 'email' => 'changer.sponsor@example.com', + 'password' => Hash::make('Password@123'), + 'role' => 'employer', + ]); + + // 1. Try with wrong current password + $response = $this->withHeaders([ + 'Authorization' => 'Bearer sponsor-changer-token', + ])->postJson('/api/sponsors/change-password', [ + 'current_password' => 'WrongPassword', + 'new_password' => 'NewPassword@123', + 'new_password_confirmation' => 'NewPassword@123', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['current_password']); + + // 2. Try with correct current password + $response = $this->withHeaders([ + 'Authorization' => 'Bearer sponsor-changer-token', + ])->postJson('/api/sponsors/change-password', [ + 'current_password' => 'Password@123', + 'new_password' => 'NewPassword@123', + 'new_password_confirmation' => 'NewPassword@123', + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'message' => 'Password changed successfully.' + ]); + + // Verify password is updated for both Sponsor and User + $this->assertTrue(Hash::check('NewPassword@123', $sponsor->fresh()->password)); + $this->assertTrue(Hash::check('NewPassword@123', $user->fresh()->password)); + } } diff --git a/tests/Feature/WorkerJourneyApiTest.php b/tests/Feature/WorkerJourneyApiTest.php index a3c2f5b..dfdf483 100644 --- a/tests/Feature/WorkerJourneyApiTest.php +++ b/tests/Feature/WorkerJourneyApiTest.php @@ -1540,4 +1540,55 @@ public function test_profile_response_does_not_contain_deprecated_fields() $this->assertArrayNotHasKey('city', $json); $this->assertArrayNotHasKey('area', $json); } + + /** + * Test worker can change password successfully. + */ + public function test_worker_can_change_password() + { + $worker = Worker::create([ + 'name' => 'Password Changer', + 'email' => 'changer@example.com', + 'phone' => '+971509990099', + 'language' => 'HI', + 'password' => bcrypt('OldPassword@123'), + 'nationality' => 'Indian', + 'age' => 25, + 'salary' => 1500, + 'availability' => 'Immediate', + 'experience' => 'None', + 'religion' => 'Christian', + 'bio' => 'Some Bio', + 'api_token' => 'changer-token', + ]); + + // 1. Invalid current password + $response = $this->withHeaders([ + 'Authorization' => 'Bearer changer-token', + ])->postJson('/api/workers/change-password', [ + 'current_password' => 'WrongPassword', + 'new_password' => 'NewPassword@123', + 'new_password_confirmation' => 'NewPassword@123', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['current_password']); + + // 2. Successful change + $response = $this->withHeaders([ + 'Authorization' => 'Bearer changer-token', + ])->postJson('/api/workers/change-password', [ + 'current_password' => 'OldPassword@123', + 'new_password' => 'NewPassword@123', + 'new_password_confirmation' => 'NewPassword@123', + ]); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'message' => 'Password changed successfully.' + ]); + + $this->assertTrue(\Illuminate\Support\Facades\Hash::check('NewPassword@123', $worker->fresh()->password)); + } }