Add Telegram Notification Integration (#732)

* Add Telegram Notification Integration

- Introduce Telegram Notification integration to the forms configuration.
- Add necessary properties including name, icon, section name, file name, actions file name, and pro status.

This update enhances the notification options available in the application, allowing users to integrate Telegram for notifications.

* Add Telegram integration support

- Updated .env.example to include Telegram bot configuration variables.
- Added the Telegram driver to OAuthProviderService for handling Telegram authentication.
- Created OAuthTelegramDriver class to manage Telegram OAuth interactions.
- Registered Telegram service in services.php with necessary credentials.
- Updated ProviderModal and TelegramIntegration components to support Telegram account connection and widget integration.
- Refactored integration forms to utilize FlatSelectInput for selecting Telegram accounts.

These changes enable users to connect their Telegram accounts for notifications, enhancing the integration capabilities of the application.

* Enhance Telegram integration and refactor settings components

- Added Telegram configuration to FeatureFlagsController for bot and redirect settings.
- Updated nuxt.config.ts to include settings components path for better organization.
- Refactored TelegramIntegration component to navigate to settings on connect.
- Renamed and updated ProviderModal and ProviderWidgetModal for consistency.
- Introduced TelegramWidget component for dynamic loading of the Telegram login widget.
- Refactored access tokens and connections pages to use updated component names.

These changes improve the Telegram integration and streamline the settings management interface.

* Refactor Telegram integration for widget-based authentication

- Updated .env.example to rename Telegram bot configuration variable from TELEGRAM_BOT_NAME to TELEGRAM_BOT_ID.
- Removed the Telegram provider from composer.json to streamline dependencies.
- Enhanced FeatureFlagsController to support new bot_id configuration.
- Implemented handleWidgetRedirect method in OAuthProviderController for handling widget authentication.
- Created WidgetOAuthDriver interface and refactored OAuthTelegramDriver to implement widget-based authentication methods.
- Updated services.php to reflect changes in Telegram configuration.
- Added widget callback route for handling authentication responses.
- Refactored TelegramWidget component to utilize the new widget authentication flow.

These changes improve the Telegram integration by enabling widget-based authentication, enhancing user experience and security.

* Enhance Telegram integration by adding provider support and refactoring data retrieval

- Introduced a new protected property `provider` in `AbstractIntegrationHandler` to store provider information.
- Updated the constructor to assign the provider from `formIntegration`.
- Refactored `getChatId` method in `TelegramIntegration` to utilize `provider_user_id` instead of the removed bot token.
- Adjusted the `shouldRun` method to include a check for `oauth_id` in `formIntegration`.

These changes improve the handling of provider data within the Telegram integration, enhancing its functionality and reliability.

* Enhance Telegram integration by adding MarkdownV2 escaping functionality

- Updated the message text formatting in the TelegramIntegration class to escape special characters for MarkdownV2.
- Introduced a new protected method `escapeMarkdownV2` to handle the escaping of special characters, improving message rendering in Telegram.

These changes enhance the Telegram integration by ensuring that messages are properly formatted for MarkdownV2, preventing potential rendering issues.

* fix pint

* Fix ESLint warning for unused variable in TelegramWidget component

* Remove unused variable declaration in TelegramWidget component to address ESLint warning. This change enhances code quality by adhering to linting rules and improving maintainability.

* Refactor Telegram integration components by removing unused code and improving maintainability

- Removed the 'refresh_token' field from the OAuthTelegramDriver as it is no longer needed.
- Eliminated unused variable declarations and interval logic in the TelegramIntegrationActions component to address ESLint warnings and enhance code quality.

These changes streamline the codebase and adhere to best practices for maintainability.

* Enhance Telegram integration by adding validation messages and improving message formatting

- Introduced custom validation messages for the `oauth_id` field in `FormIntegrationsRequest`, enhancing user feedback during integration setup.
- Refactored message construction in `TelegramIntegration` to utilize an array for building the message text, improving readability and maintainability.
- Updated the `handle` method to include error logging for missing `chat_id` and `bot token`, enhancing error handling and debugging capabilities.

These changes improve the user experience and reliability of the Telegram integration by providing clearer validation feedback and more robust error handling.

* Update environment variables documentation for clarity and consistency

- Reformatted the environment variables table for improved readability by aligning headers and descriptions.
- Added new environment variables for Telegram bot integration, including `TELEGRAM_BOT_ID` and `TELEGRAM_BOT_TOKEN`, to support enhanced notification features.
- Adjusted the warning message regarding Docker commands to improve clarity on environment variable reloading.

These changes enhance the documentation by ensuring that it is clear, consistent, and up-to-date with the latest configuration requirements.

---------

Co-authored-by: JhumanJ <julien@nahum.net>
This commit is contained in:
Chirag Chhatrala 2025-04-08 16:12:47 +05:30 committed by GitHub
parent bedd4541b5
commit 865eac19cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 712 additions and 54 deletions

View File

@ -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

View File

@ -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'),

View File

@ -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);

View File

@ -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<string, string>
*/
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.',
];
}
}

View File

@ -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

View File

@ -0,0 +1,169 @@
<?php
namespace App\Integrations\Handlers;
use App\Models\Forms\Form;
use App\Open\MentionParser;
use App\Service\Forms\FormSubmissionFormatter;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Arr;
use Vinkla\Hashids\Facades\Hashids;
class TelegramIntegration extends AbstractIntegrationHandler
{
protected const TELEGRAM_API_BASE = 'https://api.telegram.org/bot';
public static function getValidationRules(?Form $form): array
{
return [
'include_submission_data' => '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
]);
}
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Integrations\OAuth\Drivers\Contracts;
interface WidgetOAuthDriver extends OAuthDriver
{
/**
* Verify the widget authentication data
*
* @param array $data The data received from the widget
* @return bool Whether the data is valid
*/
public function verifyWidgetData(array $data): bool;
/**
* Get user data from widget authentication data
*
* @param array $data The data received from the widget
* @return array User data in a standardized format
*/
public function getUserFromWidgetData(array $data): array;
/**
* Check if this driver uses widget-based authentication
*
* @return bool
*/
public function isWidgetBased(): bool;
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Integrations\OAuth\Drivers;
use App\Integrations\OAuth\Drivers\Contracts\WidgetOAuthDriver;
use Laravel\Socialite\Contracts\User;
class OAuthTelegramDriver implements WidgetOAuthDriver
{
protected string $redirectUrl;
protected array $scopes = [];
public function getRedirectUrl(): string
{
return ''; // Not used for widget-based auth
}
public function setRedirectUrl(string $url): self
{
$this->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' => []
];
}
}

View File

@ -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(),
};
}
}

View File

@ -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),
],

View File

@ -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",

View File

@ -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');
});
});

View File

@ -0,0 +1,76 @@
<template>
<IntegrationWrapper
v-model="props.integrationData"
:integration="props.integration"
:form="form"
>
<div class="mb-4">
<p class="text-gray-500 mb-4">
Receive Telegram messages on each form submission.
</p>
<template v-if="providers.length">
<FlatSelectInput
v-model="integrationData.oauth_id"
name="provider"
:options="providers"
display-key="name"
option-key="id"
emit-key="id"
:required="true"
label="Select Telegram Account"
>
<template #help>
<InputHelp>
<span>
<NuxtLink
class="text-blue-500"
:to="{ name: 'settings-connections' }"
>
Click here
</NuxtLink>
to connect another account.
</span>
</InputHelp>
</template>
</FlatSelectInput>
<h4 class="font-bold mt-4">
Telegram message actions
</h4>
<notifications-message-actions
v-model="integrationData.settings"
:form="form"
/>
</template>
<v-button
v-else
color="white"
:loading="providersStore.loading"
@click.prevent="connect"
>
Connect Telegram account
</v-button>
</div>
</IntegrationWrapper>
</template>
<script setup>
import FlatSelectInput from '~/components/forms/FlatSelectInput.vue'
import IntegrationWrapper from './components/IntegrationWrapper.vue'
import NotificationsMessageActions from './components/NotificationsMessageActions.vue'
const props = defineProps({
integration: { type: Object, required: true },
form: { type: Object, required: true },
integrationData: { type: Object, required: true },
formIntegrationId: { type: Number, required: false, default: null }
})
const providersStore = useOAuthProvidersStore()
const providers = computed(() => providersStore.getAll.filter(provider => provider.provider == 'telegram'))
function connect () {
useRouter().push({ name: 'settings-connections' })
}
</script>

View File

@ -0,0 +1,30 @@
<template>
<div class="flex flex-1 items-center">
<div
v-if="integration"
class="hidden md:block space-y-1"
>
<UBadge
:label="mentionAsText(integration.data.message)"
color="gray"
size="xs"
class="max-w-[300px] block truncate"
/>
</div>
</div>
</template>
<script setup>
import { mentionAsText } from '~/lib/utils.js'
defineProps({
integration: {
type: Object,
required: true,
},
form: {
type: Object,
required: true,
}
})
</script>

View File

@ -25,12 +25,18 @@
Connect account
</template>
<div class="px-4">
<div v-if="loading">
<Loader class="h-6 w-6 mx-auto" />
</div>
<div
v-else
class="flex"
>
<div
v-for="service in services"
:key="service.name"
role="button"
class="bg-gray-50 border border-gray-200 rounded-md transition-colors p-4 pb-2 items-center justify-center w-[170px] h-[110px] flex flex-col relative"
class="mr-2 bg-gray-50 border border-gray-200 rounded-md transition-colors p-4 pb-2 items-center justify-center w-[170px] h-[110px] flex flex-col relative"
:class="{
'hover:bg-blue-50 group cursor-pointer': service.enabled,
'cursor-not-allowed': !service.enabled,
@ -55,6 +61,14 @@
</div>
</div>
</modal>
<!-- Add widget modal -->
<ProviderWidgetModal
v-if="showWidgetModal"
:show="showWidgetModal"
:service="selectedService"
@close="showWidgetModal = false"
/>
</template>
<script setup>
@ -67,7 +81,24 @@ const emit = defineEmits(['close'])
const providersStore = useOAuthProvidersStore()
const services = computed(() => providersStore.services)
const loading = ref(false)
const showWidgetModal = ref(false)
const selectedService = ref(null)
function connect(service) {
providersStore.connect(service.name)
if (!service.enabled) {
useAlert().error('This service is not enabled. Please contact support.')
return
}
if (service.auth_type === 'widget') {
emit('close')
selectedService.value = service
showWidgetModal.value = true
} else {
// Use existing redirect flow
loading.value = true
providersStore.connect(service.name)
}
}
</script>
</script>

View File

@ -0,0 +1,70 @@
<template>
<modal
:show="show"
max-width="md"
@close="emit('close')"
>
<template #title>
<div class="flex items-center justify-center mb-4">
<Icon
:name="service.icon"
class="h-8 w-8 text-gray-500"
/>
<span class="ml-2 text-gray-700 font-medium">{{ service.title }}</span>
</div>
</template>
<div class="p-4 flex items-center justify-center">
<!-- Dynamic component loading based on service -->
<component
:is="widgetComponent"
v-if="widgetComponent"
:service="service"
@auth-data="handleAuthData"
/>
</div>
</modal>
</template>
<script setup>
const props = defineProps({
show: Boolean,
service: Object
})
const providersStore = useOAuthProvidersStore()
const router = useRouter()
const alert = useAlert()
const emit = defineEmits(['close'])
// Dynamically compute which widget component to load
const widgetComponent = computed(() => {
if (!props.service?.widget_file) return null
return resolveComponent(props.service.widget_file)
})
const handleAuthData = async (data) => {
try {
if (!data) {
alert.error('Authentication failed')
return
}
const response = await opnFetch(`/settings/providers/widget-callback/${props.service.name}`, {
method: 'POST',
body: data
})
if (response.intention) {
router.push(response.intention)
} else {
alert.success('Successfully connected')
emit('close')
providersStore.fetchOAuthProviders()
}
} catch (error) {
alert.error(error?.data?.message || 'Failed to authenticate')
providersStore.fetchOAuthProviders()
}
}
</script>

View File

@ -0,0 +1,67 @@
<template>
<div
id="widget_login"
class="flex flex-col gap-4"
>
<p class="text-sm text-gray-500">
<a
href="https://telegram.org"
target="_blank"
class="text-primary-500 hover:underline"
>Telegram</a> is a secure messaging app that works across all devices and platforms.
Connect your account to receive instant notifications whenever someone submits this form!
</p>
<div class="flex justify-center">
<UButton
:disabled="!botId"
icon="i-mdi-telegram"
@click.prevent="handleAuth"
>
Log in with Telegram
</UButton>
</div>
</div>
</template>
<script setup>
import { useFeatureFlagsStore } from '~/stores/featureFlags'
defineProps({
service: {
type: Object,
required: true
}
})
const emit = defineEmits(['auth-data'])
const featureFlagsStore = useFeatureFlagsStore()
const botId = computed(() => featureFlagsStore.getFlag('services.telegram.bot_id'))
const loadTelegramWidget = () => {
if (!botId.value) return
const script = document.createElement('script')
script.async = true
script.src = 'https://telegram.org/js/telegram-widget.js'
document.head.appendChild(script)
}
const handleAuth = () => {
if (window.Telegram?.Login) {
window.Telegram.Login.auth(
{ bot_id: botId.value, request_access: 'write' },
(data) => {
emit('auth-data', data)
}
)
} else {
useAlert().error('Telegram login is not available')
}
}
onMounted(() => {
loadTelegramWidget()
})
</script>

View File

@ -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",

View File

@ -106,6 +106,11 @@ export default defineNuxtConfig({
pathPrefix: false,
global: true,
},
{
path: '~/components/settings',
pathPrefix: false,
global: true,
},
'~/components',
],

View File

@ -24,14 +24,14 @@
</div>
<div class="py-6">
<SettingsAccessTokenCard
<AccessTokenCard
v-for="token in tokens"
:key="token.id"
:token="token"
/>
</div>
<SettingsAccessTokenModal
<AccessTokenModal
:show="accessTokenModal"
@close="accessTokenModal = false"
/>

View File

@ -23,14 +23,14 @@
</div>
<div class="py-6">
<SettingsProviderCard
<ProviderCard
v-for="provider in providers"
:key="provider.id"
:provider="provider"
/>
</div>
<SettingsProviderModal
<ProviderModal
:show="providerModal"
@close="providerModal = false"
/>

View File

@ -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) => {

View File

@ -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";
<CloudVersion/>
<CloudVersion />
## Docker Environment Variables
@ -88,5 +89,7 @@ docker compose up -d
```
<Warning>
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.
</Warning>
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.
</Warning>