Merge pull request 'added support for inviting list collaborators' (#2) from movielist-invitations into main

Reviewed-on: tiradoe/movie-night-api-laravel#2
This commit is contained in:
Edward Tirado Jr 2026-04-05 05:45:27 +00:00
commit 985f339725
22 changed files with 470 additions and 50 deletions

1
.gitignore vendored
View file

@ -22,3 +22,4 @@
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml
Thumbs.db Thumbs.db
CLAUDE.md

View file

@ -3,11 +3,14 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\LoginRequest; use App\Http\Requests\LoginRequest;
use App\Http\Requests\PasswordResetRequest;
use App\Http\Requests\RegisterRequest; use App\Http\Requests\RegisterRequest;
use App\Models\Invitation;
use App\Models\User; use App\Models\User;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
class AuthController extends Controller class AuthController extends Controller
{ {
@ -15,13 +18,55 @@ class AuthController extends Controller
{ {
$user = User::create($request->validated()); $user = User::create($request->validated());
Auth::login($user); Password::sendResetLink(['email' => $user->email]);
$this->processAcceptedInvitations($user);
$request->session()->regenerate();
return response()->json($user, 201); return response()->json($user, 201);
} }
private function processAcceptedInvitations(User $user)
{
$invitations = Invitation::query()
->where('status', 'accepted_login_pending')
->where('email', $user->email)
->get();
foreach ($invitations as $invitation) {
$user->sharedLists()->attach($invitation->movie_list_id);
$invitation->update(['status' => 'accepted']);
$invitation->delete();
}
}
public function forgotPassword(Request $request)
{
$request->validate(['email' => 'required|email']);
Password::sendResetLink(['email' => $request->email]);
return response()->json(['message' => 'Password reset link sent!']);
}
public function resetPassword(PasswordResetRequest $request)
{
$updatedUser = null;
$status = Password::reset($request->validated(), function (User $user, string $password) use (&$updatedUser) {
$user->forceFill(['password' => $password])->save();
$updatedUser = $user;
});
if ($status === Password::PASSWORD_RESET && $updatedUser) {
Auth::login($updatedUser);
return response()->json(['message' => 'Password reset successfully.']);
} elseif ($status === Password::INVALID_TOKEN) {
return response()->json(['message' => 'Token expired'], 401);
}
return response()->json(['message' => 'Unable to reset password'], 400);
}
public function login(LoginRequest $request): JsonResponse public function login(LoginRequest $request): JsonResponse
{ {
if (! Auth::attempt($request->validated())) { if (! Auth::attempt($request->validated())) {
@ -29,8 +74,10 @@ class AuthController extends Controller
} }
$request->session()->regenerate(); $request->session()->regenerate();
$user = Auth::user();
$this->processAcceptedInvitations($user);
return response()->json(Auth::user()); return response()->json($user);
} }
public function logout(Request $request): JsonResponse public function logout(Request $request): JsonResponse

View file

@ -2,11 +2,28 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\CreateInvitationRequest;
use App\Mail\ListCollaboratorInvite;
use App\Models\Invitation; use App\Models\Invitation;
use Exception;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Throwable;
class InvitationController extends Controller class InvitationController extends Controller
{ {
private Invitation $invitation;
public function __construct(Invitation $invitation)
{
$this->invitation = $invitation;
}
/** /**
* Display a listing of the resource. * Display a listing of the resource.
*/ */
@ -16,19 +33,32 @@ class InvitationController extends Controller
} }
/** /**
* Store a newly created resource in storage. * Display a listing of the resource.
*/ */
public function store(Request $request) public function accept(Request $request, string $token)
{ {
// try {
} $invitation = $this->invitation::where('token', $token)->firstOrFail();
} catch (ModelNotFoundException $e) {
return response()->json(['message' => 'Invitation not found', 'status' => 'not_found'], 404);
} catch (Throwable $e) {
Log::error('Failed to accept invitation: '.$e->getMessage());
/** return response()->json(['message' => 'Failed to accept invitation', 'status' => 'failed'], 500);
* Display the specified resource. }
*/
public function show(Invitation $invitation) $user = Auth::user();
{ if ($user) {
// $user->sharedLists()->attach($invitation->movie_list_id, ['role' => 'viewer']);
$invitation->update(['status' => 'accepted']);
$invitation->delete();
} else {
$invitation->update(['status' => 'accepted_login_pending']);
return response()->json(['message' => 'Unauthorized', 'status' => 'pending'], 401);
}
return response()->json(['message' => 'Invitation accepted', 'status' => 'accepted']);
} }
/** /**
@ -39,6 +69,54 @@ class InvitationController extends Controller
// //
} }
/**
* Display a listing of the resource.
*/
public function decline()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(CreateInvitationRequest $request)
{
$validated = $request->validated();
$invitations = [];
try {
DB::transaction(function () use ($validated, &$invitations) {
foreach ($validated['emails'] as $email) {
$invitations[] = Invitation::create([
'email' => $email,
'movie_list_id' => $validated['movie_list_id'],
'token' => Str::uuid(),
'expires_at' => now()->addDays(Invitation::EXPIRATION_DAYS),
]);
}
});
} catch (Exception $e) {
logger()->error('Failed to create invitation: '.$e->getMessage());
return response()->json(['message' => 'Failed to create invitations.'], 500);
}
foreach ($invitations as $invitation) {
Mail::to($invitation->email)->queue(new ListCollaboratorInvite(Auth::user(), $invitation));
}
return response()->json(['message' => 'Invitations sent!'], 201);
}
/**
* Display the specified resource.
*/
public function show(Invitation $invitation)
{
//
}
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
*/ */

View file

@ -4,11 +4,13 @@ namespace App\Http\Controllers;
use App\Http\Requests\CreateMovieListRequest; use App\Http\Requests\CreateMovieListRequest;
use App\Http\Requests\UpdateMovieListRequest; use App\Http\Requests\UpdateMovieListRequest;
use App\Http\Resources\MovieListResource;
use App\Interfaces\MovieDbInterface; use App\Interfaces\MovieDbInterface;
use App\Models\Movie; use App\Models\Movie;
use App\Models\MovieList; use App\Models\MovieList;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class MovieListController extends Controller class MovieListController extends Controller
@ -16,9 +18,15 @@ class MovieListController extends Controller
/** /**
* Display a listing of the resource. * Display a listing of the resource.
*/ */
public function index() public function index(): JsonResponse
{ {
return MovieList::all(); $user = Auth::user();
return response()->json([
'movie_lists' => $user->movieLists,
'shared_lists' => $user->sharedLists,
], 200);
} }
/** /**
@ -41,37 +49,36 @@ class MovieListController extends Controller
/** /**
* Display the specified resource. * Display the specified resource.
*/ */
public function show(MovieList $movieList) public function show(MovieList $movieList): MovieListResource
{ {
$this->authorize('view', $movieList); $this->authorize('view', $movieList);
try {
return $movieList->load('movies'); return MovieListResource::make($movieList->load('movies', 'collaborators'));
} catch (ModelNotFoundException $e) {
return response()->json(['message' => 'Movie list not found'], 404);
}
} }
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
*/ */
public function update(UpdateMovieListRequest $request, MovieList $movieList) public function update(UpdateMovieListRequest $request, MovieList $movieList): MovieListResource
{ {
$validated = $request->validated(); $validated = $request->validated();
$movieList->update($validated); $movieList->update($validated);
return response()->json($movieList, 200); return MovieListResource::make($movieList);
} }
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
*/ */
public function destroy(MovieList $movieList) public function destroy(MovieList $movieList): JsonResponse
{ {
$this->authorize('delete', $movieList); $this->authorize('delete', $movieList);
$movieList->delete(); $movieList->delete();
return response()->json(['message', 'Movie list deleted successfully'], 204);
} }
public function addMovie(MovieDbInterface $movieDb, Request $request, MovieList $movieList) public function addMovie(MovieDbInterface $movieDb, Request $request, MovieList $movieList): MovieListResource
{ {
$this->authorize('update', $movieList); $this->authorize('update', $movieList);
$movieResult = $movieDb->find($request->input('movie')['imdbId'], ['type' => 'imdb']); $movieResult = $movieDb->find($request->input('movie')['imdbId'], ['type' => 'imdb']);
@ -80,16 +87,16 @@ class MovieListController extends Controller
$movieList->movies()->attach($movie); $movieList->movies()->attach($movie);
$movieList->load('movies'); $movieList->load('movies');
return response()->json($movieList); return MovieListResource::make($movieList);
} }
public function removeMovie(MovieDbInterface $movieDb, Request $request, MovieList $movieList, Movie $movie) public function removeMovie(Request $request, MovieList $movieList, Movie $movie): MovieListResource
{ {
$this->authorize('update', $movieList); $this->authorize('update', $movieList);
$movieList->movies()->detach($movie); $movieList->movies()->detach($movie);
$movieList->load('movies'); $movieList->load('movies');
return response()->json($movieList); return MovieListResource::make($movieList);
} }
} }

View file

@ -0,0 +1,55 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
class CreateInvitationRequest 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 [
'emails' => 'required|array',
'emails.*' => [
'required',
'email',
// They haven't been invited to the movie list
Rule::unique('invitations', 'email')
->where('movie_list_id', $this->input('movie_list_id')),
// The user isn't already a collaborator
Rule::notIn(
DB::table('movie_list_user')
->join('users', 'users.id', '=', 'movie_list_user.user_id')
->where('movie_list_id', $this->input('movie_list_id'))
->pluck('users.email')
->push($this->user()->email)
->toArray()
),
],
'movie_list_id' => 'required|exists:movie_lists,id',
];
}
public function messages()
{
return [
'emails.*.unique' => 'The email address is already invited to this movie list.',
'emails.*.not_in' => ':input is already a collaborator.',
];
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PasswordResetRequest 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 [
'password' => 'required|string|min:8|confirmed',
'password_confirmation' => 'string',
'token' => 'required|string',
'email' => 'required|email|exists:users,email',
];
}
}

View file

@ -24,7 +24,6 @@ class RegisterRequest extends FormRequest
return [ return [
'username' => 'required|string|max:255|unique:users', 'username' => 'required|string|max:255|unique:users',
'email' => 'required|string|email|max:255|unique:users', 'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]; ];
} }
} }

View file

@ -0,0 +1,42 @@
<?php
namespace App\Http\Resources;
use Auth;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class MovieListResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$user_id = Auth::id();
return [
'id' => $this->id,
'name' => $this->name,
'is_public' => $this->is_public,
'owner' => $this->listOwner->username,
'role' => $this->getRole($this->owner, $user_id),
'collaborators' => $this->whenLoaded('collaborators', fn () => $this->collaborators->map(fn ($user) => [
'username' => $user->username,
'role' => $user->pivot->role,
])),
'movies' => $this->whenLoaded('movies'),
];
}
private function getRole(int $owner_id, int $user_id): ?string
{
if ($owner_id === $user_id) {
return 'owner';
}
return $this->getUserRole($user_id);
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Mail;
use App\Models\Invitation;
use App\Models\MovieList;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ListCollaboratorInvite extends Mailable
{
use Queueable, SerializesModels;
public MovieList $movieList;
public string $acceptUrl;
public string $declineUrl;
/**
* Create a new message instance.
*/
public function __construct(
public User $list_owner,
public Invitation $invitation,
) {
$this->movieList = MovieList::find($invitation->movie_list_id);
$this->acceptUrl = config('app.frontend_url').'/invitations/'.$invitation->token.'/accept';
$this->declineUrl = config('app.frontend_url').'/invitations/'.$invitation->token.'/decline';
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'List Collaborator Invite',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'email.list-collaborator-invite',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View file

@ -6,5 +6,13 @@ use Illuminate\Database\Eloquent\Model;
class Invitation extends Model class Invitation extends Model
{ {
// const EXPIRATION_DAYS = 7;
protected $fillable = [
'email',
'token',
'movie_list_id',
'status',
'expires_at',
];
} }

View file

@ -3,6 +3,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class MovieList extends Model class MovieList extends Model
@ -14,8 +15,30 @@ class MovieList extends Model
'slug', 'slug',
]; ];
protected $with = ['listOwner'];
public function listOwner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner');
}
public function movies(): BelongsToMany public function movies(): BelongsToMany
{ {
return $this->belongsToMany(Movie::class); return $this->belongsToMany(Movie::class);
} }
public function getUserRole($userId)
{
return $this->collaborators()
->where('user_id', $userId)
->first()
?->pivot
->role;
}
public function collaborators(): BelongsToMany
{
return $this->belongsToMany(User::class, 'movie_list_user')
->withPivot('role');
}
} }

View file

@ -4,6 +4,8 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail; // use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
@ -21,7 +23,6 @@ class User extends Authenticatable
protected $fillable = [ protected $fillable = [
'username', 'username',
'email', 'email',
'password',
]; ];
/** /**
@ -34,6 +35,21 @@ class User extends Authenticatable
'remember_token', 'remember_token',
]; ];
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
public function movieLists(): HasMany
{
return $this->hasMany(MovieList::class, 'owner');
}
public function sharedLists(): BelongsToMany
{
return $this->belongsToMany(MovieList::class)->withPivot('role')->withTimestamps();
}
/** /**
* Get the attributes that should be cast. * Get the attributes that should be cast.
* *
@ -46,9 +62,4 @@ class User extends Authenticatable
'password' => 'hashed', 'password' => 'hashed',
]; ];
} }
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
} }

View file

@ -22,7 +22,7 @@ class MovieListPolicy
public function view(User $user, MovieList $movieList): bool public function view(User $user, MovieList $movieList): bool
{ {
if ($movieList->owner === $user->getKey() || $movieList->isPublic) { if ($movieList->owner === $user->getKey() || $movieList->isPublic || $user->sharedLists->contains($movieList)) {
return true; return true;
} }
@ -31,6 +31,7 @@ class MovieListPolicy
public function update(User $user, MovieList $movieList): bool public function update(User $user, MovieList $movieList): bool
{ {
if ($movieList->owner === $user->getKey()) { if ($movieList->owner === $user->getKey()) {
return true; return true;
} }

View file

@ -3,7 +3,9 @@
namespace App\Providers; namespace App\Providers;
use App\Interfaces\MovieDbInterface; use App\Interfaces\MovieDbInterface;
use App\Models\User;
use App\Services\OmdbMovieService; use App\Services\OmdbMovieService;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -21,6 +23,8 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// ResetPassword::createUrlUsing(function (User $user, string $token) {
return config('app.frontend_url')."/auth/reset-password/$token?email=".urlencode($user->email);
});
} }
} }

View file

@ -82,7 +82,7 @@ class OmdbMovieService implements MovieDbInterface
'plot' => $movieDetails->plot, 'plot' => $movieDetails->plot,
'genre' => $movieDetails->genre, 'genre' => $movieDetails->genre,
'mpaa_rating' => $movieDetails->mpaaRating, 'mpaa_rating' => $movieDetails->mpaaRating,
'critic_scores' => json_encode($movieDetails->criticScores), 'critic_scores' => $movieDetails->criticScores,
'poster' => $movieDetails->poster, 'poster' => $movieDetails->poster,
'added_by' => auth()->id(), 'added_by' => auth()->id(),
]); ]);
@ -169,7 +169,7 @@ class OmdbMovieService implements MovieDbInterface
'plot' => $movieDetails->plot, 'plot' => $movieDetails->plot,
'genre' => $movieDetails->genre, 'genre' => $movieDetails->genre,
'mpaa_rating' => $movieDetails->mpaaRating, 'mpaa_rating' => $movieDetails->mpaaRating,
'critic_scores' => json_encode($movieDetails->criticScores), 'critic_scores' => $movieDetails->criticScores,
'poster' => $movieDetails->poster, 'poster' => $movieDetails->poster,
'added_by' => auth()->id(), 'added_by' => auth()->id(),
]); ]);

View file

@ -53,6 +53,7 @@ return [
*/ */
'url' => env('APP_URL', 'http://localhost'), 'url' => env('APP_URL', 'http://localhost'),
'frontend_url' => env('FRONTEND_URL', 'http://localhost:3000'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -36,7 +36,9 @@ return [
*/ */
'mailers' => [ 'mailers' => [
'mailgun' => [
'transport' => 'mailgun',
],
'smtp' => [ 'smtp' => [
'transport' => 'smtp', 'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'), 'scheme' => env('MAIL_SCHEME'),

View file

@ -16,7 +16,7 @@ return new class extends Migration
$table->string('username')->unique(); $table->string('username')->unique();
$table->string('email')->unique(); $table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable(); $table->timestamp('email_verified_at')->nullable();
$table->string('password'); $table->string('password')->nullable();
$table->rememberToken(); $table->rememberToken();
$table->timestamps(); $table->timestamps();
}); });

View file

@ -16,6 +16,7 @@ return new class extends Migration
$table->string('email'); $table->string('email');
$table->string('token')->unique(); $table->string('token')->unique();
$table->foreignId('movie_list_id')->constrained()->cascadeOnDelete(); $table->foreignId('movie_list_id')->constrained()->cascadeOnDelete();
$table->enum('status', ['pending', 'accepted_login_pending', 'accepted', 'declined'])->default('pending');
$table->dateTime('expires_at'); $table->dateTime('expires_at');
$table->softDeletes(); $table->softDeletes();
$table->timestamps(); $table->timestamps();

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('movie_list_user', function (Blueprint $table) {
$table->id();
$table->foreignId('movie_list_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->enum('role', ['viewer', 'editor', 'admin'])->default('viewer');
$table->unique(['movie_list_id', 'user_id']);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('movie_list_user');
}
};

View file

@ -0,0 +1,7 @@
<div>
<h1>Welcome to the fucking show</h1>
<p>You have been invited to collaborate on {{$movieList->name}}</p>
<a href="{{ $acceptUrl }}">Click here to accept</a>
<a href="{{ $declineUrl }}">Click here to decline</a>
</div>

View file

@ -1,26 +1,32 @@
<?php <?php
use App\Http\Controllers\AuthController; use App\Http\Controllers\AuthController;
use App\Http\Controllers\InvitationController;
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\Support\Facades\Route; use Illuminate\Support\Facades\Route;
// Public auth routes // Public auth routes
Route::post('/register', [AuthController::class, 'register']); Route::post('/register', [AuthController::class, 'register'])->name('auth.register');
Route::post('/login', [AuthController::class, 'login']); Route::post('/login', [AuthController::class, 'login'])->name('auth.login');
Route::post('/reset-password', [AuthController::class, 'resetPassword'])->name('auth.reset-password');
Route::post('/forgot-password', [AuthController::class, 'forgotPassword'])->name('auth.forgot-password');
Route::get('/invitations/{token}/accept', [InvitationController::class, 'accept'])->name('invitations.accept');
Route::get('/invitations/{token}/decline', [InvitationController::class, 'decline'])->name('invitations.decline');
// Authenticated routes // Authenticated routes
Route::middleware('auth:sanctum')->group(function () { Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']); Route::post('/logout', [AuthController::class, 'logout'])->name('auth.logout');
Route::get('/user', fn (Request $request) => $request->user());
// Invitations
Route::post('/invitations', [InvitationController::class, 'store'])->name('invitations.store');
// Movies // Movies
Route::get('/movies/search/{query}', [MovieController::class, 'search'])->name('movies.search'); Route::get('/movies/search/{query}', [MovieController::class, 'search'])->name('movies.search');
// Movie Lists // Movie Lists
Route::get('/movielists', [MovieListController::class, 'index'])->name('movielists.index'); Route::get('/movielists', [MovieListController::class, 'index'])->name('movielists.index');
Route::put('/movielists/', [MovieListController::class, 'index'])->name('movielists.index'); Route::put('/movielists/{movieList}', [MovieListController::class, 'update'])->name('movielists.update');
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::post('/movielists/{movieList}/movies', [MovieListController::class, 'addMovie'])->name('movielists.addMovie'); Route::post('/movielists/{movieList}/movies', [MovieListController::class, 'addMovie'])->name('movielists.addMovie');