diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 009afd92c..74c010948 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -171,14 +171,15 @@ class RegisterController extends Controller $invitedBy = 0; $invitation = null; - if (!empty($inviteCode)) { + if (! empty($inviteCode)) { $invitation = Invitation::findValidByToken($inviteCode); if ($invitation) { $invitedBy = $invitation->invited_by; // Validate email matches invitation - if (!empty($invitation->email) && $invitation->email !== $email) { + if (! empty($invitation->email) && $invitation->email !== $email) { $error = 'Email address does not match the invitation.'; + return $this->showRegistrationForm($request, $error); } } @@ -275,7 +276,7 @@ class RegisterController extends Controller // Pre-fill email if invitation has one $invitation = Invitation::findValidByToken($inviteCode); - if ($invitation && !empty($invitation->email)) { + if ($invitation && ! empty($invitation->email)) { app('smarty.view')->assign('email', $invitation->email); } } else { @@ -323,12 +324,12 @@ class RegisterController extends Controller $invitation = Invitation::findValidByToken($token); - if (!$invitation) { + if (! $invitation) { return false; } // If invitation has specific email, validate it matches - if (!empty($invitation->email) && $invitation->email !== $email) { + if (! empty($invitation->email) && $invitation->email !== $email) { return false; } @@ -345,6 +346,7 @@ class RegisterController extends Controller } $invitation = Invitation::findValidByToken($token); + return $invitation !== null; } } diff --git a/app/Http/Controllers/InvitationController.php b/app/Http/Controllers/InvitationController.php index 2f2bfbf1f..263b2ac4a 100644 --- a/app/Http/Controllers/InvitationController.php +++ b/app/Http/Controllers/InvitationController.php @@ -4,9 +4,9 @@ namespace App\Http\Controllers; use App\Models\Invitation; use App\Services\InvitationService; -use Illuminate\Http\Request; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; class InvitationController extends BasePageController { @@ -137,7 +137,7 @@ class InvitationController extends BasePageController $request->validate([ 'email' => 'required|email|unique:users,email', 'expiry_days' => 'sometimes|integer|min:1|max:30', - 'role' => 'sometimes|integer|in:' . implode(',', array_keys(config('nntmux.user_roles', []))), + 'role' => 'sometimes|integer|in:'.implode(',', array_keys(config('nntmux.user_roles', []))), ]); try { @@ -156,7 +156,7 @@ class InvitationController extends BasePageController ); return redirect()->route('invitations.index') - ->with('success', 'Invitation sent successfully to ' . $request->email); + ->with('success', 'Invitation sent successfully to '.$request->email); } catch (\Exception $e) { return redirect()->back() @@ -282,6 +282,7 @@ class InvitationController extends BasePageController public function stats(): JsonResponse { $stats = $this->invitationService->getUserInvitationStats(auth()->id()); + return response()->json($stats); } @@ -291,7 +292,7 @@ class InvitationController extends BasePageController public function cleanup(): JsonResponse { // Check if user is admin - if (!auth()->user()->hasRole('admin')) { + if (! auth()->user()->hasRole('admin')) { abort(403, 'Unauthorized'); } @@ -299,7 +300,7 @@ class InvitationController extends BasePageController return response()->json([ 'message' => "Cleaned up {$cleanedCount} expired invitations", - 'count' => $cleanedCount + 'count' => $cleanedCount, ]); } } diff --git a/app/Mail/InvitationMail.php b/app/Mail/InvitationMail.php index f59657ddd..ada8e5756 100644 --- a/app/Mail/InvitationMail.php +++ b/app/Mail/InvitationMail.php @@ -29,7 +29,7 @@ class InvitationMail extends Mailable public function envelope(): Envelope { return new Envelope( - subject: 'You\'re invited to join ' . config('app.name'), + subject: 'You\'re invited to join '.config('app.name'), ); } diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 1ba39f651..7e938c718 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -2,7 +2,6 @@ namespace App\Models; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -37,6 +36,7 @@ use Illuminate\Support\Str; class Invitation extends Model { public const DEFAULT_INVITES = 1; + public const DEFAULT_INVITE_EXPIRY_DAYS = 7; /** @@ -154,7 +154,7 @@ class Invitation extends Model */ public function isUsed(): bool { - return !is_null($this->used_at); + return ! is_null($this->used_at); } /** @@ -175,6 +175,7 @@ class Invitation extends Model public function markAsExpired(): bool { $this->is_active = false; + return $this->save(); } @@ -237,6 +238,7 @@ class Invitation extends Model if ($invitation) { return $invitation->delete(); } + return false; } diff --git a/app/Services/InvitationService.php b/app/Services/InvitationService.php index 1ecfa91cb..297da5bf6 100644 --- a/app/Services/InvitationService.php +++ b/app/Services/InvitationService.php @@ -2,9 +2,9 @@ namespace App\Services; +use App\Mail\InvitationMail; use App\Models\Invitation; use App\Models\User; -use App\Mail\InvitationMail; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\ValidationException; @@ -22,7 +22,7 @@ class InvitationService ): Invitation { // Get the user sending the invitation $user = User::find($invitedBy); - if (!$user) { + if (! $user) { throw new \Exception('User not found.'); } @@ -36,12 +36,12 @@ class InvitationService // Check if user has invites available (total invites - active pending invitations) $availableInvites = $user->invites - $activeInvitations; if ($availableInvites <= 0) { - throw new \Exception('You have no invitations available. You have ' . $activeInvitations . ' pending invitation(s). Contact an administrator if you need more invitations.'); + throw new \Exception('You have no invitations available. You have '.$activeInvitations.' pending invitation(s). Contact an administrator if you need more invitations.'); } // Validate email $validator = Validator::make(['email' => $email], [ - 'email' => 'required|email|unique:users,email' + 'email' => 'required|email|unique:users,email', ]); if ($validator->fails()) { @@ -81,7 +81,7 @@ class InvitationService { $invitation = Invitation::findOrFail($invitationId); - if (!$invitation->isValid()) { + if (! $invitation->isValid()) { throw new \Exception('Cannot resend an invalid invitation.'); } @@ -102,6 +102,7 @@ class InvitationService } $invitation->is_active = false; + return $invitation->save(); } @@ -112,7 +113,7 @@ class InvitationService { $invitation = Invitation::findValidByToken($token); - if (!$invitation) { + if (! $invitation) { throw new \Exception('Invalid or expired invitation token.'); } @@ -158,7 +159,7 @@ class InvitationService ->with(['usedBy']) ->orderBy('created_at', 'desc'); - return match($status) { + return match ($status) { 'valid' => $query->valid()->paginate(15), 'used' => $query->used()->paginate(15), 'expired' => $query->expired()->paginate(15), @@ -178,13 +179,14 @@ class InvitationService /** * Check if a user can send more invitations */ - public function canUserSendInvitation(int $userId, int $maxInvitations = null): bool + public function canUserSendInvitation(int $userId, ?int $maxInvitations = null): bool { if ($maxInvitations === null) { return true; // No limit set } $sentCount = Invitation::where('invited_by', $userId)->count(); + return $sentCount < $maxInvitations; } @@ -195,7 +197,7 @@ class InvitationService { $invitation = Invitation::findByToken($token); - if (!$invitation) { + if (! $invitation) { return null; } @@ -216,7 +218,7 @@ class InvitationService public function getUserInvitationDetails(int $userId): array { $user = User::find($userId); - if (!$user) { + if (! $user) { return []; } @@ -253,7 +255,7 @@ class InvitationService 'expired_invitations' => $expiredInvitations, 'cancelled_invitations' => $cancelledInvitations, 'calculated_available' => $availableInvites, - 'can_send_invite' => $availableInvites > 0 + 'can_send_invite' => $availableInvites > 0, ]; } } diff --git a/database/migrations/2025_01_01_000000_create_invitations_table.php b/database/migrations/2025_01_01_000000_create_invitations_table.php index 877c9744e..46578a559 100644 --- a/database/migrations/2025_01_01_000000_create_invitations_table.php +++ b/database/migrations/2025_01_01_000000_create_invitations_table.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; return new class extends Migration { @@ -16,42 +16,42 @@ return new class extends Migration // Table exists, let's modify it to add missing columns for our custom system Schema::table('invitations', function (Blueprint $table) { // Check and add columns that might be missing - if (!Schema::hasColumn('invitations', 'token')) { + if (! Schema::hasColumn('invitations', 'token')) { $table->string('token', 64)->unique()->after('id'); } - if (!Schema::hasColumn('invitations', 'email')) { + if (! Schema::hasColumn('invitations', 'email')) { $table->string('email')->after('token'); } - if (!Schema::hasColumn('invitations', 'invited_by')) { + if (! Schema::hasColumn('invitations', 'invited_by')) { $table->unsignedBigInteger('invited_by')->after('email'); $table->index('invited_by'); } - if (!Schema::hasColumn('invitations', 'expires_at')) { + if (! Schema::hasColumn('invitations', 'expires_at')) { $table->timestamp('expires_at')->after('invited_by'); } - if (!Schema::hasColumn('invitations', 'used_at')) { + if (! Schema::hasColumn('invitations', 'used_at')) { $table->timestamp('used_at')->nullable()->after('expires_at'); } - if (!Schema::hasColumn('invitations', 'used_by')) { + if (! Schema::hasColumn('invitations', 'used_by')) { $table->unsignedBigInteger('used_by')->nullable()->after('used_at'); $table->index('used_by'); } - if (!Schema::hasColumn('invitations', 'is_active')) { + if (! Schema::hasColumn('invitations', 'is_active')) { $table->boolean('is_active')->default(true)->after('used_by'); } - if (!Schema::hasColumn('invitations', 'metadata')) { + if (! Schema::hasColumn('invitations', 'metadata')) { $table->json('metadata')->nullable()->after('is_active'); } // Add timestamps if they don't exist - if (!Schema::hasColumn('invitations', 'created_at')) { + if (! Schema::hasColumn('invitations', 'created_at')) { $table->timestamps(); } }); @@ -138,7 +138,7 @@ return new class extends Migration { try { $indexes = DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]); - if (!empty($indexes)) { + if (! empty($indexes)) { Schema::table($table, function (Blueprint $table) use ($indexName) { $table->dropIndex($indexName); }); diff --git a/database/migrations/2025_08_10_195505_fix_invitations_column_names.php b/database/migrations/2025_08_10_195505_fix_invitations_column_names.php index 0748198d9..3ee55d6a1 100644 --- a/database/migrations/2025_08_10_195505_fix_invitations_column_names.php +++ b/database/migrations/2025_08_10_195505_fix_invitations_column_names.php @@ -2,8 +2,8 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; return new class extends Migration { @@ -14,7 +14,7 @@ return new class extends Migration { Schema::table('invitations', function (Blueprint $table) { // First, check if we have the old column structure - if (Schema::hasColumn('invitations', 'users_id') && !Schema::hasColumn('invitations', 'invited_by')) { + if (Schema::hasColumn('invitations', 'users_id') && ! Schema::hasColumn('invitations', 'invited_by')) { // Drop the existing foreign key constraint first try { $table->dropForeign('FK_users_inv'); @@ -27,37 +27,37 @@ return new class extends Migration } // Add missing columns that our system needs - if (!Schema::hasColumn('invitations', 'token')) { + if (! Schema::hasColumn('invitations', 'token')) { $table->string('token', 64)->unique()->after('id'); } - if (!Schema::hasColumn('invitations', 'email')) { + if (! Schema::hasColumn('invitations', 'email')) { $table->string('email')->after('token'); } - if (!Schema::hasColumn('invitations', 'expires_at')) { + if (! Schema::hasColumn('invitations', 'expires_at')) { $table->timestamp('expires_at')->after('invited_by'); } - if (!Schema::hasColumn('invitations', 'used_at')) { + if (! Schema::hasColumn('invitations', 'used_at')) { $table->timestamp('used_at')->nullable()->after('expires_at'); } - if (!Schema::hasColumn('invitations', 'used_by')) { + if (! Schema::hasColumn('invitations', 'used_by')) { $table->unsignedBigInteger('used_by')->nullable()->after('used_at'); $table->index('used_by'); } - if (!Schema::hasColumn('invitations', 'is_active')) { + if (! Schema::hasColumn('invitations', 'is_active')) { $table->boolean('is_active')->default(true)->after('used_by'); } - if (!Schema::hasColumn('invitations', 'metadata')) { + if (! Schema::hasColumn('invitations', 'metadata')) { $table->json('metadata')->nullable()->after('is_active'); } // Add timestamps if they don't exist - if (!Schema::hasColumn('invitations', 'created_at')) { + if (! Schema::hasColumn('invitations', 'created_at')) { $table->timestamps(); } }); @@ -83,7 +83,7 @@ return new class extends Migration $this->dropIndexSafely('invitations', 'invitations_used_by_index'); // Rename back to original column name if it was renamed - if (Schema::hasColumn('invitations', 'invited_by') && !Schema::hasColumn('invitations', 'users_id')) { + if (Schema::hasColumn('invitations', 'invited_by') && ! Schema::hasColumn('invitations', 'users_id')) { $table->renameColumn('invited_by', 'users_id'); } @@ -121,7 +121,7 @@ return new class extends Migration { try { $indexes = DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$indexName]); - if (!empty($indexes)) { + if (! empty($indexes)) { Schema::table($table, function (Blueprint $table) use ($indexName) { $table->dropIndex($indexName); }); diff --git a/routes/web.php b/routes/web.php index e6c12a8d2..ea8fa5a81 100644 --- a/routes/web.php +++ b/routes/web.php @@ -58,6 +58,7 @@ use App\Http\Controllers\FailedReleasesController; use App\Http\Controllers\FileListController; use App\Http\Controllers\GamesController; use App\Http\Controllers\GetNzbController; +use App\Http\Controllers\InvitationController; use App\Http\Controllers\MovieController; use App\Http\Controllers\MusicController; use App\Http\Controllers\MyMoviesController; @@ -70,7 +71,6 @@ use App\Http\Controllers\RssController; use App\Http\Controllers\SearchController; use App\Http\Controllers\SeriesController; use App\Http\Controllers\TermsController; -use App\Http\Controllers\InvitationController; // Auth::routes();