core/apps/webapp/app/services/oauth/oauth.server.ts
Harshith Mullapudi 54e535d57d
Feat: v2 (#12)
* Feat: v2

* feat: add chat functionality

* First cut: integrations

* Feat: add conversation API

* Enhance conversation handling and memory management

* Feat: added conversation

---------

Co-authored-by: Manoj K <saimanoj58@gmail.com>
2025-07-08 22:41:00 +05:30

246 lines
6.9 KiB
TypeScript

import { IntegrationPayloadEventType, type OAuth2Params } from "@core/types";
import * as simpleOauth2 from "simple-oauth2";
import { tasks } from "@trigger.dev/sdk/v3";
import {
getSimpleOAuth2ClientConfig,
getTemplate,
type OAuthBodyInterface,
type ProviderTemplateOAuth2,
type SessionRecord,
} from "./oauth-utils.server";
import { getIntegrationDefinitionWithId } from "../integrationDefinition.server";
import { type scheduler } from "~/trigger/integrations/scheduler";
import { logger } from "../logger.service";
import { runIntegrationTrigger } from "../integration.server";
import type { IntegrationDefinitionV2 } from "@core/database";
import { env } from "~/env.server";
// Use process.env for config in Remix
const CALLBACK_URL = process.env.OAUTH_CALLBACK_URL ?? "";
// Session store (in-memory, for single server)
const session: Record<string, SessionRecord> = {};
export type CallbackParams = Record<string, string>;
// Remix-style callback handler
// Accepts a Remix LoaderFunctionArgs-like object: { request }
export async function callbackHandler(
params: CallbackParams,
request: Request,
) {
if (!params.state) {
throw new Error("No state found");
}
const sessionRecord = session[params.state];
// Delete the session once it's used
delete session[params.state];
if (!sessionRecord) {
throw new Error("No session found");
}
const integrationDefinition = await getIntegrationDefinitionWithId(
sessionRecord.integrationDefinitionId,
);
const template = (await getTemplate(
integrationDefinition as IntegrationDefinitionV2,
)) as ProviderTemplateOAuth2;
if (integrationDefinition === null) {
const errorMessage = "No matching integration definition found";
return new Response(null, {
status: 302,
headers: {
Location: `${sessionRecord.redirectURL}?success=false&error=${encodeURIComponent(
errorMessage,
)}`,
},
});
}
let additionalTokenParams: Record<string, string> = {};
if (template.token_params !== undefined) {
const deepCopy = JSON.parse(JSON.stringify(template.token_params));
additionalTokenParams = deepCopy;
}
if (template.refresh_params) {
additionalTokenParams = template.refresh_params;
}
const headers: Record<string, string> = {};
const integrationConfig = integrationDefinition.config as any;
const integrationSpec = integrationDefinition.spec as any;
if (template.token_request_auth_method === "basic") {
headers["Authorization"] = `Basic ${Buffer.from(
`${integrationConfig?.clientId}:${integrationConfig.clientSecret}`,
).toString("base64")}`;
}
const accountIdentifier = sessionRecord.accountIdentifier
? `&accountIdentifier=${encodeURIComponent(sessionRecord.accountIdentifier)}`
: "";
const integrationKeys = sessionRecord.integrationKeys
? `&integrationKeys=${encodeURIComponent(sessionRecord.integrationKeys)}`
: "";
try {
const scopes = (integrationSpec.auth.OAuth2 as OAuth2Params)
.scopes as string[];
const simpleOAuthClient = new simpleOauth2.AuthorizationCode(
getSimpleOAuth2ClientConfig(
{
client_id: integrationConfig.clientId,
client_secret: integrationConfig.clientSecret,
scopes: scopes.join(","),
},
template,
sessionRecord.config,
),
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tokensResponse: any = await simpleOAuthClient.getToken(
{
code: params.code as string,
redirect_uri: CALLBACK_URL,
...additionalTokenParams,
},
{
headers,
},
);
const integrationAccount = await runIntegrationTrigger(
integrationDefinition,
{
event: IntegrationPayloadEventType.INTEGRATION_ACCOUNT_CREATED,
eventBody: {
oauthResponse: tokensResponse.token,
oauthParams: {
...params,
redirect_uri: CALLBACK_URL,
},
integrationDefinition,
},
},
sessionRecord.userId,
sessionRecord.workspaceId,
);
await tasks.trigger<typeof scheduler>("scheduler", {
integrationAccountId: integrationAccount.id,
});
return new Response(null, {
status: 302,
headers: {
Location: `${sessionRecord.redirectURL}?success=true&integrationName=${encodeURIComponent(
integrationDefinition.name,
)}${accountIdentifier}${integrationKeys}`,
},
});
} catch (e: any) {
logger.error(e);
return new Response(null, {
status: 302,
headers: {
Location: `${sessionRecord.redirectURL}?success=false&error=${encodeURIComponent(
e.message,
)}${accountIdentifier}${integrationKeys}`,
},
});
}
}
export async function getRedirectURL(
oAuthBody: OAuthBodyInterface,
userId: string,
workspaceId?: string,
specificScopes?: string,
) {
const { integrationDefinitionId, personal } = oAuthBody;
const redirectURL = `${env.APP_ORIGIN}/integrations`;
logger.info(
`We got OAuth request for ${workspaceId}: ${integrationDefinitionId}`,
);
const integrationDefinition = await getIntegrationDefinitionWithId(
integrationDefinitionId,
);
if (!integrationDefinition) {
throw new Error("No integration definition ");
}
const spec = integrationDefinition.spec as any;
const externalConfig = spec.auth.OAuth2 as OAuth2Params;
const template = await getTemplate(integrationDefinition);
const scopesString =
specificScopes || (externalConfig.scopes as string[]).join(",");
const additionalAuthParams = template.authorization_params || {};
const integrationConfig = integrationDefinition.config as any;
try {
const simpleOAuthClient = new simpleOauth2.AuthorizationCode(
getSimpleOAuth2ClientConfig(
{
client_id: integrationConfig.clientId,
client_secret: integrationConfig.clientSecret,
scopes: scopesString,
},
template,
externalConfig,
),
);
const uniqueId = Date.now().toString(36);
session[uniqueId] = {
integrationDefinitionId: integrationDefinition.id,
redirectURL,
workspaceId: workspaceId as string,
config: externalConfig,
userId,
personal,
};
const scopes = [
...scopesString.split(","),
...(template.default_scopes || []),
];
const scopeIdentifier = externalConfig.scope_identifier ?? "scope";
const authorizationUri = simpleOAuthClient.authorizeURL({
redirect_uri: CALLBACK_URL,
[scopeIdentifier]: scopes.join(template.scope_separator || " "),
state: uniqueId,
...additionalAuthParams,
});
logger.debug(
`OAuth 2.0 for ${integrationDefinition.name} - redirecting to: ${authorizationUri}`,
);
return {
status: 200,
redirectURL: authorizationUri,
};
} catch (e: any) {
logger.warn(e);
throw new Error(e.message);
}
}