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; }};
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;
}
};