Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Appsumo #232

Merged
merged 4 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading