Skip to content

Commit e84e4b0

Browse files
authored
Mariano/fix 13 (#1900)
* refactor(integration): add support for additional OAuth settings in integrations
1 parent 60a1e60 commit e84e4b0

File tree

6 files changed

+94
-35
lines changed

6 files changed

+94
-35
lines changed

apps/api/src/integration-platform/controllers/admin-integrations.controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ export class AdminIntegrationsController {
7272
createAppUrl: manifest.auth.config.createAppUrl,
7373
requiredScopes: manifest.auth.config.scopes,
7474
authorizeUrl: manifest.auth.config.authorizeUrl,
75+
additionalOAuthSettings:
76+
manifest.auth.config.additionalOAuthSettings || [],
7577
}),
7678
};
7779
});
@@ -114,6 +116,8 @@ export class AdminIntegrationsController {
114116
createAppUrl: manifest.auth.config.createAppUrl,
115117
requiredScopes: manifest.auth.config.scopes,
116118
callbackUrl: `${process.env.BASE_URL || 'http://localhost:3333'}/v1/integrations/oauth/callback`,
119+
additionalOAuthSettings:
120+
manifest.auth.config.additionalOAuthSettings || [],
117121
}),
118122
};
119123
}

apps/api/src/integration-platform/controllers/oauth.controller.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,17 @@ export class OAuthController {
146146
redirectUrl,
147147
});
148148

149-
// Build authorization URL, replacing any placeholders with custom settings
149+
// Build authorization URL, replacing any placeholders with additional OAuth settings
150150
let authorizeUrl = oauthConfig.authorizeUrl;
151-
if (credentials.customSettings) {
152-
// Replace {APP_NAME} placeholder with custom setting (used by Rippling, etc.)
153-
if (credentials.customSettings.appName) {
154-
authorizeUrl = authorizeUrl.replace(
155-
'{APP_NAME}',
156-
String(credentials.customSettings.appName),
157-
);
151+
if (credentials.customSettings && oauthConfig.additionalOAuthSettings) {
152+
// Dynamically replace tokens based on additionalOAuthSettings definition
153+
for (const setting of oauthConfig.additionalOAuthSettings) {
154+
if (setting.token && credentials.customSettings[setting.id]) {
155+
authorizeUrl = authorizeUrl.replace(
156+
setting.token,
157+
String(credentials.customSettings[setting.id]),
158+
);
159+
}
158160
}
159161
}
160162
const authUrl = new URL(authorizeUrl);

apps/app/src/app/(app)/admin/integrations/page.tsx

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Card, CardContent } from '@comp/ui/card';
77
import { Input } from '@comp/ui/input';
88
import { Label } from '@comp/ui/label';
99
import {
10-
AlertCircle,
1110
CheckCircle2,
1211
ExternalLink,
1312
Key,
@@ -21,6 +20,17 @@ import Image from 'next/image';
2120
import { useState } from 'react';
2221
import useSWR from 'swr';
2322

23+
interface AdditionalOAuthSetting {
24+
id: string;
25+
label: string;
26+
type: 'text' | 'password' | 'textarea' | 'select' | 'combobox';
27+
placeholder?: string;
28+
helpText?: string;
29+
required: boolean;
30+
options?: { value: string; label: string }[];
31+
token?: string;
32+
}
33+
2434
interface Integration {
2535
id: string;
2636
name: string;
@@ -38,6 +48,7 @@ interface Integration {
3848
createAppUrl?: string;
3949
requiredScopes?: string[];
4050
authorizeUrl?: string;
51+
additionalOAuthSettings?: AdditionalOAuthSetting[];
4152
}
4253

4354
function IntegrationCard({
@@ -50,17 +61,21 @@ function IntegrationCard({
5061
const [showConfig, setShowConfig] = useState(false);
5162
const [clientId, setClientId] = useState('');
5263
const [clientSecret, setClientSecret] = useState('');
53-
const [appName, setAppName] = useState('');
64+
const [customSettingsValues, setCustomSettingsValues] = useState<Record<string, string>>({});
5465
const [isSaving, setIsSaving] = useState(false);
5566
const [isDeleting, setIsDeleting] = useState(false);
5667
const [error, setError] = useState<string | null>(null);
5768

58-
// Check if this integration needs an app name (has {APP_NAME} placeholder in authorize URL)
59-
const needsAppName = integration.authorizeUrl?.includes('{APP_NAME}');
69+
const additionalSettings = integration.additionalOAuthSettings || [];
6070

6171
const handleSave = async () => {
6272
if (!clientId || !clientSecret) return;
63-
if (needsAppName && !appName) return;
73+
74+
// Validate required additional settings
75+
const hasAllRequiredSettings = additionalSettings.every(
76+
(setting) => !setting.required || customSettingsValues[setting.id],
77+
);
78+
if (!hasAllRequiredSettings) return;
6479

6580
setIsSaving(true);
6681
setError(null);
@@ -69,14 +84,16 @@ function IntegrationCard({
6984
providerSlug: integration.id,
7085
clientId,
7186
clientSecret,
72-
customSettings: needsAppName ? { appName } : undefined,
87+
customSettings:
88+
Object.keys(customSettingsValues).length > 0 ? customSettingsValues : undefined,
7389
});
7490

7591
if (response.error) {
7692
setError(response.error);
7793
} else {
7894
setClientId('');
7995
setClientSecret('');
96+
setCustomSettingsValues({});
8097
setShowConfig(false);
8198
onRefresh();
8299
}
@@ -227,26 +244,51 @@ function IntegrationCard({
227244
onChange={(e) => setClientSecret(e.target.value)}
228245
/>
229246
</div>
230-
{needsAppName && (
231-
<div>
232-
<Label className="text-sm">App Name</Label>
233-
<Input
234-
className="font-mono text-sm"
235-
placeholder="e.g., compai533c"
236-
value={appName}
237-
onChange={(e) => setAppName(e.target.value)}
238-
/>
239-
<p className="text-xs text-muted-foreground mt-1">
240-
The app name from your Rippling developer portal (used in the authorize
241-
URL)
242-
</p>
243-
</div>
247+
248+
{/* Additional OAuth Settings - provider-specific OAuth configuration */}
249+
{additionalSettings.length > 0 && (
250+
<>
251+
<div className="border-t pt-3 mt-1">
252+
<h4 className="text-xs font-medium text-muted-foreground mb-3 uppercase tracking-wide">
253+
Additional OAuth Settings
254+
</h4>
255+
</div>
256+
{additionalSettings.map((setting) => (
257+
<div key={setting.id}>
258+
<Label className="text-sm">
259+
{setting.label}
260+
{setting.required && <span className="text-destructive ml-1">*</span>}
261+
</Label>
262+
<Input
263+
className="font-mono text-sm"
264+
placeholder={setting.placeholder}
265+
value={customSettingsValues[setting.id] || ''}
266+
onChange={(e) =>
267+
setCustomSettingsValues({
268+
...customSettingsValues,
269+
[setting.id]: e.target.value,
270+
})
271+
}
272+
/>
273+
{setting.helpText && (
274+
<p className="text-xs text-muted-foreground mt-1">
275+
{setting.helpText}
276+
</p>
277+
)}
278+
</div>
279+
))}
280+
</>
244281
)}
245282
</div>
246283

247284
<Button
248285
onClick={handleSave}
249-
disabled={!clientId || !clientSecret || (needsAppName && !appName) || isSaving}
286+
disabled={
287+
!clientId ||
288+
!clientSecret ||
289+
additionalSettings.some((s) => s.required && !customSettingsValues[s.id]) ||
290+
isSaving
291+
}
250292
>
251293
{isSaving ? (
252294
<Loader2 className="h-4 w-4 animate-spin mr-2" />

packages/integration-platform/src/manifests/rippling/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ export const ripplingManifest: IntegrationManifest = {
3737
4. Enable the required scopes under HR information: "Read access to Workers"
3838
5. Note your app name - it's used in the authorize URL`,
3939
createAppUrl: 'https://app.rippling.com/partner',
40+
additionalOAuthSettings: [
41+
{
42+
id: 'appName',
43+
label: 'Rippling App Name',
44+
type: 'text',
45+
helpText:
46+
'Your app name from the Rippling developer portal. This appears in the authorize URL (app.rippling.com/apps/PLATFORM/{appName}/authorize).',
47+
required: true,
48+
token: '{APP_NAME}',
49+
},
50+
],
4051
},
4152
},
4253

packages/integration-platform/src/manifests/vercel/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ Copy the **Client ID** and **Client Secret** (click Reveal)
3636
Enter the Client ID, Secret, and the integration slug (from \`vercel.com/integrations/{slug}\`) in the admin page.
3737
3838
> **Team Support**: When connecting, you'll choose whether to install for your personal account or a team. If you select a team, all API calls will be scoped to that team.`,
39-
customSettings: [
39+
additionalOAuthSettings: [
4040
{
4141
id: 'appSlug',
4242
label: 'Vercel Integration Slug',
4343
type: 'text',
44-
placeholder: 'comp-ai',
4544
helpText:
4645
'The slug from your Vercel integration URL (https://vercel.com/integrations/{slug}). Used to launch the correct install page.',
4746
required: true,

packages/integration-platform/src/types.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ export const OAuthConfigSchema = z.object({
3737
*/
3838
refreshUrl: z.string().url().optional(),
3939
/**
40-
* Custom settings that admins need to configure alongside client ID/secret.
41-
* These are used for provider-specific settings like Vercel's integration slug
42-
* or Rippling's app name. The `token` field replaces placeholders in authorizeUrl.
40+
* Additional OAuth settings that admins configure alongside client ID/secret.
41+
* These are provider-specific settings like Vercel's integration slug or Rippling's app name.
42+
* The `token` field allows replacing placeholders in authorizeUrl with these values.
43+
* Example: Vercel uses {APP_SLUG} in the authorize URL which gets replaced with the configured slug.
4344
*/
44-
customSettings: z
45+
additionalOAuthSettings: z
4546
.array(
4647
z.object({
4748
id: z.string(),

0 commit comments

Comments
 (0)