diff --git a/.env.example b/.env.example index f781ac1..355837f 100644 --- a/.env.example +++ b/.env.example @@ -74,3 +74,6 @@ ADMIN_EMAILS= TEMPLATE_EDITOR_EMAILS= OPEN_AI_API_KEY= + +CADDY_SECRET= +CADDY_AUTHORIZED_IP= diff --git a/_ide_helper_models.php b/_ide_helper_models.php new file mode 100644 index 0000000..ca91bea --- /dev/null +++ b/_ide_helper_models.php @@ -0,0 +1,462 @@ + + */ + + +namespace App\Models\Forms\AI{ +/** + * App\Models\Forms\AI\AiFormCompletion + * + * @property int $id + * @property string $form_prompt + * @property string $status + * @property mixed|null $result + * @property string $ip + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion query() + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion whereFormPrompt($value) + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion whereIp($value) + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion whereResult($value) + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion whereStatus($value) + * @method static \Illuminate\Database\Eloquent\Builder|AiFormCompletion whereUpdatedAt($value) + */ + class AiFormCompletion extends \Eloquent {} +} + +namespace App\Models\Forms{ +/** + * App\Models\Forms\Form + * + * @property int $id + * @property int $workspace_id + * @property string $title + * @property string $slug + * @property array $properties + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property bool $notifies + * @property string|null $description + * @property string $submit_button_text + * @property bool $re_fillable + * @property string $re_fill_button_text + * @property string $color + * @property bool $uppercase_labels + * @property bool $no_branding + * @property bool $hide_title + * @property string $submitted_text + * @property string $dark_mode + * @property string|null $webhook_url + * @property bool $send_submission_confirmation + * @property string|null $logo_picture + * @property string|null $cover_picture + * @property string|null $redirect_url + * @property string|null $custom_code + * @property string|null $notification_emails + * @property string $theme + * @property array|null $database_fields_update + * @property string $width + * @property bool $transparent_background + * @property \Illuminate\Support\Carbon|null $closes_at + * @property string|null $closed_text + * @property string $notification_subject + * @property string $notification_body + * @property bool $notifications_include_submission + * @property bool $use_captcha + * @property bool $can_be_indexed + * @property string|null $password + * @property string $notification_sender + * @property array $tags + * @property \Illuminate\Support\Carbon|null $deleted_at + * @property int $creator_id + * @property array $removed_properties + * @property int|null $max_submissions_count + * @property string|null $max_submissions_reached_text + * @property string|null $slack_webhook_url + * @property string $visibility + * @property bool $editable_submissions + * @property string|null $discord_webhook_url + * @property string $editable_submissions_button_text + * @property bool $confetti_on_submission + * @property object $seo_meta + * @property object|null $notification_settings + * @property bool $auto_save + * @property-read \App\Models\User $creator + * @property-read mixed $edit_url + * @property-read mixed $form_pending_submission_key + * @property-read mixed $has_password + * @property-read mixed $is_closed + * @property-read mixed $is_pro + * @property-read mixed $max_number_of_submissions_reached + * @property-read mixed $notifies_discord + * @property-read mixed $notifies_slack + * @property-read mixed $notifies_webhook + * @property-read mixed $share_url + * @property-read int|null $submissions_count + * @property-read int|null $views_count + * @property-read \Illuminate\Database\Eloquent\Collection $statistics + * @property-read int|null $statistics_count + * @property-read \Illuminate\Database\Eloquent\Collection $submissions + * @property-read \Illuminate\Database\Eloquent\Collection $views + * @property-read \App\Models\Workspace|null $workspace + * @property-read \Illuminate\Database\Eloquent\Collection $zappierHooks + * @property-read int|null $zappier_hooks_count + * @method static \Database\Factories\FormFactory factory($count = null, $state = []) + * @method static \Illuminate\Database\Eloquent\Builder|Form newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Form newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Form onlyTrashed() + * @method static \Illuminate\Database\Eloquent\Builder|Form query() + * @method static \Illuminate\Database\Eloquent\Builder|Form whereAutoSave($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereCanBeIndexed($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereClosedText($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereClosesAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereColor($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereConfettiOnSubmission($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereCoverPicture($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereCreatorId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereCustomCode($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereDarkMode($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereDatabaseFieldsUpdate($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereDeletedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereDescription($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereDiscordWebhookUrl($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereEditableSubmissions($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereEditableSubmissionsButtonText($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereHideTitle($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereLogoPicture($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereMaxSubmissionsCount($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereMaxSubmissionsReachedText($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereNoBranding($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationBody($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationEmails($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationSender($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationSettings($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationSubject($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereNotificationsIncludeSubmission($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereNotifies($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form wherePassword($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereProperties($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereReFillButtonText($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereReFillable($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereRedirectUrl($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereRemovedProperties($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereSendSubmissionConfirmation($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereSeoMeta($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereSlackWebhookUrl($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereSubmitButtonText($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereSubmittedText($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereTags($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereTheme($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereTitle($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereTransparentBackground($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereUppercaseLabels($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereUseCaptcha($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereVisibility($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereWebhookUrl($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereWidth($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form whereWorkspaceId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Form withTrashed() + * @method static \Illuminate\Database\Eloquent\Builder|Form withoutTrashed() + */ + class Form extends \Eloquent {} +} + +namespace App\Models\Forms{ +/** + * App\Models\Forms\FormStatistic + * + * @property int $id + * @property int $form_id + * @property array $data + * @property string $date + * @property-read \App\Models\Forms\Form|null $form + * @method static \Illuminate\Database\Eloquent\Builder|FormStatistic newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|FormStatistic newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|FormStatistic query() + * @method static \Illuminate\Database\Eloquent\Builder|FormStatistic whereData($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormStatistic whereDate($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormStatistic whereFormId($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormStatistic whereId($value) + */ + class FormStatistic extends \Eloquent {} +} + +namespace App\Models\Forms{ +/** + * App\Models\Forms\FormSubmission + * + * @property int $id + * @property int $form_id + * @property array $data + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \App\Models\Forms\Form|null $form + * @method static \Illuminate\Database\Eloquent\Builder|FormSubmission newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|FormSubmission newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|FormSubmission query() + * @method static \Illuminate\Database\Eloquent\Builder|FormSubmission whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormSubmission whereData($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormSubmission whereFormId($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormSubmission whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormSubmission whereUpdatedAt($value) + */ + class FormSubmission extends \Eloquent {} +} + +namespace App\Models\Forms{ +/** + * App\Models\Forms\FormView + * + * @property int $id + * @property int $form_id + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \App\Models\Forms\Form|null $form + * @method static \Illuminate\Database\Eloquent\Builder|FormView newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|FormView newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|FormView query() + * @method static \Illuminate\Database\Eloquent\Builder|FormView whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormView whereFormId($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormView whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormView whereUpdatedAt($value) + */ + class FormView extends \Eloquent {} +} + +namespace App\Models\Integration{ +/** + * App\Models\Integration\FormZapierWebhook + * + * @property int $id + * @property int $form_id + * @property string $hook_url + * @property \Illuminate\Support\Carbon|null $deleted_at + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \App\Models\Forms\Form|null $form + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook onlyTrashed() + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook query() + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook whereDeletedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook whereFormId($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook whereHookUrl($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook withTrashed() + * @method static \Illuminate\Database\Eloquent\Builder|FormZapierWebhook withoutTrashed() + */ + class FormZapierWebhook extends \Eloquent {} +} + +namespace App\Models{ +/** + * App\Models\License + * + * @property int $id + * @property string $license_key + * @property int|null $user_id + * @property string $license_provider + * @property string $status + * @property array $meta + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read mixed $custom_domain_limit_count + * @property-read mixed $max_file_size + * @property-read \App\Models\User|null $user + * @method static \Illuminate\Database\Eloquent\Builder|License active() + * @method static \Illuminate\Database\Eloquent\Builder|License newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|License newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|License query() + * @method static \Illuminate\Database\Eloquent\Builder|License whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|License whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|License whereLicenseKey($value) + * @method static \Illuminate\Database\Eloquent\Builder|License whereLicenseProvider($value) + * @method static \Illuminate\Database\Eloquent\Builder|License whereMeta($value) + * @method static \Illuminate\Database\Eloquent\Builder|License whereStatus($value) + * @method static \Illuminate\Database\Eloquent\Builder|License whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|License whereUserId($value) + */ + class License extends \Eloquent {} +} + +namespace App\Models{ +/** + * App\Models\OAuthProvider + * + * @property int $id + * @property int $user_id + * @property string $provider + * @property string $provider_user_id + * @property string|null $access_token + * @property string|null $refresh_token + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property-read \App\Models\User $user + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider query() + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereAccessToken($value) + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereProvider($value) + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereProviderUserId($value) + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereRefreshToken($value) + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|OAuthProvider whereUserId($value) + */ + class OAuthProvider extends \Eloquent {} +} + +namespace App\Models{ +/** + * App\Models\Template + * + * @property int $id + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property string $name + * @property string $slug + * @property string $description + * @property string|null $image_url + * @property array $structure + * @property array $questions + * @property bool $publicly_listed + * @property array $industries + * @property array $types + * @property string|null $short_description + * @property array $related_templates + * @property int|null $creator_id + * @property-read mixed $share_url + * @method static \Illuminate\Database\Eloquent\Builder|Template newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Template newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Template publiclyListed() + * @method static \Illuminate\Database\Eloquent\Builder|Template query() + * @method static \Illuminate\Database\Eloquent\Builder|Template whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereCreatorId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereDescription($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereImageUrl($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereIndustries($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template wherePubliclyListed($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereQuestions($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereRelatedTemplates($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereShortDescription($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereSlug($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereStructure($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereTypes($value) + * @method static \Illuminate\Database\Eloquent\Builder|Template whereUpdatedAt($value) + */ + class Template extends \Eloquent {} +} + +namespace App\Models{ +/** + * App\Models\User + * + * @property int $id + * @property string $name + * @property string $email + * @property \Illuminate\Support\Carbon|null $email_verified_at + * @property string|null $password + * @property string|null $remember_token + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property string|null $stripe_id + * @property string|null $pm_type + * @property string|null $pm_last_four + * @property string|null $trial_ends_at + * @property string|null $hear_about_us + * @property-read \Illuminate\Database\Eloquent\Collection $formTemplates + * @property-read int|null $form_templates_count + * @property-read \Illuminate\Database\Eloquent\Collection $forms + * @property-read int|null $forms_count + * @property-read mixed $admin + * @property-read mixed $has_customer_id + * @property-read mixed $has_forms + * @property-read mixed $is_risky + * @property-read mixed $is_subscribed + * @property-read string $photo_url + * @property-read mixed $template_editor + * @property-read \Illuminate\Database\Eloquent\Collection $licenses + * @property-read int|null $licenses_count + * @property-read \Illuminate\Notifications\DatabaseNotificationCollection $notifications + * @property-read int|null $notifications_count + * @property-read \Illuminate\Database\Eloquent\Collection $oauthProviders + * @property-read int|null $oauth_providers_count + * @property-read \Illuminate\Database\Eloquent\Collection $subscriptions + * @property-read int|null $subscriptions_count + * @property-read \Illuminate\Database\Eloquent\Collection $workspaces + * @property-read int|null $workspaces_count + * @method static \Database\Factories\UserFactory factory($count = null, $state = []) + * @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|User newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|User query() + * @method static \Illuminate\Database\Eloquent\Builder|User whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereEmail($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereEmailVerifiedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereHearAboutUs($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|User wherePassword($value) + * @method static \Illuminate\Database\Eloquent\Builder|User wherePmLastFour($value) + * @method static \Illuminate\Database\Eloquent\Builder|User wherePmType($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereRememberToken($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereStripeId($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereTrialEndsAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|User whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|User withActiveSubscription() + */ + class User extends \Eloquent implements \Tymon\JWTAuth\Contracts\JWTSubject {} +} + +namespace App\Models{ +/** + * App\Models\Workspace + * + * @property int $id + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property string $name + * @property string|null $icon + * @property mixed|null $custom_domains + * @property-read \Illuminate\Database\Eloquent\Collection $forms + * @property-read int|null $forms_count + * @property-read mixed $custom_domain_count_limit + * @property-read mixed $is_enterprise + * @property-read mixed $is_pro + * @property-read mixed $is_risky + * @property-read mixed $max_file_size + * @property-read mixed $submissions_count + * @property-read \Illuminate\Database\Eloquent\Collection $users + * @property-read int|null $users_count + * @method static \Illuminate\Database\Eloquent\Builder|Workspace newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Workspace newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|Workspace query() + * @method static \Illuminate\Database\Eloquent\Builder|Workspace whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|Workspace whereCustomDomains($value) + * @method static \Illuminate\Database\Eloquent\Builder|Workspace whereIcon($value) + * @method static \Illuminate\Database\Eloquent\Builder|Workspace whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|Workspace whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|Workspace whereUpdatedAt($value) + */ + class Workspace extends \Eloquent {} +} + diff --git a/app/Http/Controllers/CaddyController.php b/app/Http/Controllers/CaddyController.php new file mode 100644 index 0000000..8977fbf --- /dev/null +++ b/app/Http/Controllers/CaddyController.php @@ -0,0 +1,36 @@ +validate([ + 'domain' => 'required|string', + ]); + // make sure domain is valid + $domain = $request->input('domain'); + if (!preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}$/', $domain)) { + return $this->error([ + 'success' => false, + 'message' => 'Invalid domain', + ]); + } + + if (Workspace::whereJsonContains('custom_domains',$domain)->exists()) { + return $this->success([ + 'success' => true, + 'message' => 'OK', + ]); + } + + return $this->error([ + 'success' => false, + 'message' => 'Unauthorized domain', + ]); + } +} diff --git a/app/Http/Controllers/WorkspaceController.php b/app/Http/Controllers/WorkspaceController.php index ae5efc2..4efebee 100644 --- a/app/Http/Controllers/WorkspaceController.php +++ b/app/Http/Controllers/WorkspaceController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Http\Controllers\Controller; +use App\Http\Requests\Workspace\CustomDomainRequest; use App\Models\Workspace; use Illuminate\Http\Request; use App\Service\WorkspaceHelper; @@ -29,6 +30,13 @@ class WorkspaceController extends Controller return (new WorkspaceHelper($workspace))->getAllUsers(); } + public function saveCustomDomain(CustomDomainRequest $request) + { + $request->workspace->custom_domains = $request->customDomains; + $request->workspace->save(); + return $request->workspace; + } + public function delete($id) { $workspace = Workspace::findOrFail($id); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 970e320..54c54dd 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -2,6 +2,7 @@ namespace App\Http; +use App\Http\Middleware\CustomDomainRestriction; use App\Http\Middleware\EmbeddableForms; use App\Http\Middleware\IsAdmin; use App\Http\Middleware\IsNotSubscribed; @@ -26,6 +27,7 @@ class Kernel extends HttpKernel \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \App\Http\Middleware\SetLocale::class, + CustomDomainRestriction::class, ]; /** diff --git a/app/Http/Middleware/CaddyRequestMiddleware.php b/app/Http/Middleware/CaddyRequestMiddleware.php new file mode 100644 index 0000000..d367461 --- /dev/null +++ b/app/Http/Middleware/CaddyRequestMiddleware.php @@ -0,0 +1,39 @@ +json([ + 'success' => false, + 'message' => 'Custom domains not enabled', + ], 401); + } + + if (config('custom-domains.enabled') && !in_array($request->ip(), config('custom-domains.authorized_ips'))) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized', + ], 401); + } + + $secret = $request->route('secret'); + if (config('custom-domains.caddy_secret') && (!$secret || $secret !== config('custom-domains.caddy_secret'))) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthorized', + ], 401); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/CustomDomainRestriction.php b/app/Http/Middleware/CustomDomainRestriction.php new file mode 100644 index 0000000..0b970b7 --- /dev/null +++ b/app/Http/Middleware/CustomDomainRestriction.php @@ -0,0 +1,55 @@ +hasHeader(self::CUSTOM_DOMAIN_HEADER) || !config('custom-domains.enabled')) { + return $next($request); + } + + $customDomain = $request->header(self::CUSTOM_DOMAIN_HEADER); + if (!preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}$/', $customDomain)) { + return response()->json([ + 'success' => false, + 'message' => 'Invalid domain', + ], 400); + } + + // Check if domain is different from current domain + $notionFormsDomain = parse_url(config('app.url'))['host']; + if ($customDomain == $notionFormsDomain) { + return $next($request); + } + + // Check if domain is known + if (!$workspace = Workspace::whereJsonContains('custom_domains',$customDomain)->first()) { + return response()->json([ + 'success' => false, + 'message' => 'Unknown domain', + ], 400); + } + + Workspace::addGlobalScope('domain-restricted', function (Builder $builder) use ($workspace) { + $builder->where('workspaces.id', $workspace->id); + }); + Form::addGlobalScope('domain-restricted', function (Builder $builder) use ($workspace) { + $builder->where('forms.workspace_id', $workspace->id); + }); + + return $next($request); + } +} diff --git a/app/Http/Requests/UserFormRequest.php b/app/Http/Requests/UserFormRequest.php index 1006095..81b0da7 100644 --- a/app/Http/Requests/UserFormRequest.php +++ b/app/Http/Requests/UserFormRequest.php @@ -125,7 +125,8 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest 'password' => 'sometimes|nullable', // Custom SEO - 'seo_meta' => 'nullable|array' + 'seo_meta' => 'nullable|array', + 'custom_domain' => 'sometimes|nullable|regex:/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}$/' ]; } diff --git a/app/Http/Requests/Workspace/CustomDomainRequest.php b/app/Http/Requests/Workspace/CustomDomainRequest.php new file mode 100644 index 0000000..82a3e66 --- /dev/null +++ b/app/Http/Requests/Workspace/CustomDomainRequest.php @@ -0,0 +1,58 @@ +workspace = Workspace::findOrFail($request->workspaceId); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'custom_domains' => [ + 'present', + 'array', + function($attribute, $value, $fail) { + $errors = []; + $domains = collect($value)->filter(function ($domain) { + return !empty( trim($domain) ); + })->each(function($domain) use (&$errors) { + if (!preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}$/', $domain)) { + $errors[] = 'Invalid domain: ' . $domain; + } + }); + + if (count($errors)) { + $fail($errors); + } + + $limit = $this->workspace->custom_domain_count_limit; + if ($limit && $domains->count() > $limit) { + $fail('You can only add ' . $limit . ' domain(s).'); + } + + $this->customDomains = $domains->toArray(); + } + ] + ]; + } + + protected function passedValidation(){ + $this->replace(['custom_domains' => $this->customDomains]); + } +} diff --git a/app/Models/Forms/Form.php b/app/Models/Forms/Form.php index ede1be7..497a1e3 100644 --- a/app/Models/Forms/Form.php +++ b/app/Models/Forms/Form.php @@ -53,6 +53,7 @@ class Form extends Model 'visibility', // Customization + 'custom_domain', 'theme', 'width', 'cover_picture', @@ -141,12 +142,15 @@ class Form extends Model public function getShareUrlAttribute() { - return url('/forms/'.$this->slug); + if ($this->custom_domain) { + return 'https://' . $this->custom_domain . '/forms/' . $this->slug; + } + return url('/forms/' . $this->slug); } public function getEditUrlAttribute() { - return url('/forms/'.$this->slug.'/show'); + return url('/forms/' . $this->slug . '/show'); } public function getSubmissionsCountAttribute() @@ -156,12 +160,12 @@ class Form extends Model public function getViewsCountAttribute() { - if(env('DB_CONNECTION') == 'pgsql') { + if (env('DB_CONNECTION') == 'pgsql') { return $this->views()->count() + - $this->statistics()->sum(DB::raw("cast(data->>'views' as integer)")); - } elseif(env('DB_CONNECTION') == 'mysql') { + $this->statistics()->sum(DB::raw("cast(data->>'views' as integer)")); + } elseif (env('DB_CONNECTION') == 'mysql') { return (int)($this->views()->count() + - $this->statistics()->sum(DB::raw("json_extract(data, '$.views')"))); + $this->statistics()->sum(DB::raw("json_extract(data, '$.views')"))); } return 0; } @@ -219,7 +223,8 @@ class Form extends Model return !empty($this->password); } - protected function removedProperties(): Attribute { + protected function removedProperties(): Attribute + { return Attribute::make( get: function ($value) { return $value ? json_decode($value, true) : []; @@ -283,7 +288,7 @@ class Form extends Model { return !empty($this->webhook_url); } - + public function getNotifiesDiscordAttribute() { return !empty($this->discord_webhook_url); diff --git a/app/Models/License.php b/app/Models/License.php index bb72f34..5102737 100644 --- a/app/Models/License.php +++ b/app/Models/License.php @@ -42,4 +42,13 @@ class License extends Model 3 => 75000000, // 75 MB, ][$this->meta['tier']]; } + + public function getCustomDomainLimitCountAttribute() + { + return [ + 1 => 5, + 2 => 25, + 3 => null, + ][$this->meta['tier']]; + } } diff --git a/app/Models/Workspace.php b/app/Models/Workspace.php index c0efa23..262633a 100644 --- a/app/Models/Workspace.php +++ b/app/Models/Workspace.php @@ -14,10 +14,13 @@ class Workspace extends Model const MAX_FILE_SIZE_FREE = 5000000; // 5 MB const MAX_FILE_SIZE_PRO = 50000000; // 50 MB + const MAX_DOMAIN_PRO = 1; + protected $fillable = [ 'name', 'icon', 'user_id', + 'custom_domain', ]; protected $appends = [ @@ -25,6 +28,10 @@ class Workspace extends Model 'is_enterprise' ]; + protected $casts = [ + 'custom_domains' => 'array', + ]; + public function getIsProAttribute() { if(is_null(config('cashier.key'))){ @@ -60,6 +67,26 @@ class Workspace extends Model return self::MAX_FILE_SIZE_FREE; } + public function getCustomDomainCountLimitAttribute() + { + if(is_null(config('cashier.key'))){ + return null; + } + + // 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->custom_domain_limit_count; + } + } + return self::MAX_DOMAIN_PRO; + } + + return 0; + } + public function getIsEnterpriseAttribute() { if(is_null(config('cashier.key'))){ diff --git a/config/custom-domains.php b/config/custom-domains.php new file mode 100644 index 0000000..197a1e7 --- /dev/null +++ b/config/custom-domains.php @@ -0,0 +1,9 @@ + !empty(env('CADDY_SECRET')) && !empty(env('CADDY_AUTHORIZED_IPS', [])), + 'caddy_secret' => env('CADDY_SECRET'), + 'authorized_ips' => env('CADDY_AUTHORIZED_IPS', []), + +]; diff --git a/database/migrations/2023_11_28_104644_add_custom_domains_to_workspaces_table.php b/database/migrations/2023_11_28_104644_add_custom_domains_to_workspaces_table.php new file mode 100644 index 0000000..166d075 --- /dev/null +++ b/database/migrations/2023_11_28_104644_add_custom_domains_to_workspaces_table.php @@ -0,0 +1,37 @@ +json('custom_domains')->default(new Expression("(JSON_OBJECT())"))->nullable(true); + } else { + $table->json('custom_domains')->default('{}')->nullable(true); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('workspaces', function (Blueprint $table) { + $table->dropColumn('custom_domains'); + }); + } +}; diff --git a/database/migrations/2023_11_28_161121_add_custom_domain_to_forms_table.php b/database/migrations/2023_11_28_161121_add_custom_domain_to_forms_table.php new file mode 100644 index 0000000..456a758 --- /dev/null +++ b/database/migrations/2023_11_28_161121_add_custom_domain_to_forms_table.php @@ -0,0 +1,32 @@ +string('custom_domain')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('forms', function (Blueprint $table) { + $table->dropColumn(['custom_domain']); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index ab16766..d6d676f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4253,9 +4253,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", + "version": "1.0.30001565", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", + "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", "dev": true, "funding": [ { @@ -4265,6 +4265,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -16655,9 +16659,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", + "version": "1.0.30001565", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz", + "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==", "dev": true }, "caw": { diff --git a/resources/js/components/forms/TextAreaInput.vue b/resources/js/components/forms/TextAreaInput.vue index a25e83a..8c1476e 100644 --- a/resources/js/components/forms/TextAreaInput.vue +++ b/resources/js/components/forms/TextAreaInput.vue @@ -22,9 +22,9 @@ - + - {{ charCount }}/{{ maxCharLimit }} + {{ charCount }}/{{ maxCharLimift }} @@ -40,10 +40,10 @@ export default { props: { maxCharLimit: { type: Number, required: false, default: null }, - showCharLimit: { type: Boolean, required: false, default: false }, + showCharLimit: { type: Boolean, required: false, default: false } }, computed: { - charCount() { + charCount () { return (this.compVal) ? this.compVal.length : 0 } } diff --git a/resources/js/components/open/forms/components/form-components/FormCustomSeo.vue b/resources/js/components/open/forms/components/form-components/FormCustomSeo.vue index 44a136c..feed7f6 100644 --- a/resources/js/components/open/forms/components/form-components/FormCustomSeo.vue +++ b/resources/js/components/open/forms/components/form-components/FormCustomSeo.vue @@ -8,8 +8,11 @@

- Customize the image and text that appear when you share your form on other sites (Open Graph). + Customize the link, images and text that appear when you share your form on other sites (Open Graph).

+ @@ -41,6 +44,20 @@ export default { set (value) { this.$store.commit('open/working_form/set', value) } + }, + workspace () { + return this.$store.getters['open/workspaces/getCurrent']() + }, + customDomainOptions () { + return this.workspace.custom_domains.map((domain) => { + return { + name: domain, + value: domain + } + }) + }, + customDomainAllowed () { + return window.config.custom_domains_enabled } }, watch: {}, diff --git a/resources/js/components/pages/forms/show/ExtraMenu.vue b/resources/js/components/pages/forms/show/ExtraMenu.vue index d59a55a..f74b58b 100644 --- a/resources/js/components/pages/forms/show/ExtraMenu.vue +++ b/resources/js/components/pages/forms/show/ExtraMenu.vue @@ -1,103 +1,116 @@ diff --git a/resources/js/router/index.js b/resources/js/router/index.js index 4e7b466..8004e5d 100644 --- a/resources/js/router/index.js +++ b/resources/js/router/index.js @@ -3,14 +3,14 @@ import store from '~/store' import Meta from 'vue-meta' import routes from './routes' import Router from 'vue-router' -import {sync} from 'vuex-router-sync' +import { sync } from 'vuex-router-sync' import * as Sentry from '@sentry/vue' Vue.use(Meta) Vue.use(Router) // The middleware for every page of the application. -const globalMiddleware = ['locale', 'check-auth', 'notion-connection'] +const globalMiddleware = ['locale', 'check-auth', 'custom-domains'] // Load middleware modules dynamically. const requireContext = import.meta.glob('../middleware/**/*.js', { eager: true }) @@ -253,7 +253,7 @@ function resolveMiddleware (requireContext) { .map(file => [file.match(/[^/]*(?=\.[^.]*$)/)[0], requireContext[file]] ).forEach(([name, middleware]) => { - middlewares[name] = middleware.default || middleware - }) + middlewares[name] = middleware.default || middleware + }) return middlewares } diff --git a/resources/js/store/modules/open/workspaces.js b/resources/js/store/modules/open/workspaces.js index c14becf..ff56b11 100644 --- a/resources/js/store/modules/open/workspaces.js +++ b/resources/js/store/modules/open/workspaces.js @@ -61,6 +61,10 @@ export const mutations = { }, remove (state, itemId) { state.content = state.content.filter((val) => val.id !== itemId) + if (state.currentId === itemId) { + state.currentId = state.content.length > 0 ? state.content[0].id : null + localStorage.setItem(localStorageCurrentWorkspaceKey, state.currentId) + } }, startLoading () { state.loading = true diff --git a/resources/views/spa.blade.php b/resources/views/spa.blade.php index a4f4ab8..7bfab3a 100644 --- a/resources/views/spa.blade.php +++ b/resources/views/spa.blade.php @@ -16,7 +16,8 @@ 'crisp_website_id' => config('services.crisp_website_id'), 'ai_features_enabled' => !is_null(config('services.openai.api_key')), 's3_enabled' => config('filesystems.default') === 's3', - 'paid_plans_enabled' => !is_null(config('cashier.key')) + 'paid_plans_enabled' => !is_null(config('cashier.key')), + 'custom_domains_enabled' => config('custom-domains.enabled'), ]; @endphp diff --git a/routes/api.php b/routes/api.php index 1719bea..5368579 100644 --- a/routes/api.php +++ b/routes/api.php @@ -69,6 +69,7 @@ Route::group(['middleware' => 'auth:api'], function () { Route::get('/forms', [FormController::class, 'index'])->name('forms.index'); + Route::put('/custom-domains', [WorkspaceController::class, 'saveCustomDomain'])->name('save-custom-domains'); Route::delete('/', [WorkspaceController::class, 'delete'])->name('delete'); Route::get('form-stats/{formId}', [FormStatsController::class, 'getFormStats'])->name('form.stats'); diff --git a/routes/spa.php b/routes/spa.php index 918171f..91f771d 100644 --- a/routes/spa.php +++ b/routes/spa.php @@ -14,4 +14,4 @@ use Illuminate\Support\Facades\Route; | */ -Route::get('{path}', SpaController::class)->where('path', '^(?!(api|stats|mailcoach|vapor|sitemap|dist)).*$'); \ No newline at end of file +Route::get('{path}', SpaController::class)->where('path', '^(?!(api|stats|mailcoach|vapor|sitemap|caddy|dist)).*$'); diff --git a/routes/web.php b/routes/web.php index a7abe4f..e74bf09 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,3 +38,6 @@ Route::get('local/temp/{path}', function (Request $request, string $path){ })->where('path', '(.*)')->name('local.temp'); Route::get('/sitemap.xml', [\App\Http\Controllers\SitemapController::class, 'getSitemap'])->name('sitemap'); + +Route::get('caddy/ask-certificate/{secret?}', [\App\Http\Controllers\CaddyController::class, 'ask']) + ->name('caddy.ask')->middleware(\App\Http\Middleware\CaddyRequestMiddleware::class); diff --git a/vite.config.js b/vite.config.js index 2aea756..0393366 100644 --- a/vite.config.js +++ b/vite.config.js @@ -7,7 +7,8 @@ const plugins = [ laravel({ input: [ 'resources/js/app.js' - ] + ], + refresh: true }), vue({ template: { @@ -46,5 +47,11 @@ export default defineConfig({ '~': '/resources/js', '@': '/resources' } + }, + server: { + hmr: { + host: 'localhost', + protocol: 'ws' + } } })