updated plan, and oxapay integration

This commit is contained in:
Gitea
2025-06-21 04:09:08 +05:30
parent 930144f8f8
commit 30bd0c2712
14 changed files with 360 additions and 3 deletions

View File

@@ -46,6 +46,7 @@ class PlanResource extends Resource
TextInput::make('product_id')->required(), TextInput::make('product_id')->required(),
TextInput::make('pricing_id')->required(), TextInput::make('pricing_id')->required(),
TextInput::make('shoppy_product_id')->nullable(), TextInput::make('shoppy_product_id')->nullable(),
TextInput::make('oxapay_link')->nullable(),
TextInput::make('price')->numeric()->required(), TextInput::make('price')->numeric()->required(),
TextInput::make('mailbox_limit')->numeric()->required(), TextInput::make('mailbox_limit')->numeric()->required(),
Select::make('monthly_billing')->options([ Select::make('monthly_billing')->options([
@@ -60,6 +61,10 @@ class PlanResource extends Resource
1 => 'Activate', 1 => 'Activate',
0 => 'Disable', 0 => 'Disable',
])->required(), ])->required(),
Select::make('accept_oxapay')->options([
1 => 'Activate',
0 => 'Disable',
])->required(),
KeyValue::make('details') KeyValue::make('details')
->label('Plan Details (Optional)') ->label('Plan Details (Optional)')
->keyPlaceholder('Name') ->keyPlaceholder('Name')

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Http\Controllers;
use App\NotifyMe;
use Carbon\Carbon;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
use NotifyMe;
public function oxapay(Request $request)
{
// Get the request data
$postData = $request->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);
}
}
}

View File

@@ -14,6 +14,8 @@ class Plan extends Model
'shoppy_product_id', 'shoppy_product_id',
'accept_stripe', 'accept_stripe',
'accept_shoppy', 'accept_shoppy',
'oxapay_link',
'accept_oxapay',
'price', 'price',
'mailbox_limit', 'mailbox_limit',
'monthly_billing', 'monthly_billing',

View File

@@ -12,11 +12,12 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Laravel\Cashier\Billable; use Laravel\Cashier\Billable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable implements FilamentUser, MustVerifyEmail class User extends Authenticatable implements FilamentUser, MustVerifyEmail
{ {
/** @use HasFactory<\Database\Factories\UserFactory> */ /** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable, Billable; use HasFactory, Notifiable, Billable, HasApiTokens;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.

View File

@@ -7,6 +7,7 @@ use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
web: __DIR__.'/../routes/web.php', web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php', commands: __DIR__.'/../routes/console.php',
health: '/up', health: '/up',
) )
@@ -16,6 +17,7 @@ return Application::configure(basePath: dirname(__DIR__))
]); ]);
$middleware->validateCsrfTokens(except: [ $middleware->validateCsrfTokens(except: [
'stripe/*', 'stripe/*',
'webhook/oxapay',
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {

View File

@@ -12,6 +12,7 @@
"filament/filament": "3.3", "filament/filament": "3.3",
"laravel/cashier": "^15.6", "laravel/cashier": "^15.6",
"laravel/framework": "^12.0", "laravel/framework": "^12.0",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"livewire/flux": "^2.1", "livewire/flux": "^2.1",
"livewire/livewire": "^3.6", "livewire/livewire": "^3.6",

66
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "972e884837f3870524619dc37aa08d0f", "content-hash": "c28ee3c8ad2c6071685462887cfb5ee5",
"packages": [ "packages": [
{ {
"name": "anourvalar/eloquent-serialize", "name": "anourvalar/eloquent-serialize",
@@ -2462,6 +2462,70 @@
}, },
"time": "2025-02-11T13:34:40+00:00" "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", "name": "laravel/serializable-closure",
"version": "v2.0.4", "version": "v2.0.4",

84
config/sanctum.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
use Laravel\Sanctum\Sanctum;
return [
/*
|--------------------------------------------------------------------------
| Stateful Domains
|--------------------------------------------------------------------------
|
| Requests from the following domains / hosts will receive stateful API
| authentication cookies. Typically, these should include your local
| and production domains which access your API via a frontend SPA.
|
*/
'stateful' => 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,
],
];

View File

@@ -34,5 +34,9 @@ return [
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
], ],
], ],
'oxapay' => [
'merchant_api_key' => env('OXAPAY_MERCHANT_API_KEY', ''),
'payout_api_key' => env('OXAPAY_PAYOUT_API_KEY', ''),
]
]; ];

View File

@@ -0,0 +1,28 @@
<?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::table('plans', function (Blueprint $table) {
$table->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']);
});
}
};

View File

@@ -0,0 +1,33 @@
<?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('personal_access_tokens', function (Blueprint $table) {
$table->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');
}
};

View File

@@ -1,5 +1,5 @@
<div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-12 lg:px-8 "> <div class="mx-auto max-w-3xl px-4 py-8 sm:px-6 sm:py-12 lg:px-8 ">
<script src="https://shoppy.gg/api/embed.js"></script> {{-- <script src="https://shoppy.gg/api/embed.js"></script>--}}
<div class="w-full mb-8 items-center flex justify-center"> <div class="w-full mb-8 items-center flex justify-center">
<h1 class="text-center text-3xl text-gray-900 dark:text-gray-200">Purchase Subscription</h1> <h1 class="text-center text-3xl text-gray-900 dark:text-gray-200">Purchase Subscription</h1>
</div> </div>
@@ -46,6 +46,17 @@
Pay with crypto Pay with crypto
</flux:button> </flux:button>
@endif @endif
@if($plan->accept_oxapay && $plan->oxapay_link !== null)
<flux:button
variant="filled"
class="w-full mt-2 cursor-pointer"
tag="a"
href="{{ $plan->oxapay_link }}"
target="_blank"
>
Pay with crypto
</flux:button>
@endif
</div> </div>
@endforeach @endforeach
@endif @endif

8
routes/api.php Normal file
View File

@@ -0,0 +1,8 @@
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/user', function (Request $request) {
return $request->user();
})->middleware('auth:sanctum');

View File

@@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\AppController; use App\Http\Controllers\AppController;
use App\Http\Controllers\WebhookController;
use App\Http\Middleware\CheckPageSlug; use App\Http\Middleware\CheckPageSlug;
use App\Http\Middleware\CheckUserBanned; use App\Http\Middleware\CheckUserBanned;
use App\Livewire\AddOn; use App\Livewire\AddOn;
@@ -145,6 +146,8 @@ Route::middleware(['auth'])->group(function () {
Route::get('settings/appearance', Appearance::class)->name('settings.appearance'); Route::get('settings/appearance', Appearance::class)->name('settings.appearance');
}); });
Route::post('/webhook/oxapay', [WebhookController::class, 'oxapay'])->name('webhook.oxapay');
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';
Route::get('{slug}', Page::class)->where('slug', '.*')->name('page')->middleware(CheckPageSlug::class); Route::get('{slug}', Page::class)->where('slug', '.*')->name('page')->middleware(CheckPageSlug::class);