From 7e4b2b71791d090300109fd968e99184c7abbd0d Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 10 Dec 2024 17:08:03 +0530 Subject: [PATCH 01/14] Add hCaptcha on register page --- .../Controllers/Auth/RegisterController.php | 16 ++++++-- api/app/Models/User.php | 3 ++ ...4_12_10_094605_add_meta_to_users_table.php | 27 +++++++++++++ .../pages/auth/components/RegisterForm.vue | 39 +++++++++++++++++-- 4 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php diff --git a/api/app/Http/Controllers/Auth/RegisterController.php b/api/app/Http/Controllers/Auth/RegisterController.php index 81675f61..0731af4c 100644 --- a/api/app/Http/Controllers/Auth/RegisterController.php +++ b/api/app/Http/Controllers/Auth/RegisterController.php @@ -12,6 +12,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; +use App\Rules\ValidHCaptcha; class RegisterController extends Controller { @@ -27,6 +28,8 @@ class RegisterController extends Controller public function __construct() { $this->middleware('guest'); + $this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute + $this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour } /** @@ -56,7 +59,7 @@ protected function registered(Request $request, User $user) */ protected function validator(array $data) { - return Validator::make($data, [ + $rules = [ 'name' => 'required|max:255', 'email' => 'required|email:filter|max:255|unique:users|indisposable', 'password' => 'required|min:6|confirmed', @@ -64,8 +67,14 @@ protected function validator(array $data) 'agree_terms' => ['required', Rule::in([true])], 'appsumo_license' => ['nullable'], 'invite_token' => ['nullable', 'string'], - 'utm_data' => ['nullable', 'array'] - ], [ + 'utm_data' => ['nullable', 'array'], + ]; + + if (!config('app.self_hosted')) { + $rules['h-captcha-response'] = [new ValidHCaptcha()]; + } + + return Validator::make($data, $rules, [ 'agree_terms' => 'Please agree with the terms and conditions.', ]); } @@ -84,6 +93,7 @@ protected function create(array $data) 'password' => bcrypt($data['password']), 'hear_about_us' => $data['hear_about_us'], 'utm_data' => array_key_exists('utm_data', $data) ? $data['utm_data'] : null, + 'meta' => ['registration_ip' => request()->ip()], ]); // Add relation with user diff --git a/api/app/Models/User.php b/api/app/Models/User.php index 3c5624cd..3180c1c1 100644 --- a/api/app/Models/User.php +++ b/api/app/Models/User.php @@ -33,6 +33,7 @@ class User extends Authenticatable implements JWTSubject 'password', 'hear_about_us', 'utm_data', + 'meta' ]; /** @@ -44,6 +45,7 @@ class User extends Authenticatable implements JWTSubject 'password', 'remember_token', 'hear_about_us', + 'meta' ]; /** @@ -56,6 +58,7 @@ protected function casts() return [ 'email_verified_at' => 'datetime', 'utm_data' => 'array', + 'meta' => 'array', ]; } diff --git a/api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php b/api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php new file mode 100644 index 00000000..855e6a66 --- /dev/null +++ b/api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php @@ -0,0 +1,27 @@ +json('meta')->default('{}'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('meta'); + }); + } +}; diff --git a/client/components/pages/auth/components/RegisterForm.vue b/client/components/pages/auth/components/RegisterForm.vue index d927b762..432108c2 100644 --- a/client/components/pages/auth/components/RegisterForm.vue +++ b/client/components/pages/auth/components/RegisterForm.vue @@ -52,6 +52,21 @@ label="Confirm Password" /> + +
+ + +
+ import {opnFetch} from "~/composables/useOpnApi.js" -import {fetchAllWorkspaces} from "~/stores/workspaces.js" +import { fetchAllWorkspaces } from "~/stores/workspaces.js" +import VueHcaptcha from '@hcaptcha/vue3-hcaptcha' export default { name: "RegisterForm", - components: {}, + components: {VueHcaptcha}, props: { isQuick: { type: Boolean, @@ -146,6 +162,7 @@ export default { formsStore: useFormsStore(), workspaceStore: useWorkspacesStore(), providersStore: useOAuthProvidersStore(), + runtimeConfig: useRuntimeConfig(), logEvent: useAmplitude().logEvent, $utm } @@ -159,12 +176,18 @@ export default { password_confirmation: "", agree_terms: false, appsumo_license: null, - utm_data: null + utm_data: null, + 'h-captcha-response': null }), - disableEmail:false + disableEmail: false, + hcaptcha: null, + isSelfHosted: useFeatureFlag('self_hosted') }), computed: { + hCaptchaSiteKey() { + return this.runtimeConfig.public.hCaptchaSiteKey + }, hearAboutUsOptions() { const options = [ {name: "Facebook", value: "facebook"}, @@ -187,6 +210,10 @@ export default { }, mounted() { + if (!this.isSelfHosted) { + this.hcaptcha = this.$refs.hcaptcha + } + // Set appsumo license if ( this.$route.query.appsumo_license !== undefined && @@ -208,6 +235,10 @@ export default { async register() { let data this.form.utm_data = this.$utm.value + if (!this.isSelfHosted) { + this.form['h-captcha-response'] = document.getElementsByName('h-captcha-response')[0].value + this.hcaptcha.reset() + } try { // Register the user. data = await this.form.post("/register") From c63571e8fe97ebff77c0da412ea704bf68272bd2 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 10 Dec 2024 20:01:42 +0530 Subject: [PATCH 02/14] register page captcha test cases --- api/tests/Feature/RegisterTest.php | 35 ++++++++++++++++++++---- api/tests/Feature/UserManagementTest.php | 10 +++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/api/tests/Feature/RegisterTest.php b/api/tests/Feature/RegisterTest.php index 05a0277e..9cbeacaf 100644 --- a/api/tests/Feature/RegisterTest.php +++ b/api/tests/Feature/RegisterTest.php @@ -1,8 +1,15 @@ Http::response(['success' => true]) + ]); + $this->postJson('/register', [ 'name' => 'Test User', 'email' => 'test@test.app', @@ -10,13 +17,15 @@ 'password' => 'secret', 'password_confirmation' => 'secret', 'agree_terms' => true, + 'h-captcha-response' => 'test-token', // Mock token for testing ]) ->assertSuccessful() ->assertJsonStructure(['id', 'name', 'email']); - $this->assertDatabaseHas('users', [ - 'name' => 'Test User', - 'email' => 'test@test.app', - ]); + + $user = User::where('email', 'test@test.app')->first(); + expect($user)->not->toBeNull(); + expect($user->meta)->toHaveKey('registration_ip'); + expect($user->meta['registration_ip'])->toBe(request()->ip()); }); it('cannot register with existing email', function () { @@ -27,6 +36,7 @@ 'email' => 'test@test.app', 'password' => 'secret', 'password_confirmation' => 'secret', + 'h-captcha-response' => 'test-token', ]) ->assertStatus(422) ->assertJsonValidationErrors(['email']); @@ -48,11 +58,12 @@ 'password' => 'secret', 'password_confirmation' => 'secret', 'agree_terms' => true, + 'h-captcha-response' => 'test-token', ]) ->assertStatus(422) ->assertJsonValidationErrors(['email']) ->assertJson([ - 'message' => 'Disposable email addresses are not allowed.', + 'message' => 'Disposable email addresses are not allowed. (and 1 more error)', 'errors' => [ 'email' => [ 'Disposable email addresses are not allowed.', @@ -60,3 +71,17 @@ ], ]); }); + +it('requires hcaptcha token in production', function () { + app()->detectEnvironment(fn() => 'production'); + + $this->postJson('/register', [ + 'name' => 'Test User', + 'email' => 'test@test.app', + 'hear_about_us' => 'google', + 'password' => 'secret', + 'password_confirmation' => 'secret', + ]) + ->assertStatus(422) + ->assertJsonValidationErrors(['h-captcha-response']); +}); diff --git a/api/tests/Feature/UserManagementTest.php b/api/tests/Feature/UserManagementTest.php index 3113b4e2..e3b5531b 100644 --- a/api/tests/Feature/UserManagementTest.php +++ b/api/tests/Feature/UserManagementTest.php @@ -2,10 +2,15 @@ use App\Models\UserInvite; use Carbon\Carbon; +use App\Rules\ValidHCaptcha; +use Illuminate\Support\Facades\Http; beforeEach(function () { $this->user = $this->actingAsProUser(); $this->workspace = $this->createUserWorkspace($this->user); + Http::fake([ + ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true]) + ]); }); @@ -31,6 +36,7 @@ 'password_confirmation' => 'secret', 'agree_terms' => true, 'invite_token' => $token, + 'h-captcha-response' => 'test-token', ]); $response->assertSuccessful(); expect($this->workspace->users()->count())->toBe(2); @@ -59,6 +65,7 @@ 'password_confirmation' => 'secret', 'agree_terms' => true, 'invite_token' => $token, + 'h-captcha-response' => 'test-token', ]); $response->assertStatus(400)->assertJson([ 'message' => 'Invite token has expired.', @@ -88,6 +95,7 @@ 'password_confirmation' => 'secret', 'agree_terms' => true, 'invite_token' => $token, + 'h-captcha-response' => 'test-token', ]); $response->assertSuccessful(); expect($this->workspace->users()->count())->toBe(2); @@ -104,6 +112,7 @@ 'password_confirmation' => 'secret', 'agree_terms' => true, 'invite_token' => $token, + 'h-captcha-response' => 'test-token', ]); $response->assertStatus(422)->assertJson([ @@ -138,6 +147,7 @@ 'password_confirmation' => 'secret', 'agree_terms' => true, 'invite_token' => $token, + 'h-captcha-response' => 'test-token', ]); $response->assertStatus(400)->assertJson([ 'message' => 'Invite token is invalid.', From 01f7fa224f6dee639df2812a39bde74ab833f018 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 10 Dec 2024 20:06:23 +0530 Subject: [PATCH 03/14] Refactor integration validation rules to include form context - Updated the `getValidationRules` method in various integration handlers (Discord, Email, Google Sheets, Slack, Webhook, Zapier) to accept an optional `Form` parameter, allowing for context-aware validation. - Enhanced the `EmailIntegration` handler to enforce restrictions based on user plans, ensuring free users can only create one email integration per form and can only send to a single email address. - Added a new test suite for `EmailIntegration` to validate the new restrictions and ensure proper functionality for both free and pro users. - Introduced loading state management in the `IntegrationModal` component to improve user experience during save operations. These changes improve the flexibility and user experience of form integrations, particularly for email handling. --- .../Integration/FormIntegrationsRequest.php | 5 +- .../Handlers/AbstractIntegrationHandler.php | 2 +- .../Handlers/DiscordIntegration.php | 3 +- .../Handlers/EmailIntegration.php | 34 +++- .../Handlers/GoogleSheetsIntegration.php | 7 +- .../Handlers/SlackIntegration.php | 3 +- .../Handlers/WebhookIntegration.php | 4 +- .../Handlers/ZapierIntegration.php | 3 +- .../Email/EmailIntegrationTest.php | 175 ++++++++++++++++++ .../components/IntegrationModal.vue | 8 +- 10 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 api/tests/Feature/Integrations/Email/EmailIntegrationTest.php diff --git a/api/app/Http/Requests/Integration/FormIntegrationsRequest.php b/api/app/Http/Requests/Integration/FormIntegrationsRequest.php index 128a4c0e..7856548e 100644 --- a/api/app/Http/Requests/Integration/FormIntegrationsRequest.php +++ b/api/app/Http/Requests/Integration/FormIntegrationsRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests\Integration; +use App\Models\Forms\Form; use App\Models\Integration\FormIntegration; use App\Rules\IntegrationLogicRule; use Illuminate\Foundation\Http\FormRequest; @@ -14,9 +15,11 @@ class FormIntegrationsRequest extends FormRequest public array $integrationRules = []; private ?string $integrationClassName = null; + private ?Form $form = null; public function __construct(Request $request) { + $this->form = Form::findOrFail(request()->route('id')); if ($request->integration_id) { // Load integration class, and get rules $integration = FormIntegration::getIntegration($request->integration_id); @@ -77,7 +80,7 @@ protected function isOAuthRequired(): bool private function loadIntegrationRules() { - foreach ($this->integrationClassName::getValidationRules() as $key => $value) { + foreach ($this->integrationClassName::getValidationRules($this->form) as $key => $value) { $this->integrationRules['settings.' . $key] = $value; } } diff --git a/api/app/Integrations/Handlers/AbstractIntegrationHandler.php b/api/app/Integrations/Handlers/AbstractIntegrationHandler.php index 47fc78cb..cf90f267 100644 --- a/api/app/Integrations/Handlers/AbstractIntegrationHandler.php +++ b/api/app/Integrations/Handlers/AbstractIntegrationHandler.php @@ -94,7 +94,7 @@ public function handle(): void Http::throw()->post($this->getWebhookUrl(), $this->getWebhookData()); } - abstract public static function getValidationRules(): array; + abstract public static function getValidationRules(?Form $form): array; public static function isOAuthRequired(): bool { diff --git a/api/app/Integrations/Handlers/DiscordIntegration.php b/api/app/Integrations/Handlers/DiscordIntegration.php index 1a3fbaf1..e9977fd1 100644 --- a/api/app/Integrations/Handlers/DiscordIntegration.php +++ b/api/app/Integrations/Handlers/DiscordIntegration.php @@ -2,6 +2,7 @@ namespace App\Integrations\Handlers; +use App\Models\Forms\Form; use App\Open\MentionParser; use App\Service\Forms\FormSubmissionFormatter; use Illuminate\Support\Arr; @@ -9,7 +10,7 @@ class DiscordIntegration extends AbstractIntegrationHandler { - public static function getValidationRules(): array + public static function getValidationRules(?Form $form): array { return [ 'discord_webhook_url' => 'required|url|starts_with:https://discord.com/api/webhooks', diff --git a/api/app/Integrations/Handlers/EmailIntegration.php b/api/app/Integrations/Handlers/EmailIntegration.php index b20a31e7..4655f366 100644 --- a/api/app/Integrations/Handlers/EmailIntegration.php +++ b/api/app/Integrations/Handlers/EmailIntegration.php @@ -2,20 +2,23 @@ namespace App\Integrations\Handlers; +use App\Models\Forms\Form; +use App\Models\Integration\FormIntegration; use App\Notifications\Forms\FormEmailNotification; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Notification; use App\Open\MentionParser; use App\Service\Forms\FormSubmissionFormatter; +use Illuminate\Validation\ValidationException; class EmailIntegration extends AbstractEmailIntegrationHandler { public const RISKY_USERS_LIMIT = 120; - public static function getValidationRules(): array + public static function getValidationRules(?Form $form): array { - return [ - 'send_to' => 'required', + $rules = [ + 'send_to' => ['required'], 'sender_name' => 'required', 'sender_email' => 'email|nullable', 'subject' => 'required', @@ -24,6 +27,31 @@ public static function getValidationRules(): array 'include_hidden_fields_submission_data' => ['nullable', 'boolean'], 'reply_to' => 'nullable', ]; + + if ($form->is_pro) { + return $rules; + } + + // Free plan users can only send to a single email address (avoid spam) + $rules['send_to'][] = function ($attribute, $value, $fail) use ($form) { + if (count(explode("\n", trim($value))) > 1 || count(explode(',', $value)) > 1) { + $fail('You can only send to a single email address on the free plan. Please upgrade to the Pro plan to create a new integration.'); + } + }; + + // Free plan users can only have a single email integration per form (avoid spam) + if (!request()->route('integrationid')) { + $existingEmailIntegrations = FormIntegration::where('form_id', $form->id) + ->where('integration_id', 'email') + ->count(); + if ($existingEmailIntegrations > 0) { + throw ValidationException::withMessages([ + 'settings.send_to' => ['Free users are limited to 1 email integration per form.'] + ]); + } + } + + return $rules; } protected function shouldRun(): bool diff --git a/api/app/Integrations/Handlers/GoogleSheetsIntegration.php b/api/app/Integrations/Handlers/GoogleSheetsIntegration.php index d2fd3ff7..c903a79f 100644 --- a/api/app/Integrations/Handlers/GoogleSheetsIntegration.php +++ b/api/app/Integrations/Handlers/GoogleSheetsIntegration.php @@ -4,6 +4,7 @@ use App\Events\Forms\FormSubmitted; use App\Integrations\Google\Google; +use App\Models\Forms\Form; use App\Models\Integration\FormIntegration; use Exception; use Illuminate\Support\Facades\Log; @@ -22,11 +23,9 @@ public function __construct( $this->client = new Google($formIntegration); } - public static function getValidationRules(): array + public static function getValidationRules(?Form $form): array { - return [ - - ]; + return []; } public static function isOAuthRequired(): bool diff --git a/api/app/Integrations/Handlers/SlackIntegration.php b/api/app/Integrations/Handlers/SlackIntegration.php index c34b664f..41978f08 100644 --- a/api/app/Integrations/Handlers/SlackIntegration.php +++ b/api/app/Integrations/Handlers/SlackIntegration.php @@ -2,6 +2,7 @@ namespace App\Integrations\Handlers; +use App\Models\Forms\Form; use App\Open\MentionParser; use App\Service\Forms\FormSubmissionFormatter; use Illuminate\Support\Arr; @@ -9,7 +10,7 @@ class SlackIntegration extends AbstractIntegrationHandler { - public static function getValidationRules(): array + public static function getValidationRules(?Form $form): array { return [ 'slack_webhook_url' => 'required|url|starts_with:https://hooks.slack.com/', diff --git a/api/app/Integrations/Handlers/WebhookIntegration.php b/api/app/Integrations/Handlers/WebhookIntegration.php index f5d98ec5..2d745743 100644 --- a/api/app/Integrations/Handlers/WebhookIntegration.php +++ b/api/app/Integrations/Handlers/WebhookIntegration.php @@ -2,9 +2,11 @@ namespace App\Integrations\Handlers; +use App\Models\Forms\Form; + class WebhookIntegration extends AbstractIntegrationHandler { - public static function getValidationRules(): array + public static function getValidationRules(?Form $form): array { return [ 'webhook_url' => 'required|url' diff --git a/api/app/Integrations/Handlers/ZapierIntegration.php b/api/app/Integrations/Handlers/ZapierIntegration.php index 4a71ad40..c3c21884 100644 --- a/api/app/Integrations/Handlers/ZapierIntegration.php +++ b/api/app/Integrations/Handlers/ZapierIntegration.php @@ -3,6 +3,7 @@ namespace App\Integrations\Handlers; use App\Events\Forms\FormSubmitted; +use App\Models\Forms\Form; use App\Models\Integration\FormIntegration; use Exception; @@ -16,7 +17,7 @@ public function __construct( parent::__construct($event, $formIntegration, $integration); } - public static function getValidationRules(): array + public static function getValidationRules(?Form $form): array { return []; } diff --git a/api/tests/Feature/Integrations/Email/EmailIntegrationTest.php b/api/tests/Feature/Integrations/Email/EmailIntegrationTest.php new file mode 100644 index 00000000..241c5639 --- /dev/null +++ b/api/tests/Feature/Integrations/Email/EmailIntegrationTest.php @@ -0,0 +1,175 @@ +actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace); + + // First email integration should succeed + $response = $this->postJson(route('open.forms.integration.create', $form), [ + 'integration_id' => 'email', + 'status' => true, + 'settings' => [ + 'send_to' => 'test@example.com', + 'sender_name' => 'Test Sender', + 'subject' => 'Test Subject', + 'email_content' => 'Test Content', + 'include_submission_data' => true + ] + ]); + + $response->assertSuccessful(); + expect(FormIntegration::where('form_id', $form->id)->count())->toBe(1); + + // Second email integration should fail + $response = $this->postJson(route('open.forms.integration.create', $form), [ + 'integration_id' => 'email', + 'status' => true, + 'settings' => [ + 'send_to' => 'another@example.com', + 'sender_name' => 'Test Sender', + 'subject' => 'Test Subject', + 'email_content' => 'Test Content', + 'include_submission_data' => true + ] + ]); + + $response->assertStatus(422) + ->assertJson([ + 'errors' => [ + 'settings.send_to' => ['Free users are limited to 1 email integration per form.'] + ] + ]); +}); + +test('pro user can create multiple email integrations', function () { + $user = $this->actingAsProUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace); + + // First email integration + $response = $this->postJson(route('open.forms.integration.create', $form), [ + 'integration_id' => 'email', + 'status' => true, + 'settings' => [ + 'send_to' => 'test@example.com', + 'sender_name' => 'Test Sender', + 'subject' => 'Test Subject', + 'email_content' => 'Test Content', + 'include_submission_data' => true + ] + ]); + + $response->assertSuccessful(); + + // Second email integration should also succeed for pro users + $response = $this->postJson(route('open.forms.integration.create', $form), [ + 'integration_id' => 'email', + 'status' => true, + 'settings' => [ + 'send_to' => 'another@example.com', + 'sender_name' => 'Test Sender', + 'subject' => 'Test Subject', + 'email_content' => 'Test Content', + 'include_submission_data' => true + ] + ]); + + $response->assertSuccessful(); + expect(FormIntegration::where('form_id', $form->id)->count())->toBe(2); +}); + +test('free user cannot add multiple emails', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace); + + $response = $this->postJson(route('open.forms.integration.create', $form), [ + 'integration_id' => 'email', + 'status' => true, + 'settings' => [ + 'send_to' => "test@example.com\nanother@example.com", + 'sender_name' => 'Test Sender', + 'subject' => 'Test Subject', + 'email_content' => 'Test Content', + 'include_submission_data' => true + ] + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['settings.send_to']) + ->assertJson([ + 'errors' => [ + 'settings.send_to' => ['You can only send to a single email address on the free plan. Please upgrade to the Pro plan to create a new integration.'] + ] + ]); +}); + +test('pro user can add multiple emails', function () { + $user = $this->actingAsProUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace); + + $response = $this->postJson(route('open.forms.integration.create', $form), [ + 'integration_id' => 'email', + 'status' => true, + 'settings' => [ + 'send_to' => "test@example.com\nanother@example.com\nthird@example.com", + 'sender_name' => 'Test Sender', + 'subject' => 'Test Subject', + 'email_content' => 'Test Content', + 'include_submission_data' => true + ] + ]); + + $response->assertSuccessful(); + + $integration = FormIntegration::where('form_id', $form->id)->first(); + expect($integration)->not->toBeNull(); + expect($integration->data->send_to)->toContain('test@example.com'); + expect($integration->data->send_to)->toContain('another@example.com'); + expect($integration->data->send_to)->toContain('third@example.com'); +}); + +test('free user can update their single email integration', function () { + $user = $this->actingAsUser(); + $workspace = $this->createUserWorkspace($user); + $form = $this->createForm($user, $workspace); + + // Create initial integration + $response = $this->postJson(route('open.forms.integration.create', $form), [ + 'integration_id' => 'email', + 'status' => true, + 'settings' => [ + 'send_to' => 'test@example.com', + 'sender_name' => 'Test Sender', + 'subject' => 'Test Subject', + 'email_content' => 'Test Content', + 'include_submission_data' => true + ] + ]); + + $response->assertSuccessful(); + $integrationId = $response->json('form_integration.id'); + + // Update the integration + $response = $this->putJson(route('open.forms.integration.update', [$form, $integrationId]), [ + 'integration_id' => 'email', + 'status' => true, + 'settings' => [ + 'send_to' => 'updated@example.com', + 'sender_name' => 'Updated Sender', + 'subject' => 'Updated Subject', + 'email_content' => 'Updated Content', + 'include_submission_data' => true + ] + ]); + + $response->assertSuccessful(); + + $integration = FormIntegration::find($integrationId); + expect($integration->data->send_to)->toBe('updated@example.com'); + expect($integration->data->sender_name)->toBe('Updated Sender'); +}); diff --git a/client/components/open/integrations/components/IntegrationModal.vue b/client/components/open/integrations/components/IntegrationModal.vue index 3531861d..ddd2bad8 100644 --- a/client/components/open/integrations/components/IntegrationModal.vue +++ b/client/components/open/integrations/components/IntegrationModal.vue @@ -27,6 +27,7 @@
Save @@ -55,6 +56,7 @@ const props = defineProps({ const alert = useAlert() const emit = defineEmits(["close"]) +const loading = ref(false) const formIntegrationsStore = useFormIntegrationsStore() const formIntegration = computed(() => @@ -98,7 +100,8 @@ const initIntegrationData = () => { initIntegrationData() const save = () => { - if (!integrationData.value) return + if (!integrationData.value || loading.value) return + loading.value = true integrationData.value .submit( props.formIntegrationId ? "PUT" : "POST", @@ -117,5 +120,8 @@ const save = () => { alert.error("An error occurred while saving the integration") } }) + .finally(() => { + loading.value = false + }) } From 458bcbe2ce4760d208e44f07aa3e17dc021c9220 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 10 Dec 2024 20:08:36 +0530 Subject: [PATCH 04/14] for self-hosted ignore emil validation for spam --- api/app/Integrations/Handlers/EmailIntegration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/Integrations/Handlers/EmailIntegration.php b/api/app/Integrations/Handlers/EmailIntegration.php index 4655f366..834f63db 100644 --- a/api/app/Integrations/Handlers/EmailIntegration.php +++ b/api/app/Integrations/Handlers/EmailIntegration.php @@ -28,7 +28,7 @@ public static function getValidationRules(?Form $form): array 'reply_to' => 'nullable', ]; - if ($form->is_pro) { + if ($form->is_pro || config('app.self_hosted')) { return $rules; } From e23c081b8f7e02b16dcd0c733407d664ebc28446 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 10 Dec 2024 20:10:34 +0530 Subject: [PATCH 05/14] fix pint --- api/tests/Feature/RegisterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/Feature/RegisterTest.php b/api/tests/Feature/RegisterTest.php index 9cbeacaf..87f2582a 100644 --- a/api/tests/Feature/RegisterTest.php +++ b/api/tests/Feature/RegisterTest.php @@ -73,7 +73,7 @@ }); it('requires hcaptcha token in production', function () { - app()->detectEnvironment(fn() => 'production'); + app()->detectEnvironment(fn () => 'production'); $this->postJson('/register', [ 'name' => 'Test User', From 76a78405b1fce166442a8beb6a81cc3b39937fd1 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 10 Dec 2024 20:12:35 +0530 Subject: [PATCH 06/14] ignore register throttle for testing env --- api/app/Http/Controllers/Auth/RegisterController.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/app/Http/Controllers/Auth/RegisterController.php b/api/app/Http/Controllers/Auth/RegisterController.php index 0731af4c..badd5cc5 100644 --- a/api/app/Http/Controllers/Auth/RegisterController.php +++ b/api/app/Http/Controllers/Auth/RegisterController.php @@ -28,8 +28,10 @@ class RegisterController extends Controller public function __construct() { $this->middleware('guest'); - $this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute - $this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour + if (app()->environment() !== 'testing') { + $this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute + $this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour + } } /** From ca3c56027311e79396914a901ded77bab702d9dd Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 10 Dec 2024 20:17:50 +0530 Subject: [PATCH 07/14] support new migration for mysql also --- .../2024_12_10_094605_add_meta_to_users_table.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php b/api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php index 855e6a66..f92a2409 100644 --- a/api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php +++ b/api/database/migrations/2024_12_10_094605_add_meta_to_users_table.php @@ -1,7 +1,9 @@ json('meta')->default('{}'); + $driver = DB::getDriverName(); + + Schema::table('users', function (Blueprint $table) use ($driver) { + if ($driver === 'mysql') { + $table->json('meta')->default(new Expression('(JSON_OBJECT())')); + } else { + $table->json('meta')->default('{}'); + } }); } From 9260b4b64263653b503396e54c5cd629f60b363d Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Thu, 12 Dec 2024 19:18:10 +0530 Subject: [PATCH 08/14] Register page captcha enable if captcha key set --- api/app/Http/Controllers/Auth/RegisterController.php | 2 +- client/components/pages/auth/components/RegisterForm.vue | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/api/app/Http/Controllers/Auth/RegisterController.php b/api/app/Http/Controllers/Auth/RegisterController.php index badd5cc5..ae916f45 100644 --- a/api/app/Http/Controllers/Auth/RegisterController.php +++ b/api/app/Http/Controllers/Auth/RegisterController.php @@ -72,7 +72,7 @@ protected function validator(array $data) 'utm_data' => ['nullable', 'array'], ]; - if (!config('app.self_hosted')) { + if (config('services.h_captcha.secret_key')) { $rules['h-captcha-response'] = [new ValidHCaptcha()]; } diff --git a/client/components/pages/auth/components/RegisterForm.vue b/client/components/pages/auth/components/RegisterForm.vue index 432108c2..01dd3538 100644 --- a/client/components/pages/auth/components/RegisterForm.vue +++ b/client/components/pages/auth/components/RegisterForm.vue @@ -54,7 +54,7 @@
Date: Tue, 17 Dec 2024 12:20:40 +0530 Subject: [PATCH 09/14] fix test case --- api/tests/Feature/RegisterTest.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/api/tests/Feature/RegisterTest.php b/api/tests/Feature/RegisterTest.php index 87f2582a..f280bef4 100644 --- a/api/tests/Feature/RegisterTest.php +++ b/api/tests/Feature/RegisterTest.php @@ -43,6 +43,10 @@ }); it('cannot register with disposable email', function () { + Http::fake([ + ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true]) + ]); + // Select random email $email = [ 'dumliyupse@gufum.com', @@ -63,7 +67,7 @@ ->assertStatus(422) ->assertJsonValidationErrors(['email']) ->assertJson([ - 'message' => 'Disposable email addresses are not allowed. (and 1 more error)', + 'message' => 'Disposable email addresses are not allowed.', 'errors' => [ 'email' => [ 'Disposable email addresses are not allowed.', @@ -72,15 +76,14 @@ ]); }); -it('requires hcaptcha token in production', function () { - app()->detectEnvironment(fn () => 'production'); - +it('requires hcaptcha token when register', function () { $this->postJson('/register', [ 'name' => 'Test User', 'email' => 'test@test.app', 'hear_about_us' => 'google', 'password' => 'secret', 'password_confirmation' => 'secret', + 'agree_terms' => true, ]) ->assertStatus(422) ->assertJsonValidationErrors(['h-captcha-response']); From 0d24b17e27f1235be798bd4232bc14aaab8e66d0 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 17 Dec 2024 12:25:15 +0530 Subject: [PATCH 10/14] fix test case --- api/tests/Feature/RegisterTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/tests/Feature/RegisterTest.php b/api/tests/Feature/RegisterTest.php index f280bef4..a2bae46d 100644 --- a/api/tests/Feature/RegisterTest.php +++ b/api/tests/Feature/RegisterTest.php @@ -77,6 +77,10 @@ }); it('requires hcaptcha token when register', function () { + Http::fake([ + ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true]) + ]); + $this->postJson('/register', [ 'name' => 'Test User', 'email' => 'test@test.app', From 77dc58c528d527a5c60c8b45bb52ceb1e75a1d40 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 17 Dec 2024 12:31:25 +0530 Subject: [PATCH 11/14] fix test case --- api/tests/Feature/RegisterTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/tests/Feature/RegisterTest.php b/api/tests/Feature/RegisterTest.php index a2bae46d..545f6a9e 100644 --- a/api/tests/Feature/RegisterTest.php +++ b/api/tests/Feature/RegisterTest.php @@ -76,7 +76,9 @@ ]); }); -it('requires hcaptcha token when register', function () { +it('requires hcaptcha token in production', function () { + app()->detectEnvironment(fn() => 'production'); + Http::fake([ ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true]) ]); From 45c2b5f6bd5be2abee46cea775b9bbef9052a5ed Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala Date: Tue, 17 Dec 2024 12:33:05 +0530 Subject: [PATCH 12/14] fix pint --- api/tests/Feature/RegisterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/Feature/RegisterTest.php b/api/tests/Feature/RegisterTest.php index 545f6a9e..3b3e8cd1 100644 --- a/api/tests/Feature/RegisterTest.php +++ b/api/tests/Feature/RegisterTest.php @@ -77,7 +77,7 @@ }); it('requires hcaptcha token in production', function () { - app()->detectEnvironment(fn() => 'production'); + app()->detectEnvironment(fn () => 'production'); Http::fake([ ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true]) From b5f7e2f6c623855078f14cfc97249cbe6ad97e7e Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 18 Dec 2024 12:56:04 +0100 Subject: [PATCH 13/14] Refactor RegisterController middleware and update TestCase setup - Removed environment check for throttling middleware in RegisterController, ensuring consistent rate limiting for the registration endpoint. - Updated TestCase to disable throttle middleware during tests, allowing for more flexible testing scenarios without rate limiting interference. --- api/app/Http/Controllers/Auth/RegisterController.php | 7 +++---- api/tests/TestCase.php | 9 +++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/api/app/Http/Controllers/Auth/RegisterController.php b/api/app/Http/Controllers/Auth/RegisterController.php index ae916f45..447689a1 100644 --- a/api/app/Http/Controllers/Auth/RegisterController.php +++ b/api/app/Http/Controllers/Auth/RegisterController.php @@ -28,10 +28,9 @@ class RegisterController extends Controller public function __construct() { $this->middleware('guest'); - if (app()->environment() !== 'testing') { - $this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute - $this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour - } + + $this->middleware('throttle:5,1')->only('register'); // 5 attempts per minute + $this->middleware('throttle:30,60')->only('register'); // 30 attempts per hour } /** diff --git a/api/tests/TestCase.php b/api/tests/TestCase.php index 6f91c23a..eed4d651 100644 --- a/api/tests/TestCase.php +++ b/api/tests/TestCase.php @@ -4,10 +4,19 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; +use Illuminate\Routing\Middleware\ThrottleRequests; abstract class TestCase extends BaseTestCase { use CreatesApplication; use RefreshDatabase; use TestHelpers; + + protected function setUp(): void + { + parent::setUp(); + $this->withoutMiddleware( + ThrottleRequests::class + ); + } } From c5b4900ffcfde26b787a95ead6e3bd89baa54965 Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 18 Dec 2024 13:12:23 +0100 Subject: [PATCH 14/14] Enhance hCaptcha integration in tests and configuration - Added hCaptcha site and secret keys to phpunit.xml for testing purposes. - Updated RegisterTest to configure hCaptcha secret key dynamically, ensuring proper token validation in production environment. These changes improve the testing setup for hCaptcha, facilitating more accurate simulation of production conditions. --- api/phpunit.xml | 2 ++ api/tests/Feature/RegisterTest.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api/phpunit.xml b/api/phpunit.xml index 52e0c90b..b16ee9a2 100644 --- a/api/phpunit.xml +++ b/api/phpunit.xml @@ -27,6 +27,8 @@ + + diff --git a/api/tests/Feature/RegisterTest.php b/api/tests/Feature/RegisterTest.php index 3b3e8cd1..598374cd 100644 --- a/api/tests/Feature/RegisterTest.php +++ b/api/tests/Feature/RegisterTest.php @@ -77,7 +77,7 @@ }); it('requires hcaptcha token in production', function () { - app()->detectEnvironment(fn () => 'production'); + config(['services.h_captcha.secret_key' => 'test-key']); Http::fake([ ValidHCaptcha::H_CAPTCHA_VERIFY_URL => Http::response(['success' => true])