Zapier integration (#491)
* create zapier app * install sanctum * move OAuthProviderController * make `api-external` middleware * add zapier endpoints * add tests * token management * zapier event handler * add policy * use `slug` instead of `id` * wip * check policies * change api prefix to `external` * ui tweaks * validate token abilities * open zapier URL * zapier ui tweaks * update zap * Fix linting * Added sample endpoints + minor UI changes * Run PHP code linter --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
7ad62fb3ea
commit
517bccc695
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
enum AccessTokenAbility: string
|
||||||
|
{
|
||||||
|
case ManageIntegrations = 'manage-integrations';
|
||||||
|
case ListForms = 'list-forms';
|
||||||
|
case ListWorkspaces = 'list-workspaces';
|
||||||
|
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
fn (AccessTokenAbility $case) => $case->value,
|
||||||
|
static::cases()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function allowed(array $abilities): array
|
||||||
|
{
|
||||||
|
return Arr::where(
|
||||||
|
$abilities,
|
||||||
|
fn (string $ability) => in_array($ability, static::values())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,6 @@ class FormZapierWebhookController extends Controller
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
// $this->middleware('subscribed');
|
|
||||||
$this->middleware('auth');
|
$this->middleware('auth');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Integrations\Zapier;
|
||||||
|
|
||||||
|
use App\Http\Requests\Integration\Zapier\PollSubmissionRequest;
|
||||||
|
use App\Http\Requests\Zapier\CreateIntegrationRequest;
|
||||||
|
use App\Http\Requests\Zapier\DeleteIntegrationRequest;
|
||||||
|
use App\Integrations\Handlers\ZapierIntegration;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Tests\Helpers\FormSubmissionDataFactory;
|
||||||
|
|
||||||
|
class IntegrationController
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public function store(CreateIntegrationRequest $request)
|
||||||
|
{
|
||||||
|
$form = $request->form();
|
||||||
|
|
||||||
|
$this->authorize('view', $form);
|
||||||
|
|
||||||
|
$form->integrations()
|
||||||
|
->create([
|
||||||
|
'integration_id' => 'zapier',
|
||||||
|
'status' => 'active',
|
||||||
|
'data' => [
|
||||||
|
'hook_url' => $request->input('hookUrl'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return response()->json();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(DeleteIntegrationRequest $request)
|
||||||
|
{
|
||||||
|
$form = $request->form();
|
||||||
|
|
||||||
|
$this->authorize('view', $form);
|
||||||
|
|
||||||
|
$form
|
||||||
|
->integrations()
|
||||||
|
->where('data->hook_url', $request->input('hookUrl'))
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
return response()->json();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function poll(PollSubmissionRequest $request)
|
||||||
|
{
|
||||||
|
$form = $request->form();
|
||||||
|
|
||||||
|
$this->authorize('view', $form);
|
||||||
|
|
||||||
|
$lastSubmission = $form->submissions()->latest()->first();
|
||||||
|
if (!$lastSubmission) {
|
||||||
|
// Generate fake data when no previous submissions
|
||||||
|
$submissionData = (new FormSubmissionDataFactory($form))->asFormSubmissionData()->createSubmissionData();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ZapierIntegration::formatWebhookData($form, $submissionData ?? $lastSubmission->data)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Integrations\Zapier;
|
||||||
|
|
||||||
|
use App\Http\Requests\Zapier\ListFormsRequest;
|
||||||
|
use App\Http\Resources\Zapier\FormResource;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
|
||||||
|
class ListFormsController
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public function __invoke(ListFormsRequest $request)
|
||||||
|
{
|
||||||
|
$workspace = $request->workspace();
|
||||||
|
|
||||||
|
$this->authorize('view', $workspace);
|
||||||
|
|
||||||
|
return FormResource::collection(
|
||||||
|
$workspace->forms()->get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Integrations\Zapier;
|
||||||
|
|
||||||
|
use App\Http\Resources\Zapier\WorkspaceResource;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class ListWorkspacesController
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public function __invoke()
|
||||||
|
{
|
||||||
|
$this->authorize('viewAny', Workspace::class);
|
||||||
|
|
||||||
|
return WorkspaceResource::collection(
|
||||||
|
Auth::user()->workspaces()->get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Integrations\Zapier;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class ValidateAuthController
|
||||||
|
{
|
||||||
|
public function __invoke()
|
||||||
|
{
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $user->name,
|
||||||
|
'email' => $user->email,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Http\Controllers\OAuth;
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Resources\OAuthProviderResource;
|
use App\Http\Resources\OAuthProviderResource;
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Settings;
|
||||||
|
|
||||||
|
use App\Enums\AccessTokenAbility;
|
||||||
|
use App\Http\Requests\CreateTokenRequest;
|
||||||
|
use App\Http\Resources\TokenResource;
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Laravel\Sanctum\PersonalAccessToken;
|
||||||
|
|
||||||
|
class TokenController
|
||||||
|
{
|
||||||
|
use AuthorizesRequests;
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return TokenResource::collection(
|
||||||
|
Auth::user()->tokens()->get()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(CreateTokenRequest $request)
|
||||||
|
{
|
||||||
|
$token = Auth::user()->createToken(
|
||||||
|
$request->input('name'),
|
||||||
|
AccessTokenAbility::allowed($request->input('abilities'))
|
||||||
|
);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'token' => $token->plainTextToken,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(PersonalAccessToken $token)
|
||||||
|
{
|
||||||
|
$this->authorize('delete', $token);
|
||||||
|
|
||||||
|
$token->delete();
|
||||||
|
|
||||||
|
return response()->json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,6 +64,11 @@ class Kernel extends HttpKernel
|
||||||
SelfHostedCredentialsMiddleware::class,
|
SelfHostedCredentialsMiddleware::class,
|
||||||
ImpersonationMiddleware::class,
|
ImpersonationMiddleware::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'api-external' => [
|
||||||
|
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
|
||||||
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -90,5 +95,8 @@ class Kernel extends HttpKernel
|
||||||
|
|
||||||
'pro-form' => \App\Http\Middleware\Form\ProForm::class,
|
'pro-form' => \App\Http\Middleware\Form\ProForm::class,
|
||||||
'protected-form' => \App\Http\Middleware\Form\ProtectedForm::class,
|
'protected-form' => \App\Http\Middleware\Form\ProtectedForm::class,
|
||||||
|
|
||||||
|
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
|
||||||
|
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CreateTokenRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
],
|
||||||
|
'abilities' => [
|
||||||
|
'nullable',
|
||||||
|
'array'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Integration\Zapier;
|
||||||
|
|
||||||
|
use App\Models\Forms\Form;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class PollSubmissionRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'form_id' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists(Form::getModel()->getTable(), 'slug'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(): Form
|
||||||
|
{
|
||||||
|
return Form::query()
|
||||||
|
->where('slug', $this->input('form_id'))
|
||||||
|
->firstOrFail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Zapier;
|
||||||
|
|
||||||
|
use App\Models\Forms\Form;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class CreateIntegrationRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'form_id' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists(Form::getModel()->getTable(), 'slug'),
|
||||||
|
],
|
||||||
|
'hookUrl' => [
|
||||||
|
'required',
|
||||||
|
'url',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(): Form
|
||||||
|
{
|
||||||
|
return Form::query()
|
||||||
|
->where('slug', $this->input('form_id'))
|
||||||
|
->firstOrFail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Zapier;
|
||||||
|
|
||||||
|
use App\Models\Forms\Form;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class DeleteIntegrationRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'form_id' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists(Form::getModel()->getTable(), 'slug'),
|
||||||
|
],
|
||||||
|
'hookUrl' => [
|
||||||
|
'required',
|
||||||
|
'url',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(): Form
|
||||||
|
{
|
||||||
|
return Form::query()
|
||||||
|
->where('slug', $this->input('form_id'))
|
||||||
|
->firstOrFail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Zapier;
|
||||||
|
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ListFormsRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'workspace_id' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists(Workspace::getModel()->getTable(), 'id'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function workspace(): Workspace
|
||||||
|
{
|
||||||
|
return Workspace::findOrFail($this->input('workspace_id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property \Laravel\Sanctum\PersonalAccessToken $resource
|
||||||
|
*/
|
||||||
|
class TokenResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->resource->id,
|
||||||
|
'name' => $this->resource->name,
|
||||||
|
'abilities' => $this->resource->abilities,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Zapier;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property \App\Models\Forms\Form $resource
|
||||||
|
*/
|
||||||
|
class FormResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->resource->slug,
|
||||||
|
'name' => $this->resource->title,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Zapier;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property \App\Models\Workspace $resource
|
||||||
|
*/
|
||||||
|
class WorkspaceResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->resource->id,
|
||||||
|
'name' => $this->resource->name,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Integrations\Handlers;
|
||||||
|
|
||||||
use App\Models\Integration\FormIntegration;
|
use App\Models\Integration\FormIntegration;
|
||||||
use App\Events\Forms\FormSubmitted;
|
use App\Events\Forms\FormSubmitted;
|
||||||
|
use App\Models\Forms\Form;
|
||||||
use App\Models\Integration\FormIntegrationsEvent;
|
use App\Models\Integration\FormIntegrationsEvent;
|
||||||
use App\Service\Forms\FormSubmissionFormatter;
|
use App\Service\Forms\FormSubmissionFormatter;
|
||||||
use App\Service\Forms\FormLogicConditionChecker;
|
use App\Service\Forms\FormLogicConditionChecker;
|
||||||
|
|
@ -34,7 +35,7 @@ abstract class AbstractIntegrationHandler
|
||||||
|
|
||||||
protected function logicConditionsMet(): bool
|
protected function logicConditionsMet(): bool
|
||||||
{
|
{
|
||||||
if (!$this->formIntegration->logic) {
|
if (!$this->formIntegration->logic || empty((array) $this->formIntegration->logic)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return FormLogicConditionChecker::conditionsMet(
|
return FormLogicConditionChecker::conditionsMet(
|
||||||
|
|
@ -58,27 +59,7 @@ abstract class AbstractIntegrationHandler
|
||||||
*/
|
*/
|
||||||
protected function getWebhookData(): array
|
protected function getWebhookData(): array
|
||||||
{
|
{
|
||||||
$formatter = (new FormSubmissionFormatter($this->form, $this->submissionData))
|
return self::formatWebhookData($this->form, $this->submissionData);
|
||||||
->useSignedUrlForFiles()
|
|
||||||
->showHiddenFields();
|
|
||||||
|
|
||||||
$formattedData = [];
|
|
||||||
foreach ($formatter->getFieldsWithValue() as $field) {
|
|
||||||
$formattedData[$field['name']] = $field['value'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = [
|
|
||||||
'form_title' => $this->form->title,
|
|
||||||
'form_slug' => $this->form->slug,
|
|
||||||
'submission' => $formattedData,
|
|
||||||
];
|
|
||||||
if ($this->form->is_pro && $this->form->editable_submissions) {
|
|
||||||
$data['edit_link'] = $this->form->share_url . '?submission_id=' . Hashids::encode(
|
|
||||||
$this->submissionData['submission_id']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final public function run(): void
|
final public function run(): void
|
||||||
|
|
@ -125,8 +106,40 @@ abstract class AbstractIntegrationHandler
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function formatData(array $data): array
|
public static function formatWebhookData(Form $form, array $submissionData): array
|
||||||
{
|
{
|
||||||
|
$formatter = (new FormSubmissionFormatter($form, $submissionData))
|
||||||
|
->useSignedUrlForFiles()
|
||||||
|
->showHiddenFields();
|
||||||
|
|
||||||
|
// Old format - kept for retro-compatibility
|
||||||
|
$oldFormatData = [];
|
||||||
|
foreach ($formatter->getFieldsWithValue() as $field) {
|
||||||
|
$oldFormatData[$field['name']] = $field['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// New format using ID
|
||||||
|
$formattedData = [];
|
||||||
|
foreach ($formatter->getFieldsWithValue() as $field) {
|
||||||
|
$formattedData[$field['id']] = [
|
||||||
|
'value' => $field['value'],
|
||||||
|
'name' => $field['name'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'form_title' => $form->title,
|
||||||
|
'form_slug' => $form->slug,
|
||||||
|
'submission' => $oldFormatData,
|
||||||
|
'data' => $formattedData,
|
||||||
|
'message' => 'Please do not use the `submission` field. It is deprecated and will be removed in the future.'
|
||||||
|
];
|
||||||
|
if ($form->is_pro && $form->editable_submissions) {
|
||||||
|
$data['edit_link'] = $form->share_url . '?submission_id=' . Hashids::encode(
|
||||||
|
$submissionData['submission_id']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,4 +156,12 @@ abstract class AbstractIntegrationHandler
|
||||||
'message' => $e->getMessage()
|
'message' => $e->getMessage()
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in FormIntegrationRequest to format integration
|
||||||
|
*/
|
||||||
|
public static function formatData(array $data): array
|
||||||
|
{
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Integrations\Handlers;
|
||||||
|
|
||||||
|
use App\Events\Forms\FormSubmitted;
|
||||||
|
use App\Models\Integration\FormIntegration;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class ZapierIntegration extends AbstractIntegrationHandler
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected FormSubmitted $event,
|
||||||
|
protected FormIntegration $formIntegration,
|
||||||
|
protected array $integration
|
||||||
|
) {
|
||||||
|
parent::__construct($event, $formIntegration, $integration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getValidationRules(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isOAuthRequired(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getWebhookUrl(): string
|
||||||
|
{
|
||||||
|
if (!isset($this->integrationData->hook_url)) {
|
||||||
|
throw new Exception('The webhook URL is missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->integrationData->hook_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function shouldRun(): bool
|
||||||
|
{
|
||||||
|
return parent::shouldRun() && $this->getWebhookUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Laravel\Cashier\Billable;
|
use Laravel\Cashier\Billable;
|
||||||
|
use Laravel\Sanctum\HasApiTokens;
|
||||||
use Tymon\JWTAuth\Contracts\JWTSubject;
|
use Tymon\JWTAuth\Contracts\JWTSubject;
|
||||||
|
|
||||||
class User extends Authenticatable implements JWTSubject
|
class User extends Authenticatable implements JWTSubject
|
||||||
|
|
@ -16,6 +17,7 @@ class User extends Authenticatable implements JWTSubject
|
||||||
use Billable;
|
use Billable;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
use Notifiable;
|
use Notifiable;
|
||||||
|
use HasApiTokens;
|
||||||
|
|
||||||
public const ROLE_ADMIN = 'admin';
|
public const ROLE_ADMIN = 'admin';
|
||||||
public const ROLE_USER = 'user';
|
public const ROLE_USER = 'user';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Laravel\Sanctum\PersonalAccessToken;
|
||||||
|
|
||||||
|
class PersonalAccessTokenPolicy
|
||||||
|
{
|
||||||
|
public function delete(User $user, PersonalAccessToken $token)
|
||||||
|
{
|
||||||
|
return $token->tokenable()->is($user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,9 +10,11 @@ use App\Models\Workspace;
|
||||||
use App\Policies\FormPolicy;
|
use App\Policies\FormPolicy;
|
||||||
use App\Policies\Integration\FormZapierWebhookPolicy;
|
use App\Policies\Integration\FormZapierWebhookPolicy;
|
||||||
use App\Policies\OAuthProviderPolicy;
|
use App\Policies\OAuthProviderPolicy;
|
||||||
|
use App\Policies\PersonalAccessTokenPolicy;
|
||||||
use App\Policies\TemplatePolicy;
|
use App\Policies\TemplatePolicy;
|
||||||
use App\Policies\WorkspacePolicy;
|
use App\Policies\WorkspacePolicy;
|
||||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||||
|
use Laravel\Sanctum\PersonalAccessToken;
|
||||||
|
|
||||||
class AuthServiceProvider extends ServiceProvider
|
class AuthServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
|
@ -27,6 +29,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||||
FormZapierWebhook::class => FormZapierWebhookPolicy::class,
|
FormZapierWebhook::class => FormZapierWebhookPolicy::class,
|
||||||
Template::class => TemplatePolicy::class,
|
Template::class => TemplatePolicy::class,
|
||||||
OAuthProvider::class => OAuthProviderPolicy::class,
|
OAuthProvider::class => OAuthProviderPolicy::class,
|
||||||
|
PersonalAccessToken::class => PersonalAccessTokenPolicy::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ class RouteServiceProvider extends ServiceProvider
|
||||||
Route::middleware('api')
|
Route::middleware('api')
|
||||||
->namespace($this->namespace)
|
->namespace($this->namespace)
|
||||||
->group(base_path('routes/api.php'));
|
->group(base_path('routes/api.php'));
|
||||||
|
|
||||||
|
Route::middleware('api-external')
|
||||||
|
->namespace($this->namespace)
|
||||||
|
->group(base_path('routes/api-external.php'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
<v-checkbox
|
<v-checkbox
|
||||||
:id="id ? id : name"
|
:id="id ? id : name"
|
||||||
v-model="compVal"
|
v-model="compVal"
|
||||||
|
:value="value"
|
||||||
:disabled="disabled ? true : null"
|
:disabled="disabled ? true : null"
|
||||||
:name="name"
|
:name="name"
|
||||||
:color="color"
|
:color="color"
|
||||||
|
|
@ -48,6 +49,7 @@ export default {
|
||||||
components: { InputWrapper, VCheckbox },
|
components: { InputWrapper, VCheckbox },
|
||||||
props: {
|
props: {
|
||||||
...inputProps,
|
...inputProps,
|
||||||
|
value: { type: [Boolean, String, Number, Object], required: false },
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<input
|
<input
|
||||||
:id="id || name"
|
:id="id || name"
|
||||||
v-model="internalValue"
|
v-model="internalValue"
|
||||||
|
:value="value"
|
||||||
:name="name"
|
:name="name"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="rounded border-gray-500 w-10 h-10 cursor-pointer checkbox"
|
class="rounded border-gray-500 w-10 h-10 cursor-pointer checkbox"
|
||||||
|
|
@ -32,6 +33,7 @@ const props = defineProps({
|
||||||
id: { type: String, default: null },
|
id: { type: String, default: null },
|
||||||
name: { type: String, default: "checkbox" },
|
name: { type: String, default: "checkbox" },
|
||||||
modelValue: { type: [Boolean, String], default: false },
|
modelValue: { type: [Boolean, String], default: false },
|
||||||
|
value: { type: [Boolean, String, Number, Object], required: false },
|
||||||
disabled: { type: Boolean, default: false },
|
disabled: { type: Boolean, default: false },
|
||||||
theme: {
|
theme: {
|
||||||
type: Object, default: () => {
|
type: Object, default: () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap sm:flex-nowrap my-4 w-full">
|
<div class="flex flex-wrap sm:flex-nowrap my-4 w-full">
|
||||||
<div
|
<div
|
||||||
class="w-full sm:w-auto border border-gray-300 rounded-md p-2 flex-grow select-all bg-gray-100"
|
class="flex-1 truncate sm:w-auto border border-gray-300 rounded-md p-2 flex-grow select-all bg-gray-100"
|
||||||
>
|
>
|
||||||
<p class="select-all text-gray-900">
|
<p class="select-all text-gray-900">
|
||||||
{{ content }}
|
{{ content }}
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<v-button
|
<v-button
|
||||||
color="light-gray"
|
color="light-gray"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@click="copyToClipboard"
|
@click.prevent="copyToClipboard"
|
||||||
>
|
>
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
<svg
|
<svg
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@
|
||||||
</v-button>
|
</v-button>
|
||||||
</template>
|
</template>
|
||||||
<a
|
<a
|
||||||
|
v-if="integrationTypeInfo.is_editable !== false"
|
||||||
v-track.edit_form_integration_click="{
|
v-track.edit_form_integration_click="{
|
||||||
form_slug: form.slug,
|
form_slug: form.slug,
|
||||||
form_integration_id: integration.id,
|
form_integration_id: integration.id,
|
||||||
|
|
@ -106,6 +107,21 @@
|
||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
v-else-if="integrationTypeInfo.url"
|
||||||
|
v-track.edit_form_integration_click="{
|
||||||
|
form_slug: form.slug,
|
||||||
|
form_integration_id: integration.id,
|
||||||
|
}"
|
||||||
|
:href="integrationTypeInfo.url"
|
||||||
|
class="flex px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline hover:text-gray-900 items-center"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="heroicons:pencil"
|
||||||
|
class="w-5 h-5 mr-2"
|
||||||
|
/>
|
||||||
|
Edit on {{ integrationTypeInfo.name }}
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
v-track.past_events_form_integration_click="{
|
v-track.past_events_form_integration_click="{
|
||||||
form_slug: form.slug,
|
form_slug: form.slug,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<UTooltip
|
<UTooltip
|
||||||
:text="tooltipText"
|
:text="tooltipText"
|
||||||
:prevent="!unavailable"
|
:prevent="!unavailable || !tooltipText"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-track.new_integration_click="{ name: integration.id }"
|
v-track.new_integration_click="{ name: integration.id }"
|
||||||
|
|
@ -32,6 +32,11 @@
|
||||||
>
|
>
|
||||||
(coming soon)</span>
|
(coming soon)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<Icon
|
||||||
|
v-if="integration.is_external"
|
||||||
|
class="inline h-4 w-4 ml-1 inline text-gray-500"
|
||||||
|
name="heroicons:arrow-top-right-on-square-20-solid"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<pro-tag
|
<pro-tag
|
||||||
v-if="integration?.is_pro === true"
|
v-if="integration?.is_pro === true"
|
||||||
|
|
@ -42,8 +47,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(["select"])
|
import { computed } from 'vue'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
|
|
||||||
|
const emit = defineEmits(["select"])
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
integration: {
|
integration: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
@ -51,22 +58,25 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const workspacesStore = useWorkspacesStore()
|
||||||
|
const currentWorkspace = computed(() => workspacesStore.getCurrent)
|
||||||
|
|
||||||
const unavailable = computed(() => {
|
const unavailable = computed(() => {
|
||||||
return (
|
return (
|
||||||
props.integration.coming_soon || props.integration.requires_subscription
|
props.integration.coming_soon ||
|
||||||
|
(props.integration.requires_subscription && !currentWorkspace.value.is_pro)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const tooltipText = computed(() => {
|
const tooltipText = computed(() => {
|
||||||
if (props.integration.coming_soon) return "This integration is coming soon"
|
if (props.integration.coming_soon) return "This integration is coming soon"
|
||||||
if (props.integration.requires_subscription)
|
if (props.integration.requires_subscription && !currentWorkspace.value.is_pro )
|
||||||
return "You need a subscription to use this integration."
|
return "You need a subscription to use this integration."
|
||||||
return ""
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
if (props.integration.coming_soon || props.integration.requires_subscription)
|
if (unavailable.value) return
|
||||||
return
|
|
||||||
emit("select", props.integration.id)
|
emit("select", props.integration.id)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="text-gray-500 border shadow rounded-md p-5 mt-4 relative flex items-center"
|
||||||
|
>
|
||||||
|
<div class="flex-grow flex items-center">
|
||||||
|
<div>
|
||||||
|
<div class="flex space-x-3 font-semibold mr-2">
|
||||||
|
{{ token.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<span
|
||||||
|
v-for="(ability, index) in token.abilities"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
{{ accessTokenStore.getAbility(ability).title }}
|
||||||
|
<template v-if="index !== token.abilities.length - 1">
|
||||||
|
,
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<dropdown
|
||||||
|
class="inline"
|
||||||
|
>
|
||||||
|
<template #trigger="{ toggle }">
|
||||||
|
<v-button
|
||||||
|
color="white"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="heroicons:ellipsis-horizontal"
|
||||||
|
class="w-4 h-4 -mt-1"
|
||||||
|
/>
|
||||||
|
</v-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="flex px-4 py-2 text-md text-red-600 hover:bg-red-50 hover:no-underline items-center"
|
||||||
|
@click.prevent="destroy"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="heroicons:trash"
|
||||||
|
class="w-5 h-5 mr-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
Delete
|
||||||
|
</a>
|
||||||
|
</dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
token: Object
|
||||||
|
})
|
||||||
|
|
||||||
|
const accessTokenStore = useAccessTokenStore()
|
||||||
|
const alert = useAlert()
|
||||||
|
|
||||||
|
function destroy() {
|
||||||
|
alert.confirm("Do you really want to delete this token?", () => {
|
||||||
|
opnFetch(`/settings/tokens/${props.token.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
accessTokenStore.remove(props.token.id)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
try {
|
||||||
|
alert.error(error.data.message)
|
||||||
|
} catch (e) {
|
||||||
|
alert.error("An error occurred while disconnecting an account")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
:show="show"
|
||||||
|
max-width="lg"
|
||||||
|
@close="emit('close')"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<svg
|
||||||
|
class="w-8 h-8"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
Create an access token
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="px-4">
|
||||||
|
<form
|
||||||
|
@submit.prevent="createToken"
|
||||||
|
@keydown="form.onKeydown($event)"
|
||||||
|
>
|
||||||
|
<div v-if="!token">
|
||||||
|
<text-input
|
||||||
|
name="name"
|
||||||
|
class="mt-4"
|
||||||
|
:form="form"
|
||||||
|
:required="true"
|
||||||
|
label="Name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="input-label">Abilities</label>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<checkbox-input
|
||||||
|
v-for="ability in abilities"
|
||||||
|
:key="ability.name"
|
||||||
|
v-model="form.abilities"
|
||||||
|
:value="ability.name"
|
||||||
|
:label="ability.title"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="token"
|
||||||
|
icon="i-heroicons-key-20-solid"
|
||||||
|
color="green"
|
||||||
|
variant="subtle"
|
||||||
|
title="Copy your access token"
|
||||||
|
description="Your token will only be shown once. Make sure to save it safely."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<copy-content
|
||||||
|
v-if="token"
|
||||||
|
:content="token"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<Icon
|
||||||
|
name="heroicons:link"
|
||||||
|
class="w-4 h-4 -mt-1 text-blue-600 mr-3"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
Copy Token
|
||||||
|
</copy-content>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full mt-6">
|
||||||
|
<v-button
|
||||||
|
v-if="!token"
|
||||||
|
:loading="form.busy"
|
||||||
|
class="w-full my-3"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</v-button>
|
||||||
|
|
||||||
|
<v-button
|
||||||
|
v-else
|
||||||
|
class="w-full my-3"
|
||||||
|
@click.prevent="emit('close')"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</v-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import CopyContent from "../open/forms/components/CopyContent.vue"
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: Boolean
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close'])
|
||||||
|
|
||||||
|
const accessTokenStore = useAccessTokenStore()
|
||||||
|
const abilities = computed(() => accessTokenStore.abilities)
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
name: "",
|
||||||
|
abilities: abilities.value.map(ability => ability.name),
|
||||||
|
})
|
||||||
|
|
||||||
|
const token = ref('')
|
||||||
|
|
||||||
|
function createToken() {
|
||||||
|
form.post("/settings/tokens").then((data) => {
|
||||||
|
// workspacesStore.save(data.workspace)
|
||||||
|
// workspacesStore.currentId = data.workspace.id
|
||||||
|
// workspaceModal.value = false
|
||||||
|
|
||||||
|
token.value = data.token
|
||||||
|
accessTokenStore.fetchTokens()
|
||||||
|
|
||||||
|
useAlert().success(
|
||||||
|
"Access token successfully created!",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.show, () => {
|
||||||
|
form.name = ''
|
||||||
|
form.abilities = abilities.value.map(ability => ability.name),
|
||||||
|
token.value = ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -31,17 +31,19 @@
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"name": "Webhook Notification",
|
"name": "Webhook Notification",
|
||||||
"icon": "material-symbols:webhook",
|
"icon": "material-symbols:webhook",
|
||||||
"section_name": "Notifications",
|
"section_name": "Automation",
|
||||||
"file_name": "WebhookIntegration",
|
"file_name": "WebhookIntegration",
|
||||||
"is_pro": false
|
"is_pro": false
|
||||||
},
|
},
|
||||||
"zapier": {
|
"zapier": {
|
||||||
"name": "Zapier Integration",
|
"name": "Zapier",
|
||||||
"icon": "cib:zapier",
|
"icon": "cib:zapier",
|
||||||
"section_name": "Notifications",
|
"section_name": "Automation",
|
||||||
"file_name": "ZapierIntegration",
|
"file_name": "ZapierIntegration",
|
||||||
"is_pro": true,
|
"is_pro": false,
|
||||||
"coming_soon": true
|
"is_external": true,
|
||||||
|
"is_editable": false,
|
||||||
|
"url": "https://zapier.com/app/zaps"
|
||||||
},
|
},
|
||||||
"google_sheets": {
|
"google_sheets": {
|
||||||
"name": "Google Sheets",
|
"name": "Google Sheets",
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
v-for="(sectionItem, sectionItemKey) in section"
|
v-for="(sectionItem, sectionItemKey) in section"
|
||||||
:key="sectionItemKey"
|
:key="sectionItemKey"
|
||||||
:integration="sectionItem"
|
:integration="sectionItem"
|
||||||
@select="openIntegrationModal"
|
@select="openIntegration"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -107,11 +107,22 @@ onMounted(() => {
|
||||||
oAuthProvidersStore.fetchOAuthProviders(props.form.workspace_id)
|
oAuthProvidersStore.fetchOAuthProviders(props.form.workspace_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const openIntegrationModal = (itemKey) => {
|
const openIntegration = (itemKey) => {
|
||||||
if (!itemKey || !integrations.value.has(itemKey))
|
if (!itemKey || !integrations.value.has(itemKey)) {
|
||||||
return alert.error("Integration not found")
|
return alert.error("Integration not found")
|
||||||
if (integrations.value.get(itemKey).coming_soon)
|
}
|
||||||
|
|
||||||
|
const integration = integrations.value.get(itemKey)
|
||||||
|
|
||||||
|
if (integration.coming_soon) {
|
||||||
return alert.warning("This integration is not available yet")
|
return alert.warning("This integration is not available yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(integration.is_external && integration.url) {
|
||||||
|
window.open(integration.url, '_blank')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
selectedIntegrationKey.value = itemKey
|
selectedIntegrationKey.value = itemKey
|
||||||
selectedIntegration.value = integrations.value.get(
|
selectedIntegration.value = integrations.value.get(
|
||||||
selectedIntegrationKey.value,
|
selectedIntegrationKey.value,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,10 @@ const tabsList = computed(() => {
|
||||||
name: "Workspace Settings",
|
name: "Workspace Settings",
|
||||||
route: "settings-workspace",
|
route: "settings-workspace",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Access Tokens",
|
||||||
|
route: "settings-access-tokens",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Connections",
|
name: "Connections",
|
||||||
route: "settings-connections",
|
route: "settings-connections",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-y-4 flex-wrap-reverse">
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="font-semibold text-2xl text-gray-900">
|
||||||
|
Access Tokens
|
||||||
|
</h3>
|
||||||
|
<small class="text-gray-600">Manage your access tokens keys.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
label="Create new token"
|
||||||
|
icon="i-heroicons-plus"
|
||||||
|
:loading="loading"
|
||||||
|
@click="accessTokenModal = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="loading"
|
||||||
|
class="w-full text-blue-500 text-center"
|
||||||
|
>
|
||||||
|
<Loader class="h-10 w-10 p-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="py-6">
|
||||||
|
<SettingsAccessTokenCard
|
||||||
|
v-for="token in tokens"
|
||||||
|
:key="token.id"
|
||||||
|
:token="token"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsAccessTokenModal
|
||||||
|
:show="accessTokenModal"
|
||||||
|
@close="accessTokenModal = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
useOpnSeoMeta({
|
||||||
|
title: "Access Tokens",
|
||||||
|
})
|
||||||
|
|
||||||
|
const accessTokenModal = ref(false)
|
||||||
|
const accessTokenStore = useAccessTokenStore()
|
||||||
|
const tokens = computed(() => accessTokenStore.getAll)
|
||||||
|
const loading = computed(() => accessTokenStore.loading)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
accessTokenStore.fetchTokens()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { defineStore } from "pinia"
|
||||||
|
import { useContentStore } from "~/composables/stores/useContentStore.js"
|
||||||
|
|
||||||
|
export const useAccessTokenStore = defineStore("access_tokens", () => {
|
||||||
|
const contentStore = useContentStore()
|
||||||
|
|
||||||
|
const abilities = [
|
||||||
|
{
|
||||||
|
title: 'Manage integrations',
|
||||||
|
name: 'manage-integrations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'List forms',
|
||||||
|
name: 'list-forms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'List workspaces',
|
||||||
|
name: 'list-workspaces',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const fetchTokens = () => {
|
||||||
|
contentStore.resetState()
|
||||||
|
contentStore.startLoading()
|
||||||
|
|
||||||
|
return opnFetch('/settings/tokens').then(
|
||||||
|
(data) => {
|
||||||
|
contentStore.save(data)
|
||||||
|
contentStore.stopLoading()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = computed(() => contentStore.getAll.value)
|
||||||
|
|
||||||
|
const getAbility = (name) => {
|
||||||
|
return abilities.find(ability => ability.name == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...contentStore,
|
||||||
|
fetchTokens,
|
||||||
|
tokens,
|
||||||
|
abilities,
|
||||||
|
getAbility
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
"laravel/cashier": "*",
|
"laravel/cashier": "*",
|
||||||
"laravel/framework": "^11.9",
|
"laravel/framework": "^11.9",
|
||||||
"laravel/horizon": "*",
|
"laravel/horizon": "*",
|
||||||
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/socialite": "*",
|
"laravel/socialite": "*",
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9",
|
||||||
"laravel/ui": "*",
|
"laravel/ui": "*",
|
||||||
|
|
@ -48,6 +49,7 @@
|
||||||
"mockery/mockery": "^1.6",
|
"mockery/mockery": "^1.6",
|
||||||
"nunomaduro/collision": "*",
|
"nunomaduro/collision": "*",
|
||||||
"pestphp/pest": "^2.0",
|
"pestphp/pest": "^2.0",
|
||||||
|
"pestphp/pest-plugin-laravel": "^2.4",
|
||||||
"spatie/laravel-ignition": "*",
|
"spatie/laravel-ignition": "*",
|
||||||
"spatie/laravel-ray": "*"
|
"spatie/laravel-ray": "*"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "d0db593e303db3e95ccf31168c9b66c7",
|
"content-hash": "b30e4ad9df3f7463d73bf25b757fd616",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "amphp/amp",
|
"name": "amphp/amp",
|
||||||
|
|
@ -3501,6 +3501,70 @@
|
||||||
},
|
},
|
||||||
"time": "2024-06-17T13:58:22+00:00"
|
"time": "2024-06-17T13:58:22+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "laravel/sanctum",
|
||||||
|
"version": "v4.0.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/laravel/sanctum.git",
|
||||||
|
"reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/laravel/sanctum/zipball/9cfc0ce80cabad5334efff73ec856339e8ec1ac1",
|
||||||
|
"reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-json": "*",
|
||||||
|
"illuminate/console": "^11.0",
|
||||||
|
"illuminate/contracts": "^11.0",
|
||||||
|
"illuminate/database": "^11.0",
|
||||||
|
"illuminate/support": "^11.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"symfony/console": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"orchestra/testbench": "^9.0",
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"phpunit/phpunit": "^10.5"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Laravel\\Sanctum\\SanctumServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Laravel\\Sanctum\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Taylor Otwell",
|
||||||
|
"email": "taylor@laravel.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
|
||||||
|
"keywords": [
|
||||||
|
"auth",
|
||||||
|
"laravel",
|
||||||
|
"sanctum"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/laravel/sanctum/issues",
|
||||||
|
"source": "https://github.com/laravel/sanctum"
|
||||||
|
},
|
||||||
|
"time": "2024-04-10T19:39:58+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
"version": "v1.3.3",
|
"version": "v1.3.3",
|
||||||
|
|
@ -12951,6 +13015,80 @@
|
||||||
],
|
],
|
||||||
"time": "2024-01-26T09:46:42+00:00"
|
"time": "2024-01-26T09:46:42+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "pestphp/pest-plugin-laravel",
|
||||||
|
"version": "v2.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/pestphp/pest-plugin-laravel.git",
|
||||||
|
"reference": "53df51169a7f9595e06839cce638c73e59ace5e8"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/53df51169a7f9595e06839cce638c73e59ace5e8",
|
||||||
|
"reference": "53df51169a7f9595e06839cce638c73e59ace5e8",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"laravel/framework": "^10.48.9|^11.5.0",
|
||||||
|
"pestphp/pest": "^2.34.7",
|
||||||
|
"php": "^8.1.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/dusk": "^7.13.0",
|
||||||
|
"orchestra/testbench": "^8.22.3|^9.0.4",
|
||||||
|
"pestphp/pest-dev-tools": "^2.16.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Pest\\Laravel\\PestServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pest": {
|
||||||
|
"plugins": [
|
||||||
|
"Pest\\Laravel\\Plugin"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/Autoload.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Pest\\Laravel\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "The Pest Laravel Plugin",
|
||||||
|
"keywords": [
|
||||||
|
"framework",
|
||||||
|
"laravel",
|
||||||
|
"pest",
|
||||||
|
"php",
|
||||||
|
"test",
|
||||||
|
"testing",
|
||||||
|
"unit"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/pestphp/pest-plugin-laravel/tree/v2.4.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://www.paypal.com/paypalme/enunomaduro",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nunomaduro",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-04-27T10:41:54+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phar-io/manifest",
|
"name": "phar-io/manifest",
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stateful Domains
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Requests from the following domains / hosts will receive stateful API
|
||||||
|
| authentication cookies. Typically, these should include your local
|
||||||
|
| and production domains which access your API via a frontend SPA.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||||
|
'%s%s',
|
||||||
|
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||||
|
Sanctum::currentApplicationUrlWithPort()
|
||||||
|
))),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This array contains the authentication guards that will be checked when
|
||||||
|
| Sanctum is trying to authenticate a request. If none of these guards
|
||||||
|
| are able to authenticate the request, Sanctum will use the bearer
|
||||||
|
| token that's present on an incoming request for authentication.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'guard' => ['web'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Expiration Minutes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value controls the number of minutes until an issued token will be
|
||||||
|
| considered expired. This will override any values set in the token's
|
||||||
|
| "expires_at" attribute, but first-party sessions are not affected.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Token Prefix
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||||
|
| security scanning initiatives maintained by open source platforms
|
||||||
|
| that notify developers if they commit tokens into repositories.
|
||||||
|
|
|
||||||
|
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Sanctum Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When authenticating your first-party SPA with Sanctum you may need to
|
||||||
|
| customize some of the middleware Sanctum uses while processing the
|
||||||
|
| request. You may change the middleware listed below as required.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'middleware' => [
|
||||||
|
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||||
|
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
|
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('personal_access_tokens', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->morphs('tokenable');
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('token', 64)->unique();
|
||||||
|
$table->text('abilities')->nullable();
|
||||||
|
$table->timestamp('last_used_at')->nullable();
|
||||||
|
$table->timestamp('expires_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('personal_access_tokens');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# environment variables file
|
||||||
|
.env
|
||||||
|
.environment
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# zapier app config
|
||||||
|
.zapierapprc
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Zapier
|
||||||
|
|
||||||
|
Install Zapier
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install -g zapier-platform-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Install dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
cd `zapier`
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Login to Zapier
|
||||||
|
|
||||||
|
```
|
||||||
|
zapier login
|
||||||
|
```
|
||||||
|
|
||||||
|
Register the app
|
||||||
|
|
||||||
|
```
|
||||||
|
zapier register [TITLE]
|
||||||
|
```
|
||||||
|
|
||||||
|
Publish the app
|
||||||
|
|
||||||
|
```
|
||||||
|
zapier push
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the base URL to receive webhooks from Zapier. The version usually looks like 1.0.0.
|
||||||
|
|
||||||
|
```
|
||||||
|
zapier env:set [VERSION] BASE_URL=[BASE_URL]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Create an access token: http://localhost:3000/settings/access-tokens
|
||||||
|
- Create a Zap
|
||||||
|
- Authenticate using your token
|
||||||
|
- Submit a form
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
module.exports = {
|
||||||
|
type: 'custom',
|
||||||
|
test: {
|
||||||
|
removeMissingValuesFrom: { body: false, params: false },
|
||||||
|
url: '{{process.env.BASE_URL}}/external/zapier/validate',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
helpText:
|
||||||
|
'Enter your API key, located at https://opnform.com/settings/access-tokens',
|
||||||
|
computed: false,
|
||||||
|
key: 'api_key',
|
||||||
|
required: true,
|
||||||
|
label: 'API Key',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connectionLabel: '{{bundle.inputData.name}} {{bundle.inputData.email}}',
|
||||||
|
customConfig: {},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
const authentication = require('./authentication');
|
||||||
|
const newSubmissionTrigger = require('./triggers/new_submission.js');
|
||||||
|
const listWorkspacesTrigger = require('./triggers/list_workspaces.js');
|
||||||
|
const listFormsTrigger = require('./triggers/list_forms.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
version: require('./package.json').version,
|
||||||
|
platformVersion: require('zapier-platform-core').version,
|
||||||
|
requestTemplate: {
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer {{bundle.authData.api_key}}',
|
||||||
|
'X-API-KEY': '{{bundle.authData.api_key}}',
|
||||||
|
},
|
||||||
|
params: { api_key: '{{bundle.authData.api_key}}' },
|
||||||
|
body: {},
|
||||||
|
},
|
||||||
|
authentication: authentication,
|
||||||
|
searches: {},
|
||||||
|
triggers: {
|
||||||
|
[newSubmissionTrigger.key]: newSubmissionTrigger,
|
||||||
|
[listWorkspacesTrigger.key]: listWorkspacesTrigger,
|
||||||
|
[listFormsTrigger.key]: listFormsTrigger,
|
||||||
|
},
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "opnform-test-1",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "opnform-test-1",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest --testTimeout 10000"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v18",
|
||||||
|
"npm": ">=5.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zapier-platform-core": "15.8.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^29.6.0"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"zapier": {
|
||||||
|
"convertedByCLIVersion": "15.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
const zapier = require('zapier-platform-core');
|
||||||
|
|
||||||
|
// Use this to make test calls into your app:
|
||||||
|
const App = require('../../index');
|
||||||
|
const appTester = zapier.createAppTester(App);
|
||||||
|
// read the `.env` file into the environment, if available
|
||||||
|
zapier.tools.env.inject();
|
||||||
|
|
||||||
|
describe('triggers.list_forms', () => {
|
||||||
|
it('should run', async () => {
|
||||||
|
const bundle = { inputData: {} };
|
||||||
|
|
||||||
|
const results = await appTester(
|
||||||
|
App.triggers['list_forms'].operation.perform,
|
||||||
|
bundle
|
||||||
|
);
|
||||||
|
expect(results).toBeDefined();
|
||||||
|
// TODO: add more assertions
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
const zapier = require('zapier-platform-core');
|
||||||
|
|
||||||
|
// Use this to make test calls into your app:
|
||||||
|
const App = require('../../index');
|
||||||
|
const appTester = zapier.createAppTester(App);
|
||||||
|
// read the `.env` file into the environment, if available
|
||||||
|
zapier.tools.env.inject();
|
||||||
|
|
||||||
|
describe('triggers.list_workspaces', () => {
|
||||||
|
it('should run', async () => {
|
||||||
|
const bundle = { inputData: {} };
|
||||||
|
|
||||||
|
const results = await appTester(
|
||||||
|
App.triggers['list_workspaces'].operation.perform,
|
||||||
|
bundle
|
||||||
|
);
|
||||||
|
expect(results).toBeDefined();
|
||||||
|
// TODO: add more assertions
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
const zapier = require('zapier-platform-core');
|
||||||
|
|
||||||
|
// Use this to make test calls into your app:
|
||||||
|
const App = require('../../index');
|
||||||
|
const appTester = zapier.createAppTester(App);
|
||||||
|
// read the `.env` file into the environment, if available
|
||||||
|
zapier.tools.env.inject();
|
||||||
|
|
||||||
|
describe('triggers.new_submission', () => {
|
||||||
|
it('should run', async () => {
|
||||||
|
const bundle = { inputData: {} };
|
||||||
|
|
||||||
|
const results = await appTester(
|
||||||
|
App.triggers['new_submission'].operation.perform,
|
||||||
|
bundle
|
||||||
|
);
|
||||||
|
expect(results).toBeDefined();
|
||||||
|
// TODO: add more assertions
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
module.exports = {
|
||||||
|
operation: {
|
||||||
|
perform: {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
params: {
|
||||||
|
api_key: '{{bundle.authData.api_key}}',
|
||||||
|
workspace_id: '{{bundle.inputData.workspace_id}}',
|
||||||
|
},
|
||||||
|
removeMissingValuesFrom: { body: false, params: false },
|
||||||
|
url: '{{process.env.BASE_URL}}/external/zapier/forms',
|
||||||
|
},
|
||||||
|
inputFields: [
|
||||||
|
{
|
||||||
|
key: 'workspace_id',
|
||||||
|
type: 'string',
|
||||||
|
dynamic: 'list_workspaces.id.name',
|
||||||
|
label: 'Workspace',
|
||||||
|
required: true,
|
||||||
|
list: false,
|
||||||
|
altersDynamicFields: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sample: { id: 'my-form', name: 'My Form' },
|
||||||
|
outputFields: [
|
||||||
|
{ key: 'id', label: 'ID', type: 'string' },
|
||||||
|
{ key: 'name', label: 'Name', type: 'string' },
|
||||||
|
],
|
||||||
|
canPaginate: false,
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
description: 'Get the list of all forms',
|
||||||
|
hidden: true,
|
||||||
|
label: 'List Forms',
|
||||||
|
},
|
||||||
|
key: 'list_forms',
|
||||||
|
noun: 'Form',
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
module.exports = {
|
||||||
|
operation: {
|
||||||
|
perform: {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
removeMissingValuesFrom: { body: false, params: false },
|
||||||
|
url: '{{process.env.BASE_URL}}/external/zapier/workspaces',
|
||||||
|
},
|
||||||
|
sample: { id: 1, name: 'My Workspace' },
|
||||||
|
outputFields: [
|
||||||
|
{ key: 'id', label: 'ID', type: 'integer' },
|
||||||
|
{ key: 'name', label: 'Name', type: 'string' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
description: "Get the list of all user's workspaces",
|
||||||
|
hidden: true,
|
||||||
|
label: 'List Workspaces',
|
||||||
|
},
|
||||||
|
key: 'list_workspaces',
|
||||||
|
noun: 'Workspace',
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
const perform = async (z, bundle) => {
|
||||||
|
return [bundle.cleanedRequest];
|
||||||
|
};
|
||||||
|
|
||||||
|
const performList = async (z, bundle) => {
|
||||||
|
// Replace with the actual URL that returns recent submissions
|
||||||
|
const response = await z.request({
|
||||||
|
url: `${process.env.BASE_URL}/external/zapier/submissions/recent`,
|
||||||
|
params: {
|
||||||
|
form_id: bundle.inputData.form_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure the structure of the response matches the webhook data structure
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
operation: {
|
||||||
|
perform: perform,
|
||||||
|
performList: performList,
|
||||||
|
sample: {
|
||||||
|
"form_title": "Your form title",
|
||||||
|
"form_slug": "your-form-slug-og4lhg"
|
||||||
|
},
|
||||||
|
inputFields: [
|
||||||
|
{
|
||||||
|
key: 'workspace_id',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Workspace',
|
||||||
|
dynamic: 'list_workspaces.id.name',
|
||||||
|
required: true,
|
||||||
|
list: false,
|
||||||
|
altersDynamicFields: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'form_id',
|
||||||
|
type: 'string',
|
||||||
|
label: 'Form',
|
||||||
|
dynamic: 'list_forms.id.name',
|
||||||
|
required: true,
|
||||||
|
list: false,
|
||||||
|
altersDynamicFields: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'hook',
|
||||||
|
performUnsubscribe: {
|
||||||
|
body: {
|
||||||
|
hookUrl: '{{bundle.subscribeData.id}}',
|
||||||
|
form_id: '{{bundle.inputData.form_id}}',
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
method: 'DELETE',
|
||||||
|
removeMissingValuesFrom: { body: false, params: false },
|
||||||
|
url: '{{process.env.BASE_URL}}/external/zapier/webhook',
|
||||||
|
},
|
||||||
|
performSubscribe: {
|
||||||
|
body: {
|
||||||
|
hookUrl: '{{bundle.targetUrl}}',
|
||||||
|
form_id: '{{bundle.inputData.form_id}}',
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
removeMissingValuesFrom: { body: false, params: false },
|
||||||
|
url: '{{process.env.BASE_URL}}/external/zapier/webhook',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
description: 'Triggers when a new submission is created.',
|
||||||
|
hidden: false,
|
||||||
|
label: 'New Submission',
|
||||||
|
},
|
||||||
|
key: 'new_submission',
|
||||||
|
noun: 'Submission',
|
||||||
|
};
|
||||||
|
|
@ -31,17 +31,19 @@
|
||||||
"webhook": {
|
"webhook": {
|
||||||
"name": "Webhook Notification",
|
"name": "Webhook Notification",
|
||||||
"icon": "material-symbols:webhook",
|
"icon": "material-symbols:webhook",
|
||||||
"section_name": "Notifications",
|
"section_name": "Automation",
|
||||||
"file_name": "WebhookIntegration",
|
"file_name": "WebhookIntegration",
|
||||||
"is_pro": false
|
"is_pro": false
|
||||||
},
|
},
|
||||||
"zapier": {
|
"zapier": {
|
||||||
"name": "Zapier Integration",
|
"name": "Zapier",
|
||||||
"icon": "cib:zapier",
|
"icon": "cib:zapier",
|
||||||
"section_name": "Notifications",
|
"section_name": "Automation",
|
||||||
"file_name": "ZapierIntegration",
|
"file_name": "ZapierIntegration",
|
||||||
"is_pro": true,
|
"is_pro": false,
|
||||||
"coming_soon": true
|
"is_external": true,
|
||||||
|
"is_editable": false,
|
||||||
|
"url": "https://zapier.com/app/zaps"
|
||||||
},
|
},
|
||||||
"google_sheets": {
|
"google_sheets": {
|
||||||
"name": "Google Sheets",
|
"name": "Google Sheets",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External API calls
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Http\Controllers\Integrations\Zapier;
|
||||||
|
use App\Http\Controllers\Integrations\Zapier\ListFormsController;
|
||||||
|
use App\Http\Controllers\Integrations\Zapier\ListWorkspacesController;
|
||||||
|
|
||||||
|
Route::prefix('external')
|
||||||
|
->middleware('auth:sanctum')
|
||||||
|
->group(function () {
|
||||||
|
Route::prefix('zapier')->name('zapier.')->group(function () {
|
||||||
|
Route::get('validate', Zapier\ValidateAuthController::class)
|
||||||
|
->name('validate');
|
||||||
|
|
||||||
|
// Set and delete webhooks / manage integrations
|
||||||
|
Route::middleware('ability:manage-integrations')
|
||||||
|
->name('webhooks.')
|
||||||
|
->group(function () {
|
||||||
|
Route::post('webhook', [Zapier\IntegrationController::class, 'store'])
|
||||||
|
->name('store');
|
||||||
|
|
||||||
|
Route::delete('webhook', [Zapier\IntegrationController::class, 'destroy'])
|
||||||
|
->name('destroy');
|
||||||
|
Route::get('submissions/recent', [Zapier\IntegrationController::class, 'poll'])->name('poll');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::get('workspaces', ListWorkspacesController::class)
|
||||||
|
->middleware('ability:list-workspaces')
|
||||||
|
->name('workspaces');
|
||||||
|
|
||||||
|
Route::get('forms', ListFormsController::class)
|
||||||
|
->middleware('ability:list-forms')
|
||||||
|
->name('forms');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -15,9 +15,10 @@ use App\Http\Controllers\Forms\Integration\FormIntegrationsEventController;
|
||||||
use App\Http\Controllers\Forms\Integration\FormZapierWebhookController;
|
use App\Http\Controllers\Forms\Integration\FormZapierWebhookController;
|
||||||
use App\Http\Controllers\Forms\PublicFormController;
|
use App\Http\Controllers\Forms\PublicFormController;
|
||||||
use App\Http\Controllers\Forms\RecordController;
|
use App\Http\Controllers\Forms\RecordController;
|
||||||
use App\Http\Controllers\OAuth\OAuthProviderController;
|
use App\Http\Controllers\Settings\OAuthProviderController;
|
||||||
use App\Http\Controllers\Settings\PasswordController;
|
use App\Http\Controllers\Settings\PasswordController;
|
||||||
use App\Http\Controllers\Settings\ProfileController;
|
use App\Http\Controllers\Settings\ProfileController;
|
||||||
|
use App\Http\Controllers\Settings\TokenController;
|
||||||
use App\Http\Controllers\SubscriptionController;
|
use App\Http\Controllers\SubscriptionController;
|
||||||
use App\Http\Controllers\TemplateController;
|
use App\Http\Controllers\TemplateController;
|
||||||
use App\Http\Controllers\UserInviteController;
|
use App\Http\Controllers\UserInviteController;
|
||||||
|
|
@ -51,6 +52,12 @@ Route::group(['middleware' => 'auth:api'], function () {
|
||||||
Route::patch('/profile', [ProfileController::class, 'update']);
|
Route::patch('/profile', [ProfileController::class, 'update']);
|
||||||
Route::patch('/password', [PasswordController::class, 'update']);
|
Route::patch('/password', [PasswordController::class, 'update']);
|
||||||
|
|
||||||
|
Route::prefix('/tokens')->name('tokens.')->group(function () {
|
||||||
|
Route::get('/', [TokenController::class, 'index'])->name('index');
|
||||||
|
Route::post('/', [TokenController::class, 'store'])->name('store');
|
||||||
|
Route::delete('{token}', [TokenController::class, 'destroy'])->name('destroy');
|
||||||
|
});
|
||||||
|
|
||||||
Route::prefix('/providers')->name('providers.')->group(function () {
|
Route::prefix('/providers')->name('providers.')->group(function () {
|
||||||
Route::post('/connect/{service}', [OAuthProviderController::class, 'connect'])->name('connect');
|
Route::post('/connect/{service}', [OAuthProviderController::class, 'connect'])->name('connect');
|
||||||
Route::post('/callback/{service}', [OAuthProviderController::class, 'handleRedirect'])->name('callback');
|
Route::post('/callback/{service}', [OAuthProviderController::class, 'handleRedirect'])->name('callback');
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use App\Mail\Forms\SubmissionConfirmationMail;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
|
||||||
it('creates confirmation emails with the submitted data', function () {
|
it('creates confirmation emails with the submitted data', function () {
|
||||||
|
$this->withoutExceptionHandling();
|
||||||
$user = $this->actingAsUser();
|
$user = $this->actingAsUser();
|
||||||
$workspace = $this->createUserWorkspace($user);
|
$workspace = $this->createUserWorkspace($user);
|
||||||
$form = $this->createForm($user, $workspace);
|
$form = $this->createForm($user, $workspace);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Integration\FormIntegration;
|
||||||
|
use App\Models\User;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\Helpers\FormSubmissionDataFactory;
|
||||||
|
|
||||||
|
use function Pest\Laravel\assertDatabaseCount;
|
||||||
|
use function Pest\Laravel\delete;
|
||||||
|
use function Pest\Laravel\post;
|
||||||
|
use function PHPUnit\Framework\assertEquals;
|
||||||
|
|
||||||
|
test('create an integration', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
|
||||||
|
$form = createForm($user, $workspace, ['title' => 'First form']);
|
||||||
|
|
||||||
|
Sanctum::actingAs(
|
||||||
|
$user,
|
||||||
|
['manage-integrations']
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->withoutExceptionHandling();
|
||||||
|
post(route('zapier.webhooks.store'), [
|
||||||
|
'form_id' => $form->slug,
|
||||||
|
'hookUrl' => $hookUrl = 'https://zapier.com/hook/test'
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
assertDatabaseCount('form_integrations', 1);
|
||||||
|
|
||||||
|
$integration = FormIntegration::first();
|
||||||
|
|
||||||
|
assertEquals($form->id, $integration->form_id);
|
||||||
|
assertEquals($hookUrl, $integration->data->hook_url);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cannot create an integration without a corresponding ability', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
|
||||||
|
$form = createForm($user, $workspace, ['title' => 'First form']);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
post(route('zapier.webhooks.store'), [
|
||||||
|
'form_id' => $form->slug,
|
||||||
|
'hookUrl' => 'https://zapier.com/hook/test'
|
||||||
|
])
|
||||||
|
->assertForbidden();
|
||||||
|
|
||||||
|
assertDatabaseCount('form_integrations', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cannot create an integration for other users form', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$user2 = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user2);
|
||||||
|
|
||||||
|
$form = createForm($user2, $workspace, ['title' => 'First form']);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
post(route('zapier.webhooks.store'), [
|
||||||
|
'form_id' => $form->slug,
|
||||||
|
'hookUrl' => 'https://zapier.com/hook/test'
|
||||||
|
])
|
||||||
|
->assertForbidden();
|
||||||
|
|
||||||
|
assertDatabaseCount('form_integrations', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete an integration', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
|
||||||
|
$form = createForm($user, $workspace, ['title' => 'First form']);
|
||||||
|
|
||||||
|
Sanctum::actingAs(
|
||||||
|
$user,
|
||||||
|
['manage-integrations']
|
||||||
|
);
|
||||||
|
|
||||||
|
$integration = FormIntegration::factory()
|
||||||
|
->for($form)
|
||||||
|
->create([
|
||||||
|
'data' => [
|
||||||
|
'hook_url' => $hookUrl = 'https://zapier.com/hook/test'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertDatabaseCount('form_integrations', 1);
|
||||||
|
|
||||||
|
delete(route('zapier.webhooks.destroy', $integration), [
|
||||||
|
'form_id' => $form->slug,
|
||||||
|
'hookUrl' => $hookUrl,
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
assertDatabaseCount('form_integrations', 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cannot delete an integration with an incorrect hook url', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
|
||||||
|
$form = createForm($user, $workspace, ['title' => 'First form']);
|
||||||
|
|
||||||
|
Sanctum::actingAs(
|
||||||
|
$user,
|
||||||
|
['manage-integrations']
|
||||||
|
);
|
||||||
|
|
||||||
|
$integration = FormIntegration::factory()
|
||||||
|
->for($form)
|
||||||
|
->create([
|
||||||
|
'data' => [
|
||||||
|
'hook_url' => 'https://zapier.com/hook/test'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
delete(route('zapier.webhooks.destroy', $integration), [
|
||||||
|
'form_id' => $form->slug,
|
||||||
|
'hookUrl' => 'https://google.com',
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
assertDatabaseCount('form_integrations', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('poll for the latest submission', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
$form = createForm($user, $workspace, [
|
||||||
|
'properties' => [
|
||||||
|
[
|
||||||
|
'id' => 'title',
|
||||||
|
'name' => 'Name',
|
||||||
|
'type' => 'text',
|
||||||
|
'hidden' => false,
|
||||||
|
'required' => true,
|
||||||
|
'logic' => [
|
||||||
|
'conditions' => null,
|
||||||
|
'actions' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'age',
|
||||||
|
'name' => 'Age',
|
||||||
|
'type' => 'number',
|
||||||
|
'hidden' => false,
|
||||||
|
'required' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a submission for the form
|
||||||
|
$formData = FormSubmissionDataFactory::generateSubmissionData($form);
|
||||||
|
|
||||||
|
$this->postJson(route('forms.answer', $form->slug), $formData)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertJson([
|
||||||
|
'type' => 'success',
|
||||||
|
'message' => 'Form submission saved.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a webhook integration for the form
|
||||||
|
$integration = FormIntegration::factory()
|
||||||
|
->for($form)
|
||||||
|
->create([
|
||||||
|
'data' => [
|
||||||
|
'hook_url' => 'https://zapier.com/hook/test'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user, ['view', 'manage-integrations']);
|
||||||
|
|
||||||
|
// Call the poll endpoint
|
||||||
|
$response = $this->getJson(route('zapier.webhooks.poll', ['form_id' => $form->slug]));
|
||||||
|
|
||||||
|
// Assert the response status is OK
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
// Decode the response data
|
||||||
|
$responseData = $response->json()[0];
|
||||||
|
$receivedData = collect($responseData['data'])->values()->pluck('value')->toArray();
|
||||||
|
|
||||||
|
$this->assertEmpty(array_diff(array_values($formData), $receivedData));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('make up a submission when polling without any submission', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
$form = createForm($user, $workspace, [
|
||||||
|
'properties' => [
|
||||||
|
[
|
||||||
|
'id' => 'title',
|
||||||
|
'name' => 'Name',
|
||||||
|
'type' => 'text',
|
||||||
|
'hidden' => false,
|
||||||
|
'required' => true,
|
||||||
|
'logic' => [
|
||||||
|
'conditions' => null,
|
||||||
|
'actions' => [],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'age',
|
||||||
|
'name' => 'Age',
|
||||||
|
'type' => 'number',
|
||||||
|
'hidden' => false,
|
||||||
|
'required' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create a webhook integration for the form
|
||||||
|
$integration = FormIntegration::factory()
|
||||||
|
->for($form)
|
||||||
|
->create([
|
||||||
|
'data' => [
|
||||||
|
'hook_url' => 'https://zapier.com/hook/test'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user, ['view', 'manage-integrations']);
|
||||||
|
|
||||||
|
// Call the poll endpoint
|
||||||
|
$this->withoutExceptionHandling();
|
||||||
|
$response = $this->getJson(route('zapier.webhooks.poll', ['form_id' => $form->slug]));
|
||||||
|
// Assert the response status is OK
|
||||||
|
$response->assertOk();
|
||||||
|
|
||||||
|
// Decode the response data
|
||||||
|
$responseData = $response->json()[0];
|
||||||
|
|
||||||
|
ray($responseData);
|
||||||
|
|
||||||
|
ray($responseData);
|
||||||
|
$this->assertNotEmpty($responseData['data']);
|
||||||
|
$this->assertTrue(count($responseData['data']) == 2);
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
use function Pest\Laravel\get;
|
||||||
|
|
||||||
|
test('list all forms of a given workspace', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
|
||||||
|
$form1 = createForm($user, $workspace, ['title' => 'First form']);
|
||||||
|
$form2 = createForm($user, $workspace, ['title' => 'Second form']);
|
||||||
|
|
||||||
|
Sanctum::actingAs(
|
||||||
|
$user,
|
||||||
|
['list-forms']
|
||||||
|
);
|
||||||
|
|
||||||
|
get(route('zapier.forms', ['workspace_id' => $workspace->id]))
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonCount(2)
|
||||||
|
->assertJson([
|
||||||
|
[
|
||||||
|
'id' => $form1->slug,
|
||||||
|
'name' => $form1->title,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => $form2->slug,
|
||||||
|
'name' => $form2->title,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cannot list forms without a corresponding ability', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
|
||||||
|
$form1 = createForm($user, $workspace, ['title' => 'First form']);
|
||||||
|
$form2 = createForm($user, $workspace, ['title' => 'Second form']);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
get(route('zapier.forms', ['workspace_id' => $workspace->id]))
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cannot other users forms', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$user2 = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user2);
|
||||||
|
|
||||||
|
$form1 = createForm($user, $workspace, ['title' => 'First form']);
|
||||||
|
$form2 = createForm($user, $workspace, ['title' => 'Second form']);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
get(route('zapier.forms', ['workspace_id' => $workspace->id]))
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
use function Pest\Laravel\get;
|
||||||
|
|
||||||
|
test('list all workspaces of a user', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
|
||||||
|
$anotherUser = User::factory()->create();
|
||||||
|
$anotherWorkspace = createUserWorkspace($anotherUser);
|
||||||
|
|
||||||
|
Sanctum::actingAs(
|
||||||
|
$user,
|
||||||
|
['list-workspaces']
|
||||||
|
);
|
||||||
|
|
||||||
|
get(route('zapier.workspaces'))
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonCount(1)
|
||||||
|
->assertJson([
|
||||||
|
[
|
||||||
|
'id' => $workspace->id,
|
||||||
|
'name' => $workspace->name,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cannot list workspaces without a corresponding ability', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$workspace = createUserWorkspace($user);
|
||||||
|
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
get(route('zapier.workspaces'))
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
use function Pest\Laravel\get;
|
||||||
|
|
||||||
|
test('validate auth', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
get(route('zapier.validate'))
|
||||||
|
->assertOk()
|
||||||
|
->assertJson([
|
||||||
|
'name' => $user->name,
|
||||||
|
'email' => $user->email,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cannot validate auth with incorrect credentials', function () {
|
||||||
|
get(route('zapier.validate'))
|
||||||
|
->assertUnauthorized();
|
||||||
|
});
|
||||||
|
|
@ -9,6 +9,12 @@ class FormSubmissionDataFactory
|
||||||
{
|
{
|
||||||
private ?Faker\Generator $faker;
|
private ?Faker\Generator $faker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, then format expected by answer endpoint
|
||||||
|
* otherwise, format of answer as we store it in the FormSubmission's data
|
||||||
|
*/
|
||||||
|
private bool $answerFormat = true;
|
||||||
|
|
||||||
public function __construct(private Form $form)
|
public function __construct(private Form $form)
|
||||||
{
|
{
|
||||||
$this->faker = Faker\Factory::create();
|
$this->faker = Faker\Factory::create();
|
||||||
|
|
@ -19,6 +25,12 @@ class FormSubmissionDataFactory
|
||||||
return (new self($form))->createSubmissionData($data);
|
return (new self($form))->createSubmissionData($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function asFormSubmissionData()
|
||||||
|
{
|
||||||
|
$this->answerFormat = false;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function createSubmissionData($mergeData = [])
|
public function createSubmissionData($mergeData = [])
|
||||||
{
|
{
|
||||||
$data = [];
|
$data = [];
|
||||||
|
|
@ -68,9 +80,23 @@ class FormSubmissionDataFactory
|
||||||
$data[$property['id']] = $value;
|
$data[$property['id']] = $value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!$this->answerFormat) {
|
||||||
|
$data = $this->formatAsSubmissionData($data);
|
||||||
|
}
|
||||||
|
|
||||||
return array_merge($data, $mergeData);
|
return array_merge($data, $mergeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function formatAsSubmissionData($data)
|
||||||
|
{
|
||||||
|
collect($this->form->properties)->each(function ($property) use (&$data) {
|
||||||
|
if ($property['type'] === 'phone_number') {
|
||||||
|
$data[$property['id']] = '+33749119783';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
private function generateSelectValue($property)
|
private function generateSelectValue($property)
|
||||||
{
|
{
|
||||||
$values = [];
|
$values = [];
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use App\Models\Forms\Form;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
|
||||||
uses(\Tests\TestCase::class)->in('Feature');
|
uses(\Tests\TestCase::class)->in('Feature');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -38,3 +42,13 @@ expect()->extend('toBeOne', function () {
|
||||||
| global functions to help you to reduce the number of lines of code in your test files.
|
| global functions to help you to reduce the number of lines of code in your test files.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function createUserWorkspace(User $user): Workspace
|
||||||
|
{
|
||||||
|
return test()->createUserWorkspace($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createForm(User $user, Workspace $workspace, array $data = []): Form
|
||||||
|
{
|
||||||
|
return test()->createForm($user, $workspace, $data);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue