Add external subscription system
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
70
app/Livewire/UserSubscription.php
Normal file
70
app/Livewire/UserSubscription.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\SubscriptionService;
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
class UserSubscription extends Component
|
||||
{
|
||||
public $userId = 0;
|
||||
|
||||
public $subscriptionKey = '';
|
||||
|
||||
protected $rules = [
|
||||
'subscriptionKey' => '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');
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
79
app/Services/SubscriptionService.php
Normal file
79
app/Services/SubscriptionService.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SubscriptionService
|
||||
{
|
||||
private function generateEncryptedPayload(string $subscriptionKey): string
|
||||
{
|
||||
return base64_encode(Crypt::encryptString(json_encode([
|
||||
'subscription_access_key' => $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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user