* Implemented webhooks

* oAuth wip

* Implement the whole auth flow

* Implement file upload limit depending on appsumo license
This commit is contained in:
Julien Nahum
2023-11-01 16:58:10 +01:00
committed by GitHub
parent 2e52518aa7
commit e9174238e4
19 changed files with 611 additions and 102 deletions

View 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;
}
}

View File

@@ -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;
}
}

View 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.');
}
}
}

View File

@@ -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;
}
/**

View File

@@ -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
View 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']];
}
}

View File

@@ -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');
});
});
}

View File

@@ -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'))){