added auth support
This commit is contained in:
parent
67ebbbe329
commit
8970e82780
9 changed files with 289 additions and 14 deletions
45
app/Http/Controllers/AuthController.php
Normal file
45
app/Http/Controllers/AuthController.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\LoginRequest;
|
||||
use App\Http\Requests\RegisterRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function register(RegisterRequest $request): JsonResponse
|
||||
{
|
||||
$user = User::create($request->validated());
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return response()->json($user, 201);
|
||||
}
|
||||
|
||||
public function login(LoginRequest $request): JsonResponse
|
||||
{
|
||||
if (! Auth::attempt($request->validated())) {
|
||||
return response()->json(['message' => 'Invalid credentials.'], 401);
|
||||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
|
||||
return response()->json(Auth::user());
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return response()->json(['message' => 'Logged out.']);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ class MovieListController extends Controller
|
|||
$validated = $request->validated();
|
||||
$movieList = MovieList::create([
|
||||
...$validated,
|
||||
'owner' => 1, // auth()->id(),
|
||||
'owner' => auth()->id(),
|
||||
'slug' => Str::slug($validated['name']),
|
||||
]);
|
||||
|
||||
|
|
|
|||
29
app/Http/Requests/LoginRequest.php
Normal file
29
app/Http/Requests/LoginRequest.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'email' => 'required|string|email',
|
||||
'password' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
30
app/Http/Requests/RegisterRequest.php
Normal file
30
app/Http/Requests/RegisterRequest.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class RegisterRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => 'required|string|max:255|unique:users',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ class User extends Authenticatable
|
|||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'username',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
|
@ -47,7 +47,8 @@ class User extends Authenticatable
|
|||
];
|
||||
}
|
||||
|
||||
public function profile(): HasOne {
|
||||
public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(Profile::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->statefulApi();
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->renderable(function (MovieNotFoundException $e, Request $request) {
|
||||
|
|
|
|||
34
config/cors.php
Normal file
34
config/cors.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cross-Origin Resource Sharing (CORS) Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure your settings for cross-origin resource sharing
|
||||
| or "CORS". This determines what cross-origin operations may execute
|
||||
| in web browsers. You are free to adjust these settings as needed.
|
||||
|
|
||||
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
'allowed_origins' => explode(',', env('ALLOWED_ORIGINS', '')),
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
'allowed_headers' => ['*'],
|
||||
|
||||
'exposed_headers' => [],
|
||||
|
||||
'max_age' => 0,
|
||||
|
||||
'supports_credentials' => true,
|
||||
|
||||
];
|
||||
|
|
@ -1,18 +1,26 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\MovieController;
|
||||
use App\Http\Controllers\MovieListController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/user', function (Request $request) {
|
||||
return $request->user();
|
||||
})->middleware('auth:sanctum');
|
||||
// Public auth routes
|
||||
Route::post('/register', [AuthController::class, 'register']);
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
|
||||
// MOVIES
|
||||
Route::get('/movies/search', [MovieController::class, 'search'])->name('movies.search');
|
||||
// Authenticated routes
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::post('/logout', [AuthController::class, 'logout']);
|
||||
Route::get('/user', fn (Request $request) => $request->user());
|
||||
|
||||
// MOVIE LISTS
|
||||
Route::get('/movielists/{movieList}', [MovieListController::class, 'show'])->name('movielists.show');
|
||||
Route::post('/movielists', [MovieListController::class, 'store'])->name('movielists.store');
|
||||
Route::delete('/movielists/{movieList}', [MovieListController::class, 'destroy'])->name('movielists.destroy');
|
||||
// Movies
|
||||
Route::get('/movies/search', [MovieController::class, 'search'])->name('movies.search');
|
||||
|
||||
// Movie Lists
|
||||
Route::get('/movielists', [MovieListController::class, 'index'])->name('movielists.index');
|
||||
Route::get('/movielists/{movieList}', [MovieListController::class, 'show'])->name('movielists.show');
|
||||
Route::post('/movielists', [MovieListController::class, 'store'])->name('movielists.store');
|
||||
Route::delete('/movielists/{movieList}', [MovieListController::class, 'destroy'])->name('movielists.destroy');
|
||||
});
|
||||
|
|
|
|||
128
tests/Feature/AuthTest.php
Normal file
128
tests/Feature/AuthTest.php
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_user_can_register_with_valid_data(): void
|
||||
{
|
||||
$response = $this->withHeader('Origin', 'http://localhost')
|
||||
->postJson('/api/register', [
|
||||
'username' => 'johndoe',
|
||||
'email' => 'john@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirmation' => 'password123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
->assertJsonFragment(['username' => 'johndoe', 'email' => 'john@example.com']);
|
||||
|
||||
$this->assertDatabaseHas('users', ['email' => 'john@example.com', 'username' => 'johndoe']);
|
||||
}
|
||||
|
||||
public function test_registration_fails_with_invalid_data(): void
|
||||
{
|
||||
$response = $this->postJson('/api/register', [
|
||||
'username' => '',
|
||||
'email' => 'not-an-email',
|
||||
'password' => 'short',
|
||||
'password_confirmation' => 'mismatch',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['username', 'email', 'password']);
|
||||
}
|
||||
|
||||
public function test_registration_fails_with_duplicate_email(): void
|
||||
{
|
||||
User::factory()->create(['email' => 'john@example.com']);
|
||||
|
||||
$response = $this->postJson('/api/register', [
|
||||
'username' => 'johndoe',
|
||||
'email' => 'john@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirmation' => 'password123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['email']);
|
||||
}
|
||||
|
||||
public function test_registration_fails_with_duplicate_username(): void
|
||||
{
|
||||
User::factory()->create(['username' => 'johndoe']);
|
||||
|
||||
$response = $this->postJson('/api/register', [
|
||||
'username' => 'johndoe',
|
||||
'email' => 'john@example.com',
|
||||
'password' => 'password123',
|
||||
'password_confirmation' => 'password123',
|
||||
]);
|
||||
|
||||
$response->assertStatus(422)
|
||||
->assertJsonValidationErrors(['username']);
|
||||
}
|
||||
|
||||
public function test_user_can_login_with_correct_credentials(): void
|
||||
{
|
||||
User::factory()->create(['email' => 'john@example.com']);
|
||||
|
||||
$response = $this->withHeader('Origin', 'http://localhost')
|
||||
->postJson('/api/login', [
|
||||
'email' => 'john@example.com',
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment(['email' => 'john@example.com']);
|
||||
}
|
||||
|
||||
public function test_login_fails_with_wrong_credentials(): void
|
||||
{
|
||||
User::factory()->create(['email' => 'john@example.com']);
|
||||
|
||||
$response = $this->withHeader('Origin', 'http://localhost')
|
||||
->postJson('/api/login', [
|
||||
'email' => 'john@example.com',
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response->assertStatus(401)
|
||||
->assertJsonFragment(['message' => 'Invalid credentials.']);
|
||||
}
|
||||
|
||||
public function test_authenticated_user_can_logout(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withHeader('Origin', 'http://localhost')
|
||||
->postJson('/api/logout');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment(['message' => 'Logged out.']);
|
||||
}
|
||||
|
||||
public function test_unauthenticated_user_cannot_access_protected_routes(): void
|
||||
{
|
||||
$response = $this->getJson('/api/user');
|
||||
|
||||
$response->assertStatus(401);
|
||||
}
|
||||
|
||||
public function test_authenticated_user_can_access_user_endpoint(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->getJson('/api/user');
|
||||
|
||||
$response->assertOk()
|
||||
->assertJsonFragment(['email' => $user->email]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue