Skip to content

Commit

Permalink
Appsumo (#232)
Browse files Browse the repository at this point in the history
* Implemented webhooks

* oAuth wip

* Implement the whole auth flow

* Implement file upload limit depending on appsumo license
  • Loading branch information
JhumanJ authored Nov 1, 2023
1 parent 2e52518 commit e917423
Show file tree
Hide file tree
Showing 19 changed files with 612 additions and 103 deletions.
117 changes: 117 additions & 0 deletions app/Http/Controllers/Auth/AppSumoAuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\License;
use App\Models\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;

class AppSumoAuthController extends Controller
{
use AuthenticatesUsers;

public function handleCallback(Request $request)
{
$this->validate($request, [
'code' => 'required',
]);
$accessToken = $this->retrieveAccessToken($request->code);
$license = $this->fetchOrCreateLicense($accessToken);

// If user connected, attach license
if (Auth::check()) return $this->attachLicense($license);

// otherwise start login flow by passing the encrypted license key id
if (is_null($license->user_id)) {
return redirect(url('/register?appsumo_license='.encrypt($license->id)));
}

return redirect(url('/register?appsumo_error=1'));
}

private function retrieveAccessToken(string $requestCode): string
{
return Http::withHeaders([
'Content-type' => 'application/json'
])->post('https://appsumo.com/openid/token/', [
'grant_type' => 'authorization_code',
'code' => $requestCode,
'redirect_uri' => route('appsumo.callback'),
'client_id' => config('services.appsumo.client_id'),
'client_secret' => config('services.appsumo.client_secret'),
])->throw()->json('access_token');
}

private function fetchOrCreateLicense(string $accessToken): License
{
// Fetch license from API
$licenseKey = Http::get('https://appsumo.com/openid/license_key/?access_token=' . $accessToken)
->throw()
->json('license_key');

// Fetch or create license model
$license = License::where('license_provider','appsumo')->where('license_key',$licenseKey)->first();
if (!$license) {
$licenseData = Http::withHeaders([
'X-AppSumo-Licensing-Key' => config('services.appsumo.api_key'),
])->get('https://api.licensing.appsumo.com/v2/licenses/'.$licenseKey)->json();

// Create new license
$license = License::create([
'license_key' => $licenseKey,
'license_provider' => 'appsumo',
'status' => $licenseData['status'] === 'active' ? License::STATUS_ACTIVE : License::STATUS_INACTIVE,
'meta' => $licenseData,
]);
}

return $license;
}

private function attachLicense(License $license) {
if (!Auth::check()) {
throw new AuthenticationException('User not authenticated');
}

// Attach license if not already attached
if (is_null($license->user_id)) {
$license->user_id = Auth::id();
$license->save();
return redirect(url('/home?appsumo_connect=1'));
}

// Licensed already attached
return redirect(url('/home?appsumo_error=1'));
}

/**
* @param User $user
* @param string|null $licenseHash
* @return string|null
*
* Returns null if no license found
* Returns true if license was found and attached
* Returns false if there was an error (license not found or already attached)
*/
public static function registerWithLicense(User $user, ?string $licenseHash): ?bool
{
if (!$licenseHash) {
return null;
}
$licenseId = decrypt($licenseHash);
$license = License::find($licenseId);

if ($license && is_null($license->user_id)) {
$license->user_id = $user->id;
$license->save();
return true;
}

return false;
}
}
24 changes: 17 additions & 7 deletions app/Http/Controllers/Auth/RegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\Workspace;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
Expand All @@ -15,6 +16,8 @@ class RegisterController extends Controller
{
use RegistersUsers;

private ?bool $appsumoLicense = null;

/**
* Create a new controller instance.
*
Expand All @@ -28,8 +31,8 @@ public function __construct()
/**
* The user has been registered.
*
* @param \Illuminate\Http\Request $request
* @param \App\User $user
* @param \Illuminate\Http\Request $request
* @param \App\User $user
* @return \Illuminate\Http\JsonResponse
*/
protected function registered(Request $request, User $user)
Expand All @@ -38,13 +41,17 @@ protected function registered(Request $request, User $user)
return response()->json(['status' => trans('verification.sent')]);
}

return response()->json($user);
return response()->json(array_merge(
(new UserResource($user))->toArray($request),
[
'appsumo_license' => $this->appsumoLicense,
]));
}

/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
Expand All @@ -54,16 +61,17 @@ protected function validator(array $data)
'email' => 'required|email:filter|max:255|unique:users|indisposable',
'password' => 'required|min:6|confirmed',
'hear_about_us' => 'required|string',
'agree_terms' => ['required',Rule::in([true])]
],[
'agree_terms' => ['required', Rule::in([true])],
'appsumo_license' => ['nullable'],
], [
'agree_terms' => 'Please agree with the terms and conditions.'
]);
}

/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @param array $data
* @return \App\User
*/
protected function create(array $data)
Expand All @@ -87,6 +95,8 @@ protected function create(array $data)
]
], false);

$this->appsumoLicense = AppSumoAuthController::registerWithLicense($user, $data['appsumo_license'] ?? null);

return $user;
}
}
91 changes: 91 additions & 0 deletions app/Http/Controllers/Webhook/AppSumoController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace App\Http\Controllers\Webhook;

use App\Http\Controllers\Controller;
use App\Models\License;
use Illuminate\Http\Request;
use Illuminate\Validation\UnauthorizedException;

class AppSumoController extends Controller
{
public function handle(Request $request)
{
$this->validateSignature($request);

if ($request->test) {
return $this->success([
'message' => 'Webhook received.',
'event' => $request->event,
'success' => true,
]);
}

// Call the right function depending on the event using match()
match ($request->event) {
'activate' => $this->handleActivateEvent($request),
'upgrade', 'downgrade' => $this->handleChangeEvent($request),
'deactivate' => $this->handleDeactivateEvent($request),
default => null,
};

return $this->success([
'message' => 'Webhook received.',
'event' => $request->event,
'success' => true,
]);
}

private function handleActivateEvent($request)
{
$licence = License::firstOrNew([
'license_key' => $request->license_key,
'license_provider' => 'appsumo',
'status' => License::STATUS_ACTIVE,
]);
$licence->meta = $request->json()->all();
$licence->save();
}

private function handleChangeEvent($request)
{
// Deactivate old license
$oldLicense = License::where([
'license_key' => $request->prev_license_key,
'license_provider' => 'appsumo',
])->firstOrFail();
$oldLicense->update([
'status' => License::STATUS_INACTIVE,
]);

// Create new license
License::create([
'license_key' => $request->license_key,
'license_provider' => 'appsumo',
'status' => License::STATUS_ACTIVE,
'meta' => $request->json()->all(),
]);
}

private function handleDeactivateEvent($request)
{
// Deactivate old license
$oldLicense = License::where([
'license_key' => $request->prev_license_key,
'license_provider' => 'appsumo',
])->firstOrFail();
$oldLicense->update([
'status' => License::STATUS_INACTIVE,
]);
}

private function validateSignature(Request $request)
{
$signature = $request->header('x-appsumo-signature');
$payload = $request->getContent();

if ($signature === hash_hmac('sha256', $payload, config('services.appsumo.api_key'))) {
throw new UnauthorizedException('Invalid signature.');
}
}
}
10 changes: 1 addition & 9 deletions app/Http/Requests/AnswerFormRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

class AnswerFormRequest extends FormRequest
{
const MAX_FILE_SIZE_FREE = 5000000; // 5 MB
const MAX_FILE_SIZE_PRO = 50000000; // 50 MB

public Form $form;

protected array $requestRules = [];
Expand All @@ -27,12 +24,7 @@ class AnswerFormRequest extends FormRequest
public function __construct(Request $request)
{
$this->form = $request->form;

$this->maxFileSize = self::MAX_FILE_SIZE_FREE;
$workspace = $this->form->workspace;
if ($workspace && $workspace->is_pro) {
$this->maxFileSize = self::MAX_FILE_SIZE_PRO;
}
$this->maxFileSize = $this->form->workspace->max_file_size;
}

/**
Expand Down
1 change: 1 addition & 0 deletions app/Http/Resources/UserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function toArray($request)
'template_editor' => $this->template_editor,
'has_customer_id' => $this->has_customer_id,
'has_forms' => $this->has_forms,
'active_license' => $this->licenses()->active()->first(),
] : [];

return array_merge(parent::toArray($request), $personalData);
Expand Down
45 changes: 45 additions & 0 deletions app/Models/License.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class License extends Model
{
const STATUS_ACTIVE = 'active';
const STATUS_INACTIVE = 'inactive';

use HasFactory;

protected $fillable = [
'license_key',
'user_id',
'license_provider',
'status',
'meta'
];

protected $casts = [
'meta' => 'array',
];

public function user()
{
return $this->belongsTo(User::class);
}

public function scopeActive($query)
{
return $query->where('status', self::STATUS_ACTIVE);
}

public function getMaxFileSizeAttribute()
{
return [
1 => 25000000, // 25 MB,
2 => 50000000, // 50 MB,
3 => 75000000, // 75 MB,
][$this->meta['tier']];
}
}
Loading

0 comments on commit e917423

Please sign in to comment.