added auth support

This commit is contained in:
Edward Tirado Jr 2026-02-19 23:14:00 -06:00
parent 67ebbbe329
commit 8970e82780
9 changed files with 289 additions and 14 deletions

View 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.']);
}
}

View file

@ -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']),
]);

View 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',
];
}
}

View 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',
];
}
}

View file

@ -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);
}
}

View file

@ -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
View 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,
];

View file

@ -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
View 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]);
}
}