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, 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, ) => { 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.*
đŸ›Ąī¸ Learn more about this security feature 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.
`; }; /** * 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 => { 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; } };