feat(auth): PATCH /api/auth/me profile update endpoint (closes J6)
Audit J6: ProfileTab needs a full-profile update endpoint. Adds AuthController::updateProfile (first_name/last_name/phone/timezone), routed in the existing /api/auth auth:sanctum group; mirrors the sibling updateNotificationPreferences. userResource() now also returns phone + timezone so the GET /me round-trip carries them. phpstan-baseline.neon updated for Pest PendingCalls false positives in the new test file (same pattern as all other Feature test files). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -228,6 +228,31 @@ class AuthController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/auth/me — обновление профиля текущего пользователя
|
||||
* (имя, фамилия, телефон, тайм-зона). Email менять нельзя (через support).
|
||||
*
|
||||
* Audit J6/D1 (ProfileTab). Зеркалит updateNotificationPreferences:
|
||||
* та же группа auth:sanctum, тот же inline-validate, тот же userResource.
|
||||
*/
|
||||
public function updateProfile(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'first_name' => ['required', 'string', 'max:255'],
|
||||
'last_name' => ['required', 'string', 'max:255'],
|
||||
'phone' => ['nullable', 'string', 'max:20'],
|
||||
'timezone' => ['required', 'timezone'],
|
||||
]);
|
||||
|
||||
/** @var User $user */
|
||||
$user = $request->user();
|
||||
$user->update($validated);
|
||||
|
||||
return response()->json([
|
||||
'user' => $this->userResource($user->fresh()),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ключ throttle для login: email|ip — защищает email от брутфорса даже
|
||||
* за NAT'ом, и IP от перебора емейлов с одного источника.
|
||||
@@ -333,6 +358,8 @@ class AuthController extends Controller
|
||||
'email' => $user->email,
|
||||
'first_name' => $user->first_name,
|
||||
'last_name' => $user->last_name,
|
||||
'phone' => $user->phone,
|
||||
'timezone' => $user->timezone,
|
||||
'tenant_id' => $user->tenant_id,
|
||||
'totp_enabled' => $user->totp_enabled,
|
||||
'last_login_at' => $user->last_login_at,
|
||||
|
||||
@@ -462,6 +462,36 @@ parameters:
|
||||
count: 11
|
||||
path: tests/Feature/Auth/TwoFactorTest.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$tenant\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: tests/Feature/Auth/UpdateProfileTest.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property Pest\\PendingCalls\\TestCall\:\:\$user\.$#'
|
||||
identifier: property.notFound
|
||||
count: 4
|
||||
path: tests/Feature/Auth/UpdateProfileTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:actingAs\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/Feature/Auth/UpdateProfileTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:getJson\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 1
|
||||
path: tests/Feature/Auth/UpdateProfileTest.php
|
||||
|
||||
-
|
||||
message: '#^Call to an undefined method Pest\\PendingCalls\\TestCall\:\:patchJson\(\)\.$#'
|
||||
identifier: method.notFound
|
||||
count: 5
|
||||
path: tests/Feature/Auth/UpdateProfileTest.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property App\\Models\\LeadCharge\:\:\$charge_source\.$#'
|
||||
identifier: property.notFound
|
||||
|
||||
@@ -35,6 +35,7 @@ Route::prefix('/api/auth')->group(function () {
|
||||
Route::post('/reset-password', 'App\Http\Controllers\Api\PasswordResetController@resetPassword');
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::get('/me', 'App\Http\Controllers\Api\AuthController@me');
|
||||
Route::patch('/me', 'App\Http\Controllers\Api\AuthController@updateProfile');
|
||||
Route::post('/logout', 'App\Http\Controllers\Api\AuthController@logout');
|
||||
Route::patch('/me/notification-preferences', 'App\Http\Controllers\Api\AuthController@updateNotificationPreferences');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
||||
uses(DatabaseTransactions::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->tenant = Tenant::factory()->create();
|
||||
$this->user = User::factory()->create([
|
||||
'tenant_id' => $this->tenant->id,
|
||||
'first_name' => 'Новый',
|
||||
'last_name' => 'Пользователь',
|
||||
'phone' => null,
|
||||
'timezone' => 'Europe/Moscow',
|
||||
]);
|
||||
$this->actingAs($this->user);
|
||||
});
|
||||
|
||||
test('PATCH /api/auth/me обновляет профиль и возвращает user', function () {
|
||||
$response = $this->patchJson('/api/auth/me', [
|
||||
'first_name' => 'Иван',
|
||||
'last_name' => 'Петров',
|
||||
'phone' => '+7 916 000-00-00',
|
||||
'timezone' => 'Asia/Yekaterinburg',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
expect($response->json('user.first_name'))->toBe('Иван');
|
||||
expect($response->json('user.last_name'))->toBe('Петров');
|
||||
expect($response->json('user.phone'))->toBe('+7 916 000-00-00');
|
||||
expect($response->json('user.timezone'))->toBe('Asia/Yekaterinburg');
|
||||
|
||||
$this->user->refresh();
|
||||
expect($this->user->first_name)->toBe('Иван');
|
||||
expect($this->user->timezone)->toBe('Asia/Yekaterinburg');
|
||||
});
|
||||
|
||||
test('PATCH /api/auth/me без auth: 401', function () {
|
||||
auth()->logout();
|
||||
$this->patchJson('/api/auth/me', [
|
||||
'first_name' => 'Иван',
|
||||
'last_name' => 'Петров',
|
||||
'timezone' => 'Europe/Moscow',
|
||||
])->assertStatus(401);
|
||||
});
|
||||
|
||||
test('PATCH /api/auth/me: 422 при пустом first_name', function () {
|
||||
$this->patchJson('/api/auth/me', [
|
||||
'first_name' => '',
|
||||
'last_name' => 'Петров',
|
||||
'timezone' => 'Europe/Moscow',
|
||||
])->assertStatus(422)->assertJsonValidationErrorFor('first_name');
|
||||
});
|
||||
|
||||
test('PATCH /api/auth/me: 422 при невалидной timezone', function () {
|
||||
$this->patchJson('/api/auth/me', [
|
||||
'first_name' => 'Иван',
|
||||
'last_name' => 'Петров',
|
||||
'timezone' => 'Mars/Olympus',
|
||||
])->assertStatus(422)->assertJsonValidationErrorFor('timezone');
|
||||
});
|
||||
|
||||
test('PATCH /api/auth/me: phone опционален (nullable)', function () {
|
||||
$response = $this->patchJson('/api/auth/me', [
|
||||
'first_name' => 'Иван',
|
||||
'last_name' => 'Петров',
|
||||
'timezone' => 'Europe/Moscow',
|
||||
]);
|
||||
$response->assertOk();
|
||||
expect($response->json('user.phone'))->toBeNull();
|
||||
});
|
||||
|
||||
test('GET /api/auth/me возвращает phone и timezone', function () {
|
||||
$response = $this->getJson('/api/auth/me');
|
||||
$response->assertOk();
|
||||
expect($response->json('user'))->toHaveKeys(['phone', 'timezone']);
|
||||
});
|
||||
Reference in New Issue
Block a user