Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ STORAGE_PROVIDER="local"
X_URL=""
X_API_KEY=""
X_API_SECRET=""
X_CLIENT_ID=""
X_CLIENT_SECRET=""
LINKEDIN_CLIENT_ID=""
LINKEDIN_CLIENT_SECRET=""
REDDIT_CLIENT_ID=""
Expand Down
20 changes: 19 additions & 1 deletion apps/backend/src/api/routes/enterprise.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class EnterpriseController {
refreshId?: string;
provider: string;
webhookUrl: string;
authMode?: string;
};

if (!load || !load.redirectUrl || !load.apiKey || !load.provider) {
Expand All @@ -75,8 +76,25 @@ export class EnterpriseController {
load.provider
);

const selectableAuthProvider =
this._integrationManager.getSelectableAuthProvider(load.provider);
const previousIntegration =
selectableAuthProvider?.getAuthMode && load.refreshId
? (await this._integrationService.getIntegrationsList(org.id)).find(
(item) => item.internalId === load.refreshId
)
: undefined;
const resolvedAuthMode =
load.authMode ||
(previousIntegration
? selectableAuthProvider?.getAuthMode?.(previousIntegration)
: undefined);
const { codeVerifier, state, url } =
await integrationProvider.generateAuthUrl();
selectableAuthProvider && resolvedAuthMode
? await selectableAuthProvider.generateAuthUrlForMode(
resolvedAuthMode
)
: await integrationProvider.generateAuthUrl();

if (load.refreshId) {
await ioRedis.set(`refresh:${state}`, load.refreshId, 'EX', 3600);
Expand Down
21 changes: 20 additions & 1 deletion apps/backend/src/api/routes/integrations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export class IntegrationsController {
@Query('externalUrl') externalUrl: string,
@Query('redirectUrl') redirectUrl: string,
@Query('onboarding') onboarding: string,
@Query('authMode') authMode: string,
@GetOrgFromRequest() org: Organization
) {
if (
Expand All @@ -223,8 +224,26 @@ export class IntegrationsController {
}
: undefined;

const selectableAuthProvider =
this._integrationManager.getSelectableAuthProvider(integration);
const previousIntegration =
selectableAuthProvider?.getAuthMode && refresh
? (await this._integrationService.getIntegrationsList(org.id)).find(
(item) => item.internalId === refresh
)
: undefined;
const resolvedAuthMode =
authMode ||
(previousIntegration
? selectableAuthProvider?.getAuthMode?.(previousIntegration)
: undefined);
const { codeVerifier, state, url } =
await integrationProvider.generateAuthUrl(getExternalUrl);
selectableAuthProvider && resolvedAuthMode
? await selectableAuthProvider.generateAuthUrlForMode(
resolvedAuthMode,
getExternalUrl
)
: await integrationProvider.generateAuthUrl(getExternalUrl);

if (refresh) {
await ioRedis.set(`refresh:${state}`, refresh, 'EX', 3600);
Expand Down
12 changes: 7 additions & 5 deletions apps/backend/src/api/routes/no.auth.integrations.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,13 @@ export class NoAuthIntegrationsController {
: undefined
);

this._refreshIntegrationService
.startRefreshWorkflow(org.id, createUpdate.id, integrationProvider)
.catch((err) => {
console.log(err);
});
if (refreshToken) {
this._refreshIntegrationService
.startRefreshWorkflow(org.id, createUpdate.id, integrationProvider)
.catch((err) => {
console.log(err);
});
}

// Fetch pages if this is a two-step provider and not a refresh
let pages: any[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ export class PublicIntegrationsController {
async getIntegrationUrl(
@Param('integration') integration: string,
@Query('refresh') refresh: string,
@Query('authMode') authMode: string,
@GetOrgFromRequest() org: Organization
) {
Sentry.metrics.count('public_api-request', 1);
Expand All @@ -352,8 +353,25 @@ export class PublicIntegrationsController {
}

try {
const selectableAuthProvider =
this._integrationManager.getSelectableAuthProvider(integration);
const previousIntegration =
selectableAuthProvider?.getAuthMode && refresh
? (await this._integrationService.getIntegrationsList(org.id)).find(
(item) => item.internalId === refresh
)
: undefined;
const resolvedAuthMode =
authMode ||
(previousIntegration
? selectableAuthProvider?.getAuthMode?.(previousIntegration)
: undefined);
const { codeVerifier, state, url } =
await integrationProvider.generateAuthUrl();
selectableAuthProvider && resolvedAuthMode
? await selectableAuthProvider.generateAuthUrlForMode(
resolvedAuthMode
)
: await integrationProvider.generateAuthUrl();

if (refresh) {
await ioRedis.set(`refresh:${state}`, refresh, 'EX', 3600);
Expand Down
175 changes: 151 additions & 24 deletions apps/frontend/src/components/launches/add.provider.component.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useModals } from '@gitroom/frontend/components/layout/new-modal';
import React, { FC, useCallback, useMemo } from 'react';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { Input } from '@gitroom/react/form/input';
import { FieldValues, FormProvider, useForm } from 'react-hook-form';
Expand All @@ -21,6 +21,35 @@ import copy from 'copy-to-clipboard';
import { capitalize } from 'lodash';
const resolver = classValidatorResolver(ApiKeyDto);

type ProviderAuthOption = {
id: string;
title: string;
description: string;
capabilities: string[];
recommended?: boolean;
};

type SocialProviderItem = {
identifier: string;
name: string;
toolTip?: string;
isExternal: boolean;
isWeb3: boolean;
isChromeExtension?: boolean;
extensionCookies?: Array<{
name: string;
domain: string;
}>;
authOptions?: ProviderAuthOption[];
customFields?: Array<{
key: string;
label: string;
validation: string;
type: 'text' | 'password';
hint?: string;
}>;
};

export const useAddProvider = (update?: () => void, invite?: boolean) => {
const modal = useModals();
const fetch = useFetch();
Expand Down Expand Up @@ -377,25 +406,7 @@ const ChromeExtensionWarning: FC<{
};

export const AddProviderComponent: FC<{
social: Array<{
identifier: string;
name: string;
toolTip?: string;
isExternal: boolean;
isWeb3: boolean;
isChromeExtension?: boolean;
extensionCookies?: Array<{
name: string;
domain: string;
}>;
customFields?: Array<{
key: string;
label: string;
validation: string;
type: 'text' | 'password';
hint?: string;
}>;
}>;
social: SocialProviderItem[];
article: Array<{
identifier: string;
name: string;
Expand All @@ -411,6 +422,12 @@ export const AddProviderComponent: FC<{
const router = useRouter();
const fetch = useFetch();
const modal = useModals();
const t = useT();
const [authSelection, setAuthSelection] = useState<{
invite: boolean;
provider: SocialProviderItem;
}>();
const [selectedAuthMode, setSelectedAuthMode] = useState('');
const getSocialLink = useCallback(
(
invite: boolean,
Expand All @@ -425,9 +442,25 @@ export const AddProviderComponent: FC<{
defaultValue?: string;
type: 'text' | 'password';
hint?: string;
}>
}>,
authOptions?: ProviderAuthOption[],
authMode?: string
) =>
async () => {
if (authOptions && authOptions.length > 1 && !authMode) {
const provider = social.find(
(item) => item.identifier === identifier
)!;
setSelectedAuthMode(
authOptions.find((option) => option.recommended)?.id ||
authOptions[0].id
);
setAuthSelection({ invite, provider });
return;
}

const resolvedAuthMode =
authMode || (authOptions?.length === 1 ? authOptions[0].id : '');
const onboardingParam = onboarding ? 'onboarding=true' : '';
const openWeb3 = async () => {
const { component: Web3Providers } = web3List.find(
Expand Down Expand Up @@ -475,6 +508,9 @@ export const AddProviderComponent: FC<{
isMobile
? `redirectUrl=${encodeURIComponent('postiz://integrations')}`
: '',
resolvedAuthMode
? `authMode=${encodeURIComponent(resolvedAuthMode)}`
: '',
]
.filter(Boolean)
.join('&');
Expand Down Expand Up @@ -664,10 +700,100 @@ export const AddProviderComponent: FC<{
}
await gotoIntegration();
},
[onboarding]
[onboarding, social]
);

const t = useT();
if (authSelection) {
const { provider, invite } = authSelection;
return (
<div className="w-full flex flex-col gap-[20px] rounded-[4px]">
<div className="flex items-center gap-[12px]">
<img
className="w-[32px] h-[32px] rounded-full"
src={`/icons/platforms/${provider.identifier}.png`}
/>
<div>
<div className="text-[18px] font-semibold">
{t('connect_provider', 'Connect {{provider}}', {
provider: provider.name,
})}
</div>
<div className="text-[13px] text-textColor/70">
{t(
'choose_authentication_method',
'Choose how this channel should connect.'
)}
</div>
</div>
</div>

<div className="flex flex-col gap-[8px]">
{provider.authOptions!.map((option) => {
const selected = selectedAuthMode === option.id;
return (
<label
key={option.id}
className={clsx(
'flex gap-[12px] border rounded-[6px] p-[14px] cursor-pointer',
selected
? 'border-primary bg-primary/5'
: 'border-tableBorder bg-newTableHeader'
)}
>
<input
type="radio"
name="provider-auth-mode"
value={option.id}
checked={selected}
onChange={() => setSelectedAuthMode(option.id)}
className="mt-[3px]"
/>
<span className="flex min-w-0 flex-1 flex-col gap-[4px]">
<span className="flex flex-wrap items-center gap-[8px] text-[14px] font-medium">
{option.title}
{option.recommended && (
<span className="text-[11px] text-textColor/70">
{t('recommended', 'Recommended')}
</span>
)}
</span>
<span className="text-[13px] leading-[18px] text-textColor/70">
{option.description}
</span>
</span>
</label>
);
})}
</div>

<div className="flex gap-[8px]">
<Button
type="button"
className="flex-1 !bg-transparent border border-tableBorder !text-textColor"
onClick={() => setAuthSelection(undefined)}
>
{t('back', 'Back')}
</Button>
<Button
type="button"
className="flex-1"
onClick={getSocialLink(
invite,
provider.identifier,
provider.isExternal,
provider.isWeb3,
provider.isChromeExtension,
provider.customFields,
provider.authOptions,
selectedAuthMode
)}
>
{t('continue', 'Continue')}
</Button>
</div>
</div>
);
}

return (
<div className="w-full flex flex-col gap-[20px] rounded-[4px] relative]">
Expand Down Expand Up @@ -702,7 +828,8 @@ export const AddProviderComponent: FC<{
item.isExternal,
item.isWeb3,
item.isChromeExtension,
item.customFields
item.customFields,
item.authOptions
)}
{...(!!item.toolTip
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const ContinueIntegration: FC<{
refresh: searchParams.refresh || '',
};
}
if (provider === 'x') {
if (provider === 'x' && searchParams.oauth_token) {
return {
state: searchParams.oauth_token || '',
code: searchParams.oauth_verifier || '',
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ services:
X_URL: ''
X_API_KEY: ''
X_API_SECRET: ''
X_CLIENT_ID: ''
X_CLIENT_SECRET: ''
LINKEDIN_CLIENT_ID: ''
LINKEDIN_CLIENT_SECRET: ''
REDDIT_CLIENT_ID: ''
Expand Down
Loading