diff --git a/api/.env.example b/api/.env.example index 9169930d..89e525ba 100644 --- a/api/.env.example +++ b/api/.env.example @@ -94,4 +94,7 @@ GOOGLE_AUTH_REDIRECT_URL=http://localhost:3000/oauth/google/callback GOOGLE_FONTS_API_KEY= +TELEGRAM_BOT_ID= +TELEGRAM_BOT_TOKEN= + ZAPIER_ENABLED=false diff --git a/api/app/Http/Controllers/Content/FeatureFlagsController.php b/api/app/Http/Controllers/Content/FeatureFlagsController.php index 711c40b6..046d3ae3 100644 --- a/api/app/Http/Controllers/Content/FeatureFlagsController.php +++ b/api/app/Http/Controllers/Content/FeatureFlagsController.php @@ -28,6 +28,9 @@ class FeatureFlagsController extends Controller 'fonts' => !empty(config('services.google.fonts_api_key')), 'auth' => !empty(config('services.google.client_id')) && !empty(config('services.google.client_secret')), ], + 'telegram' => [ + 'bot_id' => config('services.telegram.bot_id') ?? false + ] ], 'integrations' => [ 'zapier' => config('services.zapier.enabled'), diff --git a/api/app/Http/Controllers/Settings/OAuthProviderController.php b/api/app/Http/Controllers/Settings/OAuthProviderController.php index 6f6d281b..e17ac927 100644 --- a/api/app/Http/Controllers/Settings/OAuthProviderController.php +++ b/api/app/Http/Controllers/Settings/OAuthProviderController.php @@ -56,6 +56,41 @@ class OAuthProviderController extends Controller return OAuthProviderResource::make($provider); } + public function handleWidgetRedirect(OAuthProviderService $service, Request $request) + { + $driver = $service->getDriver(); + + if (!$driver instanceof \App\Integrations\OAuth\Drivers\Contracts\WidgetOAuthDriver) { + abort(400, 'This provider does not support widget authentication'); + } + + $requestData = $request->all(); + + if (!$driver->verifyWidgetData($requestData)) { + abort(400, 'Invalid data signature'); + } + + $userData = $driver->getUserFromWidgetData($requestData); + + $provider = OAuthProvider::query() + ->updateOrCreate( + [ + 'user_id' => Auth::id(), + 'provider' => $service, + 'provider_user_id' => $userData['id'], + ], + [ + 'access_token' => $userData['access_token'], + 'refresh_token' => $userData['refresh_token'] ?? '', + 'name' => $userData['name'], + 'email' => $userData['email'], + 'scopes' => $userData['scopes'] + ] + ); + + return OAuthProviderResource::make($provider); + } + public function destroy(OAuthProvider $provider) { $this->authorize('delete', $provider); diff --git a/api/app/Http/Requests/Integration/FormIntegrationsRequest.php b/api/app/Http/Requests/Integration/FormIntegrationsRequest.php index 7856548e..ed7069d0 100644 --- a/api/app/Http/Requests/Integration/FormIntegrationsRequest.php +++ b/api/app/Http/Requests/Integration/FormIntegrationsRequest.php @@ -97,4 +97,17 @@ class FormIntegrationsRequest extends FormRequest 'oauth_id' => $this->validated('oauth_id'), ]); } + + /** + * Get the error messages for the defined validation rules. + * + * @return array + */ + public function messages(): array + { + return [ + 'oauth_id.required' => 'Please select a connected account for this integration.', + 'oauth_id.exists' => 'The selected account is not valid or no longer connected.', + ]; + } } diff --git a/api/app/Integrations/Handlers/AbstractIntegrationHandler.php b/api/app/Integrations/Handlers/AbstractIntegrationHandler.php index 650e4fd7..3b20ca8c 100644 --- a/api/app/Integrations/Handlers/AbstractIntegrationHandler.php +++ b/api/app/Integrations/Handlers/AbstractIntegrationHandler.php @@ -18,6 +18,7 @@ abstract class AbstractIntegrationHandler protected $form = null; protected $submissionData = null; protected $integrationData = null; + protected $provider = null; public function __construct( protected FormSubmitted $event, @@ -27,6 +28,7 @@ abstract class AbstractIntegrationHandler $this->form = $event->form; $this->submissionData = $event->data; $this->integrationData = $formIntegration->data; + $this->provider = $formIntegration->provider; } protected function getProviderName(): string diff --git a/api/app/Integrations/Handlers/TelegramIntegration.php b/api/app/Integrations/Handlers/TelegramIntegration.php new file mode 100644 index 00000000..ed9d7ff1 --- /dev/null +++ b/api/app/Integrations/Handlers/TelegramIntegration.php @@ -0,0 +1,169 @@ + 'boolean', + 'include_hidden_fields_submission_data' => ['nullable', 'boolean'], + 'link_open_form' => 'boolean', + 'link_edit_form' => 'boolean', + 'views_submissions_count' => 'boolean', + 'link_edit_submission' => 'boolean' + ]; + } + + protected function getChatId(): ?string + { + return $this->provider?->provider_user_id; + } + + protected function getWebhookUrl(): ?string + { + $token = config('services.telegram.bot_token'); + if (!$token) { + return null; + } + return self::TELEGRAM_API_BASE . $token . '/sendMessage'; + } + + protected function shouldRun(): bool + { + $hasValidProvider = $this->formIntegration->oauth_id && $this->provider; + $shouldRun = !is_null($this->getWebhookUrl()) && $hasValidProvider && $this->form->is_pro && parent::shouldRun(); + return $shouldRun; + } + + protected function getWebhookData(): array + { + $settings = (array) $this->integrationData ?? []; + $messageParts = []; + + $formatter = (new FormSubmissionFormatter($this->form, $this->submissionData))->outputStringsOnly(); + if (Arr::get($settings, 'include_hidden_fields_submission_data', false)) { + $formatter->showHiddenFields(); + } + $formattedData = $formatter->getFieldsWithValue(); + + $mentionMessage = Arr::get($settings, 'message', 'New form submission'); + $parsedMentionMessage = (new MentionParser($mentionMessage, $formattedData))->parse(); + $messageParts[] = $this->escapeMarkdownV2($parsedMentionMessage); + + if (Arr::get($settings, 'include_submission_data', true)) { + $messageParts[] = "\n\nšŸ“ *Submission Details:*"; + foreach ($formattedData as $field) { + $fieldName = ucfirst($field['name']); + $fieldValue = is_array($field['value']) ? implode(', ', $field['value']) : $field['value']; + $messageParts[] = "*" . $this->escapeMarkdownV2($fieldName) . "*: " . $this->escapeMarkdownV2($fieldValue ?? ''); + } + } + + if (Arr::get($settings, 'views_submissions_count', true)) { + $messageParts[] = "\nšŸ“Š *Form Statistics:*"; + $messageParts[] = "šŸ‘€ *Views*: " . (string) $this->form->views_count; + $messageParts[] = "šŸ–Šļø *Submissions*: " . (string) $this->form->submissions_count; + } + + $links = []; + if (Arr::get($settings, 'link_open_form', true)) { + $url = $this->escapeMarkdownV2($this->form->share_url); + $links[] = "[šŸ”— Open Form](" . $url . ")"; + } + if (Arr::get($settings, 'link_edit_form', true)) { + $url = $this->escapeMarkdownV2(front_url('forms/' . $this->form->slug . '/show')); + $links[] = "[āœļø Edit Form](" . $url . ")"; + } + if (Arr::get($settings, 'link_edit_submission', true) && $this->form->editable_submissions) { + $submissionId = Hashids::encode($this->submissionData['submission_id']); + $buttonText = $this->escapeMarkdownV2($this->form->editable_submissions_button_text); + $url = $this->escapeMarkdownV2($this->form->share_url . "?submission_id=" . $submissionId); + $links[] = "[āœļø " . $buttonText . "](" . $url . ")"; + } + if (count($links) > 0) { + $messageParts[] = "\n" . implode("\n", $links); + } + + $finalMessageText = implode("\n", $messageParts); + + $payload = [ + 'chat_id' => $this->getChatId(), + 'text' => $finalMessageText, + 'parse_mode' => 'MarkdownV2' + ]; + + return $payload; + } + + /** + * Escape special characters for Telegram MarkdownV2 format + * @see https://core.telegram.org/bots/api#markdownv2-style + */ + protected function escapeMarkdownV2(string $text): string + { + $specialChars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']; + $text = str_replace('\\', '\\\\', $text); + return str_replace($specialChars, array_map(fn ($char) => '\\' . $char, $specialChars), $text); + } + + public static function isOAuthRequired(): bool + { + return true; + } + + public function handle(): void + { + if (!$this->shouldRun()) { + return; + } + $url = $this->getWebhookUrl(); + if (!$url) { + logger()->error('TelegramIntegration failed: Missing bot token.', [ + 'form_id' => $this->form?->id, + 'integration_id' => $this->formIntegration?->id, + ]); + return; + } + + $data = $this->getWebhookData(); + if (empty($data['chat_id'])) { + logger()->error('TelegramIntegration failed: Missing chat_id.', [ + 'form_id' => $this->form?->id, + 'integration_id' => $this->formIntegration?->id, + 'provider_id' => $this->provider?->id + ]); + return; + } + + try { + $response = Http::post($url, $data); + + if ($response->failed()) { + logger()->warning('TelegramIntegration request failed', [ + 'form_id' => $this->form->id, + 'integration_id' => $this->formIntegration->id, + 'status' => $response->status(), + 'response' => $response->json() ?? $response->body() + ]); + } + } catch (\Exception $e) { + logger()->error('TelegramIntegration failed during API call', [ + 'form_id' => $this->form->id, + 'integration_id' => $this->formIntegration->id, + 'message' => $e->getMessage(), + 'exception' => $e + ]); + } + } +} diff --git a/api/app/Integrations/OAuth/Drivers/Contracts/WidgetOAuthDriver.php b/api/app/Integrations/OAuth/Drivers/Contracts/WidgetOAuthDriver.php new file mode 100644 index 00000000..a2717657 --- /dev/null +++ b/api/app/Integrations/OAuth/Drivers/Contracts/WidgetOAuthDriver.php @@ -0,0 +1,29 @@ +redirectUrl = $url; + return $this; + } + + public function setScopes(array $scopes): self + { + $this->scopes = $scopes; + return $this; + } + + public function getUser(): User + { + throw new \Exception('Use getUserFromWidgetData for Widget based authentication'); + } + + public function canCreateUser(): bool + { + return true; + } + + public function fullScopes(): self + { + return $this; + } + + public function isWidgetBased(): bool + { + return true; + } + + public function verifyWidgetData(array $data): bool + { + $checkHash = $data['hash']; + unset($data['hash']); + + $dataCheckArr = []; + foreach ($data as $key => $value) { + $dataCheckArr[] = $key . '=' . $value; + } + + sort($dataCheckArr); + $dataCheckString = implode("\n", $dataCheckArr); + $secretKey = hash('sha256', config('services.telegram.bot_token'), true); + $hash = hash_hmac('sha256', $dataCheckString, $secretKey); + + return hash_equals($hash, $checkHash); + } + + public function getUserFromWidgetData(array $data): array + { + return [ + 'id' => $data['id'], + 'name' => trim($data['first_name'] . ' ' . ($data['last_name'] ?? '')), + 'email' => $data['email'] ?? null, + 'provider_user_id' => $data['id'], + 'provider' => 'telegram', + 'access_token' => $data['hash'], + 'avatar' => $data['photo_url'] ?? null, + 'scopes' => [] + ]; + } +} diff --git a/api/app/Integrations/OAuth/OAuthProviderService.php b/api/app/Integrations/OAuth/OAuthProviderService.php index 2ef4bd7f..0f7a7018 100644 --- a/api/app/Integrations/OAuth/OAuthProviderService.php +++ b/api/app/Integrations/OAuth/OAuthProviderService.php @@ -4,15 +4,18 @@ namespace App\Integrations\OAuth; use App\Integrations\OAuth\Drivers\Contracts\OAuthDriver; use App\Integrations\OAuth\Drivers\OAuthGoogleDriver; +use App\Integrations\OAuth\Drivers\OAuthTelegramDriver; enum OAuthProviderService: string { case Google = 'google'; + case Telegram = 'telegram'; public function getDriver(): OAuthDriver { - return match($this) { - self::Google => new OAuthGoogleDriver() + return match ($this) { + self::Google => new OAuthGoogleDriver(), + self::Telegram => new OAuthTelegramDriver(), }; } } diff --git a/api/config/services.php b/api/config/services.php index eda2e470..458d72d3 100644 --- a/api/config/services.php +++ b/api/config/services.php @@ -79,6 +79,11 @@ return [ 'fonts_api_key' => env('GOOGLE_FONTS_API_KEY'), ], + 'telegram' => [ + 'bot_id' => env('TELEGRAM_BOT_ID'), + 'bot_token' => env('TELEGRAM_BOT_TOKEN'), + ], + 'zapier' => [ 'enabled' => env('ZAPIER_ENABLED', false), ], diff --git a/api/resources/data/forms/integrations.json b/api/resources/data/forms/integrations.json index 07bcf432..0e7d2e48 100644 --- a/api/resources/data/forms/integrations.json +++ b/api/resources/data/forms/integrations.json @@ -24,6 +24,14 @@ "actions_file_name": "DiscordIntegrationActions", "is_pro": true }, + "telegram": { + "name": "Telegram Notification", + "icon": "mdi:telegram", + "section_name": "Notifications", + "file_name": "TelegramIntegration", + "actions_file_name": "TelegramIntegrationActions", + "is_pro": true + }, "webhook": { "name": "Webhook Notification", "icon": "material-symbols:webhook", diff --git a/api/routes/api.php b/api/routes/api.php index 39d61d6a..e0107fe2 100644 --- a/api/routes/api.php +++ b/api/routes/api.php @@ -61,6 +61,7 @@ Route::group(['middleware' => 'auth:api'], function () { 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::post('widget-callback/{service}', [OAuthProviderController::class, 'handleWidgetRedirect'])->name('widget.callback'); Route::delete('/{provider}', [OAuthProviderController::class, 'destroy'])->name('destroy'); }); }); diff --git a/client/components/open/integrations/TelegramIntegration.vue b/client/components/open/integrations/TelegramIntegration.vue new file mode 100644 index 00000000..d405c6a0 --- /dev/null +++ b/client/components/open/integrations/TelegramIntegration.vue @@ -0,0 +1,76 @@ + + + \ No newline at end of file diff --git a/client/components/open/integrations/components/TelegramIntegrationActions.vue b/client/components/open/integrations/components/TelegramIntegrationActions.vue new file mode 100644 index 00000000..e6b2c2cf --- /dev/null +++ b/client/components/open/integrations/components/TelegramIntegrationActions.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/client/components/settings/ProviderModal.vue b/client/components/settings/ProviderModal.vue index 7b355eed..1da86b80 100644 --- a/client/components/settings/ProviderModal.vue +++ b/client/components/settings/ProviderModal.vue @@ -25,12 +25,18 @@ Connect account -
+
+ +
+
+ \ No newline at end of file diff --git a/client/components/settings/ProviderWidgetModal.vue b/client/components/settings/ProviderWidgetModal.vue new file mode 100644 index 00000000..c74d3fb8 --- /dev/null +++ b/client/components/settings/ProviderWidgetModal.vue @@ -0,0 +1,70 @@ + + + \ No newline at end of file diff --git a/client/components/settings/widget/TelegramWidget.vue b/client/components/settings/widget/TelegramWidget.vue new file mode 100644 index 00000000..e1baf65b --- /dev/null +++ b/client/components/settings/widget/TelegramWidget.vue @@ -0,0 +1,67 @@ + + + + diff --git a/client/data/forms/integrations.json b/client/data/forms/integrations.json index 07bcf432..0e7d2e48 100644 --- a/client/data/forms/integrations.json +++ b/client/data/forms/integrations.json @@ -24,6 +24,14 @@ "actions_file_name": "DiscordIntegrationActions", "is_pro": true }, + "telegram": { + "name": "Telegram Notification", + "icon": "mdi:telegram", + "section_name": "Notifications", + "file_name": "TelegramIntegration", + "actions_file_name": "TelegramIntegrationActions", + "is_pro": true + }, "webhook": { "name": "Webhook Notification", "icon": "material-symbols:webhook", diff --git a/client/nuxt.config.ts b/client/nuxt.config.ts index a701fdda..5d6f3f2e 100644 --- a/client/nuxt.config.ts +++ b/client/nuxt.config.ts @@ -106,6 +106,11 @@ export default defineNuxtConfig({ pathPrefix: false, global: true, }, + { + path: '~/components/settings', + pathPrefix: false, + global: true, + }, '~/components', ], diff --git a/client/pages/settings/access-tokens.vue b/client/pages/settings/access-tokens.vue index fc4b8353..864e4e3b 100644 --- a/client/pages/settings/access-tokens.vue +++ b/client/pages/settings/access-tokens.vue @@ -24,14 +24,14 @@
-
- diff --git a/client/pages/settings/connections.vue b/client/pages/settings/connections.vue index 8c28f0c0..634051ae 100644 --- a/client/pages/settings/connections.vue +++ b/client/pages/settings/connections.vue @@ -23,14 +23,14 @@
-
- diff --git a/client/stores/oauth_providers.js b/client/stores/oauth_providers.js index d70bd7cc..79d94722 100644 --- a/client/stores/oauth_providers.js +++ b/client/stores/oauth_providers.js @@ -1,10 +1,12 @@ import { defineStore } from "pinia" import { useContentStore } from "~/composables/stores/useContentStore.js" +import { useFeatureFlagsStore } from '~/stores/featureFlags' export const providersEndpoint = "/open/providers" export const useOAuthProvidersStore = defineStore("oauth_providers", () => { const contentStore = useContentStore() + const featureFlagsStore = useFeatureFlagsStore() const alert = useAlert() const googleDrivePermission = 'https://www.googleapis.com/auth/drive.file' @@ -15,7 +17,16 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => { name: 'google', title: 'Google', icon: 'mdi:google', - enabled: true + enabled: featureFlagsStore.getFlag('services.google.auth', false), + auth_type: 'redirect' + }, + { + name: 'telegram', + title: 'Telegram', + icon: 'mdi:telegram', + enabled: featureFlagsStore.getFlag('services.telegram.bot_id', false), + auth_type: 'widget', + widget_file: 'TelegramWidget' } ] }) @@ -37,11 +48,15 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => { } const connect = (service, redirect = false) => { - contentStore.resetState() + contentStore.resetState() + + const serviceConfig = getService(service) + if (serviceConfig.auth_type !== 'redirect') { + return + } + contentStore.startLoading() - const intention = new URL(window.location.href).pathname - opnFetch(`/settings/providers/connect/${service}`, { method: 'POST', body: { @@ -49,6 +64,7 @@ export const useOAuthProvidersStore = defineStore("oauth_providers", () => { } }) .then((data) => { + // Redirect flow window.location.href = data.url }) .catch((error) => { diff --git a/docs/configuration/environment-variables.mdx b/docs/configuration/environment-variables.mdx index f42f2bf8..8dff5f89 100644 --- a/docs/configuration/environment-variables.mdx +++ b/docs/configuration/environment-variables.mdx @@ -10,62 +10,63 @@ OpnForm uses two `.env` files for configuration: one for the Laravel backend loc The following environment variables are used to [configure the Laravel](https://laravel.com/docs/11.x/configuration) application (OpnForm's API). ### Dedicated guides -There are dedicated configuration pages available for more detailed setup instructions on specific topics: -- [File Storage (S3)](../configuration/aws-s3) -- [Email Configuration (SMTP)](../configuration/email-setup) -- [Custom Domain](../configuration/custom-domain) +There are dedicated configuration pages available for more detailed setup instructions on specific topics: + +- [File Storage (S3)](../configuration/aws-s3) +- [Email Configuration (SMTP)](../configuration/email-setup) +- [Custom Domain](../configuration/custom-domain) ### Other Environment Variables ### Configuration Environment Variables -| Variable Name | Description | -|------------------------------|--------------------------------------------------| -| `JWT_TTL` | Time to live for JSON Web Tokens (JWT). | -| `JWT_SECRET` | Secret key used to sign JWTs. | -| `H_CAPTCHA_SITE_KEY` | Site key for hCaptcha integration. | -| `H_CAPTCHA_SECRET_KEY` | Secret key for hCaptcha integration. | -| `RE_CAPTCHA_SITE_KEY` | Site key for reCAPTCHA integration. | -| `RE_CAPTCHA_SECRET_KEY` | Secret key for reCAPTCHA integration. | -| `OPEN_AI_API_KEY` | API key for accessing OpenAI services. | -| `UNSPLASH_ACCESS_KEY` | Access key for Unsplash API. | -| `UNSPLASH_SECRET_KEY` | Secret key for Unsplash API. | -| `GOOGLE_CLIENT_ID` | Client ID for Google OAuth. | -| `GOOGLE_CLIENT_SECRET` | Client secret for Google OAuth. | -| `GOOGLE_REDIRECT_URL` | Redirect URL for Google OAuth. | -| `GOOGLE_AUTH_REDIRECT_URL` | Authentication redirect URL for Google OAuth. | -| `GOOGLE_FONTS_API_KEY` | API key for accessing Google Fonts. | -| `FRONT_URL` | Public facing URL of the front-end. | -| `FRONT_API_SECRET` | Shared secret with the front-end. | +| Variable Name | Description | +| -------------------------- | --------------------------------------------- | +| `JWT_TTL` | Time to live for JSON Web Tokens (JWT). | +| `JWT_SECRET` | Secret key used to sign JWTs. | +| `H_CAPTCHA_SITE_KEY` | Site key for hCaptcha integration. | +| `H_CAPTCHA_SECRET_KEY` | Secret key for hCaptcha integration. | +| `RE_CAPTCHA_SITE_KEY` | Site key for reCAPTCHA integration. | +| `RE_CAPTCHA_SECRET_KEY` | Secret key for reCAPTCHA integration. | +| `OPEN_AI_API_KEY` | API key for accessing OpenAI services. | +| `UNSPLASH_ACCESS_KEY` | Access key for Unsplash API. | +| `UNSPLASH_SECRET_KEY` | Secret key for Unsplash API. | +| `GOOGLE_CLIENT_ID` | Client ID for Google OAuth. | +| `GOOGLE_CLIENT_SECRET` | Client secret for Google OAuth. | +| `GOOGLE_REDIRECT_URL` | Redirect URL for Google OAuth. | +| `GOOGLE_AUTH_REDIRECT_URL` | Authentication redirect URL for Google OAuth. | +| `GOOGLE_FONTS_API_KEY` | API key for accessing Google Fonts. | +| `FRONT_URL` | Public facing URL of the front-end. | +| `FRONT_API_SECRET` | Shared secret with the front-end. | +| `TELEGRAM_BOT_ID` | ID of your Telegram bot for notifications. | +| `TELEGRAM_BOT_TOKEN` | Authentication token for your Telegram bot. | ### User Options Environment Variables -| Variable Name | Description | -|------------------------------|--------------------------------------------------| -| `ADMIN_EMAILS` | Comma-separated list of admin email addresses. | -| `TEMPLATE_EDITOR_EMAILS` | Comma-separated list of template editor emails. | -| `EXTRA_PRO_USERS_EMAILS` | Comma-separated list of extra pro user emails. | -| `MODERATOR_EMAILS` | Comma-separated list of moderator email addresses. | -| `SHOW_OFFICIAL_TEMPLATES` | Set to `false` to hide official templates from OpnForm's template gallery (defaults to `true`). | - +| Variable Name | Description | +| ------------------------- | ----------------------------------------------------------------------------------------------- | +| `ADMIN_EMAILS` | Comma-separated list of admin email addresses. | +| `TEMPLATE_EDITOR_EMAILS` | Comma-separated list of template editor emails. | +| `EXTRA_PRO_USERS_EMAILS` | Comma-separated list of extra pro user emails. | +| `MODERATOR_EMAILS` | Comma-separated list of moderator email addresses. | +| `SHOW_OFFICIAL_TEMPLATES` | Set to `false` to hide official templates from OpnForm's template gallery (defaults to `true`). | ## Front-end Environment Variables ### Front-end Environment Variables -| Variable Name | Description | -|------------------------------|--------------------------------------------------| -| `NUXT_PUBLIC_APP_URL` | Public facing URL of the Nuxt application. | -| `NUXT_PUBLIC_API_BASE` | Base URL for the Laravel API. | -| `NUXT_PUBLIC_H_CAPTCHA_SITE_KEY` | Site key for hCaptcha integration on the front-end. | +| Variable Name | Description | +| --------------------------------- | ---------------------------------------------------- | +| `NUXT_PUBLIC_APP_URL` | Public facing URL of the Nuxt application. | +| `NUXT_PUBLIC_API_BASE` | Base URL for the Laravel API. | +| `NUXT_PUBLIC_H_CAPTCHA_SITE_KEY` | Site key for hCaptcha integration on the front-end. | | `NUXT_PUBLIC_RE_CAPTCHA_SITE_KEY` | Site key for reCAPTCHA integration on the front-end. | -| `NUXT_API_SECRET` | Shared secret key between Nuxt and Laravel backend. | - +| `NUXT_API_SECRET` | Shared secret key between Nuxt and Laravel backend. | import CloudVersion from "/snippets/cloud-version.mdx"; - + ## Docker Environment Variables @@ -88,5 +89,7 @@ docker compose up -d ``` -A simple `docker compose restart` will not reload environment variables from your `.env` files. You must use `down` and `up` commands to recreate the containers. - \ No newline at end of file + A simple `docker compose restart` will not reload environment variables from + your `.env` files. You must use `down` and `up` commands to recreate the + containers. +