Files
plane/web/ee/store/integrations/github/auth.store.ts
Saurabh Kumar 6587284555 [SILO-134] Implement new OAuth flow for GitHub integration (#3135)
* [SILO-134] RUNWAY Implement new OAuth flow for Github integration (#3089)

* oauth helper service + db helpers + modify plane sdk to accomodate oauth token

* fix indentation + create util for remove undefined in object

* add fields to workspace credential for oauth adapt

* get oauth token helper method

* auth callback flow changes to add plane oauth flow for github integration

* integrate github user and auth callback with oauth

* add user callback support with oauth for github integration

* remove token from cache on app disconnection

* Get Connection Details refactor change

* remove unwanted logs

* add missing translations

* replace plane app client id and secret fetching from env to db

* fix app get from db

* refactoring and comments resolved

* rename installation_id to plane_installation_id

* use db query methods and complete the app secret fetch flow

* remove unwanted logs and send missing values in update creds method

* move plane app db call just before oauth token call + models export in init

* Fix ssl reject error for pg

* add NODE_ENV option for ssl db config + management command to reset app_secrets + iteration fix

* fix oauth rate limit cache key breaking

* Fix redirect uri issue

* Fix pull request behaviour

* remove NODE_ENV check in DB connection
2025-05-09 18:44:35 +05:30

290 lines
9.9 KiB
TypeScript

/* eslint-disable no-useless-catch */
import set from "lodash/set";
import unset from "lodash/unset";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
import { API_BASE_URL, SILO_BASE_PATH, SILO_BASE_URL } from "@plane/constants";
import { GithubAuthorizeState, GithubUserAuthState } from "@plane/etl/github";
// plane web services
import { GithubAuthService } from "@/plane-web/services/integrations/github";
// plane web store
import { ApplicationService } from "@/plane-web/services/marketplace";
import { IGithubStore } from "@/plane-web/store/integrations";
// plane web types
import { TGithubWorkspaceConnection, TGithubWorkspaceUserConnection } from "@/plane-web/types/integrations";
export interface IGithubAuthStore {
// observables
workspaceConnectionMap: Record<string, Record<string, TGithubWorkspaceConnection>>; // workspaceId -> organizationId -> TGithubWorkspaceConnection
githubUserCredentialsMap: Record<string, Record<string, TGithubWorkspaceUserConnection>>; // workspaceId -> userId -> TGithubWorkspaceUserConnection
// computed
workspaceConnectionIds: string[];
githubUserCredentialIds: string[];
// computed functions
workspaceConnectionById: (organizationId: string) => TGithubWorkspaceConnection | undefined;
githubUserCredentialById: (userId: string) => TGithubWorkspaceUserConnection | undefined;
// actions
fetchWorkspaceConnection: () => Promise<TGithubWorkspaceConnection[] | undefined>;
connectWorkspaceConnection: () => Promise<string | undefined>;
disconnectWorkspaceConnection: () => Promise<void>;
fetchGithubUserCredential: (
workspace_id?: string,
user_id?: string
) => Promise<TGithubWorkspaceUserConnection | undefined>;
connectGithubUserCredential: (
workspace_id?: string,
workspace_slug?: string,
user_id?: string,
profileRedirect?: boolean
) => Promise<string | undefined>;
disconnectGithubUserCredential: (workspace_id?: string, user_id?: string) => Promise<void>;
}
export class GithubAuthStore implements IGithubAuthStore {
// observables
workspaceConnectionMap: Record<string, Record<string, TGithubWorkspaceConnection>> = {}; // workspaceId -> organizationId -> TGithubWorkspaceConnection
githubUserCredentialsMap: Record<string, Record<string, TGithubWorkspaceUserConnection>> = {}; // workspaceId -> userId -> TGithubWorkspaceUserConnection
// service
private service: GithubAuthService;
private applicationService: ApplicationService;
constructor(protected store: IGithubStore) {
makeObservable(this, {
// observables
workspaceConnectionMap: observable,
githubUserCredentialsMap: observable,
// computed
workspaceConnectionIds: computed,
githubUserCredentialIds: computed,
// actions
fetchWorkspaceConnection: action,
connectWorkspaceConnection: action,
disconnectWorkspaceConnection: action,
fetchGithubUserCredential: action,
connectGithubUserCredential: action,
disconnectGithubUserCredential: action,
});
this.service = new GithubAuthService(encodeURI(SILO_BASE_URL + SILO_BASE_PATH));
this.applicationService = new ApplicationService();
}
// computed
/**
* @description get organization ids
* @returns { string[] }
*/
get workspaceConnectionIds(): string[] {
const workspaceId = this.store.workspace?.id;
if (!workspaceId || !this.workspaceConnectionMap[workspaceId]) return [];
return Object.keys(this.workspaceConnectionMap[workspaceId]) || [];
}
/**
* @description get user ids
* @returns { string[] }
*/
get githubUserCredentialIds(): string[] {
const workspaceId = this.store.workspace?.id;
if (!workspaceId || !this.githubUserCredentialsMap[workspaceId]) return [];
return Object.keys(this.githubUserCredentialsMap[workspaceId]) || [];
}
// computed functions
/**
* @description get github organization by id
* @param { string } organizationId
* @returns { object | undefined }
*/
workspaceConnectionById = computedFn((organizationId: string) => {
const workspaceId = this.store.workspace?.id;
if (!workspaceId) return undefined;
return this.workspaceConnectionMap[workspaceId][organizationId] || undefined;
});
/**
* @description get github user by id
* @param { string } userId
* @returns { object | undefined }
*/
githubUserCredentialById = computedFn((userId: string) => {
const workspaceId = this.store.workspace?.id;
if (!workspaceId) return undefined;
return this.githubUserCredentialsMap[workspaceId][userId] || undefined;
});
// actions
/**
* @description fetch github organization
* @returns { Promise<TGithubWorkspaceConnection[] | undefined> }
*/
fetchWorkspaceConnection = async (): Promise<TGithubWorkspaceConnection[] | undefined> => {
try {
const workspaceId = this.store.workspace?.id;
if (!workspaceId) return undefined;
const response = await this.service.fetchOrganizationConnection(workspaceId);
if (response) {
await this.store.fetchWebhookConnection(`${SILO_BASE_PATH}/api/github/plane-webhook`);
runInAction(() => {
response.forEach((data) => {
if (data.id) set(this.workspaceConnectionMap, [workspaceId, data.id], data);
});
});
}
return response;
} catch (error) {
throw error;
}
};
/**
* @description connect github organization
* @returns { Promise<string | undefined> }
*/
connectWorkspaceConnection = async (): Promise<string | undefined> => {
try {
const workspaceId = this.store.workspace?.id;
const workspaceSlug = this.store.workspace?.slug;
const externalApiToken = this.store.externalApiToken;
const targetHostname = API_BASE_URL;
const userId = this.store.user?.id;
if (!workspaceId || !workspaceSlug || !externalApiToken || !userId) return undefined;
// create app installation and get the installation id
// send installation id as well in the state payload
// TOOD: get the app id from backend env
const appDetails = await this.service.getPlaneAppDetails();
const appInstallation = await this.applicationService.installApplication(workspaceSlug, appDetails.appId);
const payload: GithubAuthorizeState = {
workspace_id: workspaceId,
workspace_slug: workspaceSlug,
plane_api_token: externalApiToken,
target_host: targetHostname,
user_id: userId,
plane_app_installation_id: appInstallation?.id,
};
const response = await this.service.connectOrganization(payload);
await this.store.fetchWebhookConnection(`${SILO_BASE_PATH}/api/github/plane-webhook`);
return response;
} catch (error) {
throw error;
}
};
/**
* @description disconnect github organization
* @returns { Promise<void> }
*/
disconnectWorkspaceConnection = async (): Promise<void> => {
try {
const workspaceId = this.store.workspace?.id;
const organizationId = this.workspaceConnectionIds[0] || undefined;
const userId = this.store.user?.id;
if (!workspaceId || !organizationId || !userId) return undefined;
const organization = this.workspaceConnectionById(organizationId) || undefined;
const connectionId = organization?.connection_id || undefined;
if (!connectionId) return undefined;
await this.service.disconnectOrganization(workspaceId, connectionId, userId);
runInAction(() => unset(this.workspaceConnectionMap, [workspaceId]));
await this.store.removeWebhookConnection();
return undefined;
} catch (error) {
throw error;
}
};
/**
* @description fetch github user
* @returns { Promise<TGithubWorkspaceUserConnection | undefined> }
*/
fetchGithubUserCredential = async (
workspace_id?: string,
user_id?: string
): Promise<TGithubWorkspaceUserConnection | undefined> => {
try {
const workspaceId = workspace_id ?? this.store.workspace?.id;
const userId = user_id ?? this.store.user?.id;
if (!workspaceId || !userId) return undefined;
const response = await this.service.fetchUserConnection(workspaceId, userId);
if (response) {
runInAction(() => {
if (response) set(this.githubUserCredentialsMap, [workspaceId, userId], response);
});
}
return response;
} catch (error) {
throw error;
}
};
/**
* @description connect github user
* @returns { Promise<string | undefined> }
*/
connectGithubUserCredential = async (
workspace_id?: string,
workspace_slug?: string,
user_id?: string,
profileRedirect?: boolean
): Promise<string | undefined> => {
try {
const workspaceId = workspace_id ?? this.store.workspace?.id;
const workspaceSlug = workspace_slug ?? this.store.workspace?.slug;
const userId = user_id ?? this.store.user?.id;
const targetHostname = API_BASE_URL;
if (!workspaceId || !workspaceSlug || !userId) return undefined;
const payload: GithubUserAuthState = {
workspace_id: workspaceId,
workspace_slug: workspaceSlug,
profile_redirect: profileRedirect,
user_id: userId,
target_host: targetHostname,
};
const response = await this.service.connectUser(payload);
return response;
} catch (error) {
throw error;
}
};
/**
* @description disconnect github user
* @returns { Promise<void> }
*/
disconnectGithubUserCredential = async (workspace_id?: string, user_id?: string): Promise<void> => {
try {
const workspaceId = workspace_id ?? this.store.workspace?.id;
const userId = user_id ?? this.store.user?.id;
if (!workspaceId || !userId) return undefined;
await this.service.disconnectUser(workspaceId, userId);
runInAction(() => unset(this.githubUserCredentialsMap, [workspaceId]));
return undefined;
} catch (error) {
throw error;
}
};
}