222 lines · 6.0 KB · TypeScript
RawDownload
import { join } from "node:path";import { paths } from "@dokploy/server/constants";import type { apiFindGithubBranches } from "@dokploy/server/db/schema";import { findGithubById, type Github } from "@dokploy/server/services/github";import type { InferResultType } from "@dokploy/server/types/with";import { createAppAuth } from "@octokit/auth-app";import { TRPCError } from "@trpc/server";import { Octokit } from "octokit"; export const authGithub = (githubProvider: Github): Octokit => {	if (!haveGithubRequirements(githubProvider)) {		throw new TRPCError({			code: "NOT_FOUND",			message: "Github Account not configured correctly",		});	} 	const octokit: Octokit = new Octokit({		authStrategy: createAppAuth,		auth: {			appId: githubProvider?.githubAppId || 0,			privateKey: githubProvider?.githubPrivateKey || "",			installationId: githubProvider?.githubInstallationId,		},	}); 	return octokit;}; export const getGithubToken = async (	octokit: ReturnType<typeof authGithub>,) => {	const installation = (await octokit.auth({		type: "installation",	})) as {		token: string;	}; 	return installation.token;}; /** * Check if a GitHub user has write/admin permissions on a repository * This is used to validate PR authors before allowing preview deployments */export const checkUserRepositoryPermissions = async (	githubProvider: Github,	owner: string,	repo: string,	username: string,): Promise<{ hasWriteAccess: boolean; permission: string | null }> => {	try {		const octokit = authGithub(githubProvider); 		// Check if user is a collaborator with write permissions		const { data: permission } =			await octokit.rest.repos.getCollaboratorPermissionLevel({				owner,				repo,				username,			}); 		// Allow only users with 'write', 'admin', or 'maintain' permissions		// Currently exists Read, Triage, Write, Maintain, Admin		const allowedPermissions = ["write", "admin", "maintain"];		const hasWriteAccess = allowedPermissions.includes(permission.permission); 		return {			hasWriteAccess,			permission: permission.permission,		};	} catch (error) {		// If user is not a collaborator, GitHub API returns 404		console.warn(			`User ${username} is not a collaborator of ${owner}/${repo}:`,			error,		);		return {			hasWriteAccess: false,			permission: null,		};	}}; export const haveGithubRequirements = (githubProvider: Github) => {	return !!(		githubProvider?.githubAppId &&		githubProvider?.githubPrivateKey &&		githubProvider?.githubInstallationId	);}; const getErrorCloneRequirements = (entity: {	repository?: string | null;	owner?: string | null;	branch?: string | null;}) => {	const reasons: string[] = [];	const { repository, owner, branch } = entity; 	if (!repository) reasons.push("1. Repository not assigned.");	if (!owner) reasons.push("2. Owner not specified.");	if (!branch) reasons.push("3. Branch not defined."); 	return reasons;}; export type ApplicationWithGithub = InferResultType<	"applications",	{ github: true }>; export type ComposeWithGithub = InferResultType<"compose", { github: true }>; interface CloneGithubRepository {	appName: string;	owner: string | null;	branch: string | null;	githubId: string | null;	repository: string | null;	type?: "application" | "compose";	enableSubmodules: boolean;	serverId: string | null;}export const cloneGithubRepository = async ({	type = "application",	...entity}: CloneGithubRepository) => {	let command = "set -e;";	const isCompose = type === "compose";	const {		appName,		repository,		owner,		branch,		githubId,		enableSubmodules,		serverId,	} = entity;	const { APPLICATIONS_PATH, COMPOSE_PATH } = paths(!!serverId); 	if (!githubId) {		command += `echo "Error: ❌ Github Provider not found"; exit 1;`; 		return command;	} 	const requirements = getErrorCloneRequirements(entity); 	// Check if requirements are met	if (requirements.length > 0) {		command += `echo "GitHub Repository configuration failed for application: ${appName}"; echo "Reasons:"; echo "${requirements.join("\n")}"; exit 1;`;		return command;	} 	const githubProvider = await findGithubById(githubId);	const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;	const outputPath = join(basePath, appName, "code");	const octokit = authGithub(githubProvider);	const token = await getGithubToken(octokit);	const repoclone = `github.com/${owner}/${repository}.git`;	command += `rm -rf ${outputPath};`;	command += `mkdir -p ${outputPath};`;	const cloneUrl = `https://oauth2:${token}@${repoclone}`; 	command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;	command += `git clone --branch ${branch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; 	return command;}; export const getGithubRepositories = async (githubId?: string) => {	if (!githubId) {		return [];	} 	const githubProvider = await findGithubById(githubId); 	const octokit = new Octokit({		authStrategy: createAppAuth,		auth: {			appId: githubProvider.githubAppId,			privateKey: githubProvider.githubPrivateKey,			installationId: githubProvider.githubInstallationId,		},	}); 	const repositories = (await octokit.paginate(		octokit.rest.apps.listReposAccessibleToInstallation,	)) as unknown as Awaited<		ReturnType<typeof octokit.rest.apps.listReposAccessibleToInstallation>	>["data"]["repositories"]; 	return repositories;}; export const getGithubBranches = async (	input: typeof apiFindGithubBranches._type,) => {	if (!input.githubId) {		return [];	}	const githubProvider = await findGithubById(input.githubId); 	const octokit = new Octokit({		authStrategy: createAppAuth,		auth: {			appId: githubProvider.githubAppId,			privateKey: githubProvider.githubPrivateKey,			installationId: githubProvider.githubInstallationId,		},	}); 	const branches = (await octokit.paginate(octokit.rest.repos.listBranches, {		owner: input.owner,		repo: input.repo,	})) as unknown as Awaited<		ReturnType<typeof octokit.rest.repos.listBranches>	>["data"]; 	return branches;};