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:
parent
bedd4541b5
commit
865eac19cb
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -106,6 +106,11 @@ export default defineNuxtConfig({
|
|||
pathPrefix: false,
|
||||
global: true,
|
||||
},
|
||||
{
|
||||
path: '~/components/settings',
|
||||
pathPrefix: false,
|
||||
global: true,
|
||||
},
|
||||
'~/components',
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue