added support for inviting list collaborators
This commit is contained in:
parent
cd2c8adaa8
commit
0787b75780
21 changed files with 393 additions and 34 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -22,3 +22,4 @@
|
|||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
CLAUDE.md
|
||||
|
|
|
|||
|
|
@ -3,11 +3,14 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\LoginRequest;
|
||||
use App\Http\Requests\PasswordResetRequest;
|
||||
use App\Http\Requests\RegisterRequest;
|
||||
use App\Models\Invitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
|
|
@ -15,13 +18,55 @@ class AuthController extends Controller
|
|||
{
|
||||
$user = User::create($request->validated());
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
$request->session()->regenerate();
|
||||
Password::sendResetLink(['email' => $user->email]);
|
||||
$this->processAcceptedInvitations($user);
|
||||
|
||||
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
|
||||
{
|
||||
if (! Auth::attempt($request->validated())) {
|
||||
|
|
@ -29,8 +74,10 @@ class AuthController extends Controller
|
|||
}
|
||||
|
||||
$request->session()->regenerate();
|
||||
$user = Auth::user();
|
||||
$this->processAcceptedInvitations($user);
|
||||
|
||||
return response()->json(Auth::user());
|
||||
return response()->json($user);
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
|
|
|
|||
|
|
@ -2,11 +2,28 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\CreateInvitationRequest;
|
||||
use App\Mail\ListCollaboratorInvite;
|
||||
use App\Models\Invitation;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
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
|
||||
{
|
||||
private Invitation $invitation;
|
||||
|
||||
public function __construct(Invitation $invitation)
|
||||
{
|
||||
$this->invitation = $invitation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Invitation $invitation)
|
||||
{
|
||||
//
|
||||
return response()->json(['message' => 'Failed to accept invitation', 'status' => 'failed'], 500);
|
||||
}
|
||||
|
||||
$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' => 'Invitation created successfully'], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Invitation $invitation)
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use App\Models\Movie;
|
|||
use App\Models\MovieList;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MovieListController extends Controller
|
||||
|
|
@ -18,7 +19,13 @@ class MovieListController extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
return MovieList::all();
|
||||
$user = Auth::user();
|
||||
|
||||
return response()->json([
|
||||
'movie_lists' => $user->movieLists,
|
||||
'shared_lists' => $user->sharedLists,
|
||||
], 200);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
55
app/Http/Requests/CreateInvitationRequest.php
Normal file
55
app/Http/Requests/CreateInvitationRequest.php
Normal 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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Http/Requests/PasswordResetRequest.php
Normal file
31
app/Http/Requests/PasswordResetRequest.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ class RegisterRequest extends FormRequest
|
|||
return [
|
||||
'username' => 'required|string|max:255|unique:users',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
65
app/Mail/ListCollaboratorInvite.php
Normal file
65
app/Mail/ListCollaboratorInvite.php
Normal 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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -6,5 +6,13 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Invitation extends Model
|
||||
{
|
||||
//
|
||||
const EXPIRATION_DAYS = 7;
|
||||
|
||||
protected $fillable = [
|
||||
'email',
|
||||
'token',
|
||||
'movie_list_id',
|
||||
'status',
|
||||
'expires_at',
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,9 @@ class MovieList extends Model
|
|||
{
|
||||
return $this->belongsToMany(Movie::class);
|
||||
}
|
||||
|
||||
public function collaborators(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'movie_list_user');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ namespace App\Models;
|
|||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
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\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
|
@ -21,7 +23,6 @@ class User extends Authenticatable
|
|||
protected $fillable = [
|
||||
'username',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -34,6 +35,21 @@ class User extends Authenticatable
|
|||
'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.
|
||||
*
|
||||
|
|
@ -46,9 +62,4 @@ class User extends Authenticatable
|
|||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
public function profile(): HasOne
|
||||
{
|
||||
return $this->hasOne(Profile::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class MovieListPolicy
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use App\Interfaces\MovieDbInterface;
|
||||
use App\Models\User;
|
||||
use App\Services\OmdbMovieService;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
|
|
@ -21,6 +23,8 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
ResetPassword::createUrlUsing(function (User $user, string $token) {
|
||||
return config('app.frontend_url')."/auth/reset-password/$token?email=".urlencode($user->email);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class OmdbMovieService implements MovieDbInterface
|
|||
'plot' => $movieDetails->plot,
|
||||
'genre' => $movieDetails->genre,
|
||||
'mpaa_rating' => $movieDetails->mpaaRating,
|
||||
'critic_scores' => json_encode($movieDetails->criticScores),
|
||||
'critic_scores' => $movieDetails->criticScores,
|
||||
'poster' => $movieDetails->poster,
|
||||
'added_by' => auth()->id(),
|
||||
]);
|
||||
|
|
@ -169,7 +169,7 @@ class OmdbMovieService implements MovieDbInterface
|
|||
'plot' => $movieDetails->plot,
|
||||
'genre' => $movieDetails->genre,
|
||||
'mpaa_rating' => $movieDetails->mpaaRating,
|
||||
'critic_scores' => json_encode($movieDetails->criticScores),
|
||||
'critic_scores' => $movieDetails->criticScores,
|
||||
'poster' => $movieDetails->poster,
|
||||
'added_by' => auth()->id(),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ return [
|
|||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
'frontend_url' => env('FRONTEND_URL', 'http://localhost:3000'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ return [
|
|||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'mailgun' => [
|
||||
'transport' => 'mailgun',
|
||||
],
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ return new class extends Migration
|
|||
$table->string('username')->unique();
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->string('password')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ return new class extends Migration
|
|||
$table->string('email');
|
||||
$table->string('token')->unique();
|
||||
$table->foreignId('movie_list_id')->constrained()->cascadeOnDelete();
|
||||
$table->enum('status', ['pending', 'accepted_login_pending', 'accepted', 'declined'])->default('pending');
|
||||
$table->dateTime('expires_at');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
7
resources/views/email/list-collaborator-invite.blade.php
Normal file
7
resources/views/email/list-collaborator-invite.blade.php
Normal 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>
|
||||
|
|
@ -1,19 +1,25 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\InvitationController;
|
||||
use App\Http\Controllers\MovieController;
|
||||
use App\Http\Controllers\MovieListController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// Public auth routes
|
||||
Route::post('/register', [AuthController::class, 'register']);
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
Route::post('/register', [AuthController::class, 'register'])->name('auth.register');
|
||||
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
|
||||
Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::post('/logout', [AuthController::class, 'logout']);
|
||||
Route::get('/user', fn (Request $request) => $request->user());
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('auth.logout');
|
||||
|
||||
// Invitations
|
||||
Route::post('/invitations', [InvitationController::class, 'store'])->name('invitations.store');
|
||||
|
||||
// Movies
|
||||
Route::get('/movies/search/{query}', [MovieController::class, 'search'])->name('movies.search');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue