From 2844bcef9265ef9d901e170f90dc18c1060b0746 Mon Sep 17 00:00:00 2001 From: JhumanJ Date: Mon, 30 Oct 2023 15:12:26 +0100 Subject: [PATCH 1/4] Implemented webhooks --- .../Auth/AppSumoAuthController.php | 185 ++++++++++++++++++ .../Controllers/Webhook/AppSumoController.php | 92 +++++++++ app/Models/License.php | 26 +++ config/services.php | 12 +- ...023_10_30_133259_create_licenses_table.php | 39 ++++ routes/api.php | 6 + 6 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/Auth/AppSumoAuthController.php create mode 100644 app/Http/Controllers/Webhook/AppSumoController.php create mode 100644 app/Models/License.php create mode 100644 database/migrations/2023_10_30_133259_create_licenses_table.php diff --git a/app/Http/Controllers/Auth/AppSumoAuthController.php b/app/Http/Controllers/Auth/AppSumoAuthController.php new file mode 100644 index 000000000..39e7f2693 --- /dev/null +++ b/app/Http/Controllers/Auth/AppSumoAuthController.php @@ -0,0 +1,185 @@ +validate($request, [ + 'code' => 'required', + ]); + + dd($request->code); + + try { + $workspace = $this->retrieveAccessToken($request->code); + } catch (ClientException $exception) { + + // Permission issue: owner does not have full access to shared page + if (Str::of($exception->getMessage())->contains('User does not have edit access to record')) { + Log::error('Notion connection permission Error', [ + 'exception_msg' => $exception->getMessage(), + 'response' => $exception->getResponse(), + 'exception' => $exception, + 'user' => Auth::user()->id, + ]); + + return $this->callbackResponse([ + 'type' => 'error', + 'message' => 'You do not have full access to the Notion pages you selected. Please make sure you have the right permissions.' + ]); + } + + Log::error('Error while connecting to notion', [ + 'exception_msg' => $exception->getMessage(), + 'exception' => $exception, + ]); + + return $this->callbackResponse([ + 'type' => 'error', + 'message' => 'Error while connecting with Notion. Please try again!', + ]); + } catch (WorkspaceAlreadyExisting $exception) { + // TODO: notify workspace owner + return $this->callbackResponse([ + 'type' => 'error', + 'upgrade' => true, + 'message' => 'workspace_already_existing', + 'owner' => $exception->getOwner(), + ]); + } catch (WorkspaceLimit $exception) { + return $this->callbackResponse([ + 'type' => 'error', + 'upgrade' => true, + 'retry' => false, + 'message' => 'You are only allowed to connect 1 Notion workspace. Please upgrade your subscription to the Enterprise plan before adding more workspaces.', + ]); + } + + return $this->callbackResponse([ + 'type' => 'success', + 'workspace' => $workspace, + ]); + } + + private function retrieveAccessToken(string $requestCode): Workspace + { + $baseUrl = 'https://api.notion.com/' . config('notion.version') . '/oauth/'; + $client = new Client([ + 'base_uri' => $baseUrl, + ]); + + $response = $client->post('token', [ + 'form_params' => [ + 'grant_type' => 'authorization_code', + 'code' => $requestCode, + 'redirect_uri' => route('notion.callback'), + ], + 'auth' => [ + config('notion.client_id'), + config('notion.client_secret'), + ], + ]); + + $body = (string)$response->getBody(); + $body = json_decode($body, true); + + return $this->findOrCreateWorkspace($body); + } + + private function findOrCreateWorkspace(array $workspaceData): Workspace + { + // Check user's workspaces + if ($workspace = Auth::user()->workspaces() + ->where(function ($query) use ($workspaceData) { + return $query->where('name', $workspaceData['workspace_name']) + ->orWhere('notion_workspace_id', $workspaceData['workspace_id']); + }) + ->first()) { + $workspace->update([ + 'name' => utf8_encode($workspaceData['workspace_name']), + 'icon' => utf8_encode($workspaceData['workspace_icon'] ?? ucfirst($workspaceData['workspace_name'][0])), + 'bot_id' => $workspaceData['bot_id'], + 'notion_workspace_id' => $workspaceData['workspace_id'], + ]); + } // Check other existing workspaces + elseif ($workspace = Workspace::where('notion_workspace_id', $workspaceData['workspace_id'])->first()) { + $this->checkCanConnectWorkspace($workspace); + $workspace->update([ + 'name' => utf8_encode($workspaceData['workspace_name']), + 'icon' => utf8_encode($workspaceData['workspace_icon'] ?? ucfirst($workspaceData['workspace_name'][0])), + 'bot_id' => $workspaceData['bot_id'], + 'notion_workspace_id' => $workspaceData['workspace_id'], + ]); + } // New workspace, create it + else { + $this->checkCanConnectWorkspace(); + $workspace = Workspace::create([ + 'bot_id' => $workspaceData['bot_id'], + 'notion_workspace_id' => $workspaceData['workspace_id'], + 'name' => utf8_encode($workspaceData['workspace_name']), + 'icon' => utf8_encode($workspaceData['workspace_icon'] ?? ucfirst($workspaceData['workspace_name'][0])), + ]); + } + + // Add relation with user + Auth::user()->workspaces()->sync([ + $workspace->id => [ + 'access_token' => $workspaceData['access_token'], + 'is_owner' => true, + ], + ], false); + + return $workspace; + } + + private function checkCanConnectWorkspace($workspace = null) + { + $user = Auth::user(); + + if ($workspace && $workspace->haveOpenedGates()) { + return; + } + + // If user has enterprise subscription, can do everything + if ($user->has_enterprise_subscription) { + return; + } + + // If user doens't have enterprise + if ($user->workspaces()->count() > 0) { + throw new WorkspaceLimit(); + } else { + // User has room for new workspace + if ($workspace && $workspace->is_enterprise) { + return; + } elseif ($workspace) { + // User has room, but workspace not enterprise + throw new WorkspaceAlreadyExisting($workspace); + } + } + } + + private function callbackResponse(array $result) + { + return view('notion.callback', [ + 'result' => array_merge($result, [ + 'source' => 'notion_tools', + ]), + ]); + } +} diff --git a/app/Http/Controllers/Webhook/AppSumoController.php b/app/Http/Controllers/Webhook/AppSumoController.php new file mode 100644 index 000000000..ed5b0c65a --- /dev/null +++ b/app/Http/Controllers/Webhook/AppSumoController.php @@ -0,0 +1,92 @@ +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) + { + License::updateOrCreate([ + 'license_key' => $request->license_key, + 'user_identifier' => $request->user_fingerprint, + 'license_provider' => 'appsumo', + 'status' => License::STATUS_ACTIVE, + 'meta' => $request->json()->all(), + ]); + } + + 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, + 'user_identifier' => $request->user_fingerprint, + '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.'); + } + } +} diff --git a/app/Models/License.php b/app/Models/License.php new file mode 100644 index 000000000..f2c5a5baa --- /dev/null +++ b/app/Models/License.php @@ -0,0 +1,26 @@ + 'array', + ]; +} diff --git a/config/services.php b/config/services.php index fe0ad4026..7e2b9d165 100644 --- a/config/services.php +++ b/config/services.php @@ -45,7 +45,7 @@ ], 'notion' => [ - 'worker' => env('NOTION_WORKER','https://notion-forms-worker.notionforms.workers.dev/v1') + 'worker' => env('NOTION_WORKER', 'https://notion-forms-worker.notionforms.workers.dev/v1') ], 'openai' => [ @@ -53,8 +53,14 @@ ], 'unsplash' => [ - 'access_key' => env('UNSPLASH_ACCESS_KEY'), - 'secret_key' => env('UNSPLASH_SECRET_KEY'), + 'access_key' => env('UNSPLASH_ACCESS_KEY'), + 'secret_key' => env('UNSPLASH_SECRET_KEY'), + ], + + 'appsumo' => [ + 'client_id' => env('APPSUMO_CLIENT_ID'), + 'client_secret' => env('APPSUMO_CLIENT_SECRET'), + 'api_key' => env('APPSUMO_API_KEY'), ], 'google_analytics_code' => env('GOOGLE_ANALYTICS_CODE'), diff --git a/database/migrations/2023_10_30_133259_create_licenses_table.php b/database/migrations/2023_10_30_133259_create_licenses_table.php new file mode 100644 index 000000000..a568e9dcd --- /dev/null +++ b/database/migrations/2023_10_30_133259_create_licenses_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('license_key'); + $table->string('user_identifier'); + $table->string('license_provider'); + $table->string('status'); + $table->json('meta'); + $table->timestamps(); + + $table->index(['license_key', 'license_provider']); + $table->index(['user_identifier', 'license_provider']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('licenses'); + } +}; diff --git a/routes/api.php b/routes/api.php index 9ec4b922b..cfb0abad8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -129,6 +129,12 @@ Route::get('oauth/{driver}/callback', [OAuthController::class, 'handleCallback'])->name('oauth.callback'); }); + +Route::group(['prefix' => 'appsumo'], function () { + Route::get('oauth/callback', [\App\Http\Controllers\Auth\AppSumoAuthController::class, 'handleCallback'])->name('appsumo.callback'); + Route::post('webhook', [\App\Http\Controllers\Webhook\AppSumoController::class, 'handle'])->name('appsumo.webhook'); +}); + /* * Public Forms related routes */ From c103ea13862be7330d5c2770cae68aafd81748d4 Mon Sep 17 00:00:00 2001 From: JhumanJ Date: Mon, 30 Oct 2023 15:58:32 +0100 Subject: [PATCH 2/4] oAuth wip --- app/Http/Controllers/Auth/AppSumoAuthController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Auth/AppSumoAuthController.php b/app/Http/Controllers/Auth/AppSumoAuthController.php index 39e7f2693..dc4792990 100644 --- a/app/Http/Controllers/Auth/AppSumoAuthController.php +++ b/app/Http/Controllers/Auth/AppSumoAuthController.php @@ -19,11 +19,11 @@ class AppSumoAuthController extends Controller public function handleCallback(Request $request) { + ray($request->all()); $this->validate($request, [ 'code' => 'required', ]); - - dd($request->code); + dd('ok'); try { $workspace = $this->retrieveAccessToken($request->code); From afda6e94058b8b3aad1e42bd7ac0ef4005a353e1 Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 1 Nov 2023 15:17:52 +0100 Subject: [PATCH 3/4] Implement the whole auth flow --- .../Auth/AppSumoAuthController.php | 220 ++++++------------ .../Controllers/Auth/RegisterController.php | 24 +- .../Controllers/Webhook/AppSumoController.php | 7 +- app/Http/Resources/UserResource.php | 1 + app/Models/License.php | 12 +- app/Models/User.php | 33 ++- ...023_10_30_133259_create_licenses_table.php | 4 +- public/img/appsumo/as-Select-dark.png | Bin 0 -> 9252 bytes public/img/appsumo/as-taco-white-bg.png | Bin 0 -> 9644 bytes resources/js/components/common/Button.vue | 58 +++-- .../vendor/appsumo/AppSumoBilling.vue | 77 ++++++ .../vendor/appsumo/AppSumoRegister.vue | 50 ++++ .../js/pages/auth/components/RegisterForm.vue | 57 +++-- resources/js/pages/auth/register.vue | 31 ++- resources/js/pages/settings/billing.vue | 39 +++- 15 files changed, 372 insertions(+), 241 deletions(-) create mode 100644 public/img/appsumo/as-Select-dark.png create mode 100644 public/img/appsumo/as-taco-white-bg.png create mode 100644 resources/js/components/vendor/appsumo/AppSumoBilling.vue create mode 100644 resources/js/components/vendor/appsumo/AppSumoRegister.vue diff --git a/app/Http/Controllers/Auth/AppSumoAuthController.php b/app/Http/Controllers/Auth/AppSumoAuthController.php index dc4792990..505d2d966 100644 --- a/app/Http/Controllers/Auth/AppSumoAuthController.php +++ b/app/Http/Controllers/Auth/AppSumoAuthController.php @@ -2,16 +2,14 @@ namespace App\Http\Controllers\Auth; -use App\Exceptions\Workspaces\WorkspaceAlreadyExisting; -use App\Exceptions\Workspaces\WorkspaceLimit; use App\Http\Controllers\Controller; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\ClientException; +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\Log; -use Illuminate\Support\Str; +use Illuminate\Support\Facades\Http; class AppSumoAuthController extends Controller { @@ -19,167 +17,101 @@ class AppSumoAuthController extends Controller public function handleCallback(Request $request) { - ray($request->all()); $this->validate($request, [ 'code' => 'required', ]); - dd('ok'); - - try { - $workspace = $this->retrieveAccessToken($request->code); - } catch (ClientException $exception) { - - // Permission issue: owner does not have full access to shared page - if (Str::of($exception->getMessage())->contains('User does not have edit access to record')) { - Log::error('Notion connection permission Error', [ - 'exception_msg' => $exception->getMessage(), - 'response' => $exception->getResponse(), - 'exception' => $exception, - 'user' => Auth::user()->id, - ]); - - return $this->callbackResponse([ - 'type' => 'error', - 'message' => 'You do not have full access to the Notion pages you selected. Please make sure you have the right permissions.' - ]); - } - - Log::error('Error while connecting to notion', [ - 'exception_msg' => $exception->getMessage(), - 'exception' => $exception, - ]); + $accessToken = $this->retrieveAccessToken($request->code); + $license = $this->fetchOrCreateLicense($accessToken); - return $this->callbackResponse([ - 'type' => 'error', - 'message' => 'Error while connecting with Notion. Please try again!', - ]); - } catch (WorkspaceAlreadyExisting $exception) { - // TODO: notify workspace owner - return $this->callbackResponse([ - 'type' => 'error', - 'upgrade' => true, - 'message' => 'workspace_already_existing', - 'owner' => $exception->getOwner(), - ]); - } catch (WorkspaceLimit $exception) { - return $this->callbackResponse([ - 'type' => 'error', - 'upgrade' => true, - 'retry' => false, - 'message' => 'You are only allowed to connect 1 Notion workspace. Please upgrade your subscription to the Enterprise plan before adding more workspaces.', - ]); + // 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 $this->callbackResponse([ - 'type' => 'success', - 'workspace' => $workspace, - ]); + return redirect(url('/register?appsumo_error=1')); } - private function retrieveAccessToken(string $requestCode): Workspace + private function retrieveAccessToken(string $requestCode): string { - $baseUrl = 'https://api.notion.com/' . config('notion.version') . '/oauth/'; - $client = new Client([ - 'base_uri' => $baseUrl, - ]); - - $response = $client->post('token', [ - 'form_params' => [ - 'grant_type' => 'authorization_code', - 'code' => $requestCode, - 'redirect_uri' => route('notion.callback'), - ], - 'auth' => [ - config('notion.client_id'), - config('notion.client_secret'), - ], - ]); - - $body = (string)$response->getBody(); - $body = json_decode($body, true); - - return $this->findOrCreateWorkspace($body); + 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 findOrCreateWorkspace(array $workspaceData): Workspace + private function fetchOrCreateLicense(string $accessToken): License { - // Check user's workspaces - if ($workspace = Auth::user()->workspaces() - ->where(function ($query) use ($workspaceData) { - return $query->where('name', $workspaceData['workspace_name']) - ->orWhere('notion_workspace_id', $workspaceData['workspace_id']); - }) - ->first()) { - $workspace->update([ - 'name' => utf8_encode($workspaceData['workspace_name']), - 'icon' => utf8_encode($workspaceData['workspace_icon'] ?? ucfirst($workspaceData['workspace_name'][0])), - 'bot_id' => $workspaceData['bot_id'], - 'notion_workspace_id' => $workspaceData['workspace_id'], - ]); - } // Check other existing workspaces - elseif ($workspace = Workspace::where('notion_workspace_id', $workspaceData['workspace_id'])->first()) { - $this->checkCanConnectWorkspace($workspace); - $workspace->update([ - 'name' => utf8_encode($workspaceData['workspace_name']), - 'icon' => utf8_encode($workspaceData['workspace_icon'] ?? ucfirst($workspaceData['workspace_name'][0])), - 'bot_id' => $workspaceData['bot_id'], - 'notion_workspace_id' => $workspaceData['workspace_id'], - ]); - } // New workspace, create it - else { - $this->checkCanConnectWorkspace(); - $workspace = Workspace::create([ - 'bot_id' => $workspaceData['bot_id'], - 'notion_workspace_id' => $workspaceData['workspace_id'], - 'name' => utf8_encode($workspaceData['workspace_name']), - 'icon' => utf8_encode($workspaceData['workspace_icon'] ?? ucfirst($workspaceData['workspace_name'][0])), + // 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, ]); } - // Add relation with user - Auth::user()->workspaces()->sync([ - $workspace->id => [ - 'access_token' => $workspaceData['access_token'], - 'is_owner' => true, - ], - ], false); - - return $workspace; + return $license; } - private function checkCanConnectWorkspace($workspace = null) - { - $user = Auth::user(); - - if ($workspace && $workspace->haveOpenedGates()) { - return; + private function attachLicense(License $license) { + if (!Auth::check()) { + throw new AuthenticationException('User not authenticated'); } - // If user has enterprise subscription, can do everything - if ($user->has_enterprise_subscription) { - return; + // 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')); } - // If user doens't have enterprise - if ($user->workspaces()->count() > 0) { - throw new WorkspaceLimit(); - } else { - // User has room for new workspace - if ($workspace && $workspace->is_enterprise) { - return; - } elseif ($workspace) { - // User has room, but workspace not enterprise - throw new WorkspaceAlreadyExisting($workspace); - } - } + // Licensed already attached + return redirect(url('/home?appsumo_error=1')); } - private function callbackResponse(array $result) + /** + * @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 { - return view('notion.callback', [ - 'result' => array_merge($result, [ - 'source' => 'notion_tools', - ]), - ]); + 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; } } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 736eff8a4..df7e99a31 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -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 @@ 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) @@ -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) @@ -54,8 +61,9 @@ 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.' ]); } @@ -63,7 +71,7 @@ protected function validator(array $data) /** * 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 @@ protected function create(array $data) ] ], false); + $this->appsumoLicense = AppSumoAuthController::registerWithLicense($user, $data['appsumo_license'] ?? null); + return $user; } } diff --git a/app/Http/Controllers/Webhook/AppSumoController.php b/app/Http/Controllers/Webhook/AppSumoController.php index ed5b0c65a..6bbf900dc 100644 --- a/app/Http/Controllers/Webhook/AppSumoController.php +++ b/app/Http/Controllers/Webhook/AppSumoController.php @@ -38,13 +38,13 @@ public function handle(Request $request) private function handleActivateEvent($request) { - License::updateOrCreate([ + $licence = License::firstOrNew([ 'license_key' => $request->license_key, - 'user_identifier' => $request->user_fingerprint, 'license_provider' => 'appsumo', 'status' => License::STATUS_ACTIVE, - 'meta' => $request->json()->all(), ]); + $licence->meta = $request->json()->all(); + $licence->save(); } private function handleChangeEvent($request) @@ -61,7 +61,6 @@ private function handleChangeEvent($request) // Create new license License::create([ 'license_key' => $request->license_key, - 'user_identifier' => $request->user_fingerprint, 'license_provider' => 'appsumo', 'status' => License::STATUS_ACTIVE, 'meta' => $request->json()->all(), diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index cfabaa00f..5e8de1df3 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -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); diff --git a/app/Models/License.php b/app/Models/License.php index f2c5a5baa..32e1cfe50 100644 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -14,7 +14,7 @@ class License extends Model protected $fillable = [ 'license_key', - 'user_identifier', + 'user_id', 'license_provider', 'status', 'meta' @@ -23,4 +23,14 @@ class License extends Model protected $casts = [ 'meta' => 'array', ]; + + public function user() + { + return $this->belongsTo(User::class); + } + + public function scopeActive($query) + { + return $query->where('status', self::STATUS_ACTIVE); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 538136b78..3ce5c21d8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -13,6 +13,7 @@ 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 @@ public function getHasFormsAttribute() 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 @@ public function workspaces() public function forms() { - return $this->hasMany(Form::class,'creator_id'); + return $this->hasMany(Form::class, 'creator_id'); } public function formTemplates() @@ -146,6 +149,16 @@ public function formTemplates() 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 @@ public function getIsRiskyAttribute() })->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'); + }); }); } diff --git a/database/migrations/2023_10_30_133259_create_licenses_table.php b/database/migrations/2023_10_30_133259_create_licenses_table.php index a568e9dcd..034cde5db 100644 --- a/database/migrations/2023_10_30_133259_create_licenses_table.php +++ b/database/migrations/2023_10_30_133259_create_licenses_table.php @@ -16,14 +16,14 @@ public function up() Schema::create('licenses', function (Blueprint $table) { $table->id(); $table->string('license_key'); - $table->string('user_identifier'); + $table->unsignedBigInteger('user_id')->nullable(); $table->string('license_provider'); $table->string('status'); $table->json('meta'); $table->timestamps(); $table->index(['license_key', 'license_provider']); - $table->index(['user_identifier', 'license_provider']); + $table->index(['user_id', 'license_provider']); }); } diff --git a/public/img/appsumo/as-Select-dark.png b/public/img/appsumo/as-Select-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..44dc132c88bdf7884b751b607b25db7e967eb7f8 GIT binary patch literal 9252 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 - + - - - - + + + + + stroke-linejoin="round" + /> @@ -72,11 +72,11 @@ export default { target: { type: String, default: '_self' - }, + } }, computed: { - btnClasses() { + btnClasses () { const sizes = this.sizes const colorShades = this.colorShades return `v-btn ${sizes['p-y']} ${sizes['p-x']} @@ -84,14 +84,14 @@ export default { ${colorShades?.text} transition ease-in duration-200 text-center text-${sizes?.font} font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 rounded-lg flex items-center hover:no-underline` }, - colorShades() { + colorShades () { if (this.color === 'blue') { return { main: 'bg-blue-600', hover: 'hover:bg-blue-700', ring: 'focus:ring-blue-500', 'ring-offset': 'focus:ring-offset-blue-200', - text: 'text-white', + text: 'text-white' } } else if (this.color === 'outline-blue') { return { @@ -99,7 +99,15 @@ export default { hover: 'hover:bg-blue-600', ring: 'focus:ring-blue-500', 'ring-offset': 'focus:ring-offset-blue-200', - text: 'text-blue-600 hover:text-white', + text: 'text-blue-600 hover:text-white' + } + } else if (this.color === 'outline-gray') { + return { + main: 'bg-transparent border border-gray-300', + hover: 'hover:bg-gray-500', + ring: 'focus:ring-gray-500', + 'ring-offset': 'focus:ring-offset-gray-200', + text: 'text-gray-500 hover:text-white' } } else if (this.color === 'red') { return { @@ -107,7 +115,7 @@ export default { hover: 'hover:bg-red-700', ring: 'focus:ring-red-500', 'ring-offset': 'focus:ring-offset-red-200', - text: 'text-white', + text: 'text-white' } } else if (this.color === 'gray') { return { @@ -115,7 +123,7 @@ export default { hover: 'hover:bg-gray-700', ring: 'focus:ring-gray-500', 'ring-offset': 'focus:ring-offset-gray-200', - text: 'text-white', + text: 'text-white' } } else if (this.color === 'light-gray') { return { @@ -123,7 +131,7 @@ export default { hover: 'hover:bg-gray-100', ring: 'focus:ring-gray-500', 'ring-offset': 'focus:ring-offset-gray-300', - text: 'text-gray-700', + text: 'text-gray-700' } } else if (this.color === 'green') { return { @@ -131,7 +139,7 @@ export default { hover: 'hover:bg-green-700', ring: 'focus:ring-green-500', 'ring-offset': 'focus:ring-offset-green-200', - text: 'text-white', + text: 'text-white' } } else if (this.color === 'yellow') { return { @@ -139,7 +147,7 @@ export default { hover: 'hover:bg-yellow-700', ring: 'focus:ring-yellow-500', 'ring-offset': 'focus:ring-offset-yellow-200', - text: 'text-white', + text: 'text-white' } } else if (this.color === 'white') { return { @@ -147,12 +155,12 @@ export default { hover: 'hover:bg-gray-200', ring: 'focus:ring-white-500', 'ring-offset': 'focus:ring-offset-white-200', - text: 'text-gray-700', + text: 'text-gray-700' } } console.error('Unknown color') }, - sizes() { + sizes () { if (this.size === 'small') { return { font: 'sm', @@ -169,8 +177,8 @@ export default { }, methods: { - onClick(event) { - this.$emit('click',event) + onClick (event) { + this.$emit('click', event) } } } diff --git a/resources/js/components/vendor/appsumo/AppSumoBilling.vue b/resources/js/components/vendor/appsumo/AppSumoBilling.vue new file mode 100644 index 000000000..5dcffbd71 --- /dev/null +++ b/resources/js/components/vendor/appsumo/AppSumoBilling.vue @@ -0,0 +1,77 @@ + + + diff --git a/resources/js/components/vendor/appsumo/AppSumoRegister.vue b/resources/js/components/vendor/appsumo/AppSumoRegister.vue new file mode 100644 index 000000000..71573cc3a --- /dev/null +++ b/resources/js/components/vendor/appsumo/AppSumoRegister.vue @@ -0,0 +1,50 @@ + + + diff --git a/resources/js/pages/auth/components/RegisterForm.vue b/resources/js/pages/auth/components/RegisterForm.vue index a06f82d69..7eadc7dc6 100644 --- a/resources/js/pages/auth/components/RegisterForm.vue +++ b/resources/js/pages/auth/components/RegisterForm.vue @@ -1,6 +1,6 @@ diff --git a/resources/js/pages/settings/billing.vue b/resources/js/pages/settings/billing.vue index 17164ebb0..47a497f2c 100644 --- a/resources/js/pages/settings/billing.vue +++ b/resources/js/pages/settings/billing.vue @@ -1,14 +1,21 @@ @@ -16,11 +23,13 @@ import axios from 'axios' import VButton from '../../components/common/Button.vue' import SeoMeta from '../../mixins/seo-meta.js' +import { mapGetters } from 'vuex' +import AppSumoBilling from '../../components/vendor/appsumo/AppSumoBilling.vue' export default { - components: {VButton}, - scrollToTop: false, + components: { AppSumoBilling, VButton }, mixins: [SeoMeta], + scrollToTop: false, data: () => ({ metaTitle: 'Billing', @@ -28,7 +37,7 @@ export default { }), methods: { - openBillingDashboard() { + openBillingDashboard () { this.billingLoading = true axios.get('/api/subscription/billing-portal').then((response) => { const url = response.data.portal_url @@ -39,6 +48,12 @@ export default { this.billingLoading = false }) } + }, + + computed: { + ...mapGetters({ + user: 'auth/user' + }) } } From d43f0df8f09e67b855bc29a95de9ccf5772d002f Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 1 Nov 2023 15:27:00 +0100 Subject: [PATCH 4/4] Implement file upload limit depending on appsumo license --- app/Http/Requests/AnswerFormRequest.php | 10 +--------- app/Models/License.php | 9 +++++++++ app/Models/Workspace.php | 25 ++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/app/Http/Requests/AnswerFormRequest.php b/app/Http/Requests/AnswerFormRequest.php index be8085072..2a3039f77 100644 --- a/app/Http/Requests/AnswerFormRequest.php +++ b/app/Http/Requests/AnswerFormRequest.php @@ -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 = []; @@ -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; } /** diff --git a/app/Models/License.php b/app/Models/License.php index 32e1cfe50..bb72f34e6 100644 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -33,4 +33,13 @@ 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']]; + } } diff --git a/app/Models/Workspace.php b/app/Models/Workspace.php index 5974a5937..c0efa231d 100644 --- a/app/Models/Workspace.php +++ b/app/Models/Workspace.php @@ -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 @@ public function getIsProAttribute() 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'))){