mirror of
https://github.com/makeplane/plane.git
synced 2026-07-14 14:31:37 +02:00
* [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
290 lines
9.9 KiB
TypeScript
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;
|
|
}
|
|
};
|
|
}
|