diff --git a/.phpactor.json b/.phpactor.json new file mode 100644 index 00000000..969af7a8 --- /dev/null +++ b/.phpactor.json @@ -0,0 +1,4 @@ +{ + "$schema": "/Applications/Tinkerwell.app/Contents/Resources/phpactor/phpactor.schema.json", + "language_server_phpstan.enabled": false +} \ No newline at end of file diff --git a/app/Events/Models/FormIntegrationCreated.php b/app/Events/Models/FormIntegrationCreated.php new file mode 100644 index 00000000..34791c48 --- /dev/null +++ b/app/Events/Models/FormIntegrationCreated.php @@ -0,0 +1,20 @@ +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) ]); } diff --git a/app/Http/Controllers/OAuth/OAuthProviderController.php b/app/Http/Controllers/OAuth/OAuthProviderController.php new file mode 100644 index 00000000..fadcbe0c --- /dev/null +++ b/app/Http/Controllers/OAuth/OAuthProviderController.php @@ -0,0 +1,64 @@ +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(); + } +} diff --git a/app/Http/Requests/Integration/FormIntegrationsRequest.php b/app/Http/Requests/Integration/FormIntegrationsRequest.php index 7174837d..128a4c0e 100644 --- a/app/Http/Requests/Integration/FormIntegrationsRequest.php +++ b/app/Http/Requests/Integration/FormIntegrationsRequest.php @@ -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'), ]); } } diff --git a/app/Http/Resources/FormIntegrationResource.php b/app/Http/Resources/FormIntegrationResource.php new file mode 100644 index 00000000..02bd90d3 --- /dev/null +++ b/app/Http/Resources/FormIntegrationResource.php @@ -0,0 +1,19 @@ + OAuthProviderResource::make($this->resource->provider), + ]; + } +} diff --git a/app/Http/Resources/OAuthProviderResource.php b/app/Http/Resources/OAuthProviderResource.php new file mode 100644 index 00000000..a7fa2b47 --- /dev/null +++ b/app/Http/Resources/OAuthProviderResource.php @@ -0,0 +1,37 @@ +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, + ), + ]; + } +} diff --git a/app/Http/Resources/OAuthProviderUserResource.php b/app/Http/Resources/OAuthProviderUserResource.php new file mode 100644 index 00000000..05cfa792 --- /dev/null +++ b/app/Http/Resources/OAuthProviderUserResource.php @@ -0,0 +1,19 @@ + $this->resource->name, + 'email' => $this->resource->email, + ]; + } +} diff --git a/app/Integrations/Data/SpreadsheetData.php b/app/Integrations/Data/SpreadsheetData.php new file mode 100644 index 00000000..3ea9faba --- /dev/null +++ b/app/Integrations/Data/SpreadsheetData.php @@ -0,0 +1,15 @@ +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); + } +} diff --git a/app/Integrations/Google/Sheets/SpreadsheetManager.php b/app/Integrations/Google/Sheets/SpreadsheetManager.php new file mode 100644 index 00000000..6c7a9928 --- /dev/null +++ b/app/Integrations/Google/Sheets/SpreadsheetManager.php @@ -0,0 +1,189 @@ +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"; + } +} diff --git a/app/Service/Forms/Integrations/AbstractIntegrationHandler.php b/app/Integrations/Handlers/AbstractIntegrationHandler.php similarity index 92% rename from app/Service/Forms/Integrations/AbstractIntegrationHandler.php rename to app/Integrations/Handlers/AbstractIntegrationHandler.php index 344b63e2..b637e99c 100644 --- a/app/Service/Forms/Integrations/AbstractIntegrationHandler.php +++ b/app/Integrations/Handlers/AbstractIntegrationHandler.php @@ -1,6 +1,6 @@ client = new Google($formIntegration); + } + + public function handle(): void + { + $this->client->sheets() + ->create($this->formIntegration->form); + } +} diff --git a/app/Integrations/Handlers/GoogleSheetsIntegration.php b/app/Integrations/Handlers/GoogleSheetsIntegration.php new file mode 100644 index 00000000..c04f4c7a --- /dev/null +++ b/app/Integrations/Handlers/GoogleSheetsIntegration.php @@ -0,0 +1,72 @@ +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(); + } +} diff --git a/app/Service/Forms/Integrations/SlackIntegration.php b/app/Integrations/Handlers/SlackIntegration.php similarity index 98% rename from app/Service/Forms/Integrations/SlackIntegration.php rename to app/Integrations/Handlers/SlackIntegration.php index 926d7829..f0673f23 100644 --- a/app/Service/Forms/Integrations/SlackIntegration.php +++ b/app/Integrations/Handlers/SlackIntegration.php @@ -1,6 +1,6 @@ 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(); + } +} diff --git a/app/Integrations/OAuth/OAuthProviderService.php b/app/Integrations/OAuth/OAuthProviderService.php new file mode 100644 index 00000000..2ef4bd7f --- /dev/null +++ b/app/Integrations/OAuth/OAuthProviderService.php @@ -0,0 +1,18 @@ + new OAuthGoogleDriver() + }; + } +} diff --git a/app/Listeners/Forms/FormIntegrationCreatedHandler.php b/app/Listeners/Forms/FormIntegrationCreatedHandler.php new file mode 100644 index 00000000..f0ee9e3a --- /dev/null +++ b/app/Listeners/Forms/FormIntegrationCreatedHandler.php @@ -0,0 +1,37 @@ +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(); + } +} diff --git a/app/Listeners/Forms/NotifyFormSubmission.php b/app/Listeners/Forms/NotifyFormSubmission.php index a88fb859..e4774746 100644 --- a/app/Listeners/Forms/NotifyFormSubmission.php +++ b/app/Listeners/Forms/NotifyFormSubmission.php @@ -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!'); diff --git a/app/Models/Integration/FormIntegration.php b/app/Models/Integration/FormIntegration.php index 37cfdafa..97deeefe 100644 --- a/app/Models/Integration/FormIntegration.php +++ b/app/Models/Integration/FormIntegration.php @@ -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); diff --git a/app/Models/OAuthProvider.php b/app/Models/OAuthProvider.php index 7c6139cf..4f780fe1 100644 --- a/app/Models/OAuthProvider.php +++ b/app/Models/OAuthProvider.php @@ -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 */ diff --git a/app/Models/Workspace.php b/app/Models/Workspace.php index 89c24ced..d2e39d9e 100644 --- a/app/Models/Workspace.php +++ b/app/Models/Workspace.php @@ -187,4 +187,5 @@ class Workspace extends Model implements CachableAttributes { return $this->hasMany(Form::class); } + } diff --git a/app/Policies/OAuthProviderPolicy.php b/app/Policies/OAuthProviderPolicy.php new file mode 100644 index 00000000..e3b0dce9 --- /dev/null +++ b/app/Policies/OAuthProviderPolicy.php @@ -0,0 +1,82 @@ +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); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index afd477c8..ee8025ee 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -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, ]; /** diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index ab6ba28d..81817985 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -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, ], diff --git a/app/helpers.php b/app/helpers.php index b8f030e7..e8cc08a9 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,11 +1,13 @@
- Coming Soon... + + + + + + Connect Google account +
@@ -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) +} diff --git a/client/components/open/integrations/components/GoogleSheetsIntegrationActions.vue b/client/components/open/integrations/components/GoogleSheetsIntegrationActions.vue new file mode 100644 index 00000000..63d503c9 --- /dev/null +++ b/client/components/open/integrations/components/GoogleSheetsIntegrationActions.vue @@ -0,0 +1,28 @@ + + + diff --git a/client/components/open/integrations/components/IntegrationCard.vue b/client/components/open/integrations/components/IntegrationCard.vue index 025ed197..efd4ac4f 100644 --- a/client/components/open/integrations/components/IntegrationCard.vue +++ b/client/components/open/integrations/components/IntegrationCard.vue @@ -2,7 +2,7 @@
-
+
- -
- - - - - Edit - - - - Past Events - - - + - Delete Integration - - { + 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( diff --git a/client/components/open/integrations/components/IntegrationModal.vue b/client/components/open/integrations/components/IntegrationModal.vue index 61d2a5c5..e3880a10 100644 --- a/client/components/open/integrations/components/IntegrationModal.vue +++ b/client/components/open/integrations/components/IntegrationModal.vue @@ -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() diff --git a/client/components/settings/ProviderCard.vue b/client/components/settings/ProviderCard.vue new file mode 100644 index 00000000..471e50b9 --- /dev/null +++ b/client/components/settings/ProviderCard.vue @@ -0,0 +1,109 @@ + + + diff --git a/client/components/settings/ProviderModal.vue b/client/components/settings/ProviderModal.vue new file mode 100644 index 00000000..4ca6ac5f --- /dev/null +++ b/client/components/settings/ProviderModal.vue @@ -0,0 +1,81 @@ + + + diff --git a/client/data/forms/integrations.json b/client/data/forms/integrations.json index cb2812db..e193ad15 100644 --- a/client/data/forms/integrations.json +++ b/client/data/forms/integrations.json @@ -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 } } diff --git a/client/pages/forms/[slug]/show/integrations/index.vue b/client/pages/forms/[slug]/show/integrations/index.vue index d9f0f07e..b5f8f2f9 100644 --- a/client/pages/forms/[slug]/show/integrations/index.vue +++ b/client/pages/forms/[slug]/show/integrations/index.vue @@ -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) => { diff --git a/client/pages/settings.vue b/client/pages/settings.vue index 7b00cda8..75175640 100644 --- a/client/pages/settings.vue +++ b/client/pages/settings.vue @@ -66,6 +66,10 @@ const tabsList = computed(() => { name: "Workspace Settings", route: "settings-workspace", }, + { + name: "Connections", + route: "settings-connections", + }, { name: "Password", route: "settings-password", diff --git a/client/pages/settings/connections.vue b/client/pages/settings/connections.vue new file mode 100644 index 00000000..82a6d23e --- /dev/null +++ b/client/pages/settings/connections.vue @@ -0,0 +1,116 @@ + + + diff --git a/client/stores/oauth_providers.js b/client/stores/oauth_providers.js new file mode 100644 index 00000000..126107a8 --- /dev/null +++ b/client/stores/oauth_providers.js @@ -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 + } +}) diff --git a/composer.json b/composer.json index 3ef3e48b..510fded1 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index d120247d..b03e8437 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,867 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6d742b79739f9d1ca839f2b9c52aaa8f", + "content-hash": "8616475a141747e8021db6c7b53c2062", "packages": [ + { + "name": "amphp/amp", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "138801fb68cfc9c329da8a7b39d01ce7291ee4b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/138801fb68cfc9c329da8a7b39d01ce7291ee4b0", + "reference": "138801fb68cfc9c329da8a7b39d01ce7291ee4b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.0.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-05-10T21:37:46+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "daa00f2efdbd71565bf64ffefa89e37542addf93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/daa00f2efdbd71565bf64ffefa89e37542addf93", + "reference": "daa00f2efdbd71565bf64ffefa89e37542addf93", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-02-17T04:49:38+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/758266b0ea7470e2e42cd098493bc6d6c7100cf7", + "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/windows-registry": "^1.0.1", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.2.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-06-02T19:54:12+00:00" + }, + { + "name": "amphp/parallel", + "version": "v2.2.9", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/73d293f1fc4df1bebc3c4fce1432e82dd7032238", + "reference": "73d293f1fc4df1bebc3c4fce1432e82dd7032238", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.2.9" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-24T18:27:44+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/f1c2ce35d27ae86ead018adb803eccca7421dd9b", + "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-10T14:48:16+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "375ef5b54a0d12c38e12728dde05a55e30f2fbec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/375ef5b54a0d12c38e12728dde05a55e30f2fbec", + "reference": "375ef5b54a0d12c38e12728dde05a55e30f2fbec", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.2.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-12T01:00:01+00:00" + }, + { + "name": "amphp/windows-registry", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/windows-registry.git", + "reference": "0d569e8f256cca974e3842b6e78b4e434bf98306" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/windows-registry/zipball/0d569e8f256cca974e3842b6e78b4e434bf98306", + "reference": "0d569e8f256cca974e3842b6e78b4e434bf98306", + "shasum": "" + }, + "require": { + "amphp/byte-stream": "^2", + "amphp/process": "^2", + "php": ">=8.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\WindowsRegistry\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Windows Registry Reader.", + "support": { + "issues": "https://github.com/amphp/windows-registry/issues", + "source": "https://github.com/amphp/windows-registry/tree/v1.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-01-30T23:01:51+00:00" + }, { "name": "aws/aws-crt-php", "version": "v1.2.5", @@ -426,6 +1285,50 @@ ], "time": "2023-08-31T09:50:34+00:00" }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.2", @@ -1527,6 +2430,179 @@ }, "time": "2024-04-18T19:31:19+00:00" }, + { + "name": "google/apiclient", + "version": "v2.16.0", + "source": { + "type": "git", + "url": "https://github.com/googleapis/google-api-php-client.git", + "reference": "017400f609c1fb71ab5ad824c50eabd4c3eaf779" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/google-api-php-client/zipball/017400f609c1fb71ab5ad824c50eabd4c3eaf779", + "reference": "017400f609c1fb71ab5ad824c50eabd4c3eaf779", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "~6.0", + "google/apiclient-services": "~0.350", + "google/auth": "^1.37", + "guzzlehttp/guzzle": "^6.5.8||^7.4.5", + "guzzlehttp/psr7": "^1.9.1||^2.2.1", + "monolog/monolog": "^2.9||^3.0", + "php": "^7.4|^8.0", + "phpseclib/phpseclib": "^3.0.36" + }, + "require-dev": { + "cache/filesystem-adapter": "^1.1", + "composer/composer": "^1.10.23", + "phpcompatibility/php-compatibility": "^9.2", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.8", + "symfony/css-selector": "~2.1", + "symfony/dom-crawler": "~2.1" + }, + "suggest": { + "cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/aliases.php" + ], + "psr-4": { + "Google\\": "src/" + }, + "classmap": [ + "src/aliases.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Client library for Google APIs", + "homepage": "http://developers.google.com/api-client-library/php", + "keywords": [ + "google" + ], + "support": { + "issues": "https://github.com/googleapis/google-api-php-client/issues", + "source": "https://github.com/googleapis/google-api-php-client/tree/v2.16.0" + }, + "time": "2024-04-24T00:59:47+00:00" + }, + { + "name": "google/apiclient-services", + "version": "v0.358.0", + "source": { + "type": "git", + "url": "https://github.com/googleapis/google-api-php-client-services.git", + "reference": "a6daf60ee25cb45b6e3dbd04b62d1df39a609fbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/a6daf60ee25cb45b6e3dbd04b62d1df39a609fbd", + "reference": "a6daf60ee25cb45b6e3dbd04b62d1df39a609fbd", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ], + "psr-4": { + "Google\\Service\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Client library for Google APIs", + "homepage": "http://developers.google.com/api-client-library/php", + "keywords": [ + "google" + ], + "support": { + "issues": "https://github.com/googleapis/google-api-php-client-services/issues", + "source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.358.0" + }, + "time": "2024-06-03T01:02:16+00:00" + }, + { + "name": "google/auth", + "version": "v1.40.0", + "source": { + "type": "git", + "url": "https://github.com/googleapis/google-auth-library-php.git", + "reference": "bff9f2d01677e71a98394b5ac981b99523df5178" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/bff9f2d01677e71a98394b5ac981b99523df5178", + "reference": "bff9f2d01677e71a98394b5ac981b99523df5178", + "shasum": "" + }, + "require": { + "firebase/php-jwt": "^6.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.4.5", + "php": "^8.0", + "psr/cache": "^2.0||^3.0", + "psr/http-message": "^1.1||^2.0" + }, + "require-dev": { + "guzzlehttp/promises": "^2.0", + "kelvinmo/simplejwt": "0.7.1", + "phpseclib/phpseclib": "^3.0.35", + "phpspec/prophecy-phpunit": "^2.1", + "phpunit/phpunit": "^9.6", + "sebastian/comparator": ">=1.2.3", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^6.0||^7.0", + "webmozart/assert": "^1.11" + }, + "suggest": { + "phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2." + }, + "type": "library", + "autoload": { + "psr-4": { + "Google\\Auth\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Google Auth Library for PHP", + "homepage": "http://github.com/google/google-auth-library-php", + "keywords": [ + "Authentication", + "google", + "oauth2" + ], + "support": { + "docs": "https://googleapis.github.io/google-auth-library-php/main/", + "issues": "https://github.com/googleapis/google-auth-library-php/issues", + "source": "https://github.com/googleapis/google-auth-library-php/tree/v1.40.0" + }, + "time": "2024-05-31T19:16:15+00:00" + }, { "name": "graham-campbell/manager", "version": "v4.7.0", @@ -2464,6 +3540,64 @@ ], "time": "2023-08-24T08:08:34+00:00" }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, { "name": "laravel/cashier", "version": "v13.17.0", @@ -4008,6 +5142,180 @@ }, "time": "2022-04-15T14:02:14+00:00" }, + { + "name": "league/uri", + "version": "7.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/bedb6e55eff0c933668addaa7efa1e1f2c417cc4", + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.3", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.4.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-03-23T07:42:40+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/8d43ef5c841032c87e2de015972c06f3865ef718", + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-03-23T07:42:40+00:00" + }, { "name": "maatwebsite/excel", "version": "3.1.55", @@ -5926,6 +7234,117 @@ }, "time": "2024-03-15T13:55:21+00:00" }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "153ae662783729388a584b4361f2545e4d841e3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" + }, + "time": "2024-02-23T11:10:43+00:00" + }, { "name": "phpoffice/phpspreadsheet", "version": "1.29.0", @@ -6216,6 +7635,53 @@ ], "time": "2024-03-03T02:14:58+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.29.1", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" + }, + "time": "2024-05-31T08:52:43+00:00" + }, { "name": "propaganistas/laravel-disposable-email", "version": "2.2.18", @@ -7056,6 +8522,78 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "revolt/event-loop", + "version": "v1.0.6", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "25de49af7223ba039f64da4ae9a28ec2d10d0254" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/25de49af7223ba039f64da4ae9a28ec2d10d0254", + "reference": "25de49af7223ba039f64da4ae9a28ec2d10d0254", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.6" + }, + "time": "2023-11-30T05:34:44+00:00" + }, { "name": "riverline/multipart-parser", "version": "2.1.2", @@ -7680,6 +9218,91 @@ }, "time": "2024-05-16T08:48:33+00:00" }, + { + "name": "spatie/laravel-data", + "version": "3.12.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-data.git", + "reference": "d44e04839407bc32b029be59ba80090a5f720e91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/d44e04839407bc32b029be59ba80090a5f720e91", + "reference": "d44e04839407bc32b029be59ba80090a5f720e91", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.30|^10.0|^11.0", + "php": "^8.1", + "phpdocumentor/type-resolver": "^1.5", + "spatie/laravel-package-tools": "^1.9.0", + "spatie/php-structure-discoverer": "^2.0" + }, + "require-dev": { + "fakerphp/faker": "^1.14", + "friendsofphp/php-cs-fixer": "^3.0", + "inertiajs/inertia-laravel": "^0.6.3", + "mockery/mockery": "^1.6", + "nesbot/carbon": "^2.63", + "nette/php-generator": "^3.5", + "nunomaduro/larastan": "^2.0", + "orchestra/testbench": "^7.6|^8.0", + "pestphp/pest": "^1.22", + "pestphp/pest-plugin-laravel": "^1.3", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpunit/phpunit": "^9.3", + "spatie/invade": "^1.0", + "spatie/laravel-typescript-transformer": "^2.1.6", + "spatie/pest-plugin-snapshots": "^1.1", + "spatie/phpunit-snapshot-assertions": "^4.2", + "spatie/test-time": "^1.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelData\\LaravelDataServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\LaravelData\\": "src", + "Spatie\\LaravelData\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "Create unified resources and data transfer objects", + "homepage": "https://github.com/spatie/laravel-data", + "keywords": [ + "laravel", + "laravel-data", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-data/issues", + "source": "https://github.com/spatie/laravel-data/tree/3.12.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-04-24T09:27:45+00:00" + }, { "name": "spatie/laravel-package-tools", "version": "1.16.4", @@ -7873,6 +9496,86 @@ ], "time": "2024-02-26T08:33:29+00:00" }, + { + "name": "spatie/php-structure-discoverer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/php-structure-discoverer.git", + "reference": "d2e4e6cba962ce2a058ea415a123dd84b37765ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/php-structure-discoverer/zipball/d2e4e6cba962ce2a058ea415a123dd84b37765ac", + "reference": "d2e4e6cba962ce2a058ea415a123dd84b37765ac", + "shasum": "" + }, + "require": { + "amphp/amp": "^v3.0", + "amphp/parallel": "^2.2", + "illuminate/collections": "^9.30|^10.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.4.3", + "symfony/finder": "^6.0|^7.0" + }, + "require-dev": { + "illuminate/console": "^9.30|^10.0", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^6.0", + "nunomaduro/larastan": "^2.0.1", + "orchestra/testbench": "^7.0|^8.0", + "pestphp/pest": "^1.21", + "pestphp/pest-plugin-laravel": "^1.1", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^9.5", + "spatie/laravel-ray": "^1.26" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\StructureDiscoverer\\StructureDiscovererServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\StructureDiscoverer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "Automatically discover structures within your PHP application", + "homepage": "https://github.com/spatie/php-structure-discoverer", + "keywords": [ + "discover", + "laravel", + "php", + "php-structure-discoverer" + ], + "support": { + "issues": "https://github.com/spatie/php-structure-discoverer/issues", + "source": "https://github.com/spatie/php-structure-discoverer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/LaravelAutoDiscoverer", + "type": "github" + } + ], + "time": "2024-01-08T21:01:26+00:00" + }, { "name": "spatie/robots-txt", "version": "2.2.0", @@ -13113,164 +14816,6 @@ }, "time": "2023-10-20T12:21:20+00:00" }, - { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-2.x": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" - } - ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", - "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" - ], - "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" - }, - "time": "2020-06-27T09:03:43+00:00" - }, - { - "name": "phpdocumentor/type-resolver", - "version": "1.8.2", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "153ae662783729388a584b4361f2545e4d841e3c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", - "reference": "153ae662783729388a584b4361f2545e4d841e3c", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.3 || ^8.0", - "phpdocumentor/reflection-common": "^2.0", - "phpstan/phpdoc-parser": "^1.13" - }, - "require-dev": { - "ext-tokenizer": "*", - "phpbench/phpbench": "^1.2", - "phpstan/extension-installer": "^1.1", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpunit/phpunit": "^9.5", - "rector/rector": "^0.13.9", - "vimeo/psalm": "^4.25" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" - }, - "time": "2024-02-23T11:10:43+00:00" - }, - { - "name": "phpstan/phpdoc-parser", - "version": "1.29.1", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "doctrine/annotations": "^2.0", - "nikic/php-parser": "^4.15", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.5", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5", - "symfony/process": "^5.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "PHPStan\\PhpDocParser\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPDoc parser with support for nullable, intersection and generic types", - "support": { - "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" - }, - "time": "2024-05-31T08:52:43+00:00" - }, { "name": "phpstan/phpstan", "version": "1.11.3", diff --git a/config/services.php b/config/services.php index d3e04a3f..f056acb6 100644 --- a/config/services.php +++ b/config/services.php @@ -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'), + ] ]; diff --git a/database/factories/Integration/FormIntegrationFactory.php b/database/factories/Integration/FormIntegrationFactory.php new file mode 100644 index 00000000..e5b31162 --- /dev/null +++ b/database/factories/Integration/FormIntegrationFactory.php @@ -0,0 +1,21 @@ + 'i_test', + 'status' => 'active', + 'logic' => [], + 'data' => [], + ]; + } +} diff --git a/database/factories/OAuthProviderFactory.php b/database/factories/OAuthProviderFactory.php new file mode 100644 index 00000000..f73f34c7 --- /dev/null +++ b/database/factories/OAuthProviderFactory.php @@ -0,0 +1,23 @@ + 'google', + 'provider_user_id' => 'u_test', + 'email' => 'user@example.com', + 'name' => 'user', + 'access_token' => 'ac_test', + 'refresh_token' => 're_test', + ]; + } +} diff --git a/database/migrations/2024_05_10_063347_add_user_data_to_oauth_providers_table.php b/database/migrations/2024_05_10_063347_add_user_data_to_oauth_providers_table.php new file mode 100644 index 00000000..698db9f4 --- /dev/null +++ b/database/migrations/2024_05_10_063347_add_user_data_to_oauth_providers_table.php @@ -0,0 +1,33 @@ +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'); + }); + } +}; diff --git a/database/migrations/2024_05_29_002132_add_expires_at_to_oauth_providers_table.php b/database/migrations/2024_05_29_002132_add_expires_at_to_oauth_providers_table.php new file mode 100644 index 00000000..b7cf4393 --- /dev/null +++ b/database/migrations/2024_05_29_002132_add_expires_at_to_oauth_providers_table.php @@ -0,0 +1,31 @@ +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'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 9cdb8e8a..636a6d70 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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'); diff --git a/tests/Feature/Integrations/Google/Sheets/SpreadsheetManagerTest.php b/tests/Feature/Integrations/Google/Sheets/SpreadsheetManagerTest.php new file mode 100644 index 00000000..42b0c973 --- /dev/null +++ b/tests/Feature/Integrations/Google/Sheets/SpreadsheetManagerTest.php @@ -0,0 +1,164 @@ +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); +});