diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 6df0242..e1f0065 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -95,6 +95,16 @@ class ProfileController extends Controller ]); } + /** + * Display the user's subscription page. + */ + public function subscription(Request $request): View + { + return view('profile.subscription', [ + 'user' => $request->user(), + ]); + } + /** * Update user settings. */ diff --git a/app/Livewire/UserSubscription.php b/app/Livewire/UserSubscription.php new file mode 100644 index 0000000..2ef5022 --- /dev/null +++ b/app/Livewire/UserSubscription.php @@ -0,0 +1,70 @@ + 'required|string|size:48', + ]; + + public function mount(User $user) + { + $this->userId = $user ? $user->id : auth()->user()->id; + $this->subscriptionKey = $user->subscription_key ?? ''; + } + + public function applyKey(SubscriptionService $subscriptionService) + { + $this->validate(); + + $rateLimitKey = "apply-subscription:{$this->userId}"; + $rateLimitMinutes = 60 * 5; // 5 minutes + + // Rate Limit to prevent users trying random keys + if (RateLimiter::tooManyAttempts($rateLimitKey, 1)) { + $seconds = RateLimiter::availableIn($rateLimitKey); + $this->addError('subscriptionKey', "Too many attempts. Try again in {$seconds} seconds."); + return; + } + + RateLimiter::hit($rateLimitKey, $rateLimitMinutes); + + // Check if token is already being used + $alreadyUsed = User::where('subscription_key', $this->subscriptionKey) + ->whereNot('id', $this->userId) + ->exists(); + + if ($alreadyUsed) { + $this->addError('subscriptionKey', 'Key already used!'); + return; + } + + $user = User::where('id', $this->userId)->firstOrFail(); + + // Verify token + $success = $subscriptionService->checkSubscriptionStatus($user, $this->subscriptionKey); + if (!$success) { + $this->addError('subscriptionKey', 'Invalid Key! If you believe this is a bug, please report this to the admin!'); + return; + } + + $user->subscription_key = $this->subscriptionKey; + $user->save(); + } + + public function render() + { + return view('livewire.user-subscription'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 1978bf6..369bd69 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -31,6 +31,7 @@ class User extends Authenticatable implements HasPasskeys // Discord 'discord_id', 'discord_avatar', + 'subscription_key', ]; /** @@ -41,6 +42,7 @@ class User extends Authenticatable implements HasPasskeys protected $hidden = [ 'password', 'remember_token', + 'subscription_key', ]; /** diff --git a/app/Services/SubscriptionService.php b/app/Services/SubscriptionService.php new file mode 100644 index 0000000..3f4f7c6 --- /dev/null +++ b/app/Services/SubscriptionService.php @@ -0,0 +1,79 @@ + $subscriptionKey, + 'timestamp' => now()->timestamp, + 'nonce' => Str::uuid()->toString(), + ], JSON_THROW_ON_ERROR))); + } + + /** + * Gets the subscription status from the subscription service. + */ + private function getSubscriptionStatus(string $subscriptionKey): array | null + { + try { + $payload = $this->generateEncryptedPayload($subscriptionKey); + + $response = Http::post(config('services.subscription_service_host').'/api/membership/verify', [ + 'payload' => $payload, + ]); + + if (! $response->successful()) { + logger()->error('Subscription Service API error', [ + 'status' => $response->status(), + 'body' => $response->body(), + ]); + + return null; + } + + $encryptedResponse = $response->json('payload'); + + $json = json_decode( + Crypt::decryptString($encryptedResponse), + true, + flags: JSON_THROW_ON_ERROR + ); + + return $json; + } catch (Exception $e) { + logger()->error('getSubscriptionStatus Exception', [ + 'details' => $e, + ]); + } + + return null; + } + + public function checkSubscriptionStatus(User $user, string $subscriptionKey): bool + { + $subscriptionStatus = $this->getSubscriptionStatus($subscriptionKey); + if (!$subscriptionStatus) { + return false; + } + + if ($subscriptionStatus['valid'] === true && + $subscriptionStatus['active'] === true) { + $user->addRole(UserRole::SUPPORTER); + return true; + } + + $user->removeRole(UserRole::SUPPORTER); + return true; + } +} diff --git a/config/services.php b/config/services.php index 9cf8b55..82feb3d 100644 --- a/config/services.php +++ b/config/services.php @@ -54,4 +54,9 @@ return [ 'server' => env('MATRIX_SERVER'), 'shared_secret' => env('MATRIX_SHARED_SECRET'), ], + + /** + * Subscription Service + */ + 'subscription_service_host' => env('SUBSCRIPTION_SERVICE_HOST'), ]; diff --git a/database/migrations/2026_05_04_135331_add_subscription_key_to_users_table.php b/database/migrations/2026_05_04_135331_add_subscription_key_to_users_table.php new file mode 100644 index 0000000..6675415 --- /dev/null +++ b/database/migrations/2026_05_04_135331_add_subscription_key_to_users_table.php @@ -0,0 +1,31 @@ +string('subscription_key', 64) + ->unique() + ->nullable() + ->after('roles'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('subscription_key'); + }); + } +}; diff --git a/resources/views/livewire/user-subscription.blade.php b/resources/views/livewire/user-subscription.blade.php new file mode 100644 index 0000000..d164679 --- /dev/null +++ b/resources/views/livewire/user-subscription.blade.php @@ -0,0 +1,58 @@ +
+ +
+
+
+
+

Subscription Status

+

+ Your current membership status for unlimited 4k Downloads. +

+
+ + @php + $isActive = auth()->user()->hasRole(\App\Enums\UserRole::SUPPORTER); + @endphp + + + + {{ $isActive ? 'Active' : 'Inactive' }} + +
+
+ + +
+
+
+

Subscription Access Key

+

+ Paste your subscription key to apply the membership status. +

+
+ +
+
+ + + +
+ @error('subscriptionKey') {{ $message }} @enderror +
+
+
+
+
diff --git a/resources/views/profile/partials/actions.blade.php b/resources/views/profile/partials/actions.blade.php index f85aad7..da67cdd 100644 --- a/resources/views/profile/partials/actions.blade.php +++ b/resources/views/profile/partials/actions.blade.php @@ -2,6 +2,10 @@
+ + Subscription + Settings diff --git a/resources/views/profile/subscription.blade.php b/resources/views/profile/subscription.blade.php new file mode 100644 index 0000000..92bc54e --- /dev/null +++ b/resources/views/profile/subscription.blade.php @@ -0,0 +1,22 @@ + + +@include('partials.head') + +
+ @include('layouts.navigation') + +
+ @include('partials.background') +
+
+ @include('profile.partials.sidebar') +
+ @livewire('user-subscription', ['user' => $user]) +
+
+
+
+ @include('layouts.footer') +
+ + diff --git a/routes/user.php b/routes/user.php index e0bff80..0aa29af 100644 --- a/routes/user.php +++ b/routes/user.php @@ -25,6 +25,7 @@ Route::middleware('auth')->group(function () { Route::get('/user/comments', [ProfileController::class, 'comments'])->name('profile.comments'); Route::get('/user/likes', [ProfileController::class, 'likes'])->name('profile.likes'); Route::get('/user/watched', [ProfileController::class, 'watched'])->name('user.watched'); + Route::get('/user/subscription', [ProfileController::class, 'subscription'])->name('profile.subscription'); // Notifications Route::get('/user/notifications', [NotificationController::class, 'index'])->name('profile.notifications');