Google Sheet - OAuth "client" powered integrations (#415)
* fix `helpers.php` * fix `.eslintrc.cjs` * spreadsheet manager * fetch providers. set `oauth_id` for integrations * create spreadsheet on integration create event * connect OAuth accounts * display actions. connect account if missing * cleanup * handle form field change * map integration data object to `SpreadsheetData` * validate request * wip * redirect to integrations page * fix refresh token * add helper text * add extra integration info * refactor * refresh google token * fix validation * add tests * Fix linting issue * Update composer lock file --------- Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
parent
03d695c74e
commit
24d33a9ebb
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "/Applications/Tinkerwell.app/Contents/Resources/phpactor/phpactor.schema.json",
|
||||
"language_server_phpstan.enabled": false
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events\Models;
|
||||
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FormIntegrationCreated
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithSockets;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public FormIntegration $formIntegration
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers\Forms\Integration;
|
|||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Integration\FormIntegrationsRequest;
|
||||
use App\Http\Resources\FormIntegrationResource;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Integration\FormIntegration;
|
||||
|
||||
|
|
@ -19,7 +20,12 @@ class FormIntegrationsController extends Controller
|
|||
$form = Form::findOrFail((int)$id);
|
||||
$this->authorize('view', $form);
|
||||
|
||||
return FormIntegration::where('form_id', $form->id)->get();
|
||||
$integrations = FormIntegration::query()
|
||||
->where('form_id', $form->id)
|
||||
->with('provider.user')
|
||||
->get();
|
||||
|
||||
return FormIntegrationResource::collection($integrations);
|
||||
}
|
||||
|
||||
public function create(FormIntegrationsRequest $request, string $id)
|
||||
|
|
@ -27,15 +33,19 @@ class FormIntegrationsController extends Controller
|
|||
$form = Form::findOrFail((int)$id);
|
||||
$this->authorize('update', $form);
|
||||
|
||||
/** @var FormIntegration $formIntegration */
|
||||
$formIntegration = FormIntegration::create(
|
||||
array_merge([
|
||||
'form_id' => $form->id,
|
||||
], $request->toIntegrationData())
|
||||
);
|
||||
|
||||
$formIntegration->refresh();
|
||||
$formIntegration->load('provider.user');
|
||||
|
||||
return $this->success([
|
||||
'message' => 'Form Integration was created.',
|
||||
'form_integration' => $formIntegration
|
||||
'form_integration' => FormIntegrationResource::make($formIntegration)
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -46,10 +56,11 @@ class FormIntegrationsController extends Controller
|
|||
|
||||
$formIntegration = FormIntegration::findOrFail((int)$integrationid);
|
||||
$formIntegration->update($request->toIntegrationData());
|
||||
$formIntegration->load('provider.user');
|
||||
|
||||
return $this->success([
|
||||
'message' => 'Form Integration was updated.',
|
||||
'form_integration' => $formIntegration
|
||||
'form_integration' => FormIntegrationResource::make($formIntegration)
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\OAuth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\OAuthProviderResource;
|
||||
use App\Integrations\OAuth\OAuthProviderService;
|
||||
use App\Models\OAuthProvider;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class OAuthProviderController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = Auth::user();
|
||||
|
||||
$providers = $user->oauthProviders()->get();
|
||||
|
||||
return OAuthProviderResource::collection($providers);
|
||||
}
|
||||
|
||||
public function connect(Request $request, OAuthProviderService $service)
|
||||
{
|
||||
$userId = Auth::id();
|
||||
cache()->put("oauth-intention:{$userId}", $request->input('intention'), 60 * 5);
|
||||
|
||||
return response()->json([
|
||||
'url' => $service->getDriver()->getRedirectUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function handleRedirect(OAuthProviderService $service)
|
||||
{
|
||||
$driverUser = $service->getDriver()->getUser();
|
||||
|
||||
$provider = OAuthProvider::query()
|
||||
->updateOrCreate(
|
||||
[
|
||||
'user_id' => Auth::id(),
|
||||
'provider' => $service,
|
||||
'provider_user_id' => $driverUser->getId(),
|
||||
],
|
||||
[
|
||||
'access_token' => $driverUser->token,
|
||||
'refresh_token' => $driverUser->refreshToken,
|
||||
'name' => $driverUser->getName(),
|
||||
'email' => $driverUser->getEmail(),
|
||||
]
|
||||
);
|
||||
|
||||
return OAuthProviderResource::make($provider);
|
||||
}
|
||||
|
||||
public function destroy(OAuthProvider $provider)
|
||||
{
|
||||
$this->authorize('delete', $provider);
|
||||
|
||||
$provider->delete();
|
||||
|
||||
return response()->json();
|
||||
}
|
||||
}
|
||||
|
|
@ -21,9 +21,9 @@ class FormIntegrationsRequest extends FormRequest
|
|||
// Load integration class, and get rules
|
||||
$integration = FormIntegration::getIntegration($request->integration_id);
|
||||
if ($integration && isset($integration['file_name']) && class_exists(
|
||||
'App\Service\Forms\Integrations\\' . $integration['file_name']
|
||||
'App\Integrations\Handlers\\' . $integration['file_name']
|
||||
)) {
|
||||
$this->integrationClassName = 'App\Service\Forms\Integrations\\' . $integration['file_name'];
|
||||
$this->integrationClassName = 'App\Integrations\Handlers\\' . $integration['file_name'];
|
||||
$this->loadIntegrationRules();
|
||||
return;
|
||||
}
|
||||
|
|
@ -40,9 +40,13 @@ class FormIntegrationsRequest extends FormRequest
|
|||
{
|
||||
return array_merge([
|
||||
'integration_id' => ['required', Rule::in(array_keys(FormIntegration::getAllIntegrations()))],
|
||||
'oauth_id' => [
|
||||
$this->isOAuthRequired() ? 'required' : 'nullable',
|
||||
Rule::exists('oauth_providers', 'id')
|
||||
],
|
||||
'settings' => 'present|array',
|
||||
'status' => 'required|boolean',
|
||||
'logic' => [new IntegrationLogicRule()]
|
||||
'logic' => [new IntegrationLogicRule()],
|
||||
], $this->integrationRules);
|
||||
}
|
||||
|
||||
|
|
@ -53,16 +57,24 @@ class FormIntegrationsRequest extends FormRequest
|
|||
*/
|
||||
public function attributes()
|
||||
{
|
||||
$attributes = $this->integrationClassName::getValidationAttributes();
|
||||
|
||||
$fields = [];
|
||||
foreach ($this->rules() as $key => $value) {
|
||||
$fields[$key] = Str::of($key)
|
||||
$fields[$key] = $attributes[$key] ?? Str::of($key)
|
||||
->replace('settings.', '')
|
||||
->headline();
|
||||
->headline()
|
||||
->toString();
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
protected function isOAuthRequired(): bool
|
||||
{
|
||||
return $this->integrationClassName::isOAuthRequired();
|
||||
}
|
||||
|
||||
private function loadIntegrationRules()
|
||||
{
|
||||
foreach ($this->integrationClassName::getValidationRules() as $key => $value) {
|
||||
|
|
@ -78,7 +90,8 @@ class FormIntegrationsRequest extends FormRequest
|
|||
)) ? FormIntegration::STATUS_ACTIVE : FormIntegration::STATUS_INACTIVE,
|
||||
'integration_id' => $this->validated('integration_id'),
|
||||
'data' => $this->validated('settings') ?? [],
|
||||
'logic' => $this->validated('logic') ?? []
|
||||
'logic' => $this->validated('logic') ?? [],
|
||||
'oauth_id' => $this->validated('oauth_id'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @property \App\Models\Integration\FormIntegration $resource
|
||||
*/
|
||||
class FormIntegrationResource extends JsonResource
|
||||
{
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
...parent::toArray($request),
|
||||
'provider' => OAuthProviderResource::make($this->resource->provider),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
/**
|
||||
* @property \App\Models\OAuthProvider $resource
|
||||
*/
|
||||
class OAuthProviderResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
$userId = Auth::id();
|
||||
$intention = cache()->get("oauth-intention:{$userId}");
|
||||
|
||||
return [
|
||||
'id' => $this->resource->id,
|
||||
'provider' => $this->resource->provider,
|
||||
'name' => $this->resource->name,
|
||||
'email' => $this->resource->email,
|
||||
'intention' => $intention,
|
||||
'user' => $this->whenLoaded(
|
||||
'user',
|
||||
fn () => OAuthProviderUserResource::make($this->resource->user),
|
||||
null,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @property \App\Models\User $resource
|
||||
*/
|
||||
class OAuthProviderUserResource extends JsonResource
|
||||
{
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'name' => $this->resource->name,
|
||||
'email' => $this->resource->email,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\Data;
|
||||
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class SpreadsheetData extends Data
|
||||
{
|
||||
public function __construct(
|
||||
public string $url = '',
|
||||
public string $spreadsheet_id = '',
|
||||
public ?array $columns = []
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\Google;
|
||||
|
||||
use App\Integrations\Google\Sheets\SpreadsheetManager;
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use Google\Client as Client;
|
||||
|
||||
class Google
|
||||
{
|
||||
protected Client $client;
|
||||
protected ?string $token;
|
||||
protected ?string $refreshToken;
|
||||
|
||||
public function __construct(
|
||||
protected FormIntegration $formIntegration
|
||||
) {
|
||||
$this->client = new Client();
|
||||
$this->client->setClientId(config('services.google.client_id'));
|
||||
$this->client->setClientSecret(config('services.google.client_secret'));
|
||||
$this->client->setAccessToken([
|
||||
'access_token' => $this->formIntegration->provider->access_token,
|
||||
'created' => $this->formIntegration->provider->updated_at->getTimestamp(),
|
||||
'expires_in' => 3600,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getClient(): Client
|
||||
{
|
||||
if($this->client->isAccessTokenExpired()) {
|
||||
$this->refreshToken();
|
||||
}
|
||||
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
public function refreshToken(): static
|
||||
{
|
||||
$this->client->refreshToken($this->formIntegration->provider->refresh_token);
|
||||
|
||||
$token = $this->client->getAccessToken();
|
||||
|
||||
$this->formIntegration->provider->update([
|
||||
'access_token' => $token['access_token'],
|
||||
'refresh_token' => $token['refresh_token'],
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function sheets(): SpreadsheetManager
|
||||
{
|
||||
return new SpreadsheetManager($this, $this->formIntegration);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\Google\Sheets;
|
||||
|
||||
use App\Integrations\Data\SpreadsheetData;
|
||||
use App\Integrations\Google\Google;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use App\Service\Forms\FormSubmissionFormatter;
|
||||
use Google\Service\Sheets;
|
||||
use Google\Service\Sheets\BatchUpdateValuesRequest;
|
||||
use Google\Service\Sheets\Spreadsheet;
|
||||
use Google\Service\Sheets\ValueRange;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class SpreadsheetManager
|
||||
{
|
||||
protected Sheets $driver;
|
||||
protected SpreadsheetData $data;
|
||||
|
||||
public function __construct(
|
||||
protected Google $google,
|
||||
protected FormIntegration $integration
|
||||
) {
|
||||
$this->driver = new Sheets($google->getClient());
|
||||
|
||||
$this->data = empty($this->integration->data)
|
||||
? new SpreadsheetData()
|
||||
: new SpreadsheetData(
|
||||
url: $this->integration->data->url,
|
||||
spreadsheet_id: $this->integration->data->spreadsheet_id,
|
||||
columns: array_map(
|
||||
fn ($column) => (array) $column,
|
||||
$this->integration->data->columns
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function convertToArray(mixed $object): array
|
||||
{
|
||||
return is_scalar($object) || is_null($object)
|
||||
? $object
|
||||
: $this->convertToArray((array) $object);
|
||||
}
|
||||
|
||||
public function get(string $id): Spreadsheet
|
||||
{
|
||||
$spreadsheet = $this->driver
|
||||
->spreadsheets
|
||||
->get($id);
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
public function create(Form $form): Spreadsheet
|
||||
{
|
||||
$body = new Spreadsheet([
|
||||
'properties' => [
|
||||
'title' => $form->title
|
||||
]
|
||||
]);
|
||||
|
||||
$spreadsheet = $this->driver->spreadsheets->create($body);
|
||||
|
||||
$this->data->url = $spreadsheet->spreadsheetUrl;
|
||||
$this->data->spreadsheet_id = $spreadsheet->spreadsheetId;
|
||||
$this->data->columns = [];
|
||||
|
||||
$this->updateHeaders($spreadsheet->spreadsheetId);
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
public function buildColumns(): array
|
||||
{
|
||||
$properties = $this->integration->form->properties;
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$key = Arr::first(
|
||||
array_keys($this->data->columns),
|
||||
fn (int $key) => $this->data->columns[$key]['id'] === $property['id']
|
||||
);
|
||||
|
||||
$column = Arr::only($property, ['id', 'name']);
|
||||
|
||||
if (!is_null($key)) {
|
||||
$this->data->columns[$key] = $column;
|
||||
} else {
|
||||
$this->data->columns[] = $column;
|
||||
}
|
||||
}
|
||||
|
||||
$this->integration->update([
|
||||
'data' => $this->data,
|
||||
]);
|
||||
|
||||
return $this->data->columns;
|
||||
}
|
||||
|
||||
public function updateHeaders(string $id): static
|
||||
{
|
||||
$columns = $this->buildColumns();
|
||||
|
||||
$headers = array_map(
|
||||
fn ($column) => $column['name'],
|
||||
$columns
|
||||
);
|
||||
|
||||
return $this->setHeaders($id, $headers);
|
||||
}
|
||||
|
||||
protected function setHeaders(string $id, array $headers): static
|
||||
{
|
||||
$valueRange = new ValueRange([
|
||||
'values' => [$headers],
|
||||
]);
|
||||
|
||||
$valueRange->setRange(
|
||||
$this->buildRange($headers)
|
||||
);
|
||||
|
||||
$body = new BatchUpdateValuesRequest([
|
||||
'valueInputOption' => 'RAW',
|
||||
'data' => [$valueRange]
|
||||
]);
|
||||
|
||||
$this->driver
|
||||
->spreadsheets_values
|
||||
->batchUpdate($id, $body);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function buildRow(array $submissionData): array
|
||||
{
|
||||
$formatter = (new FormSubmissionFormatter($this->integration->form, $submissionData))->outputStringsOnly();
|
||||
|
||||
$fields = $formatter->getFieldsWithValue();
|
||||
|
||||
return collect($this->data->columns)
|
||||
->map(function (array $column) use ($fields) {
|
||||
$field = Arr::first($fields, fn ($field) => $field['id'] === $column['id']);
|
||||
|
||||
return $field ? $field['value'] : '';
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function submit(array $submissionData): static
|
||||
{
|
||||
$this->updateHeaders($this->data->spreadsheet_id);
|
||||
|
||||
$row = $this->buildRow($submissionData);
|
||||
|
||||
$this->addRow(
|
||||
$this->data->spreadsheet_id,
|
||||
$row
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addRow(string $id, array $values): static
|
||||
{
|
||||
$valueRange = new ValueRange([
|
||||
'values' => [$values],
|
||||
]);
|
||||
|
||||
$params = [
|
||||
'valueInputOption' => 'RAW',
|
||||
];
|
||||
|
||||
$this->driver
|
||||
->spreadsheets_values
|
||||
->append(
|
||||
$id,
|
||||
$this->buildRange($values),
|
||||
$valueRange,
|
||||
$params
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function buildRange(array $values): string
|
||||
{
|
||||
return "A1:" . chr(64 + count($values)) . "1";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service\Forms\Integrations;
|
||||
namespace App\Integrations\Handlers;
|
||||
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
|
|
@ -96,6 +96,11 @@ abstract class AbstractIntegrationHandler
|
|||
}
|
||||
}
|
||||
|
||||
public function created(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Default handle. Can be changed in child classes.
|
||||
*/
|
||||
|
|
@ -110,6 +115,16 @@ abstract class AbstractIntegrationHandler
|
|||
|
||||
abstract public static function getValidationRules(): array;
|
||||
|
||||
public static function isOAuthRequired(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function getValidationAttributes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function formatData(array $data): array
|
||||
{
|
||||
return $data;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service\Forms\Integrations;
|
||||
namespace App\Integrations\Handlers;
|
||||
|
||||
use App\Service\Forms\FormSubmissionFormatter;
|
||||
use Illuminate\Support\Arr;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service\Forms\Integrations;
|
||||
namespace App\Integrations\Handlers;
|
||||
|
||||
use App\Rules\OneEmailPerLine;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\Handlers\Events;
|
||||
|
||||
use App\Models\Integration\FormIntegration;
|
||||
|
||||
class AbstractIntegrationCreated
|
||||
{
|
||||
public function __construct(
|
||||
protected FormIntegration $formIntegration
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\Handlers\Events;
|
||||
|
||||
use App\Integrations\Google\Google;
|
||||
use App\Models\Integration\FormIntegration;
|
||||
|
||||
class GoogleSheetsIntegrationCreated extends AbstractIntegrationCreated
|
||||
{
|
||||
protected Google $client;
|
||||
|
||||
public function __construct(
|
||||
protected FormIntegration $formIntegration
|
||||
) {
|
||||
parent::__construct($formIntegration);
|
||||
|
||||
$this->client = new Google($formIntegration);
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$this->client->sheets()
|
||||
->create($this->formIntegration->form);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\Handlers;
|
||||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Integrations\Google\Google;
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GoogleSheetsIntegration extends AbstractIntegrationHandler
|
||||
{
|
||||
protected Google $client;
|
||||
|
||||
public function __construct(
|
||||
protected FormSubmitted $event,
|
||||
protected FormIntegration $formIntegration,
|
||||
protected array $integration
|
||||
) {
|
||||
parent::__construct($event, $formIntegration, $integration);
|
||||
|
||||
$this->client = new Google($formIntegration);
|
||||
}
|
||||
|
||||
public static function getValidationRules(): array
|
||||
{
|
||||
return [
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
public static function isOAuthRequired(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getValidationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'oauth_id' => 'Google Account',
|
||||
];
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (!$this->shouldRun()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log::debug('Creating Google Spreadsheet record', [
|
||||
'spreadsheet_id' => $this->getSpreadsheetId(),
|
||||
'form_id' => $this->form->id,
|
||||
'form_slug' => $this->form->slug,
|
||||
]);
|
||||
|
||||
$this->client->sheets()->submit($this->submissionData);
|
||||
}
|
||||
|
||||
protected function getSpreadsheetId(): string
|
||||
{
|
||||
if(!isset($this->integrationData->spreadsheet_id)) {
|
||||
throw new Exception('The spreadsheed is not instantiated');
|
||||
}
|
||||
|
||||
return $this->integrationData->spreadsheet_id;
|
||||
}
|
||||
|
||||
protected function shouldRun(): bool
|
||||
{
|
||||
return parent::shouldRun() && $this->formIntegration->oauth_id && $this->getSpreadsheetId();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service\Forms\Integrations;
|
||||
namespace App\Integrations\Handlers;
|
||||
|
||||
use App\Service\Forms\FormSubmissionFormatter;
|
||||
use Illuminate\Support\Arr;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service\Forms\Integrations;
|
||||
namespace App\Integrations\Handlers;
|
||||
|
||||
use App\Mail\Forms\SubmissionConfirmationMail;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service\Forms\Integrations;
|
||||
namespace App\Integrations\Handlers;
|
||||
|
||||
class WebhookIntegration extends AbstractIntegrationHandler
|
||||
{
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\OAuth\Drivers\Contracts;
|
||||
|
||||
use Laravel\Socialite\Contracts\User;
|
||||
|
||||
interface OAuthDriver
|
||||
{
|
||||
public function getRedirectUrl(): string;
|
||||
public function getUser(): User;
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\OAuth\Drivers;
|
||||
|
||||
use App\Integrations\OAuth\Drivers\Contracts\OAuthDriver;
|
||||
use Google\Service\Sheets;
|
||||
use Laravel\Socialite\Contracts\User;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Laravel\Socialite\Two\GoogleProvider;
|
||||
|
||||
class OAuthGoogleDriver implements OAuthDriver
|
||||
{
|
||||
protected GoogleProvider $provider;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->provider = Socialite::driver('google');
|
||||
}
|
||||
|
||||
public function getRedirectUrl(): string
|
||||
{
|
||||
return $this->provider
|
||||
->scopes([Sheets::DRIVE_FILE])
|
||||
->stateless()
|
||||
->with([
|
||||
'access_type' => 'offline',
|
||||
'prompt' => 'consent select_account'
|
||||
])
|
||||
->redirect()
|
||||
->getTargetUrl();
|
||||
}
|
||||
|
||||
public function getUser(): User
|
||||
{
|
||||
return $this->provider
|
||||
->stateless()
|
||||
->user();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Integrations\OAuth;
|
||||
|
||||
use App\Integrations\OAuth\Drivers\Contracts\OAuthDriver;
|
||||
use App\Integrations\OAuth\Drivers\OAuthGoogleDriver;
|
||||
|
||||
enum OAuthProviderService: string
|
||||
{
|
||||
case Google = 'google';
|
||||
|
||||
public function getDriver(): OAuthDriver
|
||||
{
|
||||
return match($this) {
|
||||
self::Google => new OAuthGoogleDriver()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners\Forms;
|
||||
|
||||
use App\Events\Models\FormIntegrationCreated;
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
||||
class FormIntegrationCreatedHandler implements ShouldQueue
|
||||
{
|
||||
use InteractsWithQueue;
|
||||
|
||||
public function handle(FormIntegrationCreated $event)
|
||||
{
|
||||
$integration = FormIntegration::getIntegration($event->formIntegration->integration_id);
|
||||
|
||||
if(!$integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isset($integration['file_name'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$className = 'App\Integrations\Handlers\Events\\' . $integration['file_name'] . 'Created';
|
||||
|
||||
if(!class_exists($className)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \App\Integrations\Handlers\Events\AbstractIntegrationCreated $eventHandler */
|
||||
$eventHandler = new $className($event->formIntegration);
|
||||
|
||||
$eventHandler->handle();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ namespace App\Listeners\Forms;
|
|||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use App\Service\Forms\Integrations\AbstractIntegrationHandler;
|
||||
use App\Integrations\Handlers\AbstractIntegrationHandler;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
||||
|
|
@ -35,9 +35,9 @@ class NotifyFormSubmission implements ShouldQueue
|
|||
): AbstractIntegrationHandler {
|
||||
$integration = FormIntegration::getIntegration($formIntegration->integration_id);
|
||||
if ($integration && isset($integration['file_name']) && class_exists(
|
||||
'App\Service\Forms\Integrations\\' . $integration['file_name']
|
||||
'App\Integrations\Handlers\\' . $integration['file_name']
|
||||
)) {
|
||||
$className = 'App\Service\Forms\Integrations\\' . $integration['file_name'];
|
||||
$className = 'App\Integrations\Handlers\\' . $integration['file_name'];
|
||||
return new $className($event, $formIntegration, $integration);
|
||||
}
|
||||
throw new \Exception('Unknown Integration!');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
namespace App\Models\Integration;
|
||||
|
||||
use App\Events\Models\FormIntegrationCreated;
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\OAuthProvider;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
|
|
@ -26,6 +28,10 @@ class FormIntegration extends Model
|
|||
'logic' => 'object'
|
||||
];
|
||||
|
||||
protected $dispatchesEvents = [
|
||||
'created' => FormIntegrationCreated::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Relationships
|
||||
*/
|
||||
|
|
@ -39,6 +45,11 @@ class FormIntegration extends Model
|
|||
return $this->hasMany(FormIntegrationsEvent::class, 'integration_id');
|
||||
}
|
||||
|
||||
public function provider()
|
||||
{
|
||||
return $this->belongsTo(OAuthProvider::class, 'oauth_id');
|
||||
}
|
||||
|
||||
public static function getAllIntegrations()
|
||||
{
|
||||
return json_decode(file_get_contents(resource_path('data/forms/integrations.json')), true);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Integrations\OAuth\OAuthProviderService;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OAuthProvider extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
|
|
@ -29,6 +33,11 @@ class OAuthProvider extends Model
|
|||
'access_token', 'refresh_token',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'provider' => OAuthProviderService::class,
|
||||
'token_expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -187,4 +187,5 @@ class Workspace extends Model implements CachableAttributes
|
|||
{
|
||||
return $this->hasMany(Form::class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\OAuthProvider;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class OAuthProviderPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function viewAny(User $user)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function view(User $user, OAuthProvider $provider)
|
||||
{
|
||||
return $provider->user()->is($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function create(User $user)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function update(User $user, OAuthProvider $provider)
|
||||
{
|
||||
return $provider->user()->is($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete(User $user, OAuthProvider $provider)
|
||||
{
|
||||
return $provider->user()->is($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function restore(User $user, OAuthProvider $provider)
|
||||
{
|
||||
return $provider->user()->is($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function forceDelete(User $user, OAuthProvider $provider)
|
||||
{
|
||||
return $provider->user()->is($user);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,10 +4,12 @@ namespace App\Providers;
|
|||
|
||||
use App\Models\Forms\Form;
|
||||
use App\Models\Integration\FormZapierWebhook;
|
||||
use App\Models\OAuthProvider;
|
||||
use App\Models\Template;
|
||||
use App\Models\Workspace;
|
||||
use App\Policies\FormPolicy;
|
||||
use App\Policies\Integration\FormZapierWebhookPolicy;
|
||||
use App\Policies\OAuthProviderPolicy;
|
||||
use App\Policies\TemplatePolicy;
|
||||
use App\Policies\WorkspacePolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
|
|
@ -24,6 +26,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||
Workspace::class => WorkspacePolicy::class,
|
||||
FormZapierWebhook::class => FormZapierWebhookPolicy::class,
|
||||
Template::class => TemplatePolicy::class,
|
||||
OAuthProvider::class => OAuthProviderPolicy::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ namespace App\Providers;
|
|||
|
||||
use App\Events\Forms\FormSubmitted;
|
||||
use App\Events\Models\FormCreated;
|
||||
use App\Events\Models\FormIntegrationCreated;
|
||||
use App\Events\Models\FormIntegrationsEventCreated;
|
||||
use App\Events\SubscriptionCreated;
|
||||
use App\Listeners\Forms\FormCreationConfirmation;
|
||||
use App\Listeners\Forms\FormIntegrationCreatedHandler;
|
||||
use App\Listeners\Forms\FormIntegrationsEventListener;
|
||||
use App\Listeners\Forms\NotifyFormSubmission;
|
||||
use App\Listeners\HandleSubscriptionCreated;
|
||||
|
|
@ -31,6 +33,9 @@ class EventServiceProvider extends ServiceProvider
|
|||
FormSubmitted::class => [
|
||||
NotifyFormSubmission::class
|
||||
],
|
||||
FormIntegrationCreated::class => [
|
||||
FormIntegrationCreatedHandler::class,
|
||||
],
|
||||
FormIntegrationsEventCreated::class => [
|
||||
FormIntegrationsEventListener::class,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<?php
|
||||
|
||||
function front_url($path = '')
|
||||
{
|
||||
$baseUrl = config('app.front_url');
|
||||
if (! $baseUrl) {
|
||||
return $path;
|
||||
}
|
||||
if(!function_exists('front_url')) {
|
||||
function front_url($path = '')
|
||||
{
|
||||
$baseUrl = config('app.front_url');
|
||||
if (! $baseUrl) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
return rtrim($baseUrl, '/').'/'.ltrim($path, '/');
|
||||
return rtrim($baseUrl, '/').'/'.ltrim($path, '/');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: ['@nuxt/eslint-config'],
|
||||
parser: 'vue-eslint-parser',
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ['@nuxt/eslint-config'],
|
||||
rules: {
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/no-mutating-props': 'off',
|
||||
semi: ['error', 'never'],
|
||||
'vue/no-v-html': 'off',
|
||||
'prefer-rest-params': 'off',
|
||||
'vue/valid-template-root': 'off',
|
||||
'no-undef': 'off',
|
||||
},
|
||||
}
|
||||
root: true,
|
||||
extends: ["@nuxt/eslint-config"],
|
||||
parser: "vue-eslint-parser",
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
rules: {
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/no-mutating-props": "off",
|
||||
semi: ["error", "never"],
|
||||
"vue/no-v-html": "off",
|
||||
"prefer-rest-params": "off",
|
||||
"vue/valid-template-root": "off",
|
||||
"no-undef": "off",
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,39 @@
|
|||
:form="form"
|
||||
>
|
||||
<div class="my-5">
|
||||
Coming Soon...
|
||||
<select-input
|
||||
v-if="providers.length"
|
||||
v-model="integrationData.oauth_id"
|
||||
name="provider"
|
||||
:options="providers"
|
||||
option-key="id"
|
||||
emit-key="id"
|
||||
:required="true"
|
||||
label="Select Google Account"
|
||||
>
|
||||
<template #help>
|
||||
<InputHelp>
|
||||
<span>
|
||||
Add an entry to spreadsheets on each form submission.
|
||||
<NuxtLink
|
||||
:to="{ name: 'settings-connections' }"
|
||||
>
|
||||
Click here
|
||||
</NuxtLink>
|
||||
to connect another account.
|
||||
</span>
|
||||
</InputHelp>
|
||||
</template>
|
||||
</select-input>
|
||||
|
||||
<v-button
|
||||
v-else
|
||||
color="white"
|
||||
:loading="providersStore.loading"
|
||||
@click.prevent="connect"
|
||||
>
|
||||
Connect Google account
|
||||
</v-button>
|
||||
</div>
|
||||
</IntegrationWrapper>
|
||||
</template>
|
||||
|
|
@ -19,4 +51,11 @@ const props = defineProps({
|
|||
integrationData: { type: Object, required: true },
|
||||
formIntegrationId: { type: Number, required: false, default: null },
|
||||
})
|
||||
|
||||
const providersStore = useOAuthProvidersStore()
|
||||
const providers = computed(() => providersStore.getAll.filter(provider => provider.provider == 'google'))
|
||||
|
||||
function connect() {
|
||||
providersStore.connect('google', true)
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<div class="flex flex-1 items-center">
|
||||
<div class="space-y-1">
|
||||
<div class="font-medium mr-2">
|
||||
{{ integration.provider.user.name }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ integration.provider.user.email }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto">
|
||||
<v-button
|
||||
:href="integration.data.url"
|
||||
target="_blank"
|
||||
color="white"
|
||||
>
|
||||
Open spreadsheet
|
||||
</v-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
integration: Object,
|
||||
})
|
||||
</script>
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<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 class="flex items-center">
|
||||
<div
|
||||
class="mr-4"
|
||||
:class="{
|
||||
|
|
@ -32,97 +32,106 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="loadingDelete"
|
||||
class="pr-4 pt-2"
|
||||
>
|
||||
<Loader class="h-6 w-6 mx-auto" />
|
||||
</div>
|
||||
<dropdown
|
||||
v-else
|
||||
class="inline"
|
||||
>
|
||||
<template #trigger="{ toggle }">
|
||||
<v-button
|
||||
color="white"
|
||||
@click="toggle"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 inline -mt-1"
|
||||
viewBox="0 0 16 4"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</v-button>
|
||||
</template>
|
||||
<a
|
||||
v-track.edit_form_integration_click="{
|
||||
form_slug: form.slug,
|
||||
form_integration_id: integration.id,
|
||||
}"
|
||||
href="#"
|
||||
class="flex px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline hover:text-gray-900 items-center"
|
||||
@click.prevent="showIntegrationModal = true"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:pencil"
|
||||
class="w-5 h-5 mr-2"
|
||||
/>
|
||||
Edit
|
||||
</a>
|
||||
<a
|
||||
v-track.past_events_form_integration_click="{
|
||||
form_slug: form.slug,
|
||||
form_integration_id: integration.id,
|
||||
}"
|
||||
href="#"
|
||||
class="flex px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline hover:text-gray-900 items-center"
|
||||
@click.prevent="showIntegrationEventsModal = true"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:clock"
|
||||
class="w-5 h-5 mr-2"
|
||||
/>
|
||||
Past Events
|
||||
</a>
|
||||
<a
|
||||
v-track.delete_form_integration_click="{
|
||||
form_integration_id: integration.id,
|
||||
}"
|
||||
href="#"
|
||||
class="flex px-4 py-2 text-md text-red-600 hover:bg-red-50 hover:no-underline items-center"
|
||||
@click.prevent="deleteFormIntegration(integration.id)"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:trash"
|
||||
class="w-5 h-5 mr-2"
|
||||
/>
|
||||
<div class="grow flex items-center gap-4 pl-4">
|
||||
<component
|
||||
:is="actionsComponent"
|
||||
v-if="actionsComponent"
|
||||
:integration="integration"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="loadingDelete"
|
||||
class="pr-4 pt-2 ml-auto"
|
||||
>
|
||||
<Loader class="h-6 w-6 mx-auto" />
|
||||
</div>
|
||||
<dropdown
|
||||
v-else
|
||||
class="inline"
|
||||
>
|
||||
<template #trigger="{ toggle }">
|
||||
<v-button
|
||||
color="white"
|
||||
@click="toggle"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 inline -mt-1"
|
||||
viewBox="0 0 16 4"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</v-button>
|
||||
</template>
|
||||
<a
|
||||
v-track.edit_form_integration_click="{
|
||||
form_slug: form.slug,
|
||||
form_integration_id: integration.id,
|
||||
}"
|
||||
href="#"
|
||||
class="flex px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline hover:text-gray-900 items-center"
|
||||
@click.prevent="showIntegrationModal = true"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:pencil"
|
||||
class="w-5 h-5 mr-2"
|
||||
/>
|
||||
Edit
|
||||
</a>
|
||||
<a
|
||||
v-track.past_events_form_integration_click="{
|
||||
form_slug: form.slug,
|
||||
form_integration_id: integration.id,
|
||||
}"
|
||||
href="#"
|
||||
class="flex px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline hover:text-gray-900 items-center"
|
||||
@click.prevent="showIntegrationEventsModal = true"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:clock"
|
||||
class="w-5 h-5 mr-2"
|
||||
/>
|
||||
Past Events
|
||||
</a>
|
||||
<a
|
||||
v-track.delete_form_integration_click="{
|
||||
form_integration_id: integration.id,
|
||||
}"
|
||||
href="#"
|
||||
class="flex px-4 py-2 text-md text-red-600 hover:bg-red-50 hover:no-underline items-center"
|
||||
@click.prevent="deleteFormIntegration(integration.id)"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:trash"
|
||||
class="w-5 h-5 mr-2"
|
||||
/>
|
||||
|
||||
Delete Integration
|
||||
</a>
|
||||
</dropdown>
|
||||
</div>
|
||||
|
||||
Delete Integration
|
||||
</a>
|
||||
</dropdown>
|
||||
<IntegrationModal
|
||||
v-if="form && integration && integrationTypeInfo"
|
||||
:form="form"
|
||||
|
|
@ -170,6 +179,14 @@ const showIntegrationModal = ref(false)
|
|||
const showIntegrationEventsModal = ref(false)
|
||||
const loadingDelete = ref(false)
|
||||
|
||||
const actionsComponent = computed(() => {
|
||||
if(integrationTypeInfo.value.actions_file_name) {
|
||||
return resolveComponent(integrationTypeInfo.value.actions_file_name)
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const deleteFormIntegration = (integrationid) => {
|
||||
alert.confirm("Do you really want to delete this form integration?", () => {
|
||||
opnFetch(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ const props = defineProps({
|
|||
integrationKey: { type: String, required: true },
|
||||
integration: { type: Object, required: true },
|
||||
formIntegrationId: { type: Number, required: false, default: null },
|
||||
providers: { type: Array, required: true }
|
||||
})
|
||||
|
||||
const alert = useAlert()
|
||||
|
|
@ -92,6 +93,7 @@ const initIntegrationData = () => {
|
|||
? formIntegration.value.logic
|
||||
: null
|
||||
: null,
|
||||
oauth_id: formIntegration.value?.oauth_id ?? null,
|
||||
})
|
||||
}
|
||||
initIntegrationData()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
<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
|
||||
class="mr-4 text-blue-500"
|
||||
>
|
||||
<Icon
|
||||
name="mdi:google"
|
||||
size="32px"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex space-x-3 font-semibold mr-2">
|
||||
{{ provider.name }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ provider.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<dropdown
|
||||
class="inline"
|
||||
>
|
||||
<template #trigger="{ toggle }">
|
||||
<v-button
|
||||
color="white"
|
||||
@click="toggle"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 inline -mt-1"
|
||||
viewBox="0 0 16 4"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<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"
|
||||
stroke="#344054"
|
||||
stroke-width="1.66667"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</v-button>
|
||||
</template>
|
||||
<a
|
||||
v-track.delete_provider_click="{
|
||||
provider_id: provider.id,
|
||||
}"
|
||||
href="#"
|
||||
class="flex px-4 py-2 text-md text-red-600 hover:bg-red-50 hover:no-underline items-center"
|
||||
@click.prevent="disconnect"
|
||||
>
|
||||
<Icon
|
||||
name="heroicons:trash"
|
||||
class="w-5 h-5 mr-2"
|
||||
/>
|
||||
|
||||
Disconnect
|
||||
</a>
|
||||
</dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
provider: Object
|
||||
})
|
||||
|
||||
const providersStore = useOAuthProvidersStore()
|
||||
const alert = useAlert()
|
||||
|
||||
function disconnect() {
|
||||
alert.confirm("Do you really want to disconnect this account?", () => {
|
||||
opnFetch(`/settings/providers/${props.provider.id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(() => {
|
||||
providersStore.remove(props.provider.id)
|
||||
})
|
||||
.catch((error) => {
|
||||
try {
|
||||
alert.error(error.data.message)
|
||||
} catch (e) {
|
||||
alert.error("An error occurred while disconnecting an account")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<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>
|
||||
Connect account
|
||||
</template>
|
||||
|
||||
<div class="px-4">
|
||||
<div
|
||||
v-for="service in services"
|
||||
:key="service.name"
|
||||
role="button"
|
||||
class="bg-gray-50 border border-gray-200 rounded-md transition-colors p-4 pb-2 items-center justify-center w-[170px] h-[110px] flex flex-col relative"
|
||||
:class="{
|
||||
'hover:bg-blue-50 group cursor-pointer': service.enabled,
|
||||
'cursor-not-allowed': !service.enabled,
|
||||
}"
|
||||
@click="connect(service)"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<div class="h-10 w-10 text-gray-500 group-hover:text-blue-500 transition-colors flex items-center">
|
||||
<Icon
|
||||
:name="service.icon"
|
||||
class=""
|
||||
size="40px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow flex items-center">
|
||||
<div class="text-gray-400 font-medium text-sm text-center">
|
||||
{{ service.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
show: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const providersStore = useOAuthProvidersStore()
|
||||
|
||||
const services = [
|
||||
{
|
||||
name: 'google',
|
||||
title: 'Google',
|
||||
icon: 'mdi:google',
|
||||
enabled: true
|
||||
},
|
||||
]
|
||||
|
||||
function connect(service) {
|
||||
providersStore.connect(service.name)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
"icon": "mdi:google-spreadsheet",
|
||||
"section_name": "Databases",
|
||||
"file_name": "GoogleSheetsIntegration",
|
||||
"is_pro": true,
|
||||
"coming_soon": true
|
||||
"actions_file_name": "GoogleSheetsIntegrationActions",
|
||||
"is_pro": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,8 @@ useOpnSeoMeta({
|
|||
|
||||
const alert = useAlert()
|
||||
|
||||
const oAuthProvidersStore = useOAuthProvidersStore()
|
||||
|
||||
const formIntegrationsStore = useFormIntegrationsStore()
|
||||
const integrationsLoading = computed(() => formIntegrationsStore.loading)
|
||||
const integrations = computed(
|
||||
|
|
@ -102,6 +104,7 @@ const selectedIntegration = ref(null)
|
|||
|
||||
onMounted(() => {
|
||||
formIntegrationsStore.fetchFormIntegrations(props.form.id)
|
||||
oAuthProvidersStore.fetchOAuthProviders(props.form.workspace_id)
|
||||
})
|
||||
|
||||
const openIntegrationModal = (itemKey) => {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ const tabsList = computed(() => {
|
|||
name: "Workspace Settings",
|
||||
route: "settings-workspace",
|
||||
},
|
||||
{
|
||||
name: "Connections",
|
||||
route: "settings-connections",
|
||||
},
|
||||
{
|
||||
name: "Password",
|
||||
route: "settings-password",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
<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">
|
||||
Connections
|
||||
</h3>
|
||||
<small class="text-gray-600">Manage your external connections.</small>
|
||||
</div>
|
||||
<v-button
|
||||
color="outline-blue"
|
||||
:loading="loading"
|
||||
@click="providerModal = 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>
|
||||
Connect new account
|
||||
</v-button>
|
||||
</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">
|
||||
<SettingsProviderCard
|
||||
v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
:provider="provider"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingsProviderModal
|
||||
:show="providerModal"
|
||||
@close="providerModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
useOpnSeoMeta({
|
||||
title: "Connections",
|
||||
})
|
||||
|
||||
definePageMeta({
|
||||
middleware: "auth",
|
||||
alias: [
|
||||
'/settings/connections/callback/:service'
|
||||
]
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const alert = useAlert()
|
||||
|
||||
const providerModal = ref(false)
|
||||
const providersStore = useOAuthProvidersStore()
|
||||
const providers = computed(() => providersStore.getAll)
|
||||
const loading = computed(() => providersStore.loading)
|
||||
|
||||
function handleCallback() {
|
||||
const code = route.query.code
|
||||
const service = route.params.service
|
||||
|
||||
if(!code || !service) {
|
||||
router.push('/settings/connections')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
opnFetch(`/settings/providers/callback/${service}`, {
|
||||
method: 'POST',
|
||||
params: {
|
||||
code
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
if(!data.intention) {
|
||||
router.push('/settings/connections')
|
||||
}
|
||||
else {
|
||||
router.push(data.intention)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
try {
|
||||
alert.error(error.data.message)
|
||||
} catch (e) {
|
||||
alert.error("An error occurred while connecting an account")
|
||||
}
|
||||
|
||||
router.push('/settings/connections')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleCallback()
|
||||
|
||||
providersStore.fetchOAuthProviders()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { defineStore } from "pinia"
|
||||
import { useContentStore } from "~/composables/stores/useContentStore.js"
|
||||
|
||||
export const providersEndpoint = "/open/providers"
|
||||
|
||||
export const useOAuthProvidersStore = defineStore("oauth_providers", () => {
|
||||
const contentStore = useContentStore()
|
||||
const alert = useAlert()
|
||||
|
||||
const fetchOAuthProviders = () => {
|
||||
contentStore.resetState()
|
||||
contentStore.startLoading()
|
||||
|
||||
return opnFetch(providersEndpoint).then(
|
||||
(data) => {
|
||||
contentStore.save(data)
|
||||
contentStore.stopLoading()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const connect = (service, redirect = false) => {
|
||||
contentStore.resetState()
|
||||
contentStore.startLoading()
|
||||
|
||||
const intention = new URL(window.location.href).pathname
|
||||
|
||||
opnFetch(`/settings/providers/connect/${service}`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
...redirect ? { intention } : {},
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
window.location.href = data.url
|
||||
})
|
||||
.catch((error) => {
|
||||
try {
|
||||
alert.error(error.data.message)
|
||||
} catch (e) {
|
||||
alert.error("An error occurred while connecting an account")
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
contentStore.stopLoading()
|
||||
})
|
||||
}
|
||||
|
||||
const providers = computed(() => contentStore.getAll.value)
|
||||
|
||||
return {
|
||||
...contentStore,
|
||||
fetchOAuthProviders,
|
||||
providers,
|
||||
connect
|
||||
}
|
||||
})
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
"aws/aws-sdk-php": "^3.183",
|
||||
"doctrine/dbal": "^3.4",
|
||||
"giggsey/libphonenumber-for-php": "^8.13",
|
||||
"google/apiclient": "^2.15.0",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
"jhumanj/laravel-model-stats": "^0.4.0",
|
||||
"laravel/cashier": "^13.4",
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
"openai-php/client": "^0.6.4",
|
||||
"propaganistas/laravel-disposable-email": "^2.2",
|
||||
"sentry/sentry-laravel": "^2.11.0",
|
||||
"spatie/laravel-data": "^3.12",
|
||||
"spatie/laravel-sitemap": "^6.0",
|
||||
"spatie/laravel-sluggable": "^3.0",
|
||||
"stevebauman/purify": "^v6.2.0",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -64,4 +64,10 @@ return [
|
|||
],
|
||||
|
||||
'crisp_website_id' => env('CRISP_WEBSITE_ID'),
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
'redirect' => env('GOOGLE_REDIRECT_URL'),
|
||||
]
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories\Integration;
|
||||
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class FormIntegrationFactory extends Factory
|
||||
{
|
||||
protected $model = FormIntegration::class;
|
||||
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'integration_id' => 'i_test',
|
||||
'status' => 'active',
|
||||
'logic' => [],
|
||||
'data' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\OAuthProvider;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class OAuthProviderFactory extends Factory
|
||||
{
|
||||
protected $model = OAuthProvider::class;
|
||||
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'provider' => 'google',
|
||||
'provider_user_id' => 'u_test',
|
||||
'email' => 'user@example.com',
|
||||
'name' => 'user',
|
||||
'access_token' => 'ac_test',
|
||||
'refresh_token' => 're_test',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?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('oauth_providers', function (Blueprint $table) {
|
||||
$table->string('email')->after('provider_user_id')->nullable();
|
||||
$table->string('name')->after('email')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('oauth_providers', function (Blueprint $table) {
|
||||
$table->dropColumn('email');
|
||||
$table->dropColumn('name');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?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('oauth_providers', function (Blueprint $table) {
|
||||
$table->timestamp('token_expires_at')->nullable()->after('refresh_token');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('oauth_providers', function (Blueprint $table) {
|
||||
$table->dropColumn('token_expires_at');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -15,6 +15,7 @@ use App\Http\Controllers\Forms\Integration\FormIntegrationsEventController;
|
|||
use App\Http\Controllers\Forms\Integration\FormZapierWebhookController;
|
||||
use App\Http\Controllers\Forms\PublicFormController;
|
||||
use App\Http\Controllers\Forms\RecordController;
|
||||
use App\Http\Controllers\OAuth\OAuthProviderController;
|
||||
use App\Http\Controllers\Settings\PasswordController;
|
||||
use App\Http\Controllers\Settings\ProfileController;
|
||||
use App\Http\Controllers\SubscriptionController;
|
||||
|
|
@ -42,8 +43,16 @@ Route::group(['middleware' => 'auth:api'], function () {
|
|||
Route::get('user', [UserController::class, 'current'])->name('user.current');
|
||||
Route::delete('user', [UserController::class, 'deleteAccount']);
|
||||
|
||||
Route::patch('settings/profile', [ProfileController::class, 'update']);
|
||||
Route::patch('settings/password', [PasswordController::class, 'update']);
|
||||
Route::prefix('/settings')->name('settings.')->group(function () {
|
||||
Route::patch('/profile', [ProfileController::class, 'update']);
|
||||
Route::patch('/password', [PasswordController::class, 'update']);
|
||||
|
||||
Route::prefix('/providers')->name('providers.')->group(function () {
|
||||
Route::post('/connect/{service}', [OAuthProviderController::class, 'connect'])->name('connect');
|
||||
Route::post('/callback/{service}', [OAuthProviderController::class, 'handleRedirect'])->name('callback');
|
||||
Route::delete('/{provider}', [OAuthProviderController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('subscription')->name('subscription.')->group(function () {
|
||||
Route::put('/update-customer-details', [SubscriptionController::class, 'updateStripeDetails'])->name('update-stripe-details');
|
||||
|
|
@ -55,6 +64,8 @@ Route::group(['middleware' => 'auth:api'], function () {
|
|||
});
|
||||
|
||||
Route::prefix('open')->name('open.')->group(function () {
|
||||
Route::get('/providers', [OAuthProviderController::class, 'index'])->name('index');
|
||||
|
||||
Route::get('/forms', [FormController::class, 'indexAll'])->name('forms.index-all');
|
||||
Route::get('/forms/{slug}', [FormController::class, 'show'])->name('forms.show');
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
use App\Integrations\Data\SpreadsheetData;
|
||||
use App\Integrations\Google\Google;
|
||||
use App\Integrations\Google\Sheets\SpreadsheetManager;
|
||||
use App\Models\Integration\FormIntegration;
|
||||
use App\Models\OAuthProvider;
|
||||
|
||||
use function PHPUnit\Framework\assertCount;
|
||||
use function PHPUnit\Framework\assertEquals;
|
||||
use function PHPUnit\Framework\assertSame;
|
||||
|
||||
test('build columns', function () {
|
||||
/** @var \App\Models\User $user */
|
||||
$user = $this->createUser();
|
||||
|
||||
/** @var \App\Models\Workspace $workspace */
|
||||
$workspace = $this->createUserWorkspace($user);
|
||||
|
||||
/** @var \App\Models\Forms $form */
|
||||
$form = $this->createForm($user, $workspace);
|
||||
|
||||
/** @var \App\Models\OAuthProvider $provider */
|
||||
$provider = OAuthProvider::factory()
|
||||
->for($user)
|
||||
->create();
|
||||
|
||||
/** @var FormIntegration $integration */
|
||||
$integration = FormIntegration::factory()
|
||||
->for($form)
|
||||
->for($provider, 'provider')
|
||||
->create([
|
||||
'data' => new SpreadsheetData(
|
||||
url: 'https://google.com',
|
||||
spreadsheet_id: 'sp_test',
|
||||
columns: []
|
||||
)
|
||||
]);
|
||||
|
||||
$google = new Google($integration);
|
||||
|
||||
$manager = new SpreadsheetManager($google, $integration);
|
||||
|
||||
$columns = $manager->buildColumns();
|
||||
|
||||
assertCount(14, $columns);
|
||||
|
||||
foreach($columns as $key => $column) {
|
||||
assertEquals($form->properties[$key]['id'], $column['id']);
|
||||
assertEquals($form->properties[$key]['name'], $column['name']);
|
||||
}
|
||||
});
|
||||
|
||||
test('update columns', function () {
|
||||
/** @var \App\Models\User $user */
|
||||
$user = $this->createUser();
|
||||
|
||||
/** @var \App\Models\Workspace $workspace */
|
||||
$workspace = $this->createUserWorkspace($user);
|
||||
|
||||
/** @var \App\Models\Forms $form */
|
||||
$form = $this->createForm($user, $workspace);
|
||||
|
||||
$form->update([
|
||||
'properties' => [
|
||||
['id' => '000', 'name' => 'First'],
|
||||
['id' => '001', 'name' => 'Second'],
|
||||
]
|
||||
]);
|
||||
|
||||
/** @var \App\Models\OAuthProvider $provider */
|
||||
$provider = OAuthProvider::factory()
|
||||
->for($user)
|
||||
->create();
|
||||
|
||||
/** @var FormIntegration $integration */
|
||||
$integration = FormIntegration::factory()
|
||||
->for($form)
|
||||
->for($provider, 'provider')
|
||||
->create([
|
||||
'data' => new SpreadsheetData(
|
||||
url: 'https://google.com',
|
||||
spreadsheet_id: 'sp_test',
|
||||
columns: [
|
||||
['id' => '000', 'name' => 'First'],
|
||||
['id' => '001', 'name' => 'Second'],
|
||||
]
|
||||
)
|
||||
]);
|
||||
|
||||
|
||||
$google = new Google($integration);
|
||||
$manager = new SpreadsheetManager($google, $integration);
|
||||
|
||||
$manager->buildColumns();
|
||||
|
||||
$form->update([
|
||||
'properties' => [
|
||||
['id' => '000', 'name' => 'First name'],
|
||||
['id' => '002', 'name' => 'Email'],
|
||||
]
|
||||
]);
|
||||
|
||||
$integration->refresh();
|
||||
$columns = $manager->buildColumns();
|
||||
|
||||
assertCount(3, $columns);
|
||||
assertEquals('First name', $columns[0]['name']);
|
||||
assertEquals('Second', $columns[1]['name']);
|
||||
assertEquals('Email', $columns[2]['name']);
|
||||
});
|
||||
|
||||
test('build row', function () {
|
||||
/** @var \App\Models\User $user */
|
||||
$user = $this->createUser();
|
||||
|
||||
/** @var \App\Models\Workspace $workspace */
|
||||
$workspace = $this->createUserWorkspace($user);
|
||||
|
||||
/** @var \App\Models\Forms $form */
|
||||
$form = $this->createForm($user, $workspace);
|
||||
|
||||
$form->update([
|
||||
'properties' => [
|
||||
['id' => '000', 'name' => 'First', 'type' => 'text'],
|
||||
['id' => '001', 'name' => 'Second', 'type' => 'text'],
|
||||
['id' => '002', 'name' => 'Third', 'type' => 'text'],
|
||||
]
|
||||
]);
|
||||
|
||||
/** @var \App\Models\OAuthProvider $provider */
|
||||
$provider = OAuthProvider::factory()
|
||||
->for($user)
|
||||
->create();
|
||||
|
||||
/** @var FormIntegration $integration */
|
||||
$integration = FormIntegration::factory()
|
||||
->for($form)
|
||||
->for($provider, 'provider')
|
||||
->create([
|
||||
'data' => new SpreadsheetData(
|
||||
url: 'https://google.com',
|
||||
spreadsheet_id: 'sp_test',
|
||||
columns: [
|
||||
['id' => '000', 'name' => 'First'],
|
||||
['id' => '001', 'name' => 'Second'],
|
||||
['id' => '002', 'name' => 'Third'],
|
||||
]
|
||||
)
|
||||
]);
|
||||
|
||||
|
||||
$google = new Google($integration);
|
||||
$manager = new SpreadsheetManager($google, $integration);
|
||||
|
||||
$submission = [
|
||||
'002' => 'Third value',
|
||||
'000' => 'First value',
|
||||
];
|
||||
|
||||
$row = $manager->buildRow($submission);
|
||||
|
||||
assertSame(['First value', '', 'Third value'], $row);
|
||||
});
|
||||
Loading…
Reference in New Issue