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();
|
$validated = $request->validated();
|
||||||
$movieList = MovieList::create([
|
$movieList = MovieList::create([
|
||||||
...$validated,
|
...$validated,
|
||||||
'owner' => 1, // auth()->id(),
|
'owner' => auth()->id(),
|
||||||
'slug' => Str::slug($validated['name']),
|
'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>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'username',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
];
|
];
|
||||||
|
|
@ -47,7 +47,8 @@ class User extends Authenticatable
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function profile(): HasOne {
|
public function profile(): HasOne
|
||||||
|
{
|
||||||
return $this->hasOne(Profile::class);
|
return $this->hasOne(Profile::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
//
|
$middleware->statefulApi();
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
$exceptions->renderable(function (MovieNotFoundException $e, Request $request) {
|
$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
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\AuthController;
|
||||||
use App\Http\Controllers\MovieController;
|
use App\Http\Controllers\MovieController;
|
||||||
use App\Http\Controllers\MovieListController;
|
use App\Http\Controllers\MovieListController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/user', function (Request $request) {
|
// Public auth routes
|
||||||
return $request->user();
|
Route::post('/register', [AuthController::class, 'register']);
|
||||||
})->middleware('auth:sanctum');
|
Route::post('/login', [AuthController::class, 'login']);
|
||||||
|
|
||||||
// MOVIES
|
// Authenticated routes
|
||||||
|
Route::middleware('auth:sanctum')->group(function () {
|
||||||
|
Route::post('/logout', [AuthController::class, 'logout']);
|
||||||
|
Route::get('/user', fn (Request $request) => $request->user());
|
||||||
|
|
||||||
|
// Movies
|
||||||
Route::get('/movies/search', [MovieController::class, 'search'])->name('movies.search');
|
Route::get('/movies/search', [MovieController::class, 'search'])->name('movies.search');
|
||||||
|
|
||||||
// MOVIE LISTS
|
// Movie Lists
|
||||||
|
Route::get('/movielists', [MovieListController::class, 'index'])->name('movielists.index');
|
||||||
Route::get('/movielists/{movieList}', [MovieListController::class, 'show'])->name('movielists.show');
|
Route::get('/movielists/{movieList}', [MovieListController::class, 'show'])->name('movielists.show');
|
||||||
Route::post('/movielists', [MovieListController::class, 'store'])->name('movielists.store');
|
Route::post('/movielists', [MovieListController::class, 'store'])->name('movielists.store');
|
||||||
Route::delete('/movielists/{movieList}', [MovieListController::class, 'destroy'])->name('movielists.destroy');
|
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