mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-12 15:06:38 +02:00
@@ -1815,6 +1815,99 @@ export class API {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* oauthProvider is the API for OAuth Provider related operations.
|
||||
*/
|
||||
oauthProvider = {
|
||||
/**
|
||||
* Get all OAuth Providers using pagination.
|
||||
*
|
||||
* @param {TableURLParams} options
|
||||
* @param {string|null} companyID
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
getAll: async (options, companyID) => {
|
||||
return await getJSON(
|
||||
this.getPath(`/oauth-provider?${appendQuery(options)}${this.appendCompanyQuery(companyID)}`)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an OAuth Provider by its ID.
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
getByID: async (id) => {
|
||||
return await getJSON(this.getPath(`/oauth-provider/${id}`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new OAuth Provider.
|
||||
*
|
||||
* @param {Object} provider
|
||||
* @param {string} provider.name
|
||||
* @param {string} provider.clientID
|
||||
* @param {string} provider.clientSecret
|
||||
* @param {string} provider.authURL
|
||||
* @param {string} provider.tokenURL
|
||||
* @param {string} provider.scopes
|
||||
* @param {string} provider.companyID
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
create: async (provider) => {
|
||||
return await postJSON(this.getPath('/oauth-provider'), provider);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update an OAuth Provider.
|
||||
*
|
||||
* @param {Object} provider
|
||||
* @param {string} provider.id
|
||||
* @param {string} provider.name
|
||||
* @param {string} provider.clientID
|
||||
* @param {string} provider.clientSecret
|
||||
* @param {string} provider.authURL
|
||||
* @param {string} provider.tokenURL
|
||||
* @param {string} provider.scopes
|
||||
* @param {string} provider.companyID
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
update: async (provider) => {
|
||||
return await patchJSON(this.getPath(`/oauth-provider/${provider.id}`), provider);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an OAuth Provider.
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
delete: async (id) => {
|
||||
return await deleteJSON(this.getPath(`/oauth-provider/${id}`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the authorization URL for an OAuth Provider.
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
getAuthorizationURL: async (id) => {
|
||||
return await getJSON(this.getPath(`/oauth-authorize/${id}`));
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove authorization tokens from an OAuth Provider.
|
||||
*
|
||||
* @param {string} id
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
removeAuthorization: async (id) => {
|
||||
return await postJSON(this.getPath(`/oauth-provider/${id}/remove-authorization`), {});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* user is the API for user related operations - these actions also affect the user's sessions
|
||||
*/
|
||||
@@ -2501,6 +2594,7 @@ export class API {
|
||||
* @param {string} sender.customField2
|
||||
* @param {string} sender.customField3
|
||||
* @param {string} sender.customField4
|
||||
* @param {string} sender.oauthProviderID
|
||||
* @param {string} sender.requestMethod
|
||||
* @param {string} sender.requestURL
|
||||
* @param {APISenderHeader[]} sender.requestHeaders
|
||||
@@ -2518,6 +2612,7 @@ export class API {
|
||||
customField2,
|
||||
customField3,
|
||||
customField4,
|
||||
oauthProviderID,
|
||||
requestMethod,
|
||||
requestURL,
|
||||
requestHeaders,
|
||||
@@ -2539,6 +2634,7 @@ export class API {
|
||||
customField2: customField2,
|
||||
customField3: customField3,
|
||||
customField4: customField4,
|
||||
oauthProviderID: oauthProviderID,
|
||||
requestMethod: requestMethod,
|
||||
requestURL: requestURL,
|
||||
requestHeaders: requestHeaders,
|
||||
@@ -2561,6 +2657,7 @@ export class API {
|
||||
* @param {string} sender.customField2
|
||||
* @param {string} sender.customField3
|
||||
* @param {string} sender.customField4
|
||||
* @param {string} sender.oauthProviderID
|
||||
* @param {string} sender.requestMethod
|
||||
* @param {string} sender.requestURL
|
||||
* @param {APISenderHeader[]} sender.requestHeaders
|
||||
@@ -2578,6 +2675,7 @@ export class API {
|
||||
customField2,
|
||||
customField3,
|
||||
customField4,
|
||||
oauthProviderID,
|
||||
requestMethod,
|
||||
requestURL,
|
||||
requestHeaders,
|
||||
@@ -2602,6 +2700,7 @@ export class API {
|
||||
customField2: customField2,
|
||||
customField3: customField3,
|
||||
customField4: customField4,
|
||||
oauthProviderID: oauthProviderID,
|
||||
requestMethod: requestMethod,
|
||||
requestURL: requestURL,
|
||||
requestHeaders: requestHeaders,
|
||||
|
||||
@@ -125,6 +125,11 @@
|
||||
api_senders: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
|
||||
</svg>
|
||||
`,
|
||||
|
||||
oauth_providers: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
|
||||
</svg>
|
||||
`,
|
||||
|
||||
proxy: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
@@ -149,7 +154,8 @@
|
||||
'/email/': 'emails_overview',
|
||||
'/attachment/': 'attachments',
|
||||
'/smtp-configuration/': 'smtp_configurations',
|
||||
'/api-sender/': 'api_senders'
|
||||
'/api-sender/': 'api_senders',
|
||||
'/oauth-provider/': 'oauth_providers'
|
||||
};
|
||||
|
||||
return icons[iconMap[route] || 'dashboard']; // fallback to dashboard if route not found
|
||||
|
||||
@@ -67,6 +67,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
|
||||
</svg>`,
|
||||
|
||||
oauth_providers: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
|
||||
</svg>`,
|
||||
|
||||
proxy: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
|
||||
</svg>`,
|
||||
@@ -123,6 +127,7 @@
|
||||
'/attachment/': 'attachments',
|
||||
'/smtp-configuration/': 'smtp_configurations',
|
||||
'/api-sender/': 'api_senders',
|
||||
'/oauth-provider/': 'oauth_providers',
|
||||
'/profile/': 'profile',
|
||||
'/sessions/': 'sessions',
|
||||
'/user/': 'users',
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export let confirm = false;
|
||||
export let confirmWord = 'confirm';
|
||||
export let permanent = true;
|
||||
export let actionMessage = '';
|
||||
|
||||
let confirmText = '';
|
||||
|
||||
@@ -47,11 +48,19 @@
|
||||
<h3 class="text-lg font-medium text-gray-900">Delete {type}</h3>
|
||||
-->
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
||||
Are you sure you want to delete
|
||||
{#if name?.length > 30}
|
||||
<br />
|
||||
{#if actionMessage}
|
||||
{actionMessage}
|
||||
{#if name?.length > 30}
|
||||
<br />
|
||||
{/if}
|
||||
<span class="font-medium text-gray-900 dark:text-gray-300">"{name}"</span>?
|
||||
{:else}
|
||||
Are you sure you want to delete
|
||||
{#if name?.length > 30}
|
||||
<br />
|
||||
{/if}
|
||||
<span class="font-medium text-gray-900 dark:text-gray-300">"{name}"</span>?
|
||||
{/if}
|
||||
<span class="font-medium text-gray-900 dark:text-gray-300">"{name}"</span>?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -81,6 +81,10 @@ export const route = {
|
||||
label: 'API Senders',
|
||||
route: '/api-sender/'
|
||||
},
|
||||
oauthProviders: {
|
||||
label: 'OAuth',
|
||||
route: '/oauth-provider/'
|
||||
},
|
||||
allowDeny: {
|
||||
label: 'Filters',
|
||||
route: '/filter/'
|
||||
@@ -122,7 +126,13 @@ export const menu = [
|
||||
{
|
||||
label: 'Emails',
|
||||
type: 'submenu',
|
||||
items: [route.emails, route.attachments, route.smtpConfigurations, route.apiSenders]
|
||||
items: [
|
||||
route.emails,
|
||||
route.attachments,
|
||||
route.smtpConfigurations,
|
||||
route.apiSenders,
|
||||
route.oauthProviders
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -11,27 +11,30 @@
|
||||
import TableUpdateButton from '$lib/components/table/TableUpdateButton.svelte';
|
||||
import TableDeleteButton from '$lib/components/table/TableDeleteButton2.svelte';
|
||||
import FormError from '$lib/components/FormError.svelte';
|
||||
import { addToast } from '$lib/store/toast';
|
||||
import { AppStateService } from '$lib/service/appState';
|
||||
import TableCellAction from '$lib/components/table/TableCellAction.svelte';
|
||||
import TableCellEmpty from '$lib/components/table/TableCellEmpty.svelte';
|
||||
import FormGrid from '$lib/components/FormGrid.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import TextareaField from '$lib/components/TextareaField.svelte';
|
||||
import TableCellScope from '$lib/components/table/TableCellScope.svelte';
|
||||
import BigButton from '$lib/components/BigButton.svelte';
|
||||
import FormColumn from '$lib/components/FormColumn.svelte';
|
||||
import FormColumns from '$lib/components/FormColumns.svelte';
|
||||
import FormColumn from '$lib/components/FormColumn.svelte';
|
||||
import FormFooter from '$lib/components/FormFooter.svelte';
|
||||
import Table from '$lib/components/table/Table.svelte';
|
||||
import HeadTitle from '$lib/components/HeadTitle.svelte';
|
||||
import TableViewButton from '$lib/components/table/TableViewButton.svelte';
|
||||
import { getModalText } from '$lib/utils/common';
|
||||
import TableCopyButton from '$lib/components/table/TableCopyButton.svelte';
|
||||
import { showIsLoading, hideIsLoading } from '$lib/store/loading.js';
|
||||
import TableDropDownEllipsis from '$lib/components/table/TableDropDownEllipsis.svelte';
|
||||
import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte';
|
||||
import TextareaField from '$lib/components/TextareaField.svelte';
|
||||
import SimpleCodeEditor from '$lib/components/editor/SimpleCodeEditor.svelte';
|
||||
import { addToast } from '$lib/store/toast';
|
||||
import TableCellScope from '$lib/components/table/TableCellScope.svelte';
|
||||
import TableViewButton from '$lib/components/table/TableViewButton.svelte';
|
||||
import { BiMap } from '$lib/utils/maps';
|
||||
import { fetchAllRows } from '$lib/utils/api-utils';
|
||||
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
|
||||
import VimToggle from '$lib/components/editor/VimToggle.svelte';
|
||||
|
||||
// services
|
||||
@@ -40,6 +43,7 @@
|
||||
// data
|
||||
let form = null;
|
||||
let contextCompanyID = null;
|
||||
let oauthProviderMap = new BiMap({});
|
||||
let formValues = {
|
||||
name: '',
|
||||
companyID: '',
|
||||
@@ -48,6 +52,7 @@
|
||||
customField2: '',
|
||||
customField3: '',
|
||||
customField4: '',
|
||||
oauthProvider: null,
|
||||
requestMethod: '',
|
||||
requestURL: '',
|
||||
requestHeaders: '',
|
||||
@@ -87,6 +92,15 @@
|
||||
contextCompanyID = appStateService.getContext().companyID;
|
||||
formValues.companyID = contextCompanyID;
|
||||
}
|
||||
|
||||
// load oauth providers
|
||||
(async () => {
|
||||
const oauthProviders = await fetchAllRows((options) => {
|
||||
return api.oauthProvider.getAll(options, contextCompanyID);
|
||||
});
|
||||
oauthProviderMap = BiMap.FromArrayOfObjects(oauthProviders);
|
||||
})();
|
||||
|
||||
refreshConfigurations();
|
||||
tableURLParams.onChange(refreshConfigurations);
|
||||
(async () => {
|
||||
@@ -148,7 +162,10 @@
|
||||
|
||||
const onClickCreate = async () => {
|
||||
try {
|
||||
const res = await api.apiSender.create(formValues);
|
||||
const res = await api.apiSender.create({
|
||||
...formValues,
|
||||
oauthProviderID: oauthProviderMap.byValueOrNull(formValues.oauthProvider)
|
||||
});
|
||||
if (!res.success) {
|
||||
modalError = res.error;
|
||||
return;
|
||||
@@ -165,7 +182,10 @@
|
||||
|
||||
const onClickUpdate = async (saveOnly = false) => {
|
||||
try {
|
||||
const res = await api.apiSender.update(formValues);
|
||||
const res = await api.apiSender.update({
|
||||
...formValues,
|
||||
oauthProviderID: oauthProviderMap.byValueOrNull(formValues.oauthProvider)
|
||||
});
|
||||
if (!res.success) {
|
||||
modalError = res.error;
|
||||
throw res.error;
|
||||
@@ -177,6 +197,7 @@
|
||||
}
|
||||
refreshConfigurations();
|
||||
} catch (err) {
|
||||
addToast('Failed to update API sender', 'Error');
|
||||
console.error('failed to update API sender:', err);
|
||||
}
|
||||
};
|
||||
@@ -259,6 +280,7 @@
|
||||
|
||||
const assignAPISender = (apiSender) => {
|
||||
formValues = apiSender;
|
||||
formValues.oauthProvider = oauthProviderMap.byKey(apiSender.oauthProviderID);
|
||||
};
|
||||
|
||||
const closeEditModal = () => {
|
||||
@@ -418,7 +440,17 @@
|
||||
maxLength={255}
|
||||
optional={true}
|
||||
toolTipText="Use as {'{{.APIKey}}'}"
|
||||
placeholder="S3C-R37-AP1-K3Y">API Key</TextField
|
||||
placeholder="your-api-key">API Key</TextField
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<TextFieldSelect
|
||||
id="oauthProvider"
|
||||
bind:value={formValues.oauthProvider}
|
||||
options={oauthProviderMap.values()}
|
||||
optional={true}
|
||||
toolTipText="OAuth provider for token-based authentication. Access token available as {'{{.OAuthAccessToken}}'}"
|
||||
>OAuth Provider (Optional)</TextFieldSelect
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -589,7 +621,9 @@ X-Custom-Header: Hello Friend"
|
||||
</Modal>
|
||||
|
||||
<Modal headerText="API Sender Test Results" visible={isTestModalVisible} onClose={closeTestModal}>
|
||||
<div class="col-span-3 w-full overflow-y-auto px-6 py-4 space-y-6 select-text">
|
||||
<div
|
||||
class="col-span-3 w-full overflow-y-auto px-6 py-4 space-y-6 select-text overflow-x-hidden"
|
||||
>
|
||||
{#if !testResponse.error}
|
||||
<!-- Successful Test -->
|
||||
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
|
||||
@@ -599,7 +633,7 @@ X-Custom-Header: Hello Friend"
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
>
|
||||
<div class="font-medium">
|
||||
<div class="font-medium break-all">
|
||||
{testResponse.apiSender?.requestMethod}
|
||||
{testResponse?.request?.url}
|
||||
</div>
|
||||
@@ -611,21 +645,28 @@ X-Custom-Header: Hello Friend"
|
||||
Request Headers
|
||||
</h3>
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{headersToString(
|
||||
testResponse.request?.headers
|
||||
) || 'No headers'}</pre>
|
||||
<div
|
||||
class="text-xs whitespace-pre-wrap break-all max-h-60 overflow-y-auto font-mono"
|
||||
style="word-break: break-all; overflow-wrap: anywhere;"
|
||||
>
|
||||
{headersToString(testResponse.request?.headers) || 'No headers'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 pb-2 w-full">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">Request Body</h3>
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-80">{testResponse.request
|
||||
?.body || 'Empty body'}</pre>
|
||||
<div
|
||||
class="text-xs whitespace-pre-wrap break-all max-h-80 overflow-y-auto font-mono"
|
||||
style="word-break: break-all; overflow-wrap: anywhere;"
|
||||
>
|
||||
{testResponse.request?.body || 'Empty body'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- response -->
|
||||
@@ -635,7 +676,7 @@ X-Custom-Header: Hello Friend"
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<div class="text-sm font-medium mb-1">Received:</div>
|
||||
<div
|
||||
@@ -648,7 +689,7 @@ X-Custom-Header: Hello Friend"
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<div class="text-sm font-medium mb-1">Expected:</div>
|
||||
<div>{testResponse.apiSender?.expectedResponseStatusCode}</div>
|
||||
@@ -662,19 +703,26 @@ X-Custom-Header: Hello Friend"
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<div class="text-sm font-medium mb-1">Received:</div>
|
||||
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{headersToString(
|
||||
testResponse.response?.headers
|
||||
) || 'No headers'}</pre>
|
||||
<div
|
||||
class="text-xs whitespace-pre-wrap break-all max-h-60 overflow-y-auto font-mono"
|
||||
style="word-break: break-all; overflow-wrap: anywhere;"
|
||||
>
|
||||
{headersToString(testResponse.response?.headers) || 'No headers'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<div class="text-sm font-medium mb-1">Expected to contain:</div>
|
||||
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-60">{testResponse
|
||||
.apiSender?.expectedResponseHeaders || 'No validation specified'}</pre>
|
||||
<div
|
||||
class="text-xs whitespace-pre-wrap break-all max-h-60 overflow-y-auto font-mono"
|
||||
style="word-break: break-all; overflow-wrap: anywhere;"
|
||||
>
|
||||
{testResponse.apiSender?.expectedResponseHeaders || 'No validation specified'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -683,18 +731,26 @@ X-Custom-Header: Hello Friend"
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">Response Body</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<div class="text-sm font-medium mb-1">Received:</div>
|
||||
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-80">{testResponse
|
||||
.response?.body || 'Empty response'}</pre>
|
||||
<div
|
||||
class="text-xs whitespace-pre-wrap break-all max-h-80 overflow-y-auto font-mono"
|
||||
style="word-break: break-all; overflow-wrap: anywhere;"
|
||||
>
|
||||
{testResponse.response?.body || 'Empty response'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<div class="text-sm font-medium mb-1">Expected to contain:</div>
|
||||
<pre class="text-xs whitespace-pre-wrap overflow-x-auto max-h-80">{testResponse
|
||||
.apiSender?.expectedResponseBody || 'No validation specified'}</pre>
|
||||
<div
|
||||
class="text-xs whitespace-pre-wrap break-all max-h-80 overflow-y-auto font-mono"
|
||||
style="word-break: break-all; overflow-wrap: anywhere;"
|
||||
>
|
||||
{testResponse.apiSender?.expectedResponseBody || 'No validation specified'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -705,9 +761,9 @@ X-Custom-Header: Hello Friend"
|
||||
Request Details
|
||||
</h3>
|
||||
<div
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600"
|
||||
class="p-3 bg-gray-50 dark:bg-gray-800 rounded-md border border-gray-200 dark:border-gray-600 overflow-hidden"
|
||||
>
|
||||
<div class="font-medium">
|
||||
<div class="font-medium break-all">
|
||||
{testResponse.apiSender?.requestMethod}
|
||||
{testResponse.apiSender?.requestURL}
|
||||
</div>
|
||||
@@ -715,8 +771,8 @@ X-Custom-Header: Hello Friend"
|
||||
</div>
|
||||
<div class="pt-4 pb-2 w-full">
|
||||
<h3 class="text-base font-medium text-red-600 mb-3">Error</h3>
|
||||
<div class="p-4 bg-red-50 rounded-md border border-red-200">
|
||||
<div class="text-red-600 whitespace-pre-wrap">{testResponse.error}</div>
|
||||
<div class="p-4 bg-red-50 rounded-md border border-red-200 overflow-hidden">
|
||||
<div class="text-red-600 whitespace-pre-wrap break-words">{testResponse.error}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -117,8 +117,8 @@
|
||||
refreshApiSenders(),
|
||||
refreshPages(),
|
||||
refreshProxies(),
|
||||
getCampaignTemplates(),
|
||||
refreshIdentifiers()
|
||||
refreshIdentifiers(),
|
||||
getCampaignTemplates()
|
||||
]);
|
||||
tableURLParams.onChange(refreshCampaignTemplates);
|
||||
const editID = $page.url.searchParams.get('edit');
|
||||
@@ -395,7 +395,7 @@
|
||||
|
||||
const closeModal = () => {
|
||||
isModalVisible = false;
|
||||
form.reset();
|
||||
form?.reset();
|
||||
formValues = {
|
||||
id: null,
|
||||
templateType: 'Email',
|
||||
@@ -413,8 +413,6 @@
|
||||
stateIdentifier: 'session',
|
||||
urlPath: ''
|
||||
};
|
||||
modalError = '';
|
||||
showAdvancedOptions = false;
|
||||
};
|
||||
|
||||
/** @param {string} id */
|
||||
@@ -463,7 +461,7 @@
|
||||
if (template.smtpConfigurationID) {
|
||||
formValues.templateType = 'Email';
|
||||
} else {
|
||||
formValues.templateType = 'API Sender';
|
||||
formValues.templateType = 'External API';
|
||||
}
|
||||
formValues.domain = domainMap.byKey(template.domainID);
|
||||
formValues.email = emailMap.byKey(template.emailID);
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { api } from '$lib/api/apiProxy.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { newTableURLParams } from '$lib/service/tableURLParams.js';
|
||||
import { globalButtonDisabledAttributes } from '$lib/utils/form.js';
|
||||
import Headline from '$lib/components/Headline.svelte';
|
||||
import TextField from '$lib/components/TextField.svelte';
|
||||
import TableRow from '$lib/components/table/TableRow.svelte';
|
||||
import TableCell from '$lib/components/table/TableCell.svelte';
|
||||
import TableUpdateButton from '$lib/components/table/TableUpdateButton.svelte';
|
||||
import TableDeleteButton from '$lib/components/table/TableDeleteButton2.svelte';
|
||||
import FormError from '$lib/components/FormError.svelte';
|
||||
import { addToast } from '$lib/store/toast';
|
||||
import PasswordField from '$lib/components/PasswordField.svelte';
|
||||
import { AppStateService } from '$lib/service/appState';
|
||||
import TableCellAction from '$lib/components/table/TableCellAction.svelte';
|
||||
import TableCellEmpty from '$lib/components/table/TableCellEmpty.svelte';
|
||||
import FormGrid from '$lib/components/FormGrid.svelte';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import BigButton from '$lib/components/BigButton.svelte';
|
||||
import FormColumns from '$lib/components/FormColumns.svelte';
|
||||
import FormColumn from '$lib/components/FormColumn.svelte';
|
||||
import FormFooter from '$lib/components/FormFooter.svelte';
|
||||
import Table from '$lib/components/table/Table.svelte';
|
||||
import HeadTitle from '$lib/components/HeadTitle.svelte';
|
||||
import { getModalText } from '$lib/utils/common';
|
||||
import TableCopyButton from '$lib/components/table/TableCopyButton.svelte';
|
||||
import { showIsLoading, hideIsLoading } from '$lib/store/loading.js';
|
||||
import TableDropDownEllipsis from '$lib/components/table/TableDropDownEllipsis.svelte';
|
||||
import DeleteAlert from '$lib/components/modal/DeleteAlert.svelte';
|
||||
import SelectSquare from '$lib/components/SelectSquare.svelte';
|
||||
import TableCellScope from '$lib/components/table/TableCellScope.svelte';
|
||||
import TextareaField from '$lib/components/TextareaField.svelte';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
|
||||
// data
|
||||
let form = null;
|
||||
let formValues = {
|
||||
id: null,
|
||||
name: null,
|
||||
|
||||
clientID: null,
|
||||
clientSecret: null,
|
||||
authURL: null,
|
||||
tokenURL: null,
|
||||
scopes: null
|
||||
};
|
||||
let providers = [];
|
||||
let providersHasNextPage = false;
|
||||
let formError = '';
|
||||
let contextCompanyID = null;
|
||||
const tableURLParams = newTableURLParams();
|
||||
let isModalVisible = false;
|
||||
let isProviderTableLoading = false;
|
||||
let isSubmitting = false;
|
||||
let modalMode = null;
|
||||
let modalText = '';
|
||||
let isDeleteAlertVisible = false;
|
||||
let deleteValues = {
|
||||
id: null,
|
||||
name: null
|
||||
};
|
||||
let isRemoveAuthAlertVisible = false;
|
||||
let removeAuthValues = {
|
||||
id: null,
|
||||
name: null
|
||||
};
|
||||
|
||||
$: {
|
||||
modalText = getModalText('OAuth', modalMode);
|
||||
}
|
||||
|
||||
// hooks
|
||||
onMount(() => {
|
||||
if (appStateService.getContext()) {
|
||||
contextCompanyID = appStateService.getContext().companyID;
|
||||
}
|
||||
refreshProviders();
|
||||
tableURLParams.onChange(refreshProviders);
|
||||
|
||||
// listen for oauth callback messages from popup window
|
||||
const handleMessage = (event) => {
|
||||
console.log('received message:', event.data, 'from origin:', event.origin);
|
||||
// verify message is from our origin or localhost (for dev with vite proxy)
|
||||
const isValidOrigin =
|
||||
event.origin === window.location.origin ||
|
||||
(window.location.hostname === 'localhost' &&
|
||||
new URL(event.origin).hostname === 'localhost');
|
||||
if (!isValidOrigin) {
|
||||
console.log('message origin does not match, ignoring');
|
||||
return;
|
||||
}
|
||||
// handle oauth callback result
|
||||
if (event.data && event.data.type === 'oauth-callback') {
|
||||
console.log('oauth callback message received with status:', event.data.status);
|
||||
if (event.data.status === 'success') {
|
||||
addToast('OAuth authorization successful!', 'Success');
|
||||
refreshProviders();
|
||||
} else if (event.data.status === 'error') {
|
||||
addToast('OAuth authorization failed', 'Error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
console.log('message listener added for oauth callbacks');
|
||||
|
||||
(async () => {
|
||||
const editID = $page.url.searchParams.get('edit');
|
||||
if (editID) {
|
||||
await openUpdateModal(editID);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
tableURLParams.unsubscribe();
|
||||
window.removeEventListener('message', handleMessage);
|
||||
};
|
||||
});
|
||||
|
||||
// component logic
|
||||
const refreshProviders = async () => {
|
||||
try {
|
||||
isProviderTableLoading = true;
|
||||
const data = await getProviders();
|
||||
providers = data.rows;
|
||||
providersHasNextPage = data.hasNextPage;
|
||||
} catch (e) {
|
||||
addToast('Failed to get OAuth providers', 'Error');
|
||||
console.error(e);
|
||||
} finally {
|
||||
isProviderTableLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a provider by ID
|
||||
* @param {string} id
|
||||
*/
|
||||
const getProvider = async (id) => {
|
||||
try {
|
||||
showIsLoading();
|
||||
const res = await api.oauthProvider.getByID(id);
|
||||
if (res.success) {
|
||||
return res.data;
|
||||
} else {
|
||||
throw res.error;
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to get OAuth provider', 'Error');
|
||||
console.error('failed to get OAuth provider', e);
|
||||
} finally {
|
||||
hideIsLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const getProviders = async () => {
|
||||
try {
|
||||
const res = await api.oauthProvider.getAll(tableURLParams, contextCompanyID);
|
||||
if (!res.success) {
|
||||
throw res.error;
|
||||
}
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
addToast('Failed to get OAuth providers', 'Error');
|
||||
console.error('failed to get OAuth providers', e);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const onClickSubmit = async () => {
|
||||
try {
|
||||
isSubmitting = true;
|
||||
if (modalMode === 'create' || modalMode === 'copy') {
|
||||
await create();
|
||||
return;
|
||||
} else {
|
||||
await update();
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
};
|
||||
|
||||
const create = async () => {
|
||||
formError = '';
|
||||
try {
|
||||
const res = await api.oauthProvider.create({
|
||||
name: formValues.name,
|
||||
clientID: formValues.clientID,
|
||||
clientSecret: formValues.clientSecret,
|
||||
authURL: formValues.authURL,
|
||||
tokenURL: formValues.tokenURL,
|
||||
scopes: formValues.scopes,
|
||||
companyID: contextCompanyID
|
||||
});
|
||||
if (!res.success) {
|
||||
formError = res.error;
|
||||
return;
|
||||
}
|
||||
addToast('Created OAuth provider', 'Success');
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
addToast('Failed to create OAuth provider', 'Error');
|
||||
console.error('failed to create OAuth provider:', err);
|
||||
}
|
||||
refreshProviders();
|
||||
};
|
||||
|
||||
const update = async () => {
|
||||
formError = '';
|
||||
try {
|
||||
const res = await api.oauthProvider.update({
|
||||
id: formValues.id,
|
||||
name: formValues.name,
|
||||
clientID: formValues.clientID,
|
||||
clientSecret: formValues.clientSecret,
|
||||
authURL: formValues.authURL,
|
||||
tokenURL: formValues.tokenURL,
|
||||
scopes: formValues.scopes,
|
||||
companyID: formValues.companyID
|
||||
});
|
||||
if (res.success) {
|
||||
addToast('Updated OAuth provider', 'Success');
|
||||
closeModal();
|
||||
} else {
|
||||
formError = res.error;
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to update OAuth provider', 'Error');
|
||||
console.error('failed to update OAuth provider', e);
|
||||
}
|
||||
refreshProviders();
|
||||
};
|
||||
|
||||
const openDeleteAlert = async (provider) => {
|
||||
isDeleteAlertVisible = true;
|
||||
deleteValues.id = provider.id;
|
||||
deleteValues.name = provider.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes an OAuth provider
|
||||
* @param {string} id
|
||||
*/
|
||||
const onClickDelete = async (id) => {
|
||||
const action = api.oauthProvider.delete(id);
|
||||
|
||||
action
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
refreshProviders();
|
||||
return;
|
||||
}
|
||||
throw res.error;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('failed to delete oauth provider', e);
|
||||
});
|
||||
return action;
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
modalMode = 'create';
|
||||
formError = '';
|
||||
formValues = {
|
||||
id: null,
|
||||
name: null,
|
||||
clientID: null,
|
||||
clientSecret: null,
|
||||
authURL: null,
|
||||
tokenURL: null,
|
||||
scopes: null
|
||||
};
|
||||
isModalVisible = true;
|
||||
};
|
||||
|
||||
const openUpdateModal = async (id) => {
|
||||
modalMode = 'update';
|
||||
formError = '';
|
||||
const provider = await getProvider(id);
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
formValues = {
|
||||
id: provider.id,
|
||||
name: provider.name,
|
||||
clientID: provider.clientID,
|
||||
clientSecret: provider.clientSecret,
|
||||
authURL: provider.authURL,
|
||||
tokenURL: provider.tokenURL,
|
||||
scopes: provider.scopes,
|
||||
companyID: provider.companyID
|
||||
};
|
||||
isModalVisible = true;
|
||||
};
|
||||
|
||||
const openCopyModal = async (id) => {
|
||||
modalMode = 'copy';
|
||||
formError = '';
|
||||
const provider = await getProvider(id);
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
formValues = {
|
||||
id: null,
|
||||
name: provider.name + ' (copy)',
|
||||
clientID: provider.clientID,
|
||||
clientSecret: provider.clientSecret,
|
||||
authURL: provider.authURL,
|
||||
tokenURL: provider.tokenURL,
|
||||
scopes: provider.scopes
|
||||
};
|
||||
isModalVisible = true;
|
||||
};
|
||||
|
||||
const openRemoveAuthAlert = async (provider) => {
|
||||
isRemoveAuthAlertVisible = true;
|
||||
removeAuthValues.id = provider.id;
|
||||
removeAuthValues.name = provider.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes authorization from an OAuth provider
|
||||
* @param {string} id
|
||||
*/
|
||||
const onClickRemoveAuth = async (id) => {
|
||||
const action = api.oauthProvider.removeAuthorization(id);
|
||||
|
||||
action
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
addToast('Removed authorization from OAuth provider', 'Success');
|
||||
refreshProviders();
|
||||
return;
|
||||
}
|
||||
throw res.error;
|
||||
})
|
||||
.catch((e) => {
|
||||
addToast('Failed to remove authorization', 'Error');
|
||||
console.error('failed to remove authorization from oauth provider', e);
|
||||
});
|
||||
return action;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
isModalVisible = false;
|
||||
form?.reset();
|
||||
};
|
||||
|
||||
const onClickAuthorize = async (id) => {
|
||||
try {
|
||||
showIsLoading();
|
||||
const res = await api.oauthProvider.getAuthorizationURL(id);
|
||||
if (res.success && res.data.authorizationURL) {
|
||||
// open authorization url in popup window
|
||||
const width = 600;
|
||||
const height = 700;
|
||||
const left = window.screenX + (window.outerWidth - width) / 2;
|
||||
const top = window.screenY + (window.outerHeight - height) / 2;
|
||||
const popup = window.open(
|
||||
res.data.authorizationURL,
|
||||
'OAuth Authorization',
|
||||
`width=${width},height=${height},left=${left},top=${top},toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes`
|
||||
);
|
||||
if (!popup) {
|
||||
addToast('Failed to open authorization window. Please allow popups.', 'Error');
|
||||
}
|
||||
} else {
|
||||
throw res.error || 'No authorization URL returned';
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('Failed to get authorization URL', 'Error');
|
||||
console.error('failed to get authorization URL', e);
|
||||
} finally {
|
||||
hideIsLoading();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<HeadTitle title="OAuth" />
|
||||
<main>
|
||||
<Headline>OAuth</Headline>
|
||||
<BigButton on:click={openCreateModal}>New OAuth</BigButton>
|
||||
<Table
|
||||
columns={['Name', 'Status']}
|
||||
sortable={['name', 'is_authorized']}
|
||||
pagination={tableURLParams}
|
||||
hasData={isProviderTableLoading || providers.length > 0}
|
||||
hasNextPage={providersHasNextPage}
|
||||
plural="OAuth providers"
|
||||
isGhost={isProviderTableLoading}
|
||||
hasActions
|
||||
>
|
||||
{#each providers as provider}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<button
|
||||
on:click={() => {
|
||||
openUpdateModal(provider.id);
|
||||
}}
|
||||
{...globalButtonDisabledAttributes(provider, contextCompanyID)}
|
||||
title={provider.name}
|
||||
class="block w-full py-1 text-left"
|
||||
>
|
||||
{provider.name}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{#if provider.isAuthorized}
|
||||
<span class="text-green-600 dark:text-green-400">Authorized</span>
|
||||
{:else}
|
||||
<span class="text-yellow-600 dark:text-yellow-400">Not Authorized</span>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCellEmpty />
|
||||
<TableCellAction>
|
||||
<TableDropDownEllipsis>
|
||||
<TableCopyButton
|
||||
title="Copy"
|
||||
on:click={() => openCopyModal(provider.id)}
|
||||
{...globalButtonDisabledAttributes(provider, contextCompanyID)}
|
||||
/>
|
||||
<TableUpdateButton
|
||||
on:click={() => openUpdateModal(provider.id)}
|
||||
{...globalButtonDisabledAttributes(provider, contextCompanyID)}
|
||||
/>
|
||||
{#if !provider.isAuthorized}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => onClickAuthorize(provider.id)}
|
||||
class="w-full px py-1 text-slate-600 dark:text-gray-200 hover:bg-highlight-blue dark:hover:bg-highlight-blue/50 hover:text-white cursor-pointer text-left transition-colors duration-200"
|
||||
>
|
||||
<p class="ml-2 text-left">Authorize</p>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => onClickAuthorize(provider.id)}
|
||||
class="w-full px py-1 text-slate-600 dark:text-gray-200 hover:bg-highlight-blue dark:hover:bg-highlight-blue/50 hover:text-white cursor-pointer text-left transition-colors duration-200"
|
||||
>
|
||||
<p class="ml-2 text-left">Re-authorize</p>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => openRemoveAuthAlert(provider)}
|
||||
class="w-full px py-1 text-slate-600 dark:text-gray-200 hover:bg-highlight-blue dark:hover:bg-highlight-blue/50 hover:text-white cursor-pointer text-left transition-colors duration-200"
|
||||
>
|
||||
<p class="ml-2 text-left">Remove Authorization</p>
|
||||
</button>
|
||||
{/if}
|
||||
<TableDeleteButton
|
||||
on:click={() => openDeleteAlert(provider)}
|
||||
{...globalButtonDisabledAttributes(provider, contextCompanyID)}
|
||||
/>
|
||||
</TableDropDownEllipsis>
|
||||
</TableCellAction>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</Table>
|
||||
|
||||
<Modal headerText={modalText} visible={isModalVisible} onClose={closeModal} {isSubmitting}>
|
||||
<FormGrid on:submit={onClickSubmit} bind:bindTo={form} {isSubmitting}>
|
||||
<FormColumns>
|
||||
<FormColumn>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={127}
|
||||
bind:value={formValues.name}
|
||||
placeholder="My OAuth Provider"
|
||||
>
|
||||
Name
|
||||
</TextField>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={255}
|
||||
bind:value={formValues.clientID}
|
||||
placeholder="your-client-id"
|
||||
>
|
||||
Client ID
|
||||
</TextField>
|
||||
<PasswordField
|
||||
required={modalMode === 'create' || modalMode === 'copy'}
|
||||
minLength={modalMode === 'update' ? 0 : 1}
|
||||
maxLength={255}
|
||||
bind:value={formValues.clientSecret}
|
||||
placeholder={modalMode === 'update'
|
||||
? 'Leave empty to keep existing secret'
|
||||
: 'your-client-secret'}
|
||||
>
|
||||
Client Secret
|
||||
</PasswordField>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={512}
|
||||
bind:value={formValues.authURL}
|
||||
placeholder="https://example.com/oauth2/v2/auth"
|
||||
>
|
||||
Authorization URL
|
||||
</TextField>
|
||||
<TextField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={512}
|
||||
bind:value={formValues.tokenURL}
|
||||
placeholder="https://example.com/oauth2/token"
|
||||
>
|
||||
Token URL
|
||||
</TextField>
|
||||
<TextareaField
|
||||
required
|
||||
minLength={1}
|
||||
maxLength={512}
|
||||
bind:value={formValues.scopes}
|
||||
placeholder="https://example.com/auth/mail.send"
|
||||
height="small"
|
||||
toolTipText="Space-separated list of OAuth scopes">Scopes</TextareaField
|
||||
>
|
||||
</FormColumn>
|
||||
</FormColumns>
|
||||
|
||||
<FormError message={formError} />
|
||||
<FormFooter {closeModal} {isSubmitting} />
|
||||
</FormGrid>
|
||||
</Modal>
|
||||
|
||||
<DeleteAlert
|
||||
name={deleteValues.name}
|
||||
onClick={() => onClickDelete(deleteValues.id)}
|
||||
bind:isVisible={isDeleteAlertVisible}
|
||||
/>
|
||||
|
||||
<DeleteAlert
|
||||
name={removeAuthValues.name}
|
||||
onClick={() => onClickRemoveAuth(removeAuthValues.id)}
|
||||
bind:isVisible={isRemoveAuthAlertVisible}
|
||||
title="Remove Authorization"
|
||||
actionMessage="Are you sure you want to remove authorization from"
|
||||
list={[
|
||||
'Access and refresh tokens will be deleted',
|
||||
'You will need to re-authorize to use this provider for sending'
|
||||
]}
|
||||
permanent={false}
|
||||
/>
|
||||
</main>
|
||||
Reference in New Issue
Block a user