From 67063c5ea2c503222fec3c2c162b77049eeb3c9a Mon Sep 17 00:00:00 2001 From: KirillNekrasov21012 Date: Mon, 25 Aug 2025 21:54:04 +0300 Subject: [PATCH] hw 18: create api for authorization --- app/Infrastructure/EloquentModels/User.php | 31 +++ .../Controllers/Api/v2/UserController.php | 187 ++++++++++++++++++ .../v2/UserController/UpdateEmailRequest.php | 28 +++ .../v2/UserController/UpdateNameRequest.php | 28 +++ .../UserController/UpdatePasswordRequest.php | 28 +++ database/factories/UserFactory.php | 1 + routes/api.php | 15 +- tests/Feature/Api/v2/AuthControllerTest.php | 85 ++++++++ tests/Feature/Api/v2/UserControllerTest.php | 129 ++++++++++++ 9 files changed, 528 insertions(+), 4 deletions(-) create mode 100644 app/Interfaces/Http/Controllers/Api/v2/UserController.php create mode 100644 app/Interfaces/Http/Requests/Api/v2/UserController/UpdateEmailRequest.php create mode 100644 app/Interfaces/Http/Requests/Api/v2/UserController/UpdateNameRequest.php create mode 100644 app/Interfaces/Http/Requests/Api/v2/UserController/UpdatePasswordRequest.php create mode 100644 tests/Feature/Api/v2/AuthControllerTest.php create mode 100644 tests/Feature/Api/v2/UserControllerTest.php diff --git a/app/Infrastructure/EloquentModels/User.php b/app/Infrastructure/EloquentModels/User.php index 752846e0e..27a1b031c 100644 --- a/app/Infrastructure/EloquentModels/User.php +++ b/app/Infrastructure/EloquentModels/User.php @@ -5,6 +5,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Carbon; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable @@ -46,6 +47,36 @@ protected function casts(): array ]; } + public function getName(): string + { + return $this->name; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getPassword(): string + { + return $this->password; + } + + public function getRole(): string + { + return $this->role; + } + + public function getCreatedAt(): string + { + return Carbon::parse($this->created_at)->format('d.m.Y'); + } + + public function getUpdatedAt(): string + { + return Carbon::parse($this->updated_at)->format('d.m.Y'); + } + protected static function newFactory() { return \Database\Factories\UserFactory::new(); diff --git a/app/Interfaces/Http/Controllers/Api/v2/UserController.php b/app/Interfaces/Http/Controllers/Api/v2/UserController.php new file mode 100644 index 000000000..c3b76c8a9 --- /dev/null +++ b/app/Interfaces/Http/Controllers/Api/v2/UserController.php @@ -0,0 +1,187 @@ +user(); + $response = new WebResponse( + true, + [ + "name" => $user->getName(), + "role" => $user->getRole(), + "email" => $user->getEmail(), + "created_at" => $user->getCreatedAt(), + "updated_at" => $user->getUpdatedAt(), + ], + 'Сведения пользователя', + [], + 200 + ); + } catch (Throwable $th) { + $response = new WebResponse( + false, + null, + $th->getMessage(), + is_null($th->getPrevious()) ? [] : ['error' => $th->getPrevious()->getMessage()], + $th->getCode() + ); + Log::error(__METHOD__ . var_export($response, true)); + } finally { + return response()->json( + $response->toArray(), + $response->statusCode, + [ + 'Content-Type' => 'application/json; charset=utf-8', + 'JSON_UNESCAPED_UNICODE' => true + ], + JSON_UNESCAPED_UNICODE + ); + } + } + + public function updateName(UpdateNameRequest $request): JsonResponse + { + try { + $user = $request->user(); + if ($user->getPassword() === $request->get('newName')) { + throw new Exception('Новое имя совпадает со старым'); + } + $user->update([ + 'name' => $request->get('newName'), + ]); + $response = new WebResponse( + true, + [ + "name" => $user->getName(), + "role" => $user->getRole(), + "email" => $user->getEmail(), + "created_at" => $user->getCreatedAt(), + "updated_at" => $user->getUpdatedAt(), + ], + 'Имя пользователя изменено', + [], + 200 + ); + } catch (Throwable $th) { + $response = new WebResponse( + false, + null, + $th->getMessage(), + is_null($th->getPrevious()) ? [] : ['error' => $th->getPrevious()->getMessage()], + $th->getCode() + ); + Log::error(__METHOD__ . var_export($response, true)); + } finally { + return response()->json( + $response->toArray(), + $response->statusCode, + [ + 'Content-Type' => 'application/json; charset=utf-8', + 'JSON_UNESCAPED_UNICODE' => true + ], + JSON_UNESCAPED_UNICODE + ); + } + } + + public function updateEmail(UpdateEmailRequest $request): JsonResponse + { + try { + $user = $request->user(); + if ($user->getEmail() === $request->get('newEmail')) { + throw new Exception('Новый email совпадает со старым'); + } + $user->update([ + 'email' => $request->get('newEmail'), + ]); + $response = new WebResponse( + true, + [ + "name" => $user->getName(), + "role" => $user->getRole(), + "email" => $user->getEmail(), + "created_at" => $user->getCreatedAt(), + "updated_at" => $user->getUpdatedAt(), + ], + 'Email пользователя изменен', + [], + 200 + ); + } catch (Throwable $th) { + $response = new WebResponse( + false, + null, + $th->getMessage(), + is_null($th->getPrevious()) ? [] : ['error' => $th->getPrevious()->getMessage()], + $th->getCode() + ); + Log::error(__METHOD__ . var_export($response, true)); + } finally { + return response()->json( + $response->toArray(), + $response->statusCode, + [ + 'Content-Type' => 'application/json; charset=utf-8', + 'JSON_UNESCAPED_UNICODE' => true + ], + JSON_UNESCAPED_UNICODE + ); + } + } + + public function updatePassword(UpdatePasswordRequest $request): JsonResponse + { + try { + $user = $request->user(); + if ($user->getPassword() === $request->get('newPassword')) { + throw new Exception('Новый пароль совпадает со старым'); + } + $user->update([ + 'password' => Hash::make($request->get('newPassword')), + ]); + $response = new WebResponse( + true, + [ + 'password' => $user->getPassword(), + ], + 'Пароль пользователя изменен', + [], + 200 + ); + } catch (Throwable $th) { + $response = new WebResponse( + false, + null, + $th->getMessage(), + is_null($th->getPrevious()) ? [] : ['error' => $th->getPrevious()->getMessage()], + $th->getCode() + ); + Log::error(__METHOD__ . var_export($response, true)); + } finally { + return response()->json( + $response->toArray(), + $response->statusCode, + [ + 'Content-Type' => 'application/json; charset=utf-8', + 'JSON_UNESCAPED_UNICODE' => true + ], + JSON_UNESCAPED_UNICODE + ); + } + } +} diff --git a/app/Interfaces/Http/Requests/Api/v2/UserController/UpdateEmailRequest.php b/app/Interfaces/Http/Requests/Api/v2/UserController/UpdateEmailRequest.php new file mode 100644 index 000000000..1a173a55d --- /dev/null +++ b/app/Interfaces/Http/Requests/Api/v2/UserController/UpdateEmailRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'newEmail' => 'required|string|email|max:255', + ]; + } +} diff --git a/app/Interfaces/Http/Requests/Api/v2/UserController/UpdateNameRequest.php b/app/Interfaces/Http/Requests/Api/v2/UserController/UpdateNameRequest.php new file mode 100644 index 000000000..bfd9cb557 --- /dev/null +++ b/app/Interfaces/Http/Requests/Api/v2/UserController/UpdateNameRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'newName' => 'required|string', + ]; + } +} diff --git a/app/Interfaces/Http/Requests/Api/v2/UserController/UpdatePasswordRequest.php b/app/Interfaces/Http/Requests/Api/v2/UserController/UpdatePasswordRequest.php new file mode 100644 index 000000000..77f640202 --- /dev/null +++ b/app/Interfaces/Http/Requests/Api/v2/UserController/UpdatePasswordRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'newPassword' => 'required|string', + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 17273a551..3361cc452 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -32,6 +32,7 @@ public function definition(): array return [ 'name' => fake()->name(), 'email' => fake()->unique()->safeEmail(), + 'role' => 'user', 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), diff --git a/routes/api.php b/routes/api.php index c493c4ffe..42e3758cc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,24 +4,31 @@ use App\Interfaces\Http\Controllers\Api\v1\AuthController as AuthController_v1; use App\Interfaces\Http\Controllers\Api\v2\AreaController as AreaController_v2; use App\Interfaces\Http\Controllers\Api\v2\AuthController as AuthController_v2; +use App\Interfaces\Http\Controllers\Api\v2\UserController; use Illuminate\Support\Facades\Route; -Route::prefix('/v1')->name('v1.')->group(function () { +Route::prefix('/v1')->name('api.v1.')->group(function () { Route::post('/register', [AuthController_v1::class, 'register'])->name('register'); Route::post('/login', [AuthController_v1::class, 'login'])->name('login'); - Route::middleware('auth:api-v1')->name('api.')->group(function () { + Route::middleware('auth:api-v1')->group(function () { Route::apiResource('area', AreaController_v1::class); }); }); -Route::prefix('/v2')->name('v2.')->group(function () { +Route::prefix('/v2')->name('api.v2.')->group(function () { Route::post('/oauth/token', [\Laravel\Passport\Http\Controllers\AccessTokenController::class, 'issueToken'])->name('oauth.token'); Route::post('/register', [AuthController_v2::class, 'register'])->name('register'); Route::post('/login', [AuthController_v2::class, 'login'])->name('login'); - Route::middleware('auth:api-v2')->name('api.')->group(function () { + Route::middleware('auth:api-v2')->group(function () { + Route::prefix('/user')->group(function() { + Route::get('', [UserController::class, 'showProfile'])->name('user.showProfile'); + Route::post('/update_name', [UserController::class, 'updateName'])->name('user.updateName'); + Route::post('/update_email', [UserController::class, 'updateEmail'])->name('user.updateEmail'); + Route::post('/update_password', [UserController::class, 'updatePassword'])->name('user.updatePassword'); + }); Route::apiResource('area', AreaController_v2::class); }); }); diff --git a/tests/Feature/Api/v2/AuthControllerTest.php b/tests/Feature/Api/v2/AuthControllerTest.php new file mode 100644 index 000000000..c6685d776 --- /dev/null +++ b/tests/Feature/Api/v2/AuthControllerTest.php @@ -0,0 +1,85 @@ +setUpTheTestEnvironment(); + + $this->loginUrlBase = route('api.v2.login'); + $this->registerUrlBase = route('api.v2.register'); + $this->withHeaders([ + 'Accept-Language' => 'ru', + ]); + } + + #[Test()] + public function it_registers_user_successfully(): void + { + $payload = [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password123', + ]; + + $response = $this->postJson($this->registerUrlBase, $payload); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'user' => ['id','name','email','created_at','updated_at'], + 'message' + ]); + + $this->assertDatabaseHas('users', [ + 'email' => 'test@example.com', + 'name' => 'Test User', + ]); + + $user = User::where('email', 'test@example.com')->first(); + $this->assertTrue(Hash::check('password123', $user->password)); + } + + #[Test()] + public function it_fails_registration_with_invalid_email(): void + { + $payload = [ + 'name' => 'Test User', + 'email' => 'not-an-email', + 'password' => 'password123', + ]; + $response = $this->postJson($this->registerUrlBase, $payload); + $response->assertStatus(422)->assertJsonValidationErrors(['email']); + } + + #[Test()] + public function it_fails_registration_with_existing_email(): void + { + User::factory()->create(['email' => 'test@example.com']); + + $payload = [ + 'name' => 'Another User', + 'email' => 'test@example.com', + 'password' => 'password123', + ]; + $response = $this->postJson($this->registerUrlBase, $payload); + $response->assertStatus(422)->assertJsonValidationErrors(['email']); + } +} diff --git a/tests/Feature/Api/v2/UserControllerTest.php b/tests/Feature/Api/v2/UserControllerTest.php new file mode 100644 index 000000000..25e7d354a --- /dev/null +++ b/tests/Feature/Api/v2/UserControllerTest.php @@ -0,0 +1,129 @@ +setUpTheTestEnvironment(); + + $this->showProfileUrl = route('api.v2.user.showProfile'); + $this->updateNameUrlBase = route('api.v2.user.updateName'); + $this->updateEmailUrlBase = route('api.v2.user.updateEmail'); + $this->updatePasswordUrlBase = route('api.v2.user.updatePassword'); + $this->withHeaders([ + 'Accept-Language' => 'ru', + ]); + } + + #[Test()] + public function it_returns_user_profile(): void + { + $user = User::factory()->create(); + Passport::actingAs($user, [], 'api-v2'); + $response = $this->getJson($this->showProfileUrl); + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'data' => [ + 'name' => $user->getName(), + 'email' => $user->getEmail(), + ], + ]); + } + + #[Test()] + #[TestWith(['newName', 200])] + #[TestWith([1111, 422])] + public function it_updates_user_name( + string|int $newName, + int $expectedCode + ): void { + $user = User::factory()->create(); + Passport::actingAs($user, [], 'api-v2'); + $response = $this->postJson($this->updateNameUrlBase, [ + 'newName' => $newName, + ]); + if ($expectedCode === 200) { + $response->assertJsonPath('data.name', $newName); + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'name' => $newName, + ]); + } elseif ($expectedCode === 422) { + $response->assertJsonValidationErrors(['newName']); + $this->assertDatabaseMissing('users', [ + 'id' => $user->id, + 'name' => $newName, + ]); + } + } + + #[Test()] + #[TestWith(['newEmail@mail.ru', 200])] + #[TestWith(['newEmail', 422])] + public function it_updates_user_email( + string $newEmail, + int $expectedCode + ): void { + $user = User::factory()->create(); + Passport::actingAs($user, [], 'api-v2'); + $response = $this->postJson($this->updateEmailUrlBase, [ + 'newEmail' => $newEmail, + ]); + if ($expectedCode === 200) { + $response->assertJsonPath('data.email', $newEmail); + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'email' => $newEmail, + ]); + } elseif ($expectedCode === 422) { + $response->assertJsonValidationErrors(['newEmail']); + $this->assertDatabaseMissing('users', [ + 'id' => $user->id, + 'email' => $newEmail, + ]); + } + } + + #[Test()] + #[TestWith(['654321', 200])] + #[TestWith(['', 422])] + public function it_updates_user_password( + string|int $newPassword, + int $expectedCode + ): void { + $user = User::factory()->create(); + Passport::actingAs($user, [], 'api-v2'); + $response = $this->postJson($this->updatePasswordUrlBase, [ + 'newPassword' => $newPassword, + ]); + if ($expectedCode === 200) { + $user->refresh(); + $this->assertTrue(Hash::check($newPassword, $user->getPassword())); + } elseif ($expectedCode === 422) { + $response->assertJsonValidationErrors(['newPassword']); + $user->refresh(); + $this->assertFalse(Hash::check($newPassword, $user->getPassword())); + } + } +}