diff --git a/app/Filament/Resources/PlanResource.php b/app/Filament/Resources/PlanResource.php index bd44d1a..a1816d6 100644 --- a/app/Filament/Resources/PlanResource.php +++ b/app/Filament/Resources/PlanResource.php @@ -46,6 +46,7 @@ class PlanResource extends Resource TextInput::make('product_id')->required(), TextInput::make('pricing_id')->required(), TextInput::make('shoppy_product_id')->nullable(), + TextInput::make('oxapay_link')->nullable(), TextInput::make('price')->numeric()->required(), TextInput::make('mailbox_limit')->numeric()->required(), Select::make('monthly_billing')->options([ @@ -60,6 +61,10 @@ class PlanResource extends Resource 1 => 'Activate', 0 => 'Disable', ])->required(), + Select::make('accept_oxapay')->options([ + 1 => 'Activate', + 0 => 'Disable', + ])->required(), KeyValue::make('details') ->label('Plan Details (Optional)') ->keyPlaceholder('Name') diff --git a/app/Http/Controllers/WebhookController.php b/app/Http/Controllers/WebhookController.php new file mode 100644 index 0000000..9f0bb78 --- /dev/null +++ b/app/Http/Controllers/WebhookController.php @@ -0,0 +1,111 @@ +getContent(); + $data = json_decode($postData, true); + + // Validate request data + if (!$data || !isset($data['type']) || !in_array($data['type'], ['invoice', 'payment_link', 'payout'])) { + \Log::warning('Invalid Oxapay webhook data', ['data' => $data]); + return response('Invalid data.type', 400); + } + + // Determine API secret key based on type + $apiSecretKey = $data['type'] === 'invoice' + ? config('services.oxapay.merchant_api_key') + : config('services.oxapay.payout_api_key'); + + // Validate HMAC signature + $hmacHeader = $request->header('HMAC'); + $calculatedHmac = hash_hmac('sha512', $postData, $apiSecretKey); + + if (hash_equals($calculatedHmac, $hmacHeader)) { + // HMAC signature is valid + try { + if ($data['type'] === 'invoice' || $data['type'] === 'payment_link') { + // Process invoice payment data + $email = $data['email'] ?? 'Unknown'; + $amount = $data['amount'] ?? 'Unknown'; + $currency = $data['currency'] ?? 'Unknown'; + $trackId = $data['track_id'] ?? 'Unknown'; + $orderId = $data['order_id'] ?? 'N/A'; + $date = isset($data['date']) ? Carbon::createFromTimestamp($data['date'])->toDateTimeString() : now()->toDateTimeString(); + + \Log::info('Received Oxapay invoice payment callback', [ + 'track_id' => $trackId, + 'email' => $email, + 'amount' => $amount, + 'currency' => $currency, + 'order_id' => $orderId, + 'date' => $date, + ]); + + $message = "✅ Oxapay Invoice Payment Success\n" . + "Track ID: {$trackId}\n" . + "Email: {$email}\n" . + "Amount: {$amount} {$currency}\n" . + "Order ID: {$orderId}\n" . + "Time: {$date}"; + self::sendTelegramNotification($message); + } elseif ($data['type'] === 'payout') { + // Process payout data + $trackId = $data['track_id'] ?? 'Unknown'; + $amount = $data['amount'] ?? 'Unknown'; + $currency = $data['currency'] ?? 'Unknown'; + $network = $data['network'] ?? 'Unknown'; + $address = $data['address'] ?? 'Unknown'; + $txHash = $data['tx_hash'] ?? 'Unknown'; + $description = $data['description'] ?? 'N/A'; + $date = isset($data['date']) ? Carbon::createFromTimestamp($data['date'])->toDateTimeString() : now()->toDateTimeString(); + + \Log::info('Received Oxapay payout callback', [ + 'track_id' => $trackId, + 'status' => $data['status'] ?? 'Unknown', + 'amount' => $amount, + 'currency' => $currency, + 'network' => $network, + 'address' => $address, + 'tx_hash' => $txHash, + 'description' => $description, + 'date' => $date, + ]); + + $message = "📤 Oxapay Payout Confirmed\n" . + "Track ID: {$trackId}\n" . + "Amount: {$amount} {$currency}\n" . + "Network: {$network}\n" . + "Address: {$address}\n" . + "Transaction Hash: {$txHash}\n" . + "Description: {$description}\n" . + "Date: {$date}"; + self::sendTelegramNotification($message); + } + + return response('OK', 200); + } catch (\Exception $e) { + \Log::error('Oxapay webhook processing error', ['error' => $e->getMessage(), 'data' => $data]); + self::sendTelegramNotification(" + Failed to process Oxapay webhook\n + Type: {$data['type']}\n + Email/Track ID: " . ($data['type'] === 'invoice' ? ($data['email'] ?? 'Unknown') : ($data['track_id'] ?? 'Unknown')) . "\n + Error: {$e->getMessage()} + "); + return response('Processing error', 400); + } + } else { + \Log::warning('Invalid Oxapay HMAC signature', ['hmac_header' => $hmacHeader, 'calculated_hmac' => $calculatedHmac]); + return response('Invalid HMAC signature', 400); + } + } +} diff --git a/app/Models/Plan.php b/app/Models/Plan.php index 827630e..787a8d1 100644 --- a/app/Models/Plan.php +++ b/app/Models/Plan.php @@ -14,6 +14,8 @@ class Plan extends Model 'shoppy_product_id', 'accept_stripe', 'accept_shoppy', + 'oxapay_link', + 'accept_oxapay', 'price', 'mailbox_limit', 'monthly_billing', diff --git a/app/Models/User.php b/app/Models/User.php index 84dd8ba..01092a9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,11 +12,12 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Support\Str; use Laravel\Cashier\Billable; +use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable implements FilamentUser, MustVerifyEmail { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable, Billable; + use HasFactory, Notifiable, Billable, HasApiTokens; /** * The attributes that are mass assignable. diff --git a/bootstrap/app.php b/bootstrap/app.php index 8ea7f1a..c3f6815 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) @@ -16,6 +17,7 @@ return Application::configure(basePath: dirname(__DIR__)) ]); $middleware->validateCsrfTokens(except: [ 'stripe/*', + 'webhook/oxapay', ]); }) ->withExceptions(function (Exceptions $exceptions) { diff --git a/composer.json b/composer.json index 4be86c4..ce02f44 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "filament/filament": "3.3", "laravel/cashier": "^15.6", "laravel/framework": "^12.0", + "laravel/sanctum": "^4.0", "laravel/tinker": "^2.10.1", "livewire/flux": "^2.1", "livewire/livewire": "^3.6", diff --git a/composer.lock b/composer.lock index f87f7f7..9278b25 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "972e884837f3870524619dc37aa08d0f", + "content-hash": "c28ee3c8ad2c6071685462887cfb5ee5", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -2462,6 +2462,70 @@ }, "time": "2025-02-11T13:34:40+00:00" }, + { + "name": "laravel/sanctum", + "version": "v4.1.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "a360a6a1fd2400ead4eb9b6a9c1bb272939194f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/a360a6a1fd2400ead4eb9b6a9c1bb272939194f5", + "reference": "a360a6a1fd2400ead4eb9b6a9c1bb272939194f5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/database": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "php": "^8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2025-04-23T13:03:38+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.4", diff --git a/config/sanctum.php b/config/sanctum.php new file mode 100644 index 0000000..44527d6 --- /dev/null +++ b/config/sanctum.php @@ -0,0 +1,84 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + // Sanctum::currentRequestHost(), + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, + ], + +]; diff --git a/config/services.php b/config/services.php index 27a3617..e7b7363 100644 --- a/config/services.php +++ b/config/services.php @@ -34,5 +34,9 @@ return [ 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), ], ], + 'oxapay' => [ + 'merchant_api_key' => env('OXAPAY_MERCHANT_API_KEY', ''), + 'payout_api_key' => env('OXAPAY_PAYOUT_API_KEY', ''), + ] ]; diff --git a/database/migrations/2025_06_20_172251_add_oxa_pay_details_to_plans_table.php b/database/migrations/2025_06_20_172251_add_oxa_pay_details_to_plans_table.php new file mode 100644 index 0000000..2acc022 --- /dev/null +++ b/database/migrations/2025_06_20_172251_add_oxa_pay_details_to_plans_table.php @@ -0,0 +1,28 @@ +string('oxapay_link')->nullable()->after('accept_shoppy'); + $table->boolean('accept_oxapay')->default(false)->after('oxapay_link');}); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plans', function (Blueprint $table) { + $table->dropColumn(['oxapay_link', 'accept_oxapay']); + }); + } +}; diff --git a/database/migrations/2025_06_20_181026_create_personal_access_tokens_table.php b/database/migrations/2025_06_20_181026_create_personal_access_tokens_table.php new file mode 100644 index 0000000..e828ad8 --- /dev/null +++ b/database/migrations/2025_06_20_181026_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->string('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/resources/views/livewire/dashboard/pricing.blade.php b/resources/views/livewire/dashboard/pricing.blade.php index 64860a6..ad0daa4 100644 --- a/resources/views/livewire/dashboard/pricing.blade.php +++ b/resources/views/livewire/dashboard/pricing.blade.php @@ -1,5 +1,5 @@