Merge branch 'main' into vue-3

This commit is contained in:
Forms Dev 2023-12-07 16:04:55 +05:30
commit 496ef93353
42 changed files with 1371 additions and 188 deletions

View File

@ -74,3 +74,6 @@ ADMIN_EMAILS=
TEMPLATE_EDITOR_EMAILS= TEMPLATE_EDITOR_EMAILS=
OPEN_AI_API_KEY= OPEN_AI_API_KEY=
CADDY_SECRET=
CADDY_AUTHORIZED_IPS=

462
_ide_helper_models.php Normal file
View File

@ -0,0 +1,462 @@
<?php
// @formatter:off
/**
* A helper file for your Eloquent Models
* Copy the phpDocs from this file to the correct Model,
* And remove them from this file, to prevent double declarations.
*
* @author Barry vd. Heuvel <barryvdh@gmail.com>
*/
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<int, \App\Models\Forms\FormStatistic> $statistics
* @property-read int|null $statistics_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Forms\FormSubmission> $submissions
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Forms\FormView> $views
* @property-read \App\Models\Workspace|null $workspace
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Integration\FormZapierWebhook> $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<int, \App\Models\Template> $formTemplates
* @property-read int|null $form_templates_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Forms\Form> $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<int, \App\Models\License> $licenses
* @property-read int|null $licenses_count
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\OAuthProvider> $oauthProviders
* @property-read int|null $oauth_providers_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Cashier\Subscription> $subscriptions
* @property-read int|null $subscriptions_count
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Workspace> $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<int, \App\Models\Forms\Form> $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<int, \App\Models\User> $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 {}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers;
use App\Models\Workspace;
use Illuminate\Http\Request;
class CaddyController extends Controller
{
public function ask(Request $request)
{
$request->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',
]);
}
\Log::info('Caddy request received',[
'domain' => $domain,
]);
if ($workspace = Workspace::whereJsonContains('custom_domains',$domain)->first()) {
\Log::info('Caddy request successful',[
'domain' => $domain,
'workspace' => $workspace->id,
]);
return $this->success([
'success' => true,
'message' => 'OK',
]);
}
\Log::info('Caddy request failed',[
'domain' => $domain,
'workspace' => $workspace?->id,
]);
return $this->error([
'success' => false,
'message' => 'Unauthorized domain',
]);
}
}

View File

@ -34,7 +34,7 @@ class FormController extends Controller
$this->authorize('viewAny', Form::class); $this->authorize('viewAny', Form::class);
$workspaceIsPro = $workspace->is_pro; $workspaceIsPro = $workspace->is_pro;
$forms = $workspace->forms()->with(['creator','views','submissions']) $forms = $workspace->forms()
->orderByDesc('updated_at') ->orderByDesc('updated_at')
->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro){ ->paginate(10)->through(function (Form $form) use ($workspace, $workspaceIsPro){
@ -66,7 +66,7 @@ class FormController extends Controller
$this->authorize('viewAny', Form::class); $this->authorize('viewAny', Form::class);
$workspaceIsPro = $workspace->is_pro; $workspaceIsPro = $workspace->is_pro;
$newForms = $workspace->forms()->with(['creator','views','submissions'])->get()->map(function (Form $form) use ($workspace, $workspaceIsPro){ $newForms = $workspace->forms()->get()->map(function (Form $form) use ($workspace, $workspaceIsPro){
// Add attributes for faster loading // Add attributes for faster loading
$form->extra = (object) [ $form->extra = (object) [
'loadedWorkspace' => $workspace, 'loadedWorkspace' => $workspace,

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Workspace\CustomDomainRequest;
use App\Models\Workspace; use App\Models\Workspace;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Service\WorkspaceHelper; use App\Service\WorkspaceHelper;
@ -29,6 +30,13 @@ class WorkspaceController extends Controller
return (new WorkspaceHelper($workspace))->getAllUsers(); 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) public function delete($id)
{ {
$workspace = Workspace::findOrFail($id); $workspace = Workspace::findOrFail($id);

View File

@ -2,6 +2,7 @@
namespace App\Http; namespace App\Http;
use App\Http\Middleware\CustomDomainRestriction;
use App\Http\Middleware\EmbeddableForms; use App\Http\Middleware\EmbeddableForms;
use App\Http\Middleware\IsAdmin; use App\Http\Middleware\IsAdmin;
use App\Http\Middleware\IsNotSubscribed; use App\Http\Middleware\IsNotSubscribed;
@ -26,6 +27,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\SetLocale::class, \App\Http\Middleware\SetLocale::class,
CustomDomainRestriction::class,
]; ];
/** /**

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CaddyRequestMiddleware
{
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next)
{
if (!config('custom-domains.enabled')) {
return response()->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 IP',
], 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);
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Http\Middleware;
use App\Models\Forms\Form;
use App\Models\Workspace;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\Builder;
class CustomDomainRestriction
{
const CUSTOM_DOMAIN_HEADER = "User-Custom-Domain";
/**
* Handle an incoming request.
*/
public function handle(Request $request, Closure $next)
{
if (!$request->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);
}
}

View File

@ -125,7 +125,8 @@ abstract class UserFormRequest extends \Illuminate\Foundation\Http\FormRequest
'password' => 'sometimes|nullable', 'password' => 'sometimes|nullable',
// Custom SEO // 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}$/'
]; ];
} }

View File

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests\Workspace;
use App\Models\Workspace;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Request;
class CustomDomainRequest extends FormRequest
{
public Workspace $workspace;
public array $customDomains = [];
public function __construct(Request $request, Workspace $workspace)
{
$this->workspace = Workspace::findOrFail($request->workspaceId);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
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]);
}
}

View File

@ -25,7 +25,6 @@ class FormResource extends JsonResource
} }
$ownerData = $this->userIsFormOwner() ? [ $ownerData = $this->userIsFormOwner() ? [
'creator' => new UserResource($this->creator),
'views_count' => $this->views_count, 'views_count' => $this->views_count,
'submissions_count' => $this->submissions_count, 'submissions_count' => $this->submissions_count,
'notifies' => $this->notifies, 'notifies' => $this->notifies,

View File

@ -0,0 +1,21 @@
<?php
namespace App\Models\Billing;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Laravel\Cashier\Subscription as CashierSubscription;
class Subscription extends CashierSubscription
{
use HasFactory;
public static function booted(): void
{
static::saved(function (Subscription $sub) {
$sub->user->flushCache();
});
static::deleted(function (Subscription $sub) {
$sub->user->flushCache();
});
}
}

View File

@ -4,6 +4,8 @@ namespace App\Models\Forms;
use App\Events\Models\FormCreated; use App\Events\Models\FormCreated;
use App\Models\Integration\FormZapierWebhook; use App\Models\Integration\FormZapierWebhook;
use App\Models\Traits\CachableAttributes;
use App\Models\Traits\CachesAttributes;
use App\Models\User; use App\Models\User;
use App\Models\Workspace; use App\Models\Workspace;
use Database\Factories\FormFactory; use Database\Factories\FormFactory;
@ -17,8 +19,10 @@ use Stevebauman\Purify\Facades\Purify;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
class Form extends Model class Form extends Model implements CachableAttributes
{ {
use CachesAttributes;
const DARK_MODE_VALUES = ['auto', 'light', 'dark']; const DARK_MODE_VALUES = ['auto', 'light', 'dark'];
const THEMES = ['default', 'simple', 'notion']; const THEMES = ['default', 'simple', 'notion'];
const WIDTHS = ['centered', 'full']; const WIDTHS = ['centered', 'full'];
@ -53,6 +57,7 @@ class Form extends Model
'visibility', 'visibility',
// Customization // Customization
'custom_domain',
'theme', 'theme',
'width', 'width',
'cover_picture', 'cover_picture',
@ -125,6 +130,11 @@ class Form extends Model
'removed_properties' 'removed_properties'
]; ];
protected $cachableAttributes = [
'is_pro',
'views_count',
];
/** /**
* The event map for the model. * The event map for the model.
* *
@ -136,17 +146,22 @@ class Form extends Model
public function getIsProAttribute() public function getIsProAttribute()
{ {
return optional($this->workspace)->is_pro; return $this->remember('is_pro', 15 * 60, function (): ?bool {
return optional($this->workspace)->is_pro === true;
});
} }
public function getShareUrlAttribute() 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() public function getEditUrlAttribute()
{ {
return url('/forms/'.$this->slug.'/show'); return url('/forms/' . $this->slug . '/show');
} }
public function getSubmissionsCountAttribute() public function getSubmissionsCountAttribute()
@ -156,14 +171,14 @@ class Form extends Model
public function getViewsCountAttribute() public function getViewsCountAttribute()
{ {
if(env('DB_CONNECTION') == 'pgsql') { return $this->remember('views_count', 15 * 60, function (): int {
return $this->views()->count() + if (env('DB_CONNECTION') == 'mysql') {
$this->statistics()->sum(DB::raw("cast(data->>'views' as integer)"));
} elseif(env('DB_CONNECTION') == 'mysql') {
return (int)($this->views()->count() + 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; return $this->views()->count() +
$this->statistics()->sum(DB::raw("cast(data->>'views' as integer)"));
});
} }
public function setDescriptionAttribute($value) public function setDescriptionAttribute($value)
@ -219,7 +234,8 @@ class Form extends Model
return !empty($this->password); return !empty($this->password);
} }
protected function removedProperties(): Attribute { protected function removedProperties(): Attribute
{
return Attribute::make( return Attribute::make(
get: function ($value) { get: function ($value) {
return $value ? json_decode($value, true) : []; return $value ? json_decode($value, true) : [];

View File

@ -42,4 +42,27 @@ class License extends Model
3 => 75000000, // 75 MB, 3 => 75000000, // 75 MB,
][$this->meta['tier']]; ][$this->meta['tier']];
} }
public function getCustomDomainLimitCountAttribute()
{
return [
1 => 1,
2 => 10,
3 => null,
][$this->meta['tier']];
}
public static function booted(): void
{
static::saved(function (License $license) {
if ($license->user) {
$license->user->flushCache();
}
});
static::deleted(function (License $license) {
if ($license->user) {
$license->user->flushCache();
}
});
}
} }

View File

@ -0,0 +1,45 @@
<?php
namespace App\Models\Traits;
use Closure;
interface CachableAttributes
{
/**
* Get an item from the cache, or execute the given Closure and store the result.
*
* @param string $key
* @param int|null $ttl
* @param Closure $callback
*
* @return mixed
*/
public function remember(string $key, ?int $ttl, Closure $callback);
/**
* Get an item from the cache, or execute the given Closure and store the result forever.
*
* @param string $key
* @param \Closure $callback
*
* @return mixed
*/
public function rememberForever(string $key, Closure $callback);
/**
* Remove an item from the cache.
*
* @param string $key
*
* @return bool
*/
public function forget(string $key): bool;
/**
* Remove all items from the cache.
*
* @return bool
*/
public function flush(): bool;
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Models\Traits;
use Closure;
use Illuminate\Contracts\Cache\Factory as CacheFactoryContract;
use Illuminate\Contracts\Cache\Repository as CacheRepository;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
/**
* @property string|null $attributeCachePrefix
* @property string|null $attributeCacheStore
* @property string[]|null $cachableAttributes
*
* @mixin Model
*/
trait CachesAttributes
{
/** @var array<string, mixed> */
protected $attributeCache = [];
public static function bootCachesAttributes(): void
{
static::deleting(function (Model $model): void {
/** @var Model|CachableAttributes $model */
$model->flush();
});
}
public function remember(string $attribute, ?int $ttl, Closure $callback)
{
if ($ttl === 0 || ! $this->exists) {
if (! isset($this->attributeCache[$attribute])) {
$this->attributeCache[$attribute] = value($callback);
}
return $this->attributeCache[$attribute];
}
if ($ttl === null) {
return $this->getCacheRepository()->rememberForever($this->getCacheKey($attribute), $callback);
}
if ($ttl < 0) {
throw new InvalidArgumentException("The TTL has to be null, 0 or any positive number - you provided `{$ttl}`.");
}
return $this->getCacheRepository()->remember($this->getCacheKey($attribute), $ttl, $callback);
}
public function rememberForever(string $attribute, Closure $callback)
{
return $this->remember($attribute, null, $callback);
}
public function forget(string $attribute): bool
{
unset($this->attributeCache[$attribute]);
if (! $this->exists) {
return true;
}
return $this->getCacheRepository()->forget($this->getCacheKey($attribute));
}
public function flush(): bool
{
$result = true;
foreach ($this->cachableAttributes ?? [] as $attribute) {
$result = $this->forget($attribute) ? $result : false;
}
return $result;
}
protected function getCacheKey(string $attribute): string
{
return implode('.', [
$this->attributeCachePrefix ?? 'model_attribute_cache',
$this->getConnectionName() ?? 'connection',
$this->getTable(),
$this->getKey(),
$attribute,
$this->updated_at?->timestamp ?? '0'
]);
}
protected function getCacheRepository(): CacheRepository
{
return $this->getCacheFactory()->store($this->attributeCacheStore);
}
protected function getCacheFactory(): CacheFactoryContract
{
return app('cache');
}
}

View File

@ -2,9 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Http\Controllers\SubscriptionController;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use App\Models\Template;
use App\Notifications\ResetPassword; use App\Notifications\ResetPassword;
use App\Notifications\VerifyEmail; use App\Notifications\VerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -59,11 +57,14 @@ class User extends Authenticatable implements JWTSubject
'photo_url', 'photo_url',
]; ];
protected $withCount = ['workspaces'];
public function ownsForm(Form $form) public function ownsForm(Form $form)
{ {
return $this->workspaces()->find($form->workspace_id) !== null; return $this->workspaces()->where('workspaces.id',$form->workspace_id)->exists();
}
public function ownsWorkspace(Workspace $workspace)
{
return $this->workspaces()->where('workspaces.id',$workspace->id)->exists();
} }
/** /**
@ -205,6 +206,16 @@ class User extends Authenticatable implements JWTSubject
})->first()?->onTrial(); })->first()?->onTrial();
} }
public function flushCache()
{
$this->workspaces()->with('forms')->get()->each(function (Workspace $workspace) {
$workspace->flush();
$workspace->forms->each(function (Form $form) {
$form->flush();
});
});
}
public static function boot() public static function boot()
{ {
parent::boot(); parent::boot();

View File

@ -4,20 +4,25 @@ namespace App\Models;
use App\Http\Requests\AnswerFormRequest; use App\Http\Requests\AnswerFormRequest;
use App\Models\Forms\Form; use App\Models\Forms\Form;
use App\Models\Traits\CachableAttributes;
use App\Models\Traits\CachesAttributes;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Workspace extends Model class Workspace extends Model implements CachableAttributes
{ {
use HasFactory; use HasFactory, CachesAttributes;
const MAX_FILE_SIZE_FREE = 5000000; // 5 MB const MAX_FILE_SIZE_FREE = 5000000; // 5 MB
const MAX_FILE_SIZE_PRO = 50000000; // 50 MB const MAX_FILE_SIZE_PRO = 50000000; // 50 MB
const MAX_DOMAIN_PRO = 1;
protected $fillable = [ protected $fillable = [
'name', 'name',
'icon', 'icon',
'user_id', 'user_id',
'custom_domain',
]; ];
protected $appends = [ protected $appends = [
@ -25,20 +30,18 @@ class Workspace extends Model
'is_enterprise' 'is_enterprise'
]; ];
public function getIsProAttribute() protected $casts = [
{ 'custom_domains' => 'array',
if(is_null(config('cashier.key'))){ ];
return true; // If no paid plan so TRUE for ALL
}
// Make sure at least one owner is pro protected $cachableAttributes = [
foreach ($this->owners as $owner) { 'is_pro',
if ($owner->is_subscribed) { 'is_enterprise',
return true; 'is_risky',
} 'submissions_count',
} 'max_file_size',
return false; 'custom_domain_count'
} ];
public function getMaxFileSizeAttribute() public function getMaxFileSizeAttribute()
{ {
@ -46,6 +49,7 @@ class Workspace extends Model
return self::MAX_FILE_SIZE_PRO; return self::MAX_FILE_SIZE_PRO;
} }
return $this->remember('max_file_size', 15 * 60, function(): int {
// Return max file size depending on subscription // Return max file size depending on subscription
foreach ($this->owners as $owner) { foreach ($this->owners as $owner) {
if ($owner->is_subscribed) { if ($owner->is_subscribed) {
@ -58,6 +62,45 @@ class Workspace extends Model
} }
return self::MAX_FILE_SIZE_FREE; return self::MAX_FILE_SIZE_FREE;
});
}
public function getCustomDomainCountLimitAttribute()
{
if(is_null(config('cashier.key'))){
return null;
}
return $this->remember('custom_domain_count', 15 * 60, function(): int {
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 getIsProAttribute()
{
if(is_null(config('cashier.key'))){
return true; // If no paid plan so TRUE for ALL
}
return $this->remember('is_pro', 15 * 60, function(): bool {
// Make sure at least one owner is pro
foreach ($this->owners as $owner) {
if ($owner->is_subscribed) {
return true;
}
}
return false;
});
} }
public function getIsEnterpriseAttribute() public function getIsEnterpriseAttribute()
@ -66,16 +109,20 @@ class Workspace extends Model
return true; // If no paid plan so TRUE for ALL return true; // If no paid plan so TRUE for ALL
} }
return $this->remember('is_enterprise', 15 * 60, function(): bool {
// Make sure at least one owner is pro
foreach ($this->owners as $owner) { foreach ($this->owners as $owner) {
if ($owner->has_enterprise_subscription) { if ($owner->has_enterprise_subscription) {
return true; return true;
} }
} }
return false; return false;
});
} }
public function getIsRiskyAttribute() public function getIsRiskyAttribute()
{ {
return $this->remember('is_risky', 15 * 60, function(): bool {
// A workspace is risky if all of his users are risky // A workspace is risky if all of his users are risky
foreach ($this->owners as $owner) { foreach ($this->owners as $owner) {
if (!$owner->is_risky) { if (!$owner->is_risky) {
@ -84,16 +131,19 @@ class Workspace extends Model
} }
return true; return true;
});
} }
public function getSubmissionsCountAttribute() public function getSubmissionsCountAttribute()
{ {
return $this->remember('submissions_count', 15 * 60, function(): int {
$total = 0; $total = 0;
foreach ($this->forms as $form) { foreach ($this->forms as $form) {
$total += $form->submissions_count; $total += $form->submissions_count;
} }
return $total; return $total;
});
} }
/** /**

View File

@ -30,7 +30,7 @@ class FormPolicy
*/ */
public function view(User $user, Form $form) public function view(User $user, Form $form)
{ {
return $user->workspaces()->find($form->workspace_id) !== null; return $user->ownsForm($form);
} }
/** /**
@ -53,7 +53,7 @@ class FormPolicy
*/ */
public function update(User $user, Form $form) public function update(User $user, Form $form)
{ {
return $user->workspaces()->find($form->workspace_id) !== null; return $user->ownsForm($form);
} }
/** /**
@ -65,7 +65,7 @@ class FormPolicy
*/ */
public function delete(User $user, Form $form) public function delete(User $user, Form $form)
{ {
return $user->workspaces()->find($form->workspace_id) !== null; return $user->ownsForm($form);
} }
/** /**
@ -77,7 +77,7 @@ class FormPolicy
*/ */
public function restore(User $user, Form $form) public function restore(User $user, Form $form)
{ {
return $user->workspaces()->find($form->workspace_id) !== null; return $user->ownsForm($form);
} }
/** /**
@ -89,6 +89,6 @@ class FormPolicy
*/ */
public function forceDelete(User $user, Form $form) public function forceDelete(User $user, Form $form)
{ {
return $user->workspaces()->find($form->workspace_id) !== null; return $user->ownsForm($form);
} }
} }

View File

@ -30,7 +30,7 @@ class WorkspacePolicy
*/ */
public function view(User $user, Workspace $workspace) public function view(User $user, Workspace $workspace)
{ {
return $user->workspaces()->find($workspace->id)!==null; return $user->ownsWorkspace($workspace);
} }
/** /**

View File

@ -2,6 +2,7 @@
namespace App\Providers; namespace App\Providers;
use App\Models\Billing\Subscription;
use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -31,6 +32,7 @@ class AppServiceProvider extends ServiceProvider
JsonResource::withoutWrapping(); JsonResource::withoutWrapping();
Cashier::calculateTaxes(); Cashier::calculateTaxes();
Cashier::useSubscriptionModel(Subscription::class);
if ($this->app->runningUnitTests()) { if ($this->app->runningUnitTests()) {
Schema::defaultStringLength(191); Schema::defaultStringLength(191);

View File

@ -0,0 +1,9 @@
<?php
return [
'enabled' => !empty(env('CADDY_SECRET')) && !empty(env('CADDY_AUTHORIZED_IPS', [])),
'caddy_secret' => env('CADDY_SECRET'),
'authorized_ips' => explode(',', env('CADDY_AUTHORIZED_IPS')),
];

View File

@ -58,7 +58,7 @@ class CreateFormsTable extends Migration
if ($driver === 'mysql') { if ($driver === 'mysql') {
$table->jsonb('tags')->default(new Expression('(JSON_ARRAY())')); $table->jsonb('tags')->default(new Expression('(JSON_ARRAY())'));
} else { } else {
$table->jsonb('tags')->default('[]'); $table->jsonb('tags')->default('[]')->nullable();
} }
}); });
} }

View File

@ -21,7 +21,7 @@ return new class extends Migration
if ($driver === 'mysql') { if ($driver === 'mysql') {
$table->jsonb('removed_properties')->default(new Expression("(JSON_ARRAY())")); $table->jsonb('removed_properties')->default(new Expression("(JSON_ARRAY())"));
} else { } else {
$table->jsonb('removed_properties')->default("[]"); $table->jsonb('removed_properties')->default("[]")->nullable();
} }
}); });
} }

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$driver = DB::getDriverName();
Schema::table('workspaces', function (Blueprint $table) use ($driver){
if ($driver === 'mysql') {
$table->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');
});
}
};

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('forms', function (Blueprint $table) {
$table->string('custom_domain')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('forms', function (Blueprint $table) {
$table->dropColumn(['custom_domain']);
});
}
};

View File

@ -8,8 +8,11 @@
</svg> </svg>
</template> </template>
<p class="mt-4 text-gray-500 text-sm"> <p class="mt-4 text-gray-500 text-sm">
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).
</p> </p>
<select-input v-if="customDomainAllowed" v-model="form.custom_domain" :disabled="customDomainOptions.length <= 0" :options="customDomainOptions" name="type"
class="mt-4" label="Form Domain" placeholder="yourdomain.com"
/>
<text-input v-model="form.seo_meta.page_title" name="page_title" class="mt-4" <text-input v-model="form.seo_meta.page_title" name="page_title" class="mt-4"
label="Page Title" help="Under or approximately 60 characters" label="Page Title" help="Under or approximately 60 characters"
/> />
@ -48,6 +51,20 @@ export default {
set (value) { set (value) {
this.workingFormStore.set(value) this.workingFormStore.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: {}, watch: {},

View File

@ -1,90 +1,102 @@
<template> <template>
<div> <div>
<div v-if="loadingDuplicate || loadingDelete" class="pr-4 pt-2"> <div v-if="loadingDuplicate || loadingDelete" class="pr-4 pt-2">
<loader class="h-6 w-6 mx-auto"/> <loader class="h-6 w-6 mx-auto" />
</div> </div>
<dropdown v-else class="inline" dusk="nav-dropdown"> <dropdown v-else class="inline" dusk="nav-dropdown">
<template #trigger="{toggle}"> <template #trigger="{toggle}">
<v-button color="white" class="mr-2" @click="toggle"> <v-button color="white" class="mr-2" @click="toggle">
<svg class="w-4 h-4 inline -mt-1" viewBox="0 0 16 4" fill="none" <svg class="w-4 h-4 inline -mt-1" viewBox="0 0 16 4" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M8.00016 2.83366C8.4604 2.83366 8.8335 2.46056 8.8335 2.00033C8.8335 1.54009 8.4604 1.16699 8.00016 1.16699C7.53993 1.16699 7.16683 1.54009 7.16683 2.00033C7.16683 2.46056 7.53993 2.83366 8.00016 2.83366Z" d="M8.00016 2.83366C8.4604 2.83366 8.8335 2.46056 8.8335 2.00033C8.8335 1.54009 8.4604 1.16699 8.00016 1.16699C7.53993 1.16699 7.16683 1.54009 7.16683 2.00033C7.16683 2.46056 7.53993 2.83366 8.00016 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/> stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
/>
<path <path
d="M13.8335 2.83366C14.2937 2.83366 14.6668 2.46056 14.6668 2.00033C14.6668 1.54009 14.2937 1.16699 13.8335 1.16699C13.3733 1.16699 13.0002 1.54009 13.0002 2.00033C13.0002 2.46056 13.3733 2.83366 13.8335 2.83366Z" d="M13.8335 2.83366C14.2937 2.83366 14.6668 2.46056 14.6668 2.00033C14.6668 1.54009 14.2937 1.16699 13.8335 1.16699C13.3733 1.16699 13.0002 1.54009 13.0002 2.00033C13.0002 2.46056 13.3733 2.83366 13.8335 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/> stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
/>
<path <path
d="M2.16683 2.83366C2.62707 2.83366 3.00016 2.46056 3.00016 2.00033C3.00016 1.54009 2.62707 1.16699 2.16683 1.16699C1.70659 1.16699 1.3335 1.54009 1.3335 2.00033C1.3335 2.46056 1.70659 2.83366 2.16683 2.83366Z" d="M2.16683 2.83366C2.62707 2.83366 3.00016 2.46056 3.00016 2.00033C3.00016 1.54009 2.62707 1.16699 2.16683 1.16699C1.70659 1.16699 1.3335 1.54009 1.3335 2.00033C1.3335 2.46056 1.70659 2.83366 2.16683 2.83366Z"
stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/> stroke="#344054" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"
/>
</svg> </svg>
</v-button> </v-button>
</template> </template>
<router-link v-if="isMainPage && user" :to="{name:'forms.show_public', params: {slug: form.slug}}" target="_blank" <a v-if="isMainPage && user" v-track.view_form_click="{form_id:form.id, form_slug:form.slug}" :href="form.share_url"
target="_blank"
class="block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center" class="block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
v-track.view_form_click="{form_id:form.id, form_slug:form.slug}"
> >
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" <svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg"
>
<path d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z" <path d="M1 12C1 12 5 4 12 4C19 4 23 12 23 12C23 12 19 20 12 20C5 20 1 12 1 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
<path <path
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg> </svg>
View form View form
</router-link> </a>
<router-link v-if="isMainPage" :to="{name:'forms.edit', params: {slug: form.slug}}" <router-link v-if="isMainPage" v-track.edit_form_click="{form_id:form.id, form_slug:form.slug}"
:to="{name:'forms.edit', params: {slug: form.slug}}"
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center" class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
v-track.edit_form_click="{form_id:form.id, form_slug:form.slug}"
> >
<svg class="w-4 h-4 mr-2" width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4 mr-2" width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M8.99998 15.6662H16.5M1.5 15.6662H2.89545C3.3031 15.6662 3.50693 15.6662 3.69874 15.6202C3.8688 15.5793 4.03138 15.512 4.1805 15.4206C4.34869 15.3175 4.49282 15.1734 4.78107 14.8852L15.25 4.4162C15.9404 3.72585 15.9404 2.60656 15.25 1.9162C14.5597 1.22585 13.4404 1.22585 12.75 1.9162L2.28105 12.3852C1.9928 12.6734 1.84867 12.8175 1.7456 12.9857C1.65422 13.1348 1.58688 13.2974 1.54605 13.4675C1.5 13.6593 1.5 13.8631 1.5 14.2708V15.6662Z" d="M8.99998 15.6662H16.5M1.5 15.6662H2.89545C3.3031 15.6662 3.50693 15.6662 3.69874 15.6202C3.8688 15.5793 4.03138 15.512 4.1805 15.4206C4.34869 15.3175 4.49282 15.1734 4.78107 14.8852L15.25 4.4162C15.9404 3.72585 15.9404 2.60656 15.25 1.9162C14.5597 1.22585 13.4404 1.22585 12.75 1.9162L2.28105 12.3852C1.9928 12.6734 1.84867 12.8175 1.7456 12.9857C1.65422 13.1348 1.58688 13.2974 1.54605 13.4675C1.5 13.6593 1.5 13.8631 1.5 14.2708V15.6662Z"
stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/> stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"
/>
</svg> </svg>
Edit Edit
</router-link> </router-link>
<a href="#" v-if="isMainPage" <a v-if="isMainPage" href="#"
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center" class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="copyLink" @click.prevent="copyLink"
> >
<svg class="w-4 h-4 mr-2" viewBox="0 0 16 10" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-4 h-4 mr-2" viewBox="0 0 16 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.00016 8.33317H4.66683C2.82588 8.33317 1.3335 6.84079 1.3335 4.99984C1.3335 3.15889 2.82588 1.6665 4.66683 1.6665H6.00016M10.0002 8.33317H11.3335C13.1744 8.33317 14.6668 6.84079 14.6668 4.99984C14.6668 3.15889 13.1744 1.6665 11.3335 1.6665H10.0002M4.66683 4.99984L11.3335 4.99984" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6.00016 8.33317H4.66683C2.82588 8.33317 1.3335 6.84079 1.3335 4.99984C1.3335 3.15889 2.82588 1.6665 4.66683 1.6665H6.00016M10.0002 8.33317H11.3335C13.1744 8.33317 14.6668 6.84079 14.6668 4.99984C14.6668 3.15889 13.1744 1.6665 11.3335 1.6665H10.0002M4.66683 4.99984L11.3335 4.99984" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg> </svg>
Copy link to share Copy link to share
</a> </a>
<a href="#" <a v-track.duplicate_form_click="{form_id:form.id, form_slug:form.slug}"
href="#"
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center" class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
v-track.duplicate_form_click="{form_id:form.id, form_slug:form.slug}"
@click.prevent="duplicateForm" @click.prevent="duplicateForm"
> >
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"
/> />
</svg> </svg>
Duplicate form Duplicate form
</a> </a>
<a href="#" v-if="!isMainPage" v-track.create_template_click="{form_id:form.id, form_slug:form.slug}" <a v-if="!isMainPage" v-track.create_template_click="{form_id:form.id, form_slug:form.slug}" href="#"
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center" class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
@click.prevent="showFormTemplateModal=true" @click.prevent="showFormTemplateModal=true"
> >
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2"> stroke="currentColor" stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" <path stroke-linecap="round" stroke-linejoin="round"
d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"/> d="M17 14v6m-3-3h6M6 10h2a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2zm10 0h2a2 2 0 002-2V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v2a2 2 0 002 2zM6 20h2a2 2 0 002-2v-2a2 2 0 00-2-2H6a2 2 0 00-2 2v2a2 2 0 002 2z"
/>
</svg> </svg>
Create Template Create Template
</a> </a>
<a href="#" <a v-track.delete_form_click="{form_id:form.id, form_slug:form.slug}"
href="#"
class="block block px-4 py-2 text-md text-red-600 hover:bg-red-50 flex items-center" class="block block px-4 py-2 text-md text-red-600 hover:bg-red-50 flex items-center"
v-track.delete_form_click="{form_id:form.id, form_slug:form.slug}"
@click.prevent="showDeleteFormModal=true" @click.prevent="showDeleteFormModal=true"
> >
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
@ -94,10 +106,11 @@
</dropdown> </dropdown>
<!-- Delete Form Modal --> <!-- Delete Form Modal -->
<modal :show="showDeleteFormModal" icon-color="red" @close="showDeleteFormModal=false" max-width="sm"> <modal :show="showDeleteFormModal" icon-color="red" max-width="sm" @close="showDeleteFormModal=false">
<template #icon> <template #icon>
<svg class="w-10 h-10" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-10 h-10" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor"> stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
@ -111,13 +124,17 @@
If you want to permanently delete this form and all of its data, you can do so below. If you want to permanently delete this form and all of its data, you can do so below.
</p> </p>
<div class="flex mt-4"> <div class="flex mt-4">
<v-button class="sm:w-1/2 mr-4" color="white" @click.prevent="showDeleteFormModal=false">Cancel</v-button> <v-button class="sm:w-1/2 mr-4" color="white" @click.prevent="showDeleteFormModal=false">
<v-button class="sm:w-1/2" color="red" :loading="loadingDelete" @click.prevent="deleteForm">Yes, delete it</v-button> Cancel
</v-button>
<v-button class="sm:w-1/2" color="red" :loading="loadingDelete" @click.prevent="deleteForm">
Yes, delete it
</v-button>
</div> </div>
</div> </div>
</modal> </modal>
<form-template-modal v-if="!isMainPage && user" :form="form" :show="showFormTemplateModal" @close="showFormTemplateModal=false"/> <form-template-modal v-if="!isMainPage && user" :form="form" :show="showFormTemplateModal" @close="showFormTemplateModal=false" />
</div> </div>
</template> </template>

View File

@ -3,8 +3,12 @@
<div class="w-full"> <div class="w-full">
<div class="rounded-lg bg-gray-50 dark:bg-gray-800 px-6 py-8 sm:p-10 lg:flex lg:items-center"> <div class="rounded-lg bg-gray-50 dark:bg-gray-800 px-6 py-8 sm:p-10 lg:flex lg:items-center">
<div class="flex-1"> <div class="flex-1">
<h3 class="inline-flex px-4 py-1 rounded-full text-md font-bold tracking-wide uppercase bg-white text-gray-800">Custom plan</h3> <h3 class="inline-flex px-4 py-1 rounded-full text-md font-bold tracking-wide uppercase bg-white text-gray-800">
<div class="mt-4 text-md text-gray-600 dark:text-gray-400">Get a custom file upload limit, enterprise-level support, custom contract, payment via invoice/PO etc.</div> Custom plan
</h3>
<div class="mt-4 text-md text-gray-600 dark:text-gray-400">
Get a custom file upload limit, enterprise-level support, custom contract, dedicated application instance in a specific region, payment via invoice/PO etc.
</div>
</div> </div>
<div class="mt-6 rounded-md lg:mt-0 lg:ml-10 lg:flex-shrink-0"> <div class="mt-6 rounded-md lg:mt-0 lg:ml-10 lg:flex-shrink-0">
<v-button color="white" class="w-full mt-4" @click.prevent="customPlanClick"> <v-button color="white" class="w-full mt-4" @click.prevent="customPlanClick">

View File

@ -145,7 +145,7 @@ export default {
'Slack notifications', 'Slack notifications',
'Discord notifications', 'Discord notifications',
'Editable submissions', 'Editable submissions',
'Custom domain (soon)', '1 Custom domain',
'Custom code', 'Custom code',
'Larger file uploads (50mb)', 'Larger file uploads (50mb)',
'Remove OpnForm branding', 'Remove OpnForm branding',

View File

@ -0,0 +1,16 @@
export default async (to, from, next) => {
if (!window.config.custom_domains_enabled) {
next()
}
const isCustomDomain = getDomain(window.location.href) !== getDomain(window.config.app_url)
if (isCustomDomain && !['forms.show_public'].includes(to.name)) {
// If route isn't a public form, redirect
window.location.href = 'https://opnform.com/?utm_source=failed_custom_domain_redirect'
} else {
next()
}
}
function getDomain (url) {
return (new URL(url)).hostname
}

View File

@ -52,5 +52,13 @@ export default {
return this.content return this.content
} }
} }
},
watch: {
value (val) {
if (val !== this.compVal) {
this.compVal = val
}
}
} }
} }

View File

@ -23,7 +23,7 @@
<extra-menu :form="form" /> <extra-menu :form="form" />
<v-button v-track.view_form_click="{form_id:form.id, form_slug:form.slug}" target="_blank" <v-button v-track.view_form_click="{form_id:form.id, form_slug:form.slug}" target="_blank"
:to="{name:'forms.show_public', params: {slug: form.slug}}" color="white" :href="form.share_url" color="white"
class="mr-2 text-blue-600 hidden sm:block" class="mr-2 text-blue-600 hidden sm:block"
> >
<svg class="w-6 h-6 inline -mt-1" viewBox="0 0 24 24" fill="none" <svg class="w-6 h-6 inline -mt-1" viewBox="0 0 24 24" fill="none"

View File

@ -1,16 +1,30 @@
<template> <template>
<div> <div>
<h3 class="font-semibold text-2xl text-gray-900">Workspace settings</h3> <div class="flex flex-wrap items-center gap-y-4 flex-wrap-reverse">
<div class="flex-grow">
<h3 class="font-semibold text-2xl text-gray-900">
Workspace settings
</h3>
<small class="text-gray-600">Manage your workspaces.</small> <small class="text-gray-600">Manage your workspaces.</small>
</div>
<v-button color="outline-blue" :loading="loading" @click="workspaceModal=true">
<svg class="inline -mt-1 mr-1 h-4 w-4" viewBox="0 0 14 14" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6.99996 1.16699V12.8337M1.16663 7.00033H12.8333" stroke="currentColor" stroke-width="1.67"
stroke-linecap="round" stroke-linejoin="round"
/>
</svg>
Create new workspace
</v-button>
</div>
<div v-if="loading" class="w-full text-blue-500 text-center"> <div v-if="loading" class="w-full text-blue-500 text-center">
<loader class="h-10 w-10 p-5"/> <loader class="h-10 w-10 p-5" />
</div> </div>
<div v-else> <div v-else-if="workspace">
<div v-for="workspace in workspaces" :key="workspace.id" <div class="mt-4 flex group bg-white items-center">
class="mt-4 p-4 flex group bg-white hover:bg-gray-50 dark:bg-notion-dark items-center" <div class="flex space-x-4 flex-grow items-center">
>
<div class="flex space-x-4 flex-grow items-center cursor-pointer" role="button" @click.prevent="switchWorkspace(workspace)">
<img v-if="isUrl(workspace.icon)" :src="workspace.icon" :alt="workspace.name + ' icon'" <img v-if="isUrl(workspace.icon)" :src="workspace.icon" :alt="workspace.name + ' icon'"
class="rounded-full h-12 w-12" class="rounded-full h-12 w-12"
> >
@ -18,33 +32,61 @@
v-text="workspace.icon" v-text="workspace.icon"
/> />
<div class="space-y-4 py-1"> <div class="space-y-4 py-1">
<div class="font-bold truncate">{{workspace.name}}</div> <div class="font-bold truncate">
{{ workspace.name }}
</div> </div>
</div> </div>
<div v-if="workspaces.length > 1" </div>
class="block md:hidden group-hover:block text-red-500 p-2 rounded hover:bg-red-50" role="button" </div>
@click="deleteWorkspace(workspace)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <template v-if="customDomainsEnabled">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <text-area-input v-model="customDomains" name="custom_domain" class="mt-4" :required="false"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/> :disabled="!workspace.is_pro"
label="Workspace Custom Domains" wrapper-class="" placeholder="yourdomain.com - 1 per line"
/>
<p class="text-gray-500 text-sm">
Read our <a href="#"
@click.prevent="$crisp.push(['do', 'helpdesk:article:open', ['en', 'how-to-use-my-own-domain-9m77g7']])"
>custom
domain instructions</a> to learn how to use your own domain.
</p>
</template>
<div class="flex flex-wrap justify-between gap-2 mt-4">
<v-button v-if="customDomainsEnabled" class="w-full sm:w-auto" :loading="customDomainsLoading" @click="saveChanges">
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 21V13H7V21M7 3V8H15M19 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H16L21 8V19C21 19.5304 20.7893 20.0391 20.4142 20.4142C20.0391 20.7893 19.5304 21 19 21Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg> </svg>
</div> Save Domains
</div>
<v-button :loading="loading" class="mt-4" @click="workspaceModal=true">
<svg class="inline text-white mr-1 h-4 w-4" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99996 1.16699V12.8337M1.16663 7.00033H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Create new workspace
</v-button> </v-button>
<v-button v-if="workspaces.length > 1" color="white" class="group w-full sm:w-auto" :loading="loading"
@click="deleteWorkspace(workspace)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 -mt-1 inline group-hover:text-red-700" fill="none"
viewBox="0 0 24 24" stroke="currentColor"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Remove workspace
</v-button>
</div>
</div> </div>
<!-- Workspace modal --> <!-- Workspace modal -->
<modal :show="workspaceModal" @close="workspaceModal=false" max-width="lg"> <modal :show="workspaceModal" max-width="lg" @close="workspaceModal=false">
<template #icon> <template #icon>
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M12 8V16M8 12H16M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" d="M12 8V16M8 12H16M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
/>
</svg> </svg>
</template> </template>
<template #title> <template #title>
@ -62,13 +104,13 @@
</div> </div>
<div class="w-full mt-6"> <div class="w-full mt-6">
<v-button :loading="form.busy" class="w-full my-3">Save</v-button> <v-button :loading="form.busy" class="w-full my-3">
Save
</v-button>
</div> </div>
</form> </form>
</div> </div>
</modal> </modal>
</div> </div>
</template> </template>
@ -78,9 +120,13 @@ import Form from 'vform'
import { useFormsStore } from '../../stores/forms' import { useFormsStore } from '../../stores/forms'
import { useWorkspacesStore } from '../../stores/workspaces' import { useWorkspacesStore } from '../../stores/workspaces'
import SeoMeta from '../../mixins/seo-meta.js' import SeoMeta from '../../mixins/seo-meta.js'
import TextAreaInput from '../../components/forms/TextAreaInput.vue'
import axios from 'axios'
import * as domain from 'domain'
export default { export default {
components: {}, components: { TextAreaInput },
mixins: [SeoMeta],
scrollToTop: false, scrollToTop: false,
mixins: [SeoMeta], mixins: [SeoMeta],
@ -101,29 +147,59 @@ export default {
name: '', name: '',
emoji: '' emoji: ''
}), }),
workspaceModal: false workspaceModal: false,
customDomains: '',
customDomainsLoading: false
}), }),
mounted() { mounted () {
this.workspacesStore.loadIfEmpty() this.workspacesStore.loadIfEmpty()
this.initCustomDomains()
}, },
computed: {}, computed: {
workspace () {
return this.workspacesStore.getCurrent()
},
customDomainsEnabled () {
return window.config.custom_domains_enabled
}
},
methods: { methods: {
switchWorkspace(workspace) { saveChanges () {
this.workspacesStore.setCurrentId(workspace.id) if (this.customDomainsLoading) return
this.$router.push({name: 'home'}) this.customDomainsLoading = true
this.formsStore.load(workspace.id) // Update the workspace custom domain
axios.put('/api/open/workspaces/' + this.workspace.id + '/custom-domains', {
custom_domains: this.customDomains.split('\n')
.map(domain => domain.trim())
.filter(domain => domain && domain.length > 0)
}).then((response) => {
this.workspacesStore.addOrUpdate(response.data)
this.alertSuccess('Custom domains saved.')
}).catch((error) => {
this.alertError('Failed to update custom domains: ' + error.response.data.message)
}).finally(() => {
this.customDomainsLoading = false
})
}, },
deleteWorkspace(workspace) { initCustomDomains () {
if (!this.workspace) return
this.customDomains = this.workspace.custom_domains.join('\n')
},
deleteWorkspace (workspace) {
if (this.workspaces.length <= 1) {
this.alertError('You cannot delete your only workspace.')
return
}
this.alertConfirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => { this.alertConfirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => {
this.workspacesStore.delete(workspace.id).then(() => { this.workspacesStore.delete(workspace.id).then(() => {
this.alertSuccess('Workspace successfully removed.') this.alertSuccess('Workspace successfully removed.')
}) })
}) })
}, },
isUrl(str) { isUrl (str) {
const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
@ -137,6 +213,12 @@ export default {
this.workspacesStore.load() this.workspacesStore.load()
this.workspaceModal = false this.workspaceModal = false
} }
},
watch: {
workspace () {
this.initCustomDomains()
}
} }
} }
</script> </script>

View File

@ -5,7 +5,7 @@ import { useAppStore } from '../stores/app'
import { defineComponent, nextTick } from 'vue' import { defineComponent, nextTick } from 'vue'
// The middleware for every page of the application. // The middleware for every page of the application.
const globalMiddleware = ['check-auth', 'notion-connection'] const globalMiddleware = ['locale', 'check-auth', 'custom-domains']
// Load middleware modules dynamically. // Load middleware modules dynamically.
const requireContext = import.meta.glob('../middleware/**/*.js', { eager: true }) const requireContext = import.meta.glob('../middleware/**/*.js', { eager: true })

View File

@ -57,6 +57,10 @@ export const useWorkspacesStore = defineStore('workspaces', {
}, },
remove (itemId) { remove (itemId) {
this.content = this.content.filter((val) => val.id !== itemId) this.content = this.content.filter((val) => val.id !== itemId)
if (this.currentId === itemId) {
this.currentId = this.content.length > 0 ? this.content[0].id : null
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
}
}, },
startLoading () { startLoading () {
this.loading = true this.loading = true

View File

@ -1,6 +1,7 @@
@php @php
$config = [ $config = [
'appName' => config('app.name'), 'appName' => config('app.name'),
'app_url' => config('app.url'),
'locale' => $locale = app()->getLocale(), 'locale' => $locale = app()->getLocale(),
'locales' => config('app.locales'), 'locales' => config('app.locales'),
'githubAuth' => config('services.github.client_id'), 'githubAuth' => config('services.github.client_id'),
@ -16,7 +17,8 @@
'crisp_website_id' => config('services.crisp_website_id'), 'crisp_website_id' => config('services.crisp_website_id'),
'ai_features_enabled' => !is_null(config('services.openai.api_key')), 'ai_features_enabled' => !is_null(config('services.openai.api_key')),
's3_enabled' => config('filesystems.default') === 's3', '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 @endphp
<!DOCTYPE html> <!DOCTYPE html>

View File

@ -69,6 +69,7 @@ Route::group(['middleware' => 'auth:api'], function () {
Route::get('/forms', Route::get('/forms',
[FormController::class, 'index'])->name('forms.index'); [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::delete('/', [WorkspaceController::class, 'delete'])->name('delete');
Route::get('form-stats/{formId}', [FormStatsController::class, 'getFormStats'])->name('form.stats'); Route::get('form-stats/{formId}', [FormStatsController::class, 'getFormStats'])->name('form.stats');

View File

@ -14,4 +14,4 @@ use Illuminate\Support\Facades\Route;
| |
*/ */
Route::get('{path}', SpaController::class)->where('path', '^(?!(api|stats|mailcoach|vapor|sitemap|dist)).*$'); Route::get('{path}', SpaController::class)->where('path', '^(?!(api|stats|mailcoach|vapor|sitemap|caddy|dist)).*$');

View File

@ -38,3 +38,6 @@ Route::get('local/temp/{path}', function (Request $request, string $path){
})->where('path', '(.*)')->name('local.temp'); })->where('path', '(.*)')->name('local.temp');
Route::get('/sitemap.xml', [\App\Http\Controllers\SitemapController::class, 'getSitemap'])->name('sitemap'); 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);

9
vite.config.js vendored
View File

@ -7,7 +7,8 @@ const plugins = [
laravel({ laravel({
input: [ input: [
'resources/js/app.js' 'resources/js/app.js'
] ],
refresh: true
}), }),
vue({ vue({
template: { template: {
@ -52,5 +53,11 @@ export default defineConfig({
'@': '/resources', '@': '/resources',
vue: '@vue/compat' vue: '@vue/compat'
} }
},
server: {
hmr: {
host: 'localhost',
protocol: 'ws'
}
} }
}) })