Appsumo (#232)
* Implemented webhooks * oAuth wip * Implement the whole auth flow * Implement file upload limit depending on appsumo license
This commit is contained in:
117
app/Http/Controllers/Auth/AppSumoAuthController.php
Normal file
117
app/Http/Controllers/Auth/AppSumoAuthController.php
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -15,6 +16,8 @@ class RegisterController extends Controller
|
||||
{
|
||||
use RegistersUsers;
|
||||
|
||||
private ?bool $appsumoLicense = null;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
@@ -28,8 +31,8 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* 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)
|
||||
@@ -38,13 +41,17 @@ class RegisterController extends Controller
|
||||
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)
|
||||
@@ -54,8 +61,9 @@ class RegisterController extends Controller
|
||||
'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.'
|
||||
]);
|
||||
}
|
||||
@@ -63,7 +71,7 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* Create a new user instance after a valid registration.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $data
|
||||
* @return \App\User
|
||||
*/
|
||||
protected function create(array $data)
|
||||
@@ -87,6 +95,8 @@ class RegisterController extends Controller
|
||||
]
|
||||
], false);
|
||||
|
||||
$this->appsumoLicense = AppSumoAuthController::registerWithLicense($user, $data['appsumo_license'] ?? null);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
91
app/Http/Controllers/Webhook/AppSumoController.php
Normal file
91
app/Http/Controllers/Webhook/AppSumoController.php
Normal file
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,6 @@ use App\Rules\ValidUrl;
|
||||
|
||||
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 = [];
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ class UserResource extends JsonResource
|
||||
'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);
|
||||
|
||||
45
app/Models/License.php
Normal file
45
app/Models/License.php
Normal file
@@ -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']];
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Cashier\Billable;
|
||||
use Tymon\JWTAuth\Contracts\JWTSubject;
|
||||
|
||||
class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
use Notifiable, HasFactory, Billable;
|
||||
@@ -80,7 +81,9 @@ class User extends Authenticatable implements JWTSubject
|
||||
|
||||
public function getIsSubscribedAttribute()
|
||||
{
|
||||
return $this->subscribed() || in_array($this->email, config('opnform.extra_pro_users_emails'));
|
||||
return $this->subscribed()
|
||||
|| in_array($this->email, config('opnform.extra_pro_users_emails'))
|
||||
|| !is_null($this->activeLicense());
|
||||
}
|
||||
|
||||
public function getHasCustomerIdAttribute()
|
||||
@@ -138,7 +141,7 @@ class User extends Authenticatable implements JWTSubject
|
||||
|
||||
public function forms()
|
||||
{
|
||||
return $this->hasMany(Form::class,'creator_id');
|
||||
return $this->hasMany(Form::class, 'creator_id');
|
||||
}
|
||||
|
||||
public function formTemplates()
|
||||
@@ -146,6 +149,16 @@ class User extends Authenticatable implements JWTSubject
|
||||
return $this->hasMany(Template::class, 'creator_id');
|
||||
}
|
||||
|
||||
public function licenses()
|
||||
{
|
||||
return $this->hasMany(License::class);
|
||||
}
|
||||
|
||||
public function activeLicense(): License
|
||||
{
|
||||
return $this->licenses()->active()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* =================================
|
||||
* Oauth Related
|
||||
@@ -187,26 +200,26 @@ class User extends Authenticatable implements JWTSubject
|
||||
})->first()?->onTrial();
|
||||
}
|
||||
|
||||
public static function boot ()
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
static::deleting(function(User $user) {
|
||||
static::deleting(function (User $user) {
|
||||
// Remove user's workspace if he's the only one with this workspace
|
||||
foreach ($user->workspaces as $workspace) {
|
||||
if ($workspace->users()->count() == 1) {
|
||||
$workspace->delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeWithActiveSubscription($query)
|
||||
{
|
||||
return $query->whereHas('subscriptions', function($query) {
|
||||
$query->where(function($q){
|
||||
$q->where('stripe_status', 'trialing')
|
||||
->orWhere('stripe_status', 'active');
|
||||
});
|
||||
return $query->whereHas('subscriptions', function ($query) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('stripe_status', 'trialing')
|
||||
->orWhere('stripe_status', 'active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Http\Requests\AnswerFormRequest;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -11,6 +11,9 @@ class Workspace extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
const MAX_FILE_SIZE_FREE = 5000000; // 5 MB
|
||||
const MAX_FILE_SIZE_PRO = 50000000; // 50 MB
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'icon',
|
||||
@@ -37,6 +40,26 @@ class Workspace extends Model
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getMaxFileSizeAttribute()
|
||||
{
|
||||
if(is_null(config('cashier.key'))){
|
||||
return self::MAX_FILE_SIZE_PRO;
|
||||
}
|
||||
|
||||
// Return max file size depending on subscription
|
||||
foreach ($this->owners as $owner) {
|
||||
if ($owner->is_subscribed) {
|
||||
if ($license = $owner->activeLicense()) {
|
||||
// In case of special License
|
||||
return $license->max_file_size;
|
||||
}
|
||||
}
|
||||
return self::MAX_FILE_SIZE_PRO;
|
||||
}
|
||||
|
||||
return self::MAX_FILE_SIZE_FREE;
|
||||
}
|
||||
|
||||
public function getIsEnterpriseAttribute()
|
||||
{
|
||||
if(is_null(config('cashier.key'))){
|
||||
|
||||
Reference in New Issue
Block a user