mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
349 lines
8.3 KiB
TypeScript
349 lines
8.3 KiB
TypeScript
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;
|
||
}
|
||
};
|