From e6905b7bb491547097d6f599aaab5121fe7b3a83 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:39:45 +0530 Subject: [PATCH 01/27] URL input validation rule (#223) --- app/Http/Requests/AnswerFormRequest.php | 3 ++- app/Rules/ValidUrl.php | 33 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 app/Rules/ValidUrl.php diff --git a/app/Http/Requests/AnswerFormRequest.php b/app/Http/Requests/AnswerFormRequest.php index 503e7b5..be80850 100644 --- a/app/Http/Requests/AnswerFormRequest.php +++ b/app/Http/Requests/AnswerFormRequest.php @@ -12,6 +12,7 @@ use Illuminate\Validation\Rule; use Illuminate\Http\Request; use App\Rules\ValidHCaptcha; use App\Rules\ValidPhoneInputRule; +use App\Rules\ValidUrl; class AnswerFormRequest extends FormRequest { @@ -171,7 +172,7 @@ class AnswerFormRequest extends FormRequest $this->requestRules[$property['id'].'.*'] = [new StorageFile($this->maxFileSize, [], $this->form)]; return ['array']; } - return ['url']; + return [new ValidUrl]; case 'files': $allowedFileTypes = []; if(!empty($property['allowed_file_types'])){ diff --git a/app/Rules/ValidUrl.php b/app/Rules/ValidUrl.php new file mode 100644 index 0000000..cc694ec --- /dev/null +++ b/app/Rules/ValidUrl.php @@ -0,0 +1,33 @@ + Date: Fri, 20 Oct 2023 14:24:21 +0530 Subject: [PATCH 02/27] copy and paste support for file and image upload (#224) --- resources/js/components/forms/FileInput.vue | 20 ++++++++++----- resources/js/components/forms/ImageInput.vue | 27 +++++++++++++++----- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/resources/js/components/forms/FileInput.vue b/resources/js/components/forms/FileInput.vue index d5197cd..b0ac5b7 100644 --- a/resources/js/components/forms/FileInput.vue +++ b/resources/js/components/forms/FileInput.vue @@ -115,9 +115,9 @@ class="font-semibold text-nt-blue hover:text-nt-blue-dark focus:outline-none focus:underline transition duration-150 ease-in-out" @click="openFileUpload" > - Upload {{ multiple ? 'file(s)' : 'a file' }} + Upload {{ multiple ? 'file(s)' : 'a file' }}, - or drag and drop + use drag and drop or paste it

Up to {{ mbLimit }}mb @@ -198,6 +198,10 @@ export default { if(this.disabled){ this.showUploadModal = false } + document.removeEventListener('paste', this.onUploadPasteEvent) + if(this.showUploadModal){ + document.addEventListener("paste", this.onUploadPasteEvent) + } } }, files: { @@ -237,11 +241,15 @@ export default { onUploadDropEvent (e) { this.uploadDragoverEvent = false this.uploadDragoverTracking = false - this.droppedFiles(e) + this.droppedFiles(e.dataTransfer.files) }, - droppedFiles (e) { - const droppedFiles = e.dataTransfer.files - + onUploadPasteEvent (e) { + if(!this.showUploadModal) return + this.uploadDragoverEvent = false + this.uploadDragoverTracking = false + this.droppedFiles(e.clipboardData.files) + }, + droppedFiles (droppedFiles) { if (!droppedFiles) return for (let i = 0; i < droppedFiles.length; i++) { diff --git a/resources/js/components/forms/ImageInput.vue b/resources/js/components/forms/ImageInput.vue index 11de7c4..3020f1b 100644 --- a/resources/js/components/forms/ImageInput.vue +++ b/resources/js/components/forms/ImageInput.vue @@ -86,9 +86,9 @@ class="font-semibold text-nt-blue hover:text-nt-blue-dark focus:outline-none focus:underline transition duration-150 ease-in-out" @click="openFileUpload" > - Upload your image + Upload your image, - or drag and drop + use drag and drop or paste it

.jpg, .jpeg, .png, .bmp, .gif, .svg up to 5mb @@ -130,6 +130,17 @@ export default { } }, + watch: { + showUploadModal: { + handler (val) { + document.removeEventListener('paste', this.onUploadPasteEvent) + if(this.showUploadModal){ + document.addEventListener("paste", this.onUploadPasteEvent) + } + } + } + }, + methods: { clearUrl () { this.$set(this.form, this.name, null) @@ -141,11 +152,15 @@ export default { onUploadDropEvent (e) { this.uploadDragoverEvent = false this.uploadDragoverTracking = false - this.droppedFiles(e) + this.droppedFiles(e.dataTransfer.files) }, - droppedFiles (e) { - const droppedFiles = e.dataTransfer.files - + onUploadPasteEvent (e) { + if(!this.showUploadModal) return + this.uploadDragoverEvent = false + this.uploadDragoverTracking = false + this.droppedFiles(e.clipboardData.files) + }, + droppedFiles (droppedFiles) { if (!droppedFiles) return this.file = droppedFiles[0] From 4614dc0f1868ddd554d218cbcca78afa5e61fcca Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:30:35 +0530 Subject: [PATCH 03/27] Pre-fill support for file input (#222) Co-authored-by: Julien Nahum --- app/Http/Requests/UploadAssetRequest.php | 21 +++++++----- app/Http/Resources/FormSubmissionResource.php | 4 ++- app/Jobs/Form/StoreFormSubmissionJob.php | 9 +++++ app/Rules/StorageFile.php | 5 +++ resources/js/components/forms/FileInput.vue | 34 +++++++++++++++---- .../forms/fields/components/FieldOptions.vue | 7 +++- 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/app/Http/Requests/UploadAssetRequest.php b/app/Http/Requests/UploadAssetRequest.php index 05aaf72..51413a6 100644 --- a/app/Http/Requests/UploadAssetRequest.php +++ b/app/Http/Requests/UploadAssetRequest.php @@ -16,15 +16,20 @@ class UploadAssetRequest extends FormRequest */ public function rules() { + $fileTypes = [ + 'png', + 'jpeg', + 'jpg', + 'bmp', + 'gif', + 'svg' + ]; + if ($this->offsetExists('type') && $this->get('type') === 'files') { + $fileTypes = []; + } + return [ - 'url' => ['required',new StorageFile(self::FORM_ASSET_MAX_SIZE, [ - 'png', - 'jpeg', - 'jpg', - 'bmp', - 'gif', - 'svg' - ])], + 'url' => ['required', new StorageFile(self::FORM_ASSET_MAX_SIZE, $fileTypes)], ]; } } diff --git a/app/Http/Resources/FormSubmissionResource.php b/app/Http/Resources/FormSubmissionResource.php index e500dd1..6018e9e 100644 --- a/app/Http/Resources/FormSubmissionResource.php +++ b/app/Http/Resources/FormSubmissionResource.php @@ -46,7 +46,9 @@ class FormSubmissionResource extends JsonResource }); foreach ($fileFields as $field) { if (isset($data[$field['id']]) && !empty($data[$field['id']])) { - $data[$field['id']] = collect($data[$field['id']])->map(function ($file) { + $data[$field['id']] = collect($data[$field['id']])->filter(function ($file) { + return $file !== null && $file; + })->map(function ($file) { return [ 'file_url' => route('open.forms.submissions.file', [$this->form_id, $file]), 'file_name' => $file, diff --git a/app/Jobs/Form/StoreFormSubmissionJob.php b/app/Jobs/Form/StoreFormSubmissionJob.php index 54f2933..0c4e526 100644 --- a/app/Jobs/Form/StoreFormSubmissionJob.php +++ b/app/Jobs/Form/StoreFormSubmissionJob.php @@ -4,6 +4,7 @@ namespace App\Jobs\Form; use App\Events\Forms\FormSubmitted; use App\Http\Controllers\Forms\PublicFormController; +use App\Http\Controllers\Forms\FormController; use App\Http\Requests\AnswerFormRequest; use App\Models\Forms\Form; use App\Models\Forms\FormSubmission; @@ -162,6 +163,14 @@ class StoreFormSubmissionJob implements ShouldQueue return null; } + if(filter_var($value, FILTER_VALIDATE_URL) !== FALSE && str_contains($value, parse_url(config('app.url'))['host'])) { // In case of prefill we have full url so convert to s3 + $fileName = basename($value); + $path = FormController::ASSETS_UPLOAD_PATH . '/' . $fileName; + $newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id); + Storage::move($path, $newPath.'/'.$fileName); + return $fileName; + } + if($this->isSkipForUpload($value)) { return $value; } diff --git a/app/Rules/StorageFile.php b/app/Rules/StorageFile.php index f2f0a30..b7df2eb 100644 --- a/app/Rules/StorageFile.php +++ b/app/Rules/StorageFile.php @@ -40,6 +40,11 @@ class StorageFile implements Rule */ public function passes($attribute, $value): bool { + // If full path then no need to validate + if (filter_var($value, FILTER_VALIDATE_URL) !== FALSE) { + return true; + } + // This is use when updating a record, and file uploads aren't changed. if($this->form){ $newPath = Str::of(PublicFormController::FILE_UPLOAD_PATH)->replace('?', $this->form->id); diff --git a/resources/js/components/forms/FileInput.vue b/resources/js/components/forms/FileInput.vue index b0ac5b7..d7ffbd2 100644 --- a/resources/js/components/forms/FileInput.vue +++ b/resources/js/components/forms/FileInput.vue @@ -156,6 +156,7 @@ + + diff --git a/resources/js/pages/templates/types-show.vue b/resources/js/pages/templates/types-show.vue new file mode 100644 index 0000000..b91ef87 --- /dev/null +++ b/resources/js/pages/templates/types-show.vue @@ -0,0 +1,233 @@ + + + + + + diff --git a/resources/js/router/routes.js b/resources/js/router/routes.js index 281ff78..8d67a56 100644 --- a/resources/js/router/routes.js +++ b/resources/js/router/routes.js @@ -70,6 +70,8 @@ export default [ { path: '/my-templates', name: 'my_templates', component: page('templates/my_templates.vue') }, { path: '/form-templates', name: 'templates', component: page('templates/templates.vue') }, { path: '/form-templates/:slug', name: 'templates.show', component: page('templates/show.vue') }, + { path: '/form-templates/types/:slug', name: 'templates.types.show', component: page('templates/types-show.vue') }, + { path: '/form-templates/industries/:slug', name: 'templates.industries.show', component: page('templates/industries-show.vue') }, { path: '*', component: page('errors/404.vue') } ] diff --git a/resources/js/store/modules/open/templates.js b/resources/js/store/modules/open/templates.js index 3e229fa..15f96de 100644 --- a/resources/js/store/modules/open/templates.js +++ b/resources/js/store/modules/open/templates.js @@ -91,12 +91,26 @@ export const actions = { context.commit('stopLoading') }) }, - loadAll (context) { + loadAll (context, options=null) { context.commit('startLoading') context.dispatch('loadTypesAndIndustries') - return axios.get(templatesEndpoint).then((response) => { - context.commit('append', response.data) - context.commit('setAllLoaded', true) + + // Prepare with options + let queryStr = '' + if(options !== null){ + for (const [key, value] of Object.entries(options)) { + queryStr += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(value) + } + queryStr = queryStr.slice(1) + } + return axios.get((queryStr) ? templatesEndpoint + '?' + queryStr : templatesEndpoint).then((response) => { + if(options !== null){ + context.commit('set', response.data) + context.commit('setAllLoaded', false) + } else { + context.commit('append', response.data) + context.commit('setAllLoaded', true) + } context.commit('stopLoading') }).catch((error) => { context.commit('stopLoading') @@ -108,17 +122,5 @@ export const actions = { } context.commit('stopLoading') return Promise.resolve() - }, - loadWithLimit (context, limit) { - context.commit('startLoading') - context.dispatch('loadTypesAndIndustries') - - return axios.get(templatesEndpoint + '?limit=' + limit).then((response) => { - context.commit('set', response.data) - context.commit('setAllLoaded', false) - context.commit('stopLoading') - }).catch((error) => { - context.commit('stopLoading') - }) } } From b043407a4fb100ebb7a52abba3430a7757c9783b Mon Sep 17 00:00:00 2001 From: Valentin Ouvrard Date: Wed, 25 Oct 2023 19:12:08 +1100 Subject: [PATCH 05/27] feat(s3-aws): Implement use_path_style_endpoint for Minio usage (#228) * feat(s3-aws): Implement use_path_style_endpoint for Minio usage (https://min.io/) * feat(s3): moove default path style to true --------- Co-authored-by: Valentin Ouvrard Co-authored-by: Julien Nahum --- config/filesystems.php | 1 + 1 file changed, 1 insertion(+) diff --git a/config/filesystems.php b/config/filesystems.php index 94c8112..617e720 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -63,6 +63,7 @@ return [ 'bucket' => env('AWS_BUCKET'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', true), ], ], From 8e5acab8bffeb842e4b913a6b0f435836f223c83 Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 25 Oct 2023 17:55:53 +0200 Subject: [PATCH 06/27] Fix AI generation + remove valet tls --- app/Jobs/Form/GenerateAiForm.php | 38 ++++++++++++++------------------ vite.config.js | 5 ++--- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/app/Jobs/Form/GenerateAiForm.php b/app/Jobs/Form/GenerateAiForm.php index 3e19a3e..86125f4 100644 --- a/app/Jobs/Form/GenerateAiForm.php +++ b/app/Jobs/Form/GenerateAiForm.php @@ -3,11 +3,9 @@ namespace App\Jobs\Form; use App\Console\Commands\GenerateTemplate; -use App\Http\Requests\AiGenerateFormRequest; use App\Models\Forms\AI\AiFormCompletion; use App\Service\OpenAi\GptCompleter; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -40,6 +38,7 @@ class GenerateAiForm implements ShouldQueue ]); $completer = (new GptCompleter(config('services.openai.api_key'))) + ->useStreaming() ->setSystemMessage('You are a robot helping to generate forms.'); try { @@ -53,29 +52,11 @@ class GenerateAiForm implements ShouldQueue 'result' => $this->cleanOutput($completer->getArray()) ]); } catch (\Exception $e) { - $this->completion->update([ - 'status' => AiFormCompletion::STATUS_FAILED, - 'result' => ['error' => $e->getMessage()] - ]); + $this->onError($e); } } - public function generateForm(AiGenerateFormRequest $request) - { - $completer = (new GptCompleter(config('services.openai.api_key'))) - ->setSystemMessage('You are a robot helping to generate forms.'); - $completer->completeChat([ - ["role" => "user", "content" => Str::of(GenerateTemplate::FORM_STRUCTURE_PROMPT) - ->replace('[REPLACE]', $request->form_prompt)->toString()] - ], 3000); - - return $this->success([ - 'message' => 'Form successfully generated!', - 'form' => $this->cleanOutput($completer->getArray()) - ]); - } - private function cleanOutput($formData) { // Add property uuids @@ -85,4 +66,19 @@ class GenerateAiForm implements ShouldQueue return $formData; } + + /** + * Handle a job failure. + */ + public function failed(\Throwable $exception): void + { + $this->onError($exception); + } + + private function onError(\Throwable $e) { + $this->completion->update([ + 'status' => AiFormCompletion::STATUS_FAILED, + 'result' => ['error' => $e->getMessage()] + ]); + } } diff --git a/vite.config.js b/vite.config.js index d5d52e9..2aea756 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,4 +1,4 @@ -import {defineConfig} from 'vite' +import { defineConfig } from 'vite' import laravel from 'laravel-vite-plugin' import vue from '@vitejs/plugin-vue2' import { sentryVitePlugin } from '@sentry/vite-plugin' @@ -7,8 +7,7 @@ const plugins = [ laravel({ input: [ 'resources/js/app.js' - ], - valetTls: 'opnform.test' + ] }), vue({ template: { From 8a2e071c5623e9e5262553bed16917e304451996 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Thu, 26 Oct 2023 16:52:16 +0530 Subject: [PATCH 07/27] Combine integrations & notifications sections (#229) * Combine integrations & notifications sections * New section Form Access --- app/Http/Resources/FormResource.php | 1 + app/Models/Forms/Form.php | 5 ++ resources/js/components/common/Button.vue | 10 ++- .../open/forms/components/FormEditor.vue | 10 +-- .../form-components/FormAboutSubmission.vue | 24 ------ .../components/form-components/FormAccess.vue | 74 +++++++++++++++++ .../form-components/FormIntegrations.vue | 74 ----------------- .../form-components/FormNotifications.vue | 25 +++++- .../form-components/FormSecurityPrivacy.vue | 3 - .../components/FormNotificationsWebhook.vue | 82 +++++++++++++++++++ 10 files changed, 197 insertions(+), 111 deletions(-) create mode 100644 resources/js/components/open/forms/components/form-components/FormAccess.vue delete mode 100644 resources/js/components/open/forms/components/form-components/FormIntegrations.vue create mode 100644 resources/js/components/open/forms/components/form-components/components/FormNotificationsWebhook.vue diff --git a/app/Http/Resources/FormResource.php b/app/Http/Resources/FormResource.php index 9cdc44c..0cc8949 100644 --- a/app/Http/Resources/FormResource.php +++ b/app/Http/Resources/FormResource.php @@ -29,6 +29,7 @@ class FormResource extends JsonResource 'views_count' => $this->views_count, 'submissions_count' => $this->submissions_count, 'notifies' => $this->notifies, + 'notifies_webhook' => $this->notifies_webhook, 'notifies_slack' => $this->notifies_slack, 'notifies_discord' => $this->notifies_discord, 'send_submission_confirmation' => $this->send_submission_confirmation, diff --git a/app/Models/Forms/Form.php b/app/Models/Forms/Form.php index 2e2ae0d..ede1be7 100644 --- a/app/Models/Forms/Form.php +++ b/app/Models/Forms/Form.php @@ -279,6 +279,11 @@ class Form extends Model } + public function getNotifiesWebhookAttribute() + { + return !empty($this->webhook_url); + } + public function getNotifiesDiscordAttribute() { return !empty($this->discord_webhook_url); diff --git a/resources/js/components/common/Button.vue b/resources/js/components/common/Button.vue index f393d75..85e21d8 100644 --- a/resources/js/components/common/Button.vue +++ b/resources/js/components/common/Button.vue @@ -1,5 +1,8 @@ diff --git a/resources/js/components/open/forms/components/form-components/FormAccess.vue b/resources/js/components/open/forms/components/form-components/FormAccess.vue new file mode 100644 index 0000000..e98c9e2 --- /dev/null +++ b/resources/js/components/open/forms/components/form-components/FormAccess.vue @@ -0,0 +1,74 @@ + + + diff --git a/resources/js/components/open/forms/components/form-components/FormIntegrations.vue b/resources/js/components/open/forms/components/form-components/FormIntegrations.vue deleted file mode 100644 index 7d7ac77..0000000 --- a/resources/js/components/open/forms/components/form-components/FormIntegrations.vue +++ /dev/null @@ -1,74 +0,0 @@ - - - diff --git a/resources/js/components/open/forms/components/form-components/FormNotifications.vue b/resources/js/components/open/forms/components/form-components/FormNotifications.vue index 016e874..f76513a 100644 --- a/resources/js/components/open/forms/components/form-components/FormNotifications.vue +++ b/resources/js/components/open/forms/components/form-components/FormNotifications.vue @@ -6,8 +6,7 @@ :class="{'text-blue-600':isCollapseOpen, 'text-gray-500':!isCollapseOpen}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> - - Notifications + Notifications & Integrations @@ -16,6 +15,22 @@ + + +

+ + + +

+ Zapier Integration +

+
+ @@ -27,9 +42,10 @@ import FormNotificationsOption from './components/FormNotificationsOption.vue' import FormNotificationsSlack from './components/FormNotificationsSlack.vue' import FormNotificationsDiscord from './components/FormNotificationsDiscord.vue' import FormNotificationsSubmissionConfirmation from './components/FormNotificationsSubmissionConfirmation.vue' +import FormNotificationsWebhook from './components/FormNotificationsWebhook.vue' export default { - components: { FormNotificationsSubmissionConfirmation, FormNotificationsSlack, FormNotificationsDiscord, FormNotificationsOption, Collapse, ProTag }, + components: { FormNotificationsSubmissionConfirmation, FormNotificationsSlack, FormNotificationsDiscord, FormNotificationsOption, Collapse, ProTag, FormNotificationsWebhook }, props: { }, data () { @@ -47,7 +63,8 @@ export default { set (value) { this.$store.commit('open/working_form/set', value) } - } + }, + zapierUrl: () => window.config.links.zapier_integration }, watch: { diff --git a/resources/js/components/open/forms/components/form-components/FormSecurityPrivacy.vue b/resources/js/components/open/forms/components/form-components/FormSecurityPrivacy.vue index 1c54110..3186629 100644 --- a/resources/js/components/open/forms/components/form-components/FormSecurityPrivacy.vue +++ b/resources/js/components/open/forms/components/form-components/FormSecurityPrivacy.vue @@ -17,9 +17,6 @@ label="Protect your form with a Captcha" help="If enabled we will make sure respondant is a human" /> - diff --git a/resources/js/components/open/forms/components/form-components/components/FormNotificationsWebhook.vue b/resources/js/components/open/forms/components/form-components/components/FormNotificationsWebhook.vue new file mode 100644 index 0000000..eec9102 --- /dev/null +++ b/resources/js/components/open/forms/components/form-components/components/FormNotificationsWebhook.vue @@ -0,0 +1,82 @@ + + + From 7c03d20cc481965325f009536c3c74d99c3d4139 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Thu, 26 Oct 2023 18:28:35 +0530 Subject: [PATCH 08/27] Number input as Scale (#227) * Number input as Scale * scale input --------- Co-authored-by: Julien Nahum --- resources/js/components/forms/ScaleInput.vue | 102 ++++++++++++++++++ resources/js/components/forms/index.js | 4 +- .../components/open/forms/OpenFormField.vue | 7 ++ .../forms/fields/components/FieldOptions.vue | 46 +++++++- resources/js/config/form-themes.js | 18 ++++ 5 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 resources/js/components/forms/ScaleInput.vue diff --git a/resources/js/components/forms/ScaleInput.vue b/resources/js/components/forms/ScaleInput.vue new file mode 100644 index 0000000..ea162cf --- /dev/null +++ b/resources/js/components/forms/ScaleInput.vue @@ -0,0 +1,102 @@ + + + diff --git a/resources/js/components/forms/index.js b/resources/js/components/forms/index.js index a81e16e..04e542d 100644 --- a/resources/js/components/forms/index.js +++ b/resources/js/components/forms/index.js @@ -15,6 +15,7 @@ import ImageInput from './ImageInput.vue' import RatingInput from './RatingInput.vue' import FlatSelectInput from './FlatSelectInput.vue' import ToggleSwitchInput from './ToggleSwitchInput.vue' +import ScaleInput from './ScaleInput.vue' // Components that are registered globaly. [ @@ -32,7 +33,8 @@ import ToggleSwitchInput from './ToggleSwitchInput.vue' ImageInput, RatingInput, FlatSelectInput, - ToggleSwitchInput + ToggleSwitchInput, + ScaleInput ].forEach(Component => { Vue.component(Component.name, Component) }) diff --git a/resources/js/components/open/forms/OpenFormField.vue b/resources/js/components/open/forms/OpenFormField.vue index eda7871..b96ae6d 100644 --- a/resources/js/components/open/forms/OpenFormField.vue +++ b/resources/js/components/open/forms/OpenFormField.vue @@ -150,6 +150,9 @@ export default { if (field.type === 'number' && field.is_rating && field.rating_max_value) { return 'RatingInput' } + if (field.type === 'number' && field.is_scale && field.scale_max_value) { + return 'ScaleInput' + } if (['select', 'multi_select'].includes(field.type) && field.without_dropdown) { return 'FlatSelectInput' } @@ -302,6 +305,10 @@ export default { inputProperties.accept = (this.form.is_pro && field.allowed_file_types) ? field.allowed_file_types : "" } else if (field.type === 'number' && field.is_rating) { inputProperties.numberOfStars = parseInt(field.rating_max_value) + } else if (field.type === 'number' && field.is_scale) { + inputProperties.minScale = parseInt(field.scale_min_value) ?? 1 + inputProperties.maxScale = parseInt(field.scale_max_value) ?? 5 + inputProperties.stepScale = parseInt(field.scale_step_value) ?? 1 } else if (field.type === 'number' || (field.type === 'phone_number' && field.use_simple_text_input)) { inputProperties.pattern = '/\d*' } else if (field.type === 'phone_number' && !field.use_simple_text_input) { diff --git a/resources/js/components/open/forms/fields/components/FieldOptions.vue b/resources/js/components/open/forms/fields/components/FieldOptions.vue index 1e208c6..a2eb163 100644 --- a/resources/js/components/open/forms/fields/components/FieldOptions.vue +++ b/resources/js/components/open/forms/fields/components/FieldOptions.vue @@ -73,13 +73,36 @@ Rating

- If enabled then this field will be star rating input. + Transform this field into a star rating input.

+ + + Scale + +

+ Transform this field into a scale/score input. +

+ @@ -510,8 +533,25 @@ export default { } }, initRating() { - if (this.field.is_rating && !this.field.rating_max_value) { - this.$set(this.field, 'rating_max_value', 5) + if (this.field.is_rating) { + this.$set(this.field, 'is_scale', false) + if (!this.field.rating_max_value) { + this.$set(this.field, 'rating_max_value', 5) + } + } + }, + initScale () { + if (this.field.is_scale) { + this.$set(this.field, 'is_rating', false) + if (!this.field.scale_min_value) { + this.$set(this.field, 'scale_min_value', 1) + } + if (!this.field.scale_max_value) { + this.$set(this.field, 'scale_max_value', 5) + } + if (!this.field.scale_step_value) { + this.$set(this.field, 'scale_step_value', 1) + } } }, onFieldOptionsChange(val) { diff --git a/resources/js/config/form-themes.js b/resources/js/config/form-themes.js index dab4996..1e4a15e 100644 --- a/resources/js/config/form-themes.js +++ b/resources/js/config/form-themes.js @@ -25,6 +25,12 @@ export const themes = { label: 'text-gray-700 dark:text-gray-300 font-semibold', input: 'relative w-full rounded-lg border-gray-300 flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full px-4 bg-white text-gray-700 placeholder-gray-400 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-600 shadow-sm text-base focus:outline-none focus:ring-2 focus:border-transparent', help: 'text-gray-400 dark:text-gray-500' + }, + ScaleInput: { + label: 'text-gray-700 dark:text-gray-300 font-semibold', + button: 'cursor-pointer text-gray-700 inline-block rounded-lg border-gray-300 px-4 py-2 flex-grow dark:bg-notion-dark-light dark:text-gray-300 text-center', + unselectedButton: 'bg-white hover:bg-gray-50 border', + help: 'text-gray-400 dark:text-gray-500' } }, simple: { @@ -50,6 +56,12 @@ export const themes = { label: 'text-gray-700 dark:text-gray-300 font-semibold', input: 'border-transparent flex-1 appearance-none border border-gray-300 dark:border-gray-600 w-full bg-white text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-1 focus:ring-opacity-100 focus:border-transparent focus:ring-2', help: 'text-gray-400 dark:text-gray-500' + }, + ScaleInput: { + label: 'text-gray-700 dark:text-gray-300 font-semibold', + button: 'flex-1 appearance-none border-gray-300 dark:border-gray-600 w-full py-2 px-2 bg-gray-50 text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 text-center', + unselectedButton: 'bg-white hover:bg-gray-50 border -mx-4', + help: 'text-gray-400 dark:text-gray-500' } }, notion: { @@ -75,6 +87,12 @@ export const themes = { label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4', input: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion border border-gray-300 dark:border-gray-600 w-full text-gray-900 bg-notion-input-background dark:bg-notion-dark-light shadow-inner dark:placeholder-gray-500 placeholder-gray-400 text-base focus:outline-none focus:ring-0 focus:ring-opacity-100 focus:border-transparent focus:ring-0 focus:shadow-focus-notion', help: 'text-notion-input-help dark:text-gray-500' + }, + ScaleInput: { + label: 'text-gray-900 dark:text-gray-100 mb-2 block mt-4', + button: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion w-full py-2 px-2 bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 text-center', + unselectedButton: 'bg-notion-input-background dark:bg-notion-dark-light hover:bg-gray-50 border', + help: 'text-notion-input-help dark:text-gray-500' } } From 2e52518aa770a7f532ebfd35bb4fc718dd5211fc Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Fri, 27 Oct 2023 13:48:08 +0530 Subject: [PATCH 09/27] fix submission confirmation mail submission_id (#230) * fix submission confirmation mail submission_id * fix condition --- app/Mail/Forms/SubmissionConfirmationMail.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Mail/Forms/SubmissionConfirmationMail.php b/app/Mail/Forms/SubmissionConfirmationMail.php index 4ea01a3..6532e94 100644 --- a/app/Mail/Forms/SubmissionConfirmationMail.php +++ b/app/Mail/Forms/SubmissionConfirmationMail.php @@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Str; use Illuminate\Support\Arr; +use Vinkla\Hashids\Facades\Hashids; class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue { @@ -44,7 +45,7 @@ class SubmissionConfirmationMail extends OpenFormMail implements ShouldQueue 'fields' => $formatter->getFieldsWithValue(), 'form' => $form, 'noBranding' => $form->no_branding, - 'submission_id' => $this->event->data['submission_id'] ?? null + 'submission_id' => (isset($this->event->data['submission_id']) && $this->event->data['submission_id']) ? Hashids::encode($this->event->data['submission_id']) : null ]); } From e9174238e4b70c8e7d932895df767c4f4cc5505b Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 1 Nov 2023 16:58:10 +0100 Subject: [PATCH 10/27] Appsumo (#232) * Implemented webhooks * oAuth wip * Implement the whole auth flow * Implement file upload limit depending on appsumo license --- .../Auth/AppSumoAuthController.php | 117 ++++++++++++++++++ .../Controllers/Auth/RegisterController.php | 24 ++-- .../Controllers/Webhook/AppSumoController.php | 91 ++++++++++++++ app/Http/Requests/AnswerFormRequest.php | 10 +- app/Http/Resources/UserResource.php | 1 + app/Models/License.php | 45 +++++++ app/Models/User.php | 33 +++-- app/Models/Workspace.php | 25 +++- config/services.php | 12 +- ...023_10_30_133259_create_licenses_table.php | 39 ++++++ 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 | 37 ++++-- routes/api.php | 6 + 19 files changed, 611 insertions(+), 102 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 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 new file mode 100644 index 0000000..505d2d9 --- /dev/null +++ b/app/Http/Controllers/Auth/AppSumoAuthController.php @@ -0,0 +1,117 @@ +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; + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 736eff8..df7e99a 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 @@ 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; } } diff --git a/app/Http/Controllers/Webhook/AppSumoController.php b/app/Http/Controllers/Webhook/AppSumoController.php new file mode 100644 index 0000000..6bbf900 --- /dev/null +++ b/app/Http/Controllers/Webhook/AppSumoController.php @@ -0,0 +1,91 @@ +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.'); + } + } +} diff --git a/app/Http/Requests/AnswerFormRequest.php b/app/Http/Requests/AnswerFormRequest.php index be80850..2a3039f 100644 --- a/app/Http/Requests/AnswerFormRequest.php +++ b/app/Http/Requests/AnswerFormRequest.php @@ -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; } /** diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index cfabaa0..5e8de1d 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -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); diff --git a/app/Models/License.php b/app/Models/License.php new file mode 100644 index 0000000..bb72f34 --- /dev/null +++ b/app/Models/License.php @@ -0,0 +1,45 @@ + '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']]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 538136b..3ce5c21 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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'); + }); }); } diff --git a/app/Models/Workspace.php b/app/Models/Workspace.php index 5974a59..c0efa23 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 @@ 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'))){ diff --git a/config/services.php b/config/services.php index fe0ad40..7e2b9d1 100644 --- a/config/services.php +++ b/config/services.php @@ -45,7 +45,7 @@ return [ ], '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 @@ return [ ], '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 0000000..034cde5 --- /dev/null +++ b/database/migrations/2023_10_30_133259_create_licenses_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('license_key'); + $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_id', 'license_provider']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('licenses'); + } +}; 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 0000000..5dcffbd --- /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 0000000..71573cc --- /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 a06f82d..7eadc7d 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 17164eb..47a497f 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' + }) } } diff --git a/routes/api.php b/routes/api.php index 9ec4b92..cfb0aba 100644 --- a/routes/api.php +++ b/routes/api.php @@ -129,6 +129,12 @@ Route::group(['middleware' => 'guest:api'], function () { 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 dc5a828b8e51c6b529bc575d1c4fb49e43a4d038 Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 1 Nov 2023 18:14:01 +0100 Subject: [PATCH 11/27] Fix the user issue --- app/Models/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/User.php b/app/Models/User.php index 3ce5c21..f9cabff 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -154,7 +154,7 @@ class User extends Authenticatable implements JWTSubject return $this->hasMany(License::class); } - public function activeLicense(): License + public function activeLicense(): ?License { return $this->licenses()->active()->first(); } From 04a367d12073dd2fc55ee2c6252bcbf0b8f63801 Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 1 Nov 2023 20:17:39 +0100 Subject: [PATCH 12/27] Added license webhook logs --- .../Controllers/Webhook/AppSumoController.php | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/Webhook/AppSumoController.php b/app/Http/Controllers/Webhook/AppSumoController.php index 6bbf900..ffc0e46 100644 --- a/app/Http/Controllers/Webhook/AppSumoController.php +++ b/app/Http/Controllers/Webhook/AppSumoController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Webhook; use App\Http\Controllers\Controller; use App\Models\License; +use Illuminate\Support\Facades\Log; use Illuminate\Http\Request; use Illuminate\Validation\UnauthorizedException; @@ -14,6 +15,7 @@ class AppSumoController extends Controller $this->validateSignature($request); if ($request->test) { + Log::info('[APPSUMO] test request received', $request->toArray()); return $this->success([ 'message' => 'Webhook received.', 'event' => $request->event, @@ -21,6 +23,8 @@ class AppSumoController extends Controller ]); } + Log::info('[APPSUMO] request received', $request->toArray()); + // Call the right function depending on the event using match() match ($request->event) { 'activate' => $this->handleActivateEvent($request), @@ -45,6 +49,7 @@ class AppSumoController extends Controller ]); $licence->meta = $request->json()->all(); $licence->save(); + Log::info('[APPSUMO] activating license', $request->toArray()); } private function handleChangeEvent($request) @@ -58,13 +63,23 @@ class AppSumoController extends Controller 'status' => License::STATUS_INACTIVE, ]); + Log::info('[APPSUMO] De-activating license', [ + 'license_key' => $request->prev_license_key, + 'license_id' => $oldLicense->id, + ]); + // Create new license - License::create([ + $license = License::create([ 'license_key' => $request->license_key, 'license_provider' => 'appsumo', 'status' => License::STATUS_ACTIVE, 'meta' => $request->json()->all(), ]); + Log::info('[APPSUMO] creating new license', + [ + 'license_key' => $license->license_key, + 'license_id' => $license->id, + ]); } private function handleDeactivateEvent($request) @@ -77,6 +92,10 @@ class AppSumoController extends Controller $oldLicense->update([ 'status' => License::STATUS_INACTIVE, ]); + Log::info('[APPSUMO] De-activating license', [ + 'license_key' => $request->prev_license_key, + 'license_id' => $oldLicense->id, + ]); } private function validateSignature(Request $request) From 88a1e18055ed6e9207cb242649676a6d1d1baf8e Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 1 Nov 2023 20:36:03 +0100 Subject: [PATCH 13/27] Fix and improve license webhook controller --- .../Controllers/Webhook/AppSumoController.php | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/app/Http/Controllers/Webhook/AppSumoController.php b/app/Http/Controllers/Webhook/AppSumoController.php index ffc0e46..b1154e8 100644 --- a/app/Http/Controllers/Webhook/AppSumoController.php +++ b/app/Http/Controllers/Webhook/AppSumoController.php @@ -42,39 +42,30 @@ class AppSumoController extends Controller 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(); - Log::info('[APPSUMO] activating license', $request->toArray()); + $this->createLicense($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, - ]); + $this->deactivateLicense($request->prev_license_key); + $this->createLicense($request->json()->all()); + } - Log::info('[APPSUMO] De-activating license', [ - 'license_key' => $request->prev_license_key, - 'license_id' => $oldLicense->id, - ]); + private function handleDeactivateEvent($request) + { + $this->deactivateLicense($request->license_key); + } - // Create new license - $license = License::create([ - 'license_key' => $request->license_key, + private function createLicense(array $licenseData) + { + $license = License::firstOrNew([ + 'license_key' => $licenseData['license_key'], 'license_provider' => 'appsumo', 'status' => License::STATUS_ACTIVE, - 'meta' => $request->json()->all(), ]); + $license->meta = $licenseData; + $license->save(); + Log::info('[APPSUMO] creating new license', [ 'license_key' => $license->license_key, @@ -82,18 +73,17 @@ class AppSumoController extends Controller ]); } - private function handleDeactivateEvent($request) + private function deactivateLicense(string $licenseKey) { - // Deactivate old license $oldLicense = License::where([ - 'license_key' => $request->prev_license_key, + 'license_key' => $licenseKey, 'license_provider' => 'appsumo', ])->firstOrFail(); $oldLicense->update([ 'status' => License::STATUS_INACTIVE, ]); Log::info('[APPSUMO] De-activating license', [ - 'license_key' => $request->prev_license_key, + 'license_key' => $licenseKey, 'license_id' => $oldLicense->id, ]); } From cf0e92365008f012d6be9ead3647d75777cb3e05 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala <60499540+chiragchhatrala@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:09:33 +0530 Subject: [PATCH 14/27] remove extra files (#233) Co-authored-by: Forms Dev --- .../forms/fields/FormBlockOptionsModal.vue | 204 ------ .../forms/fields/FormFieldOptionsModal.vue | 579 ------------------ 2 files changed, 783 deletions(-) delete mode 100644 resources/js/components/open/forms/fields/FormBlockOptionsModal.vue delete mode 100644 resources/js/components/open/forms/fields/FormFieldOptionsModal.vue diff --git a/resources/js/components/open/forms/fields/FormBlockOptionsModal.vue b/resources/js/components/open/forms/fields/FormBlockOptionsModal.vue deleted file mode 100644 index 7c2e4e0..0000000 --- a/resources/js/components/open/forms/fields/FormBlockOptionsModal.vue +++ /dev/null @@ -1,204 +0,0 @@ - - - diff --git a/resources/js/components/open/forms/fields/FormFieldOptionsModal.vue b/resources/js/components/open/forms/fields/FormFieldOptionsModal.vue deleted file mode 100644 index fa4428c..0000000 --- a/resources/js/components/open/forms/fields/FormFieldOptionsModal.vue +++ /dev/null @@ -1,579 +0,0 @@ - - - From 8de1c94291a9c74cf454a80f7197d55bcf27c0cd Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:05:21 +0530 Subject: [PATCH 15/27] Fix the template API (#234) --- app/Http/Controllers/TemplateController.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/TemplateController.php b/app/Http/Controllers/TemplateController.php index 8a89614..04a1b2a 100644 --- a/app/Http/Controllers/TemplateController.php +++ b/app/Http/Controllers/TemplateController.php @@ -15,7 +15,7 @@ class TemplateController extends Controller { $limit = null; if ($request->offsetExists('limit') && $request->get('limit') > 0) { - $limit = (int) $request->get('limit'); + $limit = (int)$request->get('limit'); } $onlyMy = false; @@ -24,12 +24,18 @@ class TemplateController extends Controller } $templates = Template::limit($limit) - ->when(Auth::check() && !$onlyMy, function ($query) { - $query->where('publicly_listed', true); - $query->orWhere('creator_id', Auth::id()); + ->when(Auth::check(), function ($query) use ($onlyMy) { + if ($onlyMy) { + $query->where('creator_id', Auth::id()); + } else { + $query->where(function ($query) { + $query->where('publicly_listed', true) + ->orWhere('creator_id', Auth::id()); + }); + } }) - ->when(Auth::check() && $onlyMy, function ($query) { - $query->where('creator_id', Auth::id()); + ->when(!Auth::check(), function ($query) { + return $query->publiclyListed(); }) ->orderByDesc('created_at') ->get(); From 00e98f6bc6c982d5f4adc49c40a44eef4d71ebd7 Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Tue, 7 Nov 2023 12:31:35 +0100 Subject: [PATCH 16/27] Remove stripe keys from docker file --- .env.docker | 3 --- 1 file changed, 3 deletions(-) diff --git a/.env.docker b/.env.docker index b907e68..e1fc74b 100644 --- a/.env.docker +++ b/.env.docker @@ -53,9 +53,6 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" JWT_TTL=1440 JWT_SECRET= -STRIPE_KEY= -STRIPE_SECRET= - MUX_WORKSPACE_ID= MUX_API_TOKEN= From 796b69f60f390113ef5bb18c23f5ae753a8e4fec Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 8 Nov 2023 21:56:24 +0100 Subject: [PATCH 17/27] Fix license change AppSumo --- .../Controllers/Webhook/AppSumoController.php | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Webhook/AppSumoController.php b/app/Http/Controllers/Webhook/AppSumoController.php index b1154e8..c4fd44b 100644 --- a/app/Http/Controllers/Webhook/AppSumoController.php +++ b/app/Http/Controllers/Webhook/AppSumoController.php @@ -47,8 +47,10 @@ class AppSumoController extends Controller private function handleChangeEvent($request) { - $this->deactivateLicense($request->prev_license_key); - $this->createLicense($request->json()->all()); + $license = $this->deactivateLicense($request->prev_license_key); + $this->createLicense(array_merge($request->json()->all(), [ + 'user_id' => $license->user_id, + ])); } private function handleDeactivateEvent($request) @@ -56,7 +58,7 @@ class AppSumoController extends Controller $this->deactivateLicense($request->license_key); } - private function createLicense(array $licenseData) + private function createLicense(array $licenseData): License { $license = License::firstOrNew([ 'license_key' => $licenseData['license_key'], @@ -71,21 +73,25 @@ class AppSumoController extends Controller 'license_key' => $license->license_key, 'license_id' => $license->id, ]); + + return $license; } - private function deactivateLicense(string $licenseKey) + private function deactivateLicense(string $licenseKey): License { - $oldLicense = License::where([ + $license = License::where([ 'license_key' => $licenseKey, 'license_provider' => 'appsumo', ])->firstOrFail(); - $oldLicense->update([ + $license->update([ 'status' => License::STATUS_INACTIVE, ]); Log::info('[APPSUMO] De-activating license', [ 'license_key' => $licenseKey, - 'license_id' => $oldLicense->id, + 'license_id' => $license->id, ]); + + return $license; } private function validateSignature(Request $request) From 9e2bf1c2802a6b986cbf43d5ff0ad5f4f432426f Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Wed, 8 Nov 2023 22:18:22 +0100 Subject: [PATCH 18/27] Set user when upgrading from AppSumo --- app/Http/Controllers/Webhook/AppSumoController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/Webhook/AppSumoController.php b/app/Http/Controllers/Webhook/AppSumoController.php index c4fd44b..dfd0375 100644 --- a/app/Http/Controllers/Webhook/AppSumoController.php +++ b/app/Http/Controllers/Webhook/AppSumoController.php @@ -66,6 +66,7 @@ class AppSumoController extends Controller 'status' => License::STATUS_ACTIVE, ]); $license->meta = $licenseData; + $license->user_id = $licenseData['user_id'] ?? null; $license->save(); Log::info('[APPSUMO] creating new license', From 6ffe614a0ead2237dcf5480eb28685327d1b9e0f Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Fri, 10 Nov 2023 19:17:07 +0530 Subject: [PATCH 19/27] fix template api for public list (#237) --- app/Http/Controllers/TemplateController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/TemplateController.php b/app/Http/Controllers/TemplateController.php index 04a1b2a..1137a5d 100644 --- a/app/Http/Controllers/TemplateController.php +++ b/app/Http/Controllers/TemplateController.php @@ -35,7 +35,7 @@ class TemplateController extends Controller } }) ->when(!Auth::check(), function ($query) { - return $query->publiclyListed(); + $query->where('publicly_listed', true); }) ->orderByDesc('created_at') ->get(); From e99a0552bbd4d5d70236bc427af25aad1cc8cb26 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:38:53 +0530 Subject: [PATCH 20/27] Fix template limit slider (#239) --- app/Http/Controllers/TemplateController.php | 17 ++++++----------- .../pages/welcome/TemplatesSlider.vue | 9 ++++++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/Http/Controllers/TemplateController.php b/app/Http/Controllers/TemplateController.php index 1137a5d..151f0d6 100644 --- a/app/Http/Controllers/TemplateController.php +++ b/app/Http/Controllers/TemplateController.php @@ -13,18 +13,10 @@ class TemplateController extends Controller { public function index(Request $request) { - $limit = null; - if ($request->offsetExists('limit') && $request->get('limit') > 0) { - $limit = (int)$request->get('limit'); - } + $limit = (int)$request->get('limit', 0); + $onlyMy = (bool)$request->get('onlymy', false); - $onlyMy = false; - if ($request->offsetExists('onlymy') && $request->get('onlymy')) { - $onlyMy = true; - } - - $templates = Template::limit($limit) - ->when(Auth::check(), function ($query) use ($onlyMy) { + $templates = Template::when(Auth::check(), function ($query) use ($onlyMy) { if ($onlyMy) { $query->where('creator_id', Auth::id()); } else { @@ -37,6 +29,9 @@ class TemplateController extends Controller ->when(!Auth::check(), function ($query) { $query->where('publicly_listed', true); }) + ->when($limit > 0, function ($query) use ($limit) { + $query->limit($limit); + }) ->orderByDesc('created_at') ->get(); diff --git a/resources/js/components/pages/welcome/TemplatesSlider.vue b/resources/js/components/pages/welcome/TemplatesSlider.vue index 1f0309a..eb82868 100644 --- a/resources/js/components/pages/welcome/TemplatesSlider.vue +++ b/resources/js/components/pages/welcome/TemplatesSlider.vue @@ -21,7 +21,7 @@ class="w-full inline-flex flex-nowrap overflow-hidden [mask-image:_linear-gradient(to_right,transparent_0,_black_128px,_black_calc(100%-128px),transparent_100%)]" >
    -
  • +
@@ -42,7 +42,10 @@ export default { computed: { ...mapState({ templates: state => state['open/templates'].content - }) + }), + sliderTemplates () { + return this.templates.slice(0, 20) + } }, watch: { @@ -54,7 +57,7 @@ export default { }, mounted() { - store.dispatch('open/templates/loadAll', {'limit':10}) + store.dispatch('open/templates/loadAll', { limit: 20 }) }, methods: { From 7b3be36ba5a692ace448cc197dbf2599b03446b4 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Wed, 15 Nov 2023 14:47:39 +0530 Subject: [PATCH 21/27] =?UTF-8?q?Rename=20=E2=80=9CPublic=E2=80=9D=20statu?= =?UTF-8?q?s=20to=20=E2=80=9CPublished=E2=80=9D=20(#236)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julien Nahum --- .../forms/components/form-components/FormInformation.vue | 6 +++--- resources/js/pages/forms/show/index.vue | 9 ++++++--- resources/js/pages/home.vue | 6 +++++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/resources/js/components/open/forms/components/form-components/FormInformation.vue b/resources/js/components/open/forms/components/form-components/FormInformation.vue index 26a53f4..07de6a4 100644 --- a/resources/js/components/open/forms/components/form-components/FormInformation.vue +++ b/resources/js/components/open/forms/components/form-components/FormInformation.vue @@ -84,15 +84,15 @@ export default { copyFormId: null, visibilityOptions: [ { - name: "Public", + name: "Published", value: "public" }, { - name: "Draft (form won't be accessible)", + name: "Draft - not publicly accessible", value: "draft" }, { - name: "Closed", + name: "Closed - won\'t accept new submissions", value: "closed" } ], diff --git a/resources/js/pages/forms/show/index.vue b/resources/js/pages/forms/show/index.vue index 6b4d92b..095eb53 100644 --- a/resources/js/pages/forms/show/index.vue +++ b/resources/js/pages/forms/show/index.vue @@ -49,14 +49,17 @@ - {{ form.submissions_count }} submission{{ form.submissions_count > 0 ? 's' : '' }} - - Closed - - Edited {{ form.last_edited_human }} + - Edited {{ form.last_edited_human }}

-
+
Draft - not publicly accessible + + Closed - won't accept new submissions + diff --git a/resources/js/pages/home.vue b/resources/js/pages/home.vue index 5424c22..883813e 100644 --- a/resources/js/pages/home.vue +++ b/resources/js/pages/home.vue @@ -65,11 +65,15 @@
  • Edited {{ form.last_edited_human }}
  • -
    +
    Draft + + Closed + From c51d7397fad85c985d72d5e0d1ea4c8502d4d18c Mon Sep 17 00:00:00 2001 From: Nicolas Hedger <649677+nhedger@users.noreply.github.com> Date: Wed, 15 Nov 2023 10:55:31 +0100 Subject: [PATCH 22/27] feat: add support for MySQL (#238) * ci: run tests on mysql * adapt migration to support default values on MySQL --------- Co-authored-by: Julien Nahum --- .github/workflows/laravel.yml | 41 ++++++++++++++++++- .../2021_05_19_140326_create_forms_table.php | 11 ++--- ...4_234028_create_form_submissions_table.php | 3 +- .../2022_05_10_144947_form_statistic.php | 3 +- ...133641_add_removed_properties_to_forms.php | 3 +- ...22_09_22_092205_create_templates_table.php | 3 +- ...9_26_084721_add_questions_to_templates.php | 3 +- ...table_submissions_button_text_to_forms.php | 3 +- ...023_07_20_073728_add_seo_meta_to_forms.php | 3 +- ...710_add_notification_settings_to_forms.php | 3 +- ...3_09_01_052507_add_fields_to_templates.php | 7 ++-- 11 files changed, 66 insertions(+), 17 deletions(-) diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel.yml index aa89141..7635c20 100644 --- a/.github/workflows/laravel.yml +++ b/.github/workflows/laravel.yml @@ -27,6 +27,24 @@ jobs: ports: # Maps tcp port 5432 on service container to the host - 5432:5432 + mysql: + # Docker Hub image + image: mysql:8 + # Provide the password for mysql + env: + MYSQL_ROOT_PASSWORD: test + MYSQL_DATABASE: test + MYSQL_USER: test + MYSQL_PASSWORD: test + # Set health checks to wait until mysql has started + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + ports: + # Maps tcp port 3306 on service container to the host + - 3306:3306 concurrency: group: 'run-tests' @@ -35,8 +53,22 @@ jobs: fail-fast: true matrix: php: [ 8.2 ] + connection: [ pgsql, mysql ] + include: + - connection: pgsql + host: localhost + port: 5432 + user: postgres + password: postgres + database: postgres + - connection: mysql + host: '127.0.0.1' + port: 3306 + user: root + password: test + database: test - name: Run Feature & Unit tests (PHP ${{ matrix.php }}) + name: Run Feature & Unit tests (PHP ${{ matrix.php }} - ${{ matrix.connection }}) steps: - name: Checkout code @@ -80,6 +112,13 @@ jobs: - name: Run tests (Unit and Feature) run: ./vendor/bin/pest -p + env: + DB_CONNECTION: ${{ matrix.connection }} + DB_HOST: ${{ matrix.host }} + DB_PORT: ${{ matrix.port }} + DB_DATABASE: ${{ matrix.database }} + DB_USERNAME: ${{ matrix.user }} + DB_PASSWORD: ${{ matrix.password }} - name: "Archive log results" if: always() diff --git a/database/migrations/2021_05_19_140326_create_forms_table.php b/database/migrations/2021_05_19_140326_create_forms_table.php index 7622b97..a4b6af4 100644 --- a/database/migrations/2021_05_19_140326_create_forms_table.php +++ b/database/migrations/2021_05_19_140326_create_forms_table.php @@ -1,6 +1,7 @@ timestamps(); $table->boolean('notifies')->default(false); $table->text('description')->nullable(); - $table->text('submit_button_text')->default('Submit'); + $table->text('submit_button_text')->default(new Expression("('Submit')")); $table->boolean('re_fillable')->default(false); - $table->text('re_fill_button_text')->default('Fill Again'); + $table->text('re_fill_button_text')->default(new Expression("('Fill Again')")); $table->string('color')->default('#3B82F6'); $table->boolean('uppercase_labels')->default(true); $table->boolean('no_branding')->default(false); $table->boolean('hide_title')->default(false); - $table->text('submitted_text')->default('Amazing, we saved your answers. Thank you for your time and have a great day!'); + $table->text('submitted_text')->default(new Expression("('Amazing, we saved your answers. Thank you for your time and have a great day!')")); $table->string('dark_mode')->default('auto'); $table->string('webhook_url')->nullable(); $table->boolean('send_submission_confirmation')->default(false); @@ -45,13 +46,13 @@ class CreateFormsTable extends Migration $table->timestamp('closes_at')->nullable(); $table->text('closed_text')->nullable(); $table->string('notification_subject')->default("We saved your answers"); - $table->text('notification_body')->default('

    Hello there 👋
    This is a confirmation that your submission was successfully saved.

    '); + $table->text('notification_body')->default(new Expression("('

    Hello there 👋
    This is a confirmation that your submission was successfully saved.

    ')")); $table->boolean('notifications_include_submission')->default(true); $table->boolean('use_captcha')->default(false); $table->boolean('can_be_indexed')->default(true); $table->string('password')->nullable()->default(null); $table->string('notification_sender')->default("OpenForm"); - $table->jsonb('tags')->default('[]'); + $table->jsonb('tags')->default(new Expression('(JSON_ARRAY())')); }); } diff --git a/database/migrations/2021_05_24_234028_create_form_submissions_table.php b/database/migrations/2021_05_24_234028_create_form_submissions_table.php index 64cadd7..b292103 100644 --- a/database/migrations/2021_05_24_234028_create_form_submissions_table.php +++ b/database/migrations/2021_05_24_234028_create_form_submissions_table.php @@ -1,6 +1,7 @@ id(); $table->foreignIdFor(\App\Models\Forms\Form::class,'form_id'); - $table->jsonb('data')->default('{}'); + $table->jsonb('data')->default(new Expression("(JSON_OBJECT())")); $table->timestamps(); }); } diff --git a/database/migrations/2022_05_10_144947_form_statistic.php b/database/migrations/2022_05_10_144947_form_statistic.php index 85ace26..295e093 100644 --- a/database/migrations/2022_05_10_144947_form_statistic.php +++ b/database/migrations/2022_05_10_144947_form_statistic.php @@ -1,6 +1,7 @@ id(); $table->foreignIdFor(\App\Models\Forms\Form::class,'form_id'); - $table->jsonb('data')->default('{}'); + $table->jsonb('data')->default(new Expression("(JSON_OBJECT())")); $table->date('date'); }); } diff --git a/database/migrations/2022_08_18_133641_add_removed_properties_to_forms.php b/database/migrations/2022_08_18_133641_add_removed_properties_to_forms.php index 19997ed..263052c 100644 --- a/database/migrations/2022_08_18_133641_add_removed_properties_to_forms.php +++ b/database/migrations/2022_08_18_133641_add_removed_properties_to_forms.php @@ -1,6 +1,7 @@ jsonb('removed_properties')->default('[]'); + $table->jsonb('removed_properties')->default(new Expression("(JSON_ARRAY())")); }); } diff --git a/database/migrations/2022_09_22_092205_create_templates_table.php b/database/migrations/2022_09_22_092205_create_templates_table.php index 4642c58..34cf3be 100644 --- a/database/migrations/2022_09_22_092205_create_templates_table.php +++ b/database/migrations/2022_09_22_092205_create_templates_table.php @@ -1,6 +1,7 @@ string('slug'); $table->text('description'); $table->string('image_url'); - $table->jsonb('structure')->default('{}'); + $table->jsonb('structure')->default(new Expression("(JSON_OBJECT())")); }); } diff --git a/database/migrations/2022_09_26_084721_add_questions_to_templates.php b/database/migrations/2022_09_26_084721_add_questions_to_templates.php index 449fe40..dad19e1 100644 --- a/database/migrations/2022_09_26_084721_add_questions_to_templates.php +++ b/database/migrations/2022_09_26_084721_add_questions_to_templates.php @@ -1,6 +1,7 @@ jsonb('questions')->default('{}'); + $table->jsonb('questions')->default(new Expression("(JSON_ARRAY())")); }); } diff --git a/database/migrations/2023_03_13_094806_add_editable_submissions_button_text_to_forms.php b/database/migrations/2023_03_13_094806_add_editable_submissions_button_text_to_forms.php index e77e68b..d412da9 100644 --- a/database/migrations/2023_03_13_094806_add_editable_submissions_button_text_to_forms.php +++ b/database/migrations/2023_03_13_094806_add_editable_submissions_button_text_to_forms.php @@ -1,6 +1,7 @@ text('editable_submissions_button_text')->default('Edit submission'); + $table->text('editable_submissions_button_text')->default(new Expression("('Edit submission')")); }); } diff --git a/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php b/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php index e0a67d7..0a91a8a 100644 --- a/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php +++ b/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php @@ -1,6 +1,7 @@ json('seo_meta')->default('{}'); + $table->json('seo_meta')->default(new Expression("(JSON_OBJECT())")); }); } diff --git a/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php b/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php index b091f6b..21f96d1 100644 --- a/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php +++ b/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php @@ -1,6 +1,7 @@ json('notification_settings')->default('{}')->nullable(true); + $table->json('notification_settings')->default(new Expression("(JSON_OBJECT())"))->nullable(true); }); } diff --git a/database/migrations/2023_09_01_052507_add_fields_to_templates.php b/database/migrations/2023_09_01_052507_add_fields_to_templates.php index 3adfc83..2edfab3 100644 --- a/database/migrations/2023_09_01_052507_add_fields_to_templates.php +++ b/database/migrations/2023_09_01_052507_add_fields_to_templates.php @@ -1,6 +1,7 @@ boolean('publicly_listed')->default(false); - $table->jsonb('industries')->default('[]'); - $table->jsonb('types')->default('[]'); + $table->jsonb('industries')->default(new Expression("(JSON_ARRAY())")); + $table->jsonb('types')->default(new Expression("(JSON_ARRAY())")); $table->string('short_description')->nullable(); - $table->jsonb('related_templates')->default('[]'); + $table->jsonb('related_templates')->default(new Expression("(JSON_ARRAY())")); $table->string('image_url',500)->nullable()->change(); }); } From 0960af03732fa98f415b0b4c56e7425cab4e9c15 Mon Sep 17 00:00:00 2001 From: Nicolas Hedger <649677+nhedger@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:28:04 +0100 Subject: [PATCH 23/27] fix: migrations not passing on pgsql (#242) * fix: migration not passing on pgsql * pin postgres version --- .github/workflows/laravel.yml | 2 +- .../2021_05_19_140326_create_forms_table.php | 11 ++++++-- ...4_234028_create_form_submissions_table.php | 11 ++++++-- .../2022_05_10_144947_form_statistic.php | 11 ++++++-- ...133641_add_removed_properties_to_forms.php | 11 ++++++-- ...22_09_22_092205_create_templates_table.php | 11 ++++++-- ...9_26_084721_add_questions_to_templates.php | 11 ++++++-- ...023_07_20_073728_add_seo_meta_to_forms.php | 11 ++++++-- ...710_add_notification_settings_to_forms.php | 11 ++++++-- ...3_09_01_052507_add_fields_to_templates.php | 28 ++++++++++++++++--- 10 files changed, 97 insertions(+), 21 deletions(-) diff --git a/.github/workflows/laravel.yml b/.github/workflows/laravel.yml index 7635c20..6d24a2b 100644 --- a/.github/workflows/laravel.yml +++ b/.github/workflows/laravel.yml @@ -12,7 +12,7 @@ jobs: services: postgres: # Docker Hub image - image: postgres + image: postgres:14 # Provide the password for postgres env: POSTGRES_PASSWORD: postgres diff --git a/database/migrations/2021_05_19_140326_create_forms_table.php b/database/migrations/2021_05_19_140326_create_forms_table.php index a4b6af4..7aa63a9 100644 --- a/database/migrations/2021_05_19_140326_create_forms_table.php +++ b/database/migrations/2021_05_19_140326_create_forms_table.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; class CreateFormsTable extends Migration @@ -14,7 +15,9 @@ class CreateFormsTable extends Migration */ public function up() { - Schema::create('forms', function (Blueprint $table) { + $driver = DB::getDriverName(); + + Schema::create('forms', function (Blueprint $table) use ($driver) { $table->id(); $table->foreignIdFor(\App\Models\Workspace::class,'workspace_id'); $table->string('title'); @@ -52,7 +55,11 @@ class CreateFormsTable extends Migration $table->boolean('can_be_indexed')->default(true); $table->string('password')->nullable()->default(null); $table->string('notification_sender')->default("OpenForm"); - $table->jsonb('tags')->default(new Expression('(JSON_ARRAY())')); + if ($driver === 'mysql') { + $table->jsonb('tags')->default(new Expression('(JSON_ARRAY())')); + } else { + $table->jsonb('tags')->default('[]'); + } }); } diff --git a/database/migrations/2021_05_24_234028_create_form_submissions_table.php b/database/migrations/2021_05_24_234028_create_form_submissions_table.php index b292103..b7e48af 100644 --- a/database/migrations/2021_05_24_234028_create_form_submissions_table.php +++ b/database/migrations/2021_05_24_234028_create_form_submissions_table.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; class CreateFormSubmissionsTable extends Migration @@ -14,10 +15,16 @@ class CreateFormSubmissionsTable extends Migration */ public function up() { - Schema::create('form_submissions', function (Blueprint $table) { + $driver = DB::getDriverName(); + + Schema::create('form_submissions', function (Blueprint $table) use ($driver) { $table->id(); $table->foreignIdFor(\App\Models\Forms\Form::class,'form_id'); - $table->jsonb('data')->default(new Expression("(JSON_OBJECT())")); + if ($driver === 'mysql') { + $table->jsonb('data')->default(new Expression("(JSON_OBJECT())")); + } else { + $table->jsonb('data')->default("{}"); + } $table->timestamps(); }); } diff --git a/database/migrations/2022_05_10_144947_form_statistic.php b/database/migrations/2022_05_10_144947_form_statistic.php index 295e093..7cafb53 100644 --- a/database/migrations/2022_05_10_144947_form_statistic.php +++ b/database/migrations/2022_05_10_144947_form_statistic.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -14,10 +15,16 @@ return new class extends Migration */ public function up() { - Schema::create('form_statistics', function (Blueprint $table) { + $driver = DB::getDriverName(); + + Schema::create('form_statistics', function (Blueprint $table) use ($driver) { $table->id(); $table->foreignIdFor(\App\Models\Forms\Form::class,'form_id'); - $table->jsonb('data')->default(new Expression("(JSON_OBJECT())")); + if ($driver === 'mysql') { + $table->jsonb('data')->default(new Expression("(JSON_OBJECT())")); + } else { + $table->jsonb('data')->default("{}"); + } $table->date('date'); }); } diff --git a/database/migrations/2022_08_18_133641_add_removed_properties_to_forms.php b/database/migrations/2022_08_18_133641_add_removed_properties_to_forms.php index 263052c..03389a2 100644 --- a/database/migrations/2022_08_18_133641_add_removed_properties_to_forms.php +++ b/database/migrations/2022_08_18_133641_add_removed_properties_to_forms.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -14,8 +15,14 @@ return new class extends Migration */ public function up() { - Schema::table('forms', function (Blueprint $table) { - $table->jsonb('removed_properties')->default(new Expression("(JSON_ARRAY())")); + $driver = DB::getDriverName(); + + Schema::table('forms', function (Blueprint $table) use ($driver) { + if ($driver === 'mysql') { + $table->jsonb('removed_properties')->default(new Expression("(JSON_ARRAY())")); + } else { + $table->jsonb('removed_properties')->default("[]"); + } }); } diff --git a/database/migrations/2022_09_22_092205_create_templates_table.php b/database/migrations/2022_09_22_092205_create_templates_table.php index 34cf3be..0f7094f 100644 --- a/database/migrations/2022_09_22_092205_create_templates_table.php +++ b/database/migrations/2022_09_22_092205_create_templates_table.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -14,14 +15,20 @@ return new class extends Migration */ public function up() { - Schema::create('templates', function (Blueprint $table) { + $driver = DB::getDriverName(); + + Schema::create('templates', function (Blueprint $table) use ($driver) { $table->id(); $table->timestamps(); $table->string('name'); $table->string('slug'); $table->text('description'); $table->string('image_url'); - $table->jsonb('structure')->default(new Expression("(JSON_OBJECT())")); + if ($driver === 'mysql') { + $table->jsonb('structure')->default(new Expression("(JSON_OBJECT())")); + } else { + $table->jsonb('structure')->default('{}'); + } }); } diff --git a/database/migrations/2022_09_26_084721_add_questions_to_templates.php b/database/migrations/2022_09_26_084721_add_questions_to_templates.php index dad19e1..1fd0c50 100644 --- a/database/migrations/2022_09_26_084721_add_questions_to_templates.php +++ b/database/migrations/2022_09_26_084721_add_questions_to_templates.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -14,8 +15,14 @@ return new class extends Migration */ public function up() { - Schema::table('templates', function (Blueprint $table) { - $table->jsonb('questions')->default(new Expression("(JSON_ARRAY())")); + $driver = DB::getDriverName(); + + Schema::table('templates', function (Blueprint $table) use ($driver) { + if ($driver === 'mysql') { + $table->jsonb('questions')->default(new Expression("(JSON_ARRAY())")); + } else { + $table->jsonb('questions')->default('[]'); + } }); } diff --git a/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php b/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php index 0a91a8a..01fa247 100644 --- a/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php +++ b/database/migrations/2023_07_20_073728_add_seo_meta_to_forms.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -14,8 +15,14 @@ return new class extends Migration */ public function up() { - Schema::table('forms', function (Blueprint $table) { - $table->json('seo_meta')->default(new Expression("(JSON_OBJECT())")); + $driver = DB::getDriverName(); + + Schema::table('forms', function (Blueprint $table) use ($driver) { + if ($driver === 'mysql') { + $table->json('seo_meta')->default(new Expression("(JSON_OBJECT())")); + } else { + $table->json('seo_meta')->default("{}"); + } }); } diff --git a/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php b/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php index 21f96d1..37f3202 100644 --- a/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php +++ b/database/migrations/2023_08_23_100710_add_notification_settings_to_forms.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -14,8 +15,14 @@ return new class extends Migration */ public function up() { - Schema::table('forms', function (Blueprint $table) { - $table->json('notification_settings')->default(new Expression("(JSON_OBJECT())"))->nullable(true); + $driver = DB::getDriverName(); + + Schema::table('forms', function (Blueprint $table) use ($driver) { + if ($driver === 'mysql') { + $table->json('notification_settings')->default(new Expression("(JSON_OBJECT())"))->nullable(true); + } else { + $table->json('notification_settings')->default('{}')->nullable(true); + } }); } diff --git a/database/migrations/2023_09_01_052507_add_fields_to_templates.php b/database/migrations/2023_09_01_052507_add_fields_to_templates.php index 2edfab3..a4b8e74 100644 --- a/database/migrations/2023_09_01_052507_add_fields_to_templates.php +++ b/database/migrations/2023_09_01_052507_add_fields_to_templates.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -14,12 +15,31 @@ return new class extends Migration */ public function up() { - Schema::table('templates', function (Blueprint $table) { + $driver = DB::getDriverName(); + + Schema::table('templates', function (Blueprint $table) use ($driver) { $table->boolean('publicly_listed')->default(false); - $table->jsonb('industries')->default(new Expression("(JSON_ARRAY())")); - $table->jsonb('types')->default(new Expression("(JSON_ARRAY())")); + + if ($driver === 'mysql') { + $table->jsonb('industries')->default(new Expression("(JSON_ARRAY())")); + } else { + $table->jsonb('industries')->default('[]'); + } + + if ($driver === 'mysql') { + $table->jsonb('types')->default(new Expression("(JSON_ARRAY())")); + } else { + $table->jsonb('types')->default('[]'); + } + $table->string('short_description')->nullable(); - $table->jsonb('related_templates')->default(new Expression("(JSON_ARRAY())")); + + if ($driver === 'mysql') { + $table->jsonb('related_templates')->default(new Expression("(JSON_ARRAY())")); + } else { + $table->jsonb('related_templates')->default('[]'); + } + $table->string('image_url',500)->nullable()->change(); }); } From 64e79f34f2dbea65c43b10f0ab8898a359658b17 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Mon, 20 Nov 2023 13:38:50 +0530 Subject: [PATCH 24/27] Custom Plan (#243) --- .../components/pages/pricing/CustomPlan.vue | 37 +++++++++++++++++++ .../components/pages/pricing/PricingTable.vue | 4 ++ 2 files changed, 41 insertions(+) create mode 100644 resources/js/components/pages/pricing/CustomPlan.vue diff --git a/resources/js/components/pages/pricing/CustomPlan.vue b/resources/js/components/pages/pricing/CustomPlan.vue new file mode 100644 index 0000000..6180fbf --- /dev/null +++ b/resources/js/components/pages/pricing/CustomPlan.vue @@ -0,0 +1,37 @@ + + + diff --git a/resources/js/components/pages/pricing/PricingTable.vue b/resources/js/components/pages/pricing/PricingTable.vue index d79cdba..4c4e305 100644 --- a/resources/js/components/pages/pricing/PricingTable.vue +++ b/resources/js/components/pages/pricing/PricingTable.vue @@ -93,6 +93,8 @@
    + + @@ -104,11 +106,13 @@ import {mapGetters} from 'vuex' import axios from 'axios' import MonthlyYearlySelector from './MonthlyYearlySelector.vue' import CheckoutDetailsModal from './CheckoutDetailsModal.vue' +import CustomPlan from './CustomPlan.vue' export default { components: { MonthlyYearlySelector, CheckoutDetailsModal, + CustomPlan }, props: { homePage: { From d65c1be9b520eb8dfef9f4420aecf6450ba90904 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Tue, 28 Nov 2023 15:53:04 +0530 Subject: [PATCH 25/27] Create common function for userIsFormOwner & rewrite protected form (#244) * Create common function for userIsFormOwner & rewrite protected form * fix testcase --- app/Http/Kernel.php | 2 +- ...ordProtectedForm.php => ProtectedForm.php} | 42 +++++++++++-------- app/Http/Resources/FormResource.php | 18 +++----- app/Models/User.php | 5 +++ routes/api.php | 2 +- tests/Feature/Forms/FormPasswordTest.php | 4 +- 6 files changed, 39 insertions(+), 34 deletions(-) rename app/Http/Middleware/Form/{PasswordProtectedForm.php => ProtectedForm.php} (53%) diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index a786d57..970e320 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -80,6 +80,6 @@ class Kernel extends HttpKernel 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'pro-form' => \App\Http\Middleware\Form\ProForm::class, - 'password-protected-form' => \App\Http\Middleware\Form\PasswordProtectedForm::class, + 'protected-form' => \App\Http\Middleware\Form\ProtectedForm::class, ]; } diff --git a/app/Http/Middleware/Form/PasswordProtectedForm.php b/app/Http/Middleware/Form/ProtectedForm.php similarity index 53% rename from app/Http/Middleware/Form/PasswordProtectedForm.php rename to app/Http/Middleware/Form/ProtectedForm.php index b4e2e15..6bbc947 100644 --- a/app/Http/Middleware/Form/PasswordProtectedForm.php +++ b/app/Http/Middleware/Form/ProtectedForm.php @@ -7,7 +7,7 @@ use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -class PasswordProtectedForm +class ProtectedForm { const PASSWORD_HEADER_NAME = 'form-password'; @@ -20,26 +20,34 @@ class PasswordProtectedForm */ public function handle(Request $request, Closure $next) { - if ($request->route('slug')) { - $form = Form::where('slug',$request->route('slug'))->firstOrFail(); - $request->merge([ - 'form' => $form, - ]); - $userIsFormOwner = Auth::check() && Auth::user()->workspaces()->find($form->workspace_id) !== null; - if (!$userIsFormOwner && $form->has_password) { - if($this->hasCorrectPassword($request, $form)){ - return $next($request); - } - - return response([ - 'status' => 'Unauthorized', - 'message' => 'Form is password protected.', - ], 403); - } + if (!$request->route('slug')) { + return $next($request); } + + $form = Form::where('slug',$request->route('slug'))->firstOrFail(); + $request->merge([ + 'form' => $form, + ]); + $userIsFormOwner = Auth::check() && Auth::user()->ownsForm($form); + if (!$userIsFormOwner && $this->isProtected($request, $form)) { + return response([ + 'status' => 'Unauthorized', + 'message' => 'Form is protected.', + ], 403); + } + return $next($request); } + public static function isProtected(Request $request, Form $form) + { + if (!$form->has_password) { + return false; + } + + return !self::hasCorrectPassword($request, $form); + } + public static function hasCorrectPassword(Request $request, Form $form) { return $request->headers->has(self::PASSWORD_HEADER_NAME) && $request->headers->get(self::PASSWORD_HEADER_NAME) == hash('sha256', $form->password); diff --git a/app/Http/Resources/FormResource.php b/app/Http/Resources/FormResource.php index 0cc8949..7f6a86f 100644 --- a/app/Http/Resources/FormResource.php +++ b/app/Http/Resources/FormResource.php @@ -2,7 +2,7 @@ namespace App\Http\Resources; -use App\Http\Middleware\Form\PasswordProtectedForm; +use App\Http\Middleware\Form\ProtectedForm; use App\Http\Resources\UserResource; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Auth; @@ -20,8 +20,8 @@ class FormResource extends JsonResource */ public function toArray($request) { - if(!$this->userIsFormOwner() && $this->doesMissPassword($request)){ - return $this->getPasswordProtectedForm(); + if(!$this->userIsFormOwner() && ProtectedForm::isProtected($request, $this->resource)){ + return $this->getProtectedForm(); } $ownerData = $this->userIsFormOwner() ? [ @@ -96,14 +96,7 @@ class FormResource extends JsonResource return $this; } - private function doesMissPassword(Request $request) - { - if (!$this->has_password) return false; - - return !PasswordProtectedForm::hasCorrectPassword($request, $this->resource); - } - - private function getPasswordProtectedForm() + private function getProtectedForm() { return [ 'id' => $this->id, @@ -131,8 +124,7 @@ class FormResource extends JsonResource private function userIsFormOwner() { return $this->extra?->userIsOwner ?? ( - Auth::check() - && Auth::user()->workspaces()->find($this->workspace_id) !== null + Auth::check() && Auth::user()->ownsForm($this->resource) ); } diff --git a/app/Models/User.php b/app/Models/User.php index f9cabff..764d927 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -61,6 +61,11 @@ class User extends Authenticatable implements JWTSubject protected $withCount = ['workspaces']; + public function ownsForm(Form $form) + { + return $this->workspaces()->find($form->workspace_id) !== null; + } + /** * Get the profile photo URL attribute. * diff --git a/routes/api.php b/routes/api.php index cfb0aba..1719bea 100644 --- a/routes/api.php +++ b/routes/api.php @@ -139,7 +139,7 @@ Route::group(['prefix' => 'appsumo'], function () { * Public Forms related routes */ Route::prefix('forms')->name('forms.')->group(function () { - Route::middleware('password-protected-form')->group(function () { + Route::middleware('protected-form')->group(function () { Route::post('{slug}/answer', [PublicFormController::class, 'answer'])->name('answer'); // Form content endpoints (user lists, relation lists etc.) diff --git a/tests/Feature/Forms/FormPasswordTest.php b/tests/Feature/Forms/FormPasswordTest.php index 15274a2..03d2650 100644 --- a/tests/Feature/Forms/FormPasswordTest.php +++ b/tests/Feature/Forms/FormPasswordTest.php @@ -54,7 +54,7 @@ it('can not submit form without password for guest user', function () { ->assertStatus(403) ->assertJson([ 'status' => 'Unauthorized', - 'message' => 'Form is password protected.' + 'message' => 'Form is protected.' ]); }); @@ -66,7 +66,7 @@ it('can not submit form with wrong password for guest user', function () { ->assertStatus(403) ->assertJson([ 'status' => 'Unauthorized', - 'message' => 'Form is password protected.' + 'message' => 'Form is protected.' ]); }); From 730bdd1b1f38807e743bb167f8ffa6a9040283d0 Mon Sep 17 00:00:00 2001 From: formsdev <136701234+formsdev@users.noreply.github.com> Date: Tue, 28 Nov 2023 15:54:55 +0530 Subject: [PATCH 26/27] Refactor editor panels (#245) * Refactor editor panels * EditorOptionsPanel icon fixes * manage editor panel open/close --------- Co-authored-by: Julien Nahum --- .../open/editors/EditorOptionsPanel.vue | 46 +++++++++++++++++++ .../form-components/FormAboutSubmission.vue | 29 +++++------- .../components/form-components/FormAccess.vue | 28 ++++------- .../form-components/FormCustomCode.vue | 24 ++++------ .../form-components/FormCustomSeo.vue | 29 ++++-------- .../form-components/FormCustomization.vue | 29 +++++------- .../form-components/FormInformation.vue | 23 ++++------ .../form-components/FormNotifications.vue | 23 ++++------ .../form-components/FormSecurityPrivacy.vue | 22 ++++----- .../form-components/FormStructure.vue | 23 ++++------ tailwind.config.js | 1 + 11 files changed, 132 insertions(+), 145 deletions(-) create mode 100644 resources/js/components/open/editors/EditorOptionsPanel.vue diff --git a/resources/js/components/open/editors/EditorOptionsPanel.vue b/resources/js/components/open/editors/EditorOptionsPanel.vue new file mode 100644 index 0000000..47c6926 --- /dev/null +++ b/resources/js/components/open/editors/EditorOptionsPanel.vue @@ -0,0 +1,46 @@ + + + diff --git a/resources/js/components/open/forms/components/form-components/FormAboutSubmission.vue b/resources/js/components/open/forms/components/form-components/FormAboutSubmission.vue index 4407c4a..9cc5f83 100644 --- a/resources/js/components/open/forms/components/form-components/FormAboutSubmission.vue +++ b/resources/js/components/open/forms/components/form-components/FormAboutSubmission.vue @@ -1,17 +1,11 @@ diff --git a/resources/js/config/form-themes.js b/resources/js/config/form-themes.js index 1e4a15e..56c761e 100644 --- a/resources/js/config/form-themes.js +++ b/resources/js/config/form-themes.js @@ -31,6 +31,14 @@ export const themes = { button: 'cursor-pointer text-gray-700 inline-block rounded-lg border-gray-300 px-4 py-2 flex-grow dark:bg-notion-dark-light dark:text-gray-300 text-center', unselectedButton: 'bg-white hover:bg-gray-50 border', help: 'text-gray-400 dark:text-gray-500' + }, + fileInput: { + input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded-lg', + inputHover: { + light: 'bg-neutral-50', + dark: 'bg-notion-dark-light' + }, + uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light rounded-lg shadow-sm max-w-[10rem]' } }, simple: { @@ -62,6 +70,14 @@ export const themes = { button: 'flex-1 appearance-none border-gray-300 dark:border-gray-600 w-full py-2 px-2 bg-gray-50 text-gray-700 dark:bg-notion-dark-light dark:text-gray-300 text-center', unselectedButton: 'bg-white hover:bg-gray-50 border -mx-4', help: 'text-gray-400 dark:text-gray-500' + }, + fileInput: { + input: 'min-h-40 border border-dashed border-gray-300 p-4', + inputHover: { + light: 'bg-neutral-50', + dark: 'bg-notion-dark-light' + }, + uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light shadow-sm max-w-[10rem]' } }, notion: { @@ -93,6 +109,14 @@ export const themes = { button: 'rounded border-transparent flex-1 appearance-none shadow-inner-notion w-full py-2 px-2 bg-notion-input-background dark:bg-notion-dark-light text-gray-900 dark:text-gray-100 text-center', unselectedButton: 'bg-notion-input-background dark:bg-notion-dark-light hover:bg-gray-50 border', help: 'text-notion-input-help dark:text-gray-500' + }, + fileInput: { + input: 'min-h-40 border border-dashed border-gray-300 p-4 rounded bg-notion-input-background', + inputHover: { + light: 'bg-neutral-50', + dark: 'bg-notion-dark-light' + }, + uploadedFile: 'border border-gray-300 dark:border-gray-600 bg-white dark:bg-notion-dark-light rounded shadow-sm max-w-[10rem]' } }