348 lines · 8.3 KB · TypeScript
RawDownload
import { db } from "@dokploy/server/db";import {	type apiCreateGithub,	github,	gitProvider,} from "@dokploy/server/db/schema";import { TRPCError } from "@trpc/server";import { eq } from "drizzle-orm";import type { z } from "zod";import { authGithub } from "../utils/providers/github";import { updatePreviewDeployment } from "./preview-deployment"; export type Github = typeof github.$inferSelect;export const createGithub = async (	input: z.infer<typeof apiCreateGithub>,	organizationId: string,	userId: string,) => {	return await db.transaction(async (tx) => {		const newGitProvider = await tx			.insert(gitProvider)			.values({				providerType: "github",				organizationId: organizationId,				name: input.name,				userId: userId,			})			.returning()			.then((response) => response[0]); 		if (!newGitProvider) {			throw new TRPCError({				code: "BAD_REQUEST",				message: "Error creating the Git provider",			});		} 		return await tx			.insert(github)			.values({				...input,				gitProviderId: newGitProvider?.gitProviderId,			})			.returning()			.then((response) => response[0]);	});}; export const findGithubById = async (githubId: string) => {	const githubProviderResult = await db.query.github.findFirst({		where: eq(github.githubId, githubId),		with: {			gitProvider: true,		},	}); 	if (!githubProviderResult) {		throw new TRPCError({			code: "NOT_FOUND",			message: "Github Provider not found",		});	} 	return githubProviderResult;}; export const updateGithub = async (	githubId: string,	input: Partial<Github>,) => {	return await db		.update(github)		.set({			...input,		})		.where(eq(github.githubId, githubId))		.returning()		.then((response) => response[0]);}; export const getIssueComment = (	appName: string,	status: "success" | "error" | "running" | "initializing",	previewDomain: string,) => {	let statusMessage = "";	if (status === "success") {		statusMessage = "✅ Done";	} else if (status === "error") {		statusMessage = "❌ Failed";	} else if (status === "initializing") {		statusMessage = "🔄 Building";	} else {		statusMessage = "🔄 Building";	}	const finished = `| Name       | Status       | Preview                             | Updated (UTC)         ||------------|--------------|-------------------------------------|-----------------------|| ${appName}  | ${statusMessage} | [Preview URL](${previewDomain}) | ${new Date().toISOString()} |`; 	return finished;};interface CommentExists {	owner: string;	repository: string;	comment_id: number;	githubId: string;}export const issueCommentExists = async ({	owner,	repository,	comment_id,	githubId,}: CommentExists) => {	const github = await findGithubById(githubId);	const octokit = authGithub(github);	try {		await octokit.rest.issues.getComment({			owner: owner || "",			repo: repository || "",			comment_id: comment_id,		});		return true;	} catch {		return false;	}};interface Comment {	owner: string;	repository: string;	issue_number: string;	body: string;	comment_id: number;	githubId: string;}export const updateIssueComment = async ({	owner,	repository,	issue_number,	body,	comment_id,	githubId,}: Comment) => {	const github = await findGithubById(githubId);	const octokit = authGithub(github); 	await octokit.rest.issues.updateComment({		owner: owner || "",		repo: repository || "",		issue_number: issue_number,		body,		comment_id: comment_id,	});}; interface CommentCreate {	appName: string;	owner: string;	repository: string;	issue_number: string;	previewDomain: string;	githubId: string;	previewDeploymentId: string;} export const createPreviewDeploymentComment = async ({	owner,	repository,	issue_number,	previewDomain,	appName,	githubId,	previewDeploymentId,}: CommentCreate) => {	const github = await findGithubById(githubId);	const octokit = authGithub(github); 	const runningComment = getIssueComment(		appName,		"initializing",		previewDomain,	); 	const issue = await octokit.rest.issues.createComment({		owner: owner || "",		repo: repository || "",		issue_number: Number.parseInt(issue_number),		body: `### Dokploy Preview Deployment\n\n${runningComment}`,	}); 	return await updatePreviewDeployment(previewDeploymentId, {		pullRequestCommentId: `${issue.data.id}`,	}).then((response) => response[0]);}; /** * Generate security notification message for blocked PR deployments */export const getSecurityBlockedMessage = (	prAuthor: string,	repositoryName: string,	permission: string | null,) => {	return `### 🚨 Preview Deployment Blocked - Security Protection **Your pull request was blocked from triggering preview deployments** #### Why was this blocked?- **User**: \`${prAuthor}\`- **Repository**: \`${repositoryName}\`- **Permission Level**: \`${permission || "none"}\`- **Required Level**: \`write\`, \`maintain\`, or \`admin\` #### How to resolve this: **Option 1: Get Collaborator Access (Recommended)**Ask a repository maintainer to invite you as a collaborator with **write permissions** or higher. **Option 2: Request Permission Override**Ask a repository administrator to disable security validation for this specific application if appropriate. #### For Repository Administrators:To disable this security check (⚠️ **not recommended for public repositories**):Enter to preview settings and disable the security check. ---*This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments.* <details><summary>🛡️ Learn more about this security feature</summary> This protection prevents unauthorized users from:- Executing malicious code on the deployment server- Accessing environment variables and secrets- Potentially compromising the infrastructure Preview deployments are powerful but require trust. Only users with repository write access can trigger them.</details>`;}; /** * Check if a security notification comment already exists on a GitHub PR * This prevents creating duplicate security comments on subsequent pushes */export const hasExistingSecurityComment = async ({	owner,	repository,	prNumber,	githubId,}: {	owner: string;	repository: string;	prNumber: number;	githubId: string;}): Promise<boolean> => {	try {		const github = await findGithubById(githubId);		const octokit = authGithub(github); 		// Get all comments for this PR		const { data: comments } = await octokit.rest.issues.listComments({			owner,			repo: repository,			issue_number: prNumber,		}); 		// Check if any comment contains our security notification marker		const securityCommentExists = comments.some((comment) =>			comment.body?.includes(				"🚨 Preview Deployment Blocked - Security Protection",			),		); 		return securityCommentExists;	} catch (error) {		console.error(			`❌ Failed to check existing comments on PR #${prNumber}:`,			error,		);		// If we can't check, assume no comment exists to avoid blocking functionality		return false;	}}; /** * Create a security notification comment on a GitHub PR */export const createSecurityBlockedComment = async ({	owner,	repository,	prNumber,	prAuthor,	permission,	githubId,}: {	owner: string;	repository: string;	prNumber: number;	prAuthor: string;	permission: string | null;	githubId: string;}) => {	try {		// Check if a security comment already exists to prevent duplicates		const commentExists = await hasExistingSecurityComment({			owner,			repository,			prNumber,			githubId,		}); 		if (commentExists) {			console.log(				`ℹ️  Security notification comment already exists on PR #${prNumber}, skipping duplicate`,			);			return null;		} 		const github = await findGithubById(githubId);		const octokit = authGithub(github); 		const securityMessage = getSecurityBlockedMessage(			prAuthor,			repository,			permission,		); 		const issue = await octokit.rest.issues.createComment({			owner,			repo: repository,			issue_number: prNumber,			body: securityMessage,		}); 		console.log(			`✅ Security notification comment created on PR #${prNumber}: ${issue.data.html_url}`,		);		return issue.data;	} catch (error) {		console.error(			`❌ Failed to create security comment on PR #${prNumber}:`,			error,		);		// Don't throw error - security comment is nice-to-have, not critical		return null;	}};