wite
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AppleNotificationController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\AppleVerifier;
|
||||
class CommerceController extends Controller {
|
||||
public function home() {return view('home',['features'=>DB::table('pages')->where('published',true)->where('slug','features')->first()]);}
|
||||
public function page(string $slug) {return view('page',['page'=>DB::table('pages')->where('slug',$slug)->where('published',true)->firstOrFail()]);}
|
||||
public function releases() {return view('releases',['releases'=>DB::table('releases')->where('published',true)->latest()->paginate(15)]);}
|
||||
public function account(Request $r) {
|
||||
if(!$r->user()->account_token){$r->user()->forceFill(['account_token'=>(string)Str::uuid()])->save();}
|
||||
return view('account',['licenses'=>DB::table('licenses')->where('user_id',$r->user()->id)->get(),'purchases'=>DB::table('purchases')->where('user_id',$r->user()->id)->latest()->get(),'releases'=>DB::table('releases')->where('published',true)->latest()->get()]);
|
||||
}
|
||||
public function downloadLink(Request $r,int $release) {
|
||||
abort_unless(DB::table('licenses')->where('user_id',$r->user()->id)->where('status','active')->exists(),403);
|
||||
abort_unless(DB::table('releases')->where('id',$release)->where('published',true)->exists(),404);
|
||||
return redirect(URL::temporarySignedRoute('download.file',now()->addMinutes(5),['release'=>$release,'user'=>$r->user()->id]));
|
||||
}
|
||||
public function download(Request $r,int $release,int $user) {
|
||||
abort_unless($r->user()->id===$user && DB::table('licenses')->where('user_id',$user)->where('status','active')->exists(),403);
|
||||
$file=DB::table('releases')->where('id',$release)->where('published',true)->firstOrFail();abort_unless(Storage::disk('local')->exists($file->path),404);
|
||||
return Storage::disk('local')->download($file->path,'Tarnax-'.$file->version.'.'.pathinfo($file->path,PATHINFO_EXTENSION));
|
||||
}
|
||||
public function consent(Request $r) {
|
||||
$v=$r->validate(['analytics'=>'required|boolean','marketing'=>'required|boolean']);$visitor=$r->cookie('tarnax_visitor');if(!Str::isUuid($visitor??'')){$visitor=(string)Str::uuid();}
|
||||
DB::table('consents')->insert([...$v,'visitor_id'=>$visitor,'user_id'=>$r->user()?->id,'policy_version'=>config('tarnax.consent_version'),'created_at'=>now(),'updated_at'=>now()]);
|
||||
return response()->json(['saved'=>true])->cookie('tarnax_visitor',$visitor,525600,'/',null,$r->isSecure(),true,false,'lax')->cookie('tarnax_consent',json_encode([...$v,'version'=>config('tarnax.consent_version')]),525600,'/',null,$r->isSecure(),false,false,'lax');
|
||||
}
|
||||
public function export(Request $r) {
|
||||
$data=['profile'=>$r->user()->only('name','email','created_at'),'purchases'=>DB::table('purchases')->where('user_id',$r->user()->id)->get(),'support'=>DB::table('ticket_messages')->where('user_id',$r->user()->id)->get(),'consent'=>DB::table('consents')->where('user_id',$r->user()->id)->get(),'privacy_requests'=>DB::table('privacy_requests')->where('user_id',$r->user()->id)->get()];
|
||||
return response()->streamDownload(fn()=>print(json_encode($data,JSON_PRETTY_PRINT)),'tarnax-personal-data.json',['Content-Type'=>'application/json']);
|
||||
}
|
||||
public function deletion(Request $r) {
|
||||
DB::table('privacy_requests')->updateOrInsert(['user_id'=>$r->user()->id,'type'=>'deletion','status'=>'pending'],['created_at'=>now(),'updated_at'=>now()]);
|
||||
return back()->with('status','Deletion requested. Support will review retained billing records and respond within the applicable deadline.');
|
||||
}
|
||||
public function apple(Request $r,AppleVerifier $verifier) {
|
||||
$v=$r->validate(['signed_transaction'=>'required|string|max:25000']);$claims=$verifier->verify($v['signed_transaction'],$r->user()->account_token??'');
|
||||
DB::transaction(function()use($r,$claims){$id='apple:'.$claims['transactionId'];$purchase=DB::table('purchases')->where('transaction_id',$id)->lockForUpdate()->first();abort_if($purchase && $purchase->user_id!==$r->user()->id,403);if(!$purchase){DB::table('purchases')->insert(['user_id'=>$r->user()->id,'provider'=>'apple','transaction_id'=>$id,'status'=>'verified','created_at'=>now(),'updated_at'=>now()]);}});
|
||||
return back()->with('status','Apple purchase verified. App Store entitlements remain separate from direct licenses.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Services\LicenseService;
|
||||
class LicenseController extends Controller {
|
||||
public function activate(Request $r,LicenseService $service): JsonResponse {$v=$r->validate(['license_key'=>'required|string|max:100','device_id'=>'required|string|min:16|max:255']);return response()->json($service->activate($v['license_key'],$v['device_id']));}
|
||||
public function validateLicense(Request $r,LicenseService $service): JsonResponse {return response()->json($service->check($r->bearerToken()??''));}
|
||||
public function deactivate(Request $r,LicenseService $service): JsonResponse {return response()->json($service->check($r->bearerToken()??'',true));}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers;
|
||||
use App\Models\User;
|
||||
use App\Services\LicenseService;
|
||||
use App\Notifications\AccountUpdate;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\Carbon;
|
||||
class PaddleController extends Controller {
|
||||
public function checkout(Request $r): JsonResponse {
|
||||
$config=config('tarnax.paddle');abort_unless($config['api_key'] && $config['price_id'] && $config['client_token'],503,'Checkout is awaiting merchant configuration.');
|
||||
$user=$r->user();if(!$user->account_token){$user->forceFill(['account_token'=>(string)Str::uuid()])->save();}
|
||||
$base=$config['environment']==='production'?'https://api.paddle.com':'https://sandbox-api.paddle.com';
|
||||
$response=Http::withToken($config['api_key'])->timeout(15)->post($base.'/transactions',['items'=>[['price_id'=>$config['price_id'],'quantity'=>1]],'collection_mode'=>'automatic','custom_data'=>['account_token'=>$user->account_token]]);
|
||||
abort_unless($response->successful(),502,'Payment provider unavailable.');
|
||||
return response()->json(['transaction_id'=>$response->json('data.id')]);
|
||||
}
|
||||
public function webhook(Request $r,LicenseService $licenses): JsonResponse {
|
||||
$secret=config('tarnax.paddle.webhook_secret');abort_unless($secret,503);
|
||||
$parts=[];foreach(explode(';',$r->header('Paddle-Signature','')) as $part){$pair=explode('=',trim($part),2);if(count($pair)===2){$parts[$pair[0]][]=$pair[1];}}
|
||||
$ts=$parts['ts'][0]??'';abort_unless(ctype_digit($ts) && abs(time()-(int)$ts)<=300,401);
|
||||
$expected=hash_hmac('sha256',$ts.':'.$r->getContent(),$secret);$valid=false;foreach($parts['h1']??[] as $signature){$valid=$valid||hash_equals($expected,$signature);}abort_unless($valid,401);
|
||||
$event=$r->validate(['event_id'=>'required|string|max:100','event_type'=>'required|string|max:100','occurred_at'=>'required|date','data'=>'required|array']);
|
||||
DB::transaction(function()use($event,$licenses){
|
||||
$inserted=DB::table('webhook_events')->insertOrIgnore(['id'=>$event['event_id'],'type'=>$event['event_type'],'processed_at'=>now()]);if(!$inserted){return;}
|
||||
$d=$event['data'];$type=$event['event_type'];$at=Carbon::parse($event['occurred_at']);
|
||||
if($type==='transaction.completed'){
|
||||
abort_unless(($d['status']??'')==='completed',422);
|
||||
$matching=collect($d['items']??[])->contains(fn($i)=>($i['price']['id']??null)===config('tarnax.paddle.price_id') && ($i['quantity']??0)>=1);if(!$matching){return;}
|
||||
$user=User::where('account_token',$d['custom_data']['account_token']??'')->first();abort_unless($user,422,'Unknown customer.');
|
||||
$purchase=DB::table('purchases')->where('transaction_id',$d['id'])->lockForUpdate()->first();
|
||||
if($purchase && ($purchase->status==='refunded' || Carbon::parse($purchase->provider_updated_at)->gte($at))){return;}
|
||||
$id=$purchase?->id??DB::table('purchases')->insertGetId(['user_id'=>$user->id,'provider'=>'paddle','transaction_id'=>$d['id'],'subscription_id'=>$d['subscription_id']??null,'status'=>'completed','total'=>(int)($d['details']['totals']['grand_total']??0),'currency'=>$d['currency_code']??'USD','provider_updated_at'=>$at,'created_at'=>now(),'updated_at'=>now()]);
|
||||
$licenses->issue($user->id,$id);
|
||||
$user->notify(new AccountUpdate('Your Tarnax license is ready','Your purchase has been confirmed. Your license and downloads are in your account.',url('/account')));
|
||||
}
|
||||
if($type==='adjustment.updated' || $type==='adjustment.created'){
|
||||
if(($d['status']??'')!=='approved' || !in_array($d['action']??'',['refund','chargeback','chargeback_warning'])){return;}
|
||||
$purchase=DB::table('purchases')->where('transaction_id',$d['transaction_id']??'')->lockForUpdate()->first();
|
||||
if($purchase){DB::table('purchases')->where('id',$purchase->id)->update(['status'=>'refunded','provider_updated_at'=>$at,'updated_at'=>now()]);DB::table('licenses')->where('purchase_id',$purchase->id)->update(['status'=>'revoked','updated_at'=>now()]);}
|
||||
}
|
||||
if(in_array($type,['subscription.canceled','subscription.paused','subscription.past_due'])){
|
||||
$purchases=DB::table('purchases')->where('subscription_id',$d['id']??'')->where('provider_updated_at','<',$at)->get();
|
||||
foreach($purchases as $purchase){DB::table('licenses')->where('purchase_id',$purchase->id)->update(['status'=>'revoked','updated_at'=>now()]);DB::table('purchases')->where('id',$purchase->id)->update(['status'=>$d['status'],'provider_updated_at'=>$at]);}
|
||||
}
|
||||
},3);
|
||||
return response()->json(['received'=>true]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user