mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 22:25:22 +02:00
Compare commits
2 Commits
v0.24.3
...
1365-creat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80d5313dd8 | ||
|
|
da0e726326 |
@@ -29,7 +29,6 @@ const baseApp: ApplicationNested = {
|
|||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
giteaBranch: "",
|
giteaBranch: "",
|
||||||
giteaBuildPath: "",
|
giteaBuildPath: "",
|
||||||
previewRequireCollaboratorPermissions: false,
|
|
||||||
giteaId: "",
|
giteaId: "",
|
||||||
giteaOwner: "",
|
giteaOwner: "",
|
||||||
giteaRepository: "",
|
giteaRepository: "",
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ const baseApp: ApplicationNested = {
|
|||||||
appName: "",
|
appName: "",
|
||||||
autoDeploy: true,
|
autoDeploy: true,
|
||||||
enableSubmodules: false,
|
enableSubmodules: false,
|
||||||
previewRequireCollaboratorPermissions: false,
|
|
||||||
serverId: "",
|
serverId: "",
|
||||||
branch: null,
|
branch: null,
|
||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ const schema = z
|
|||||||
previewPath: z.string(),
|
previewPath: z.string(),
|
||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
previewCustomCertResolver: z.string().optional(),
|
previewCustomCertResolver: z.string().optional(),
|
||||||
previewRequireCollaboratorPermissions: z.boolean(),
|
|
||||||
})
|
})
|
||||||
.superRefine((input, ctx) => {
|
.superRefine((input, ctx) => {
|
||||||
if (
|
if (
|
||||||
@@ -84,7 +83,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
previewHttps: false,
|
previewHttps: false,
|
||||||
previewPath: "/",
|
previewPath: "/",
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
previewRequireCollaboratorPermissions: true,
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
@@ -107,8 +105,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
previewPath: data.previewPath || "/",
|
previewPath: data.previewPath || "/",
|
||||||
previewCertificateType: data.previewCertificateType || "none",
|
previewCertificateType: data.previewCertificateType || "none",
|
||||||
previewCustomCertResolver: data.previewCustomCertResolver || "",
|
previewCustomCertResolver: data.previewCustomCertResolver || "",
|
||||||
previewRequireCollaboratorPermissions:
|
|
||||||
data.previewRequireCollaboratorPermissions || true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -125,8 +121,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
previewPath: formData.previewPath,
|
previewPath: formData.previewPath,
|
||||||
previewCertificateType: formData.previewCertificateType,
|
previewCertificateType: formData.previewCertificateType,
|
||||||
previewCustomCertResolver: formData.previewCustomCertResolver,
|
previewCustomCertResolver: formData.previewCustomCertResolver,
|
||||||
previewRequireCollaboratorPermissions:
|
|
||||||
formData.previewRequireCollaboratorPermissions,
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Preview Deployments settings updated");
|
toast.success("Preview Deployments settings updated");
|
||||||
@@ -318,37 +312,6 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 lg:grid-cols-2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="previewRequireCollaboratorPermissions"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>
|
|
||||||
Require Collaborator Permissions
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Require collaborator permissions to preview
|
|
||||||
deployments, valid roles are:
|
|
||||||
<ul>
|
|
||||||
<li>Admin</li>
|
|
||||||
<li>Maintain</li>
|
|
||||||
<li>Write</li>
|
|
||||||
</ul>
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="env"
|
name="env"
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export const UpdateServer = ({
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent className="max-w-lg p-6">
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex items-center justify-between mb-8">
|
||||||
<DialogTitle className="text-2xl font-semibold">
|
<DialogTitle className="text-2xl font-semibold">
|
||||||
Web Server Update
|
Web Server Update
|
||||||
@@ -253,7 +253,7 @@ export const UpdateServer = ({
|
|||||||
<ToggleAutoCheckUpdates disabled={isLoading} />
|
<ToggleAutoCheckUpdates disabled={isLoading} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 flex items-center justify-end mt-4 ">
|
<div className="space-y-4 flex items-center justify-end">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" onClick={() => onOpenChange?.(false)}>
|
<Button variant="outline" onClick={() => onOpenChange?.(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "application" ADD COLUMN "previewRequireCollaboratorPermissions" boolean DEFAULT true;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -722,13 +722,6 @@
|
|||||||
"when": 1751848685503,
|
"when": 1751848685503,
|
||||||
"tag": "0102_opposite_grandmaster",
|
"tag": "0102_opposite_grandmaster",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 103,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1752465764072,
|
|
||||||
"tag": "0103_cultured_pestilence",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.24.3",
|
"version": "v0.24.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import {
|
|||||||
findPreviewDeploymentsByPullRequestId,
|
findPreviewDeploymentsByPullRequestId,
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
shouldDeploy,
|
shouldDeploy,
|
||||||
findGithubById,
|
|
||||||
checkUserRepositoryPermissions,
|
|
||||||
createSecurityBlockedComment,
|
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { Webhooks } from "@octokit/webhooks";
|
import { Webhooks } from "@octokit/webhooks";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
@@ -349,18 +346,6 @@ export default async function handler(
|
|||||||
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
||||||
const branch = githubBody?.pull_request?.base?.ref;
|
const branch = githubBody?.pull_request?.base?.ref;
|
||||||
const owner = githubBody?.repository?.owner?.login;
|
const owner = githubBody?.repository?.owner?.login;
|
||||||
const prAuthor = githubBody?.pull_request?.user?.login;
|
|
||||||
|
|
||||||
// Validate PR author information is present
|
|
||||||
if (!prAuthor) {
|
|
||||||
console.warn(
|
|
||||||
"⚠️ SECURITY: PR author information missing in webhook payload",
|
|
||||||
);
|
|
||||||
res.status(400).json({
|
|
||||||
message: "PR author information missing",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apps = await db.query.applications.findMany({
|
const apps = await db.query.applications.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
@@ -376,72 +361,13 @@ export default async function handler(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// SECURITY: Check collaborator permissions per application setting
|
|
||||||
const secureApps: typeof apps = [];
|
|
||||||
const blockedApps: string[] = [];
|
|
||||||
let userPermission: string | null = null;
|
|
||||||
|
|
||||||
for (const app of apps) {
|
|
||||||
// If the app requires collaborator permissions, verify them
|
|
||||||
if (app.previewRequireCollaboratorPermissions !== false) {
|
|
||||||
try {
|
|
||||||
const githubProvider = await findGithubById(githubResult.githubId);
|
|
||||||
const { hasWriteAccess, permission } =
|
|
||||||
await checkUserRepositoryPermissions(
|
|
||||||
githubProvider,
|
|
||||||
owner,
|
|
||||||
repository,
|
|
||||||
prAuthor,
|
|
||||||
);
|
|
||||||
|
|
||||||
userPermission = permission; // Store permission for comment
|
|
||||||
|
|
||||||
if (!hasWriteAccess) {
|
|
||||||
console.warn(
|
|
||||||
`🚨 SECURITY: Blocked preview deployment for ${app.name} from unauthorized user ${prAuthor} on ${owner}/${repository}. Permission: ${permission || "none"}`,
|
|
||||||
);
|
|
||||||
blockedApps.push(app.name);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`✅ SECURITY: Preview deployment authorized for ${app.name} from user ${prAuthor} on ${owner}/${repository}. Permission: ${permission}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`Error validating PR author permissions for ${app.name}:`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
blockedApps.push(app.name);
|
|
||||||
continue; // Skip this app on error
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ SECURITY: Preview deployment for ${app.name} allows deployment from any PR author (security check disabled)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
secureApps.push(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
const prBranch = githubBody?.pull_request?.head?.ref;
|
const prBranch = githubBody?.pull_request?.head?.ref;
|
||||||
|
|
||||||
const prNumber = githubBody?.pull_request?.number;
|
const prNumber = githubBody?.pull_request?.number;
|
||||||
const prTitle = githubBody?.pull_request?.title;
|
const prTitle = githubBody?.pull_request?.title;
|
||||||
const prURL = githubBody?.pull_request?.html_url;
|
const prURL = githubBody?.pull_request?.html_url;
|
||||||
|
|
||||||
// Create security notification comment if any apps were blocked
|
for (const app of apps) {
|
||||||
if (blockedApps.length > 0) {
|
|
||||||
await createSecurityBlockedComment({
|
|
||||||
owner,
|
|
||||||
repository,
|
|
||||||
prNumber: Number.parseInt(prNumber),
|
|
||||||
prAuthor,
|
|
||||||
permission: userPermission,
|
|
||||||
githubId: githubResult.githubId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const app of secureApps) {
|
|
||||||
const previewLimit = app?.previewLimit || 0;
|
const previewLimit = app?.previewLimit || 0;
|
||||||
if (app?.previewDeployments?.length > previewLimit) {
|
if (app?.previewDeployments?.length > previewLimit) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { apiFindAllByApplication } from "@/server/db/schema";
|
import { db } from "@/server/db";
|
||||||
|
import { apiFindAllByApplication, applications } from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
|
createPreviewDeployment,
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
|
findPreviewDeploymentByApplicationId,
|
||||||
findPreviewDeploymentById,
|
findPreviewDeploymentById,
|
||||||
findPreviewDeploymentsByApplicationId,
|
findPreviewDeploymentsByApplicationId,
|
||||||
|
findPreviewDeploymentsByPullRequestId,
|
||||||
|
IS_CLOUD,
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { and } from "drizzle-orm";
|
||||||
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
|
import { deploy } from "@/server/utils/deploy";
|
||||||
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
|
|
||||||
export const previewDeploymentRouter = createTRPCRouter({
|
export const previewDeploymentRouter = createTRPCRouter({
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
@@ -59,4 +69,142 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return previewDeployment;
|
return previewDeployment;
|
||||||
}),
|
}),
|
||||||
|
create: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
|
||||||
|
pullRequestId: z.string(),
|
||||||
|
repository: z.string(),
|
||||||
|
owner: z.string(),
|
||||||
|
branch: z.string(),
|
||||||
|
deploymentHash: z.string(),
|
||||||
|
prBranch: z.string(),
|
||||||
|
prNumber: z.any(),
|
||||||
|
prTitle: z.string(),
|
||||||
|
prURL: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const organizationId = ctx.session.activeOrganizationId;
|
||||||
|
const action = input.action;
|
||||||
|
const prId = input.pullRequestId;
|
||||||
|
|
||||||
|
if (action === "closed") {
|
||||||
|
const previewDeploymentResult =
|
||||||
|
await findPreviewDeploymentsByPullRequestId(prId);
|
||||||
|
|
||||||
|
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
|
||||||
|
(previewDeployment) =>
|
||||||
|
previewDeployment.application.project.organizationId ===
|
||||||
|
organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredPreviewDeploymentResult.length > 0) {
|
||||||
|
for (const previewDeployment of filteredPreviewDeploymentResult) {
|
||||||
|
try {
|
||||||
|
await removePreviewDeployment(
|
||||||
|
previewDeployment.previewDeploymentId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Preview Deployments Closed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action === "opened" ||
|
||||||
|
action === "synchronize" ||
|
||||||
|
action === "reopened"
|
||||||
|
) {
|
||||||
|
const deploymentHash = input.deploymentHash;
|
||||||
|
|
||||||
|
const prBranch = input.prBranch;
|
||||||
|
const prNumber = input.prNumber;
|
||||||
|
const prTitle = input.prTitle;
|
||||||
|
const prURL = input.prURL;
|
||||||
|
const apps = await db.query.applications.findMany({
|
||||||
|
where: and(
|
||||||
|
eq(applications.sourceType, "github"),
|
||||||
|
eq(applications.repository, input.repository),
|
||||||
|
eq(applications.branch, input.branch),
|
||||||
|
eq(applications.isPreviewDeploymentsActive, true),
|
||||||
|
eq(applications.owner, input.owner),
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
previewDeployments: true,
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredApps = apps.filter(
|
||||||
|
(app) => app.project.organizationId === organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(filteredApps);
|
||||||
|
|
||||||
|
for (const app of filteredApps) {
|
||||||
|
const previewLimit = app?.previewLimit || 0;
|
||||||
|
if (app?.previewDeployments?.length > previewLimit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const previewDeploymentResult =
|
||||||
|
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
|
||||||
|
|
||||||
|
let previewDeploymentId =
|
||||||
|
previewDeploymentResult?.previewDeploymentId || "";
|
||||||
|
|
||||||
|
if (!previewDeploymentResult) {
|
||||||
|
try {
|
||||||
|
const previewDeployment = await createPreviewDeployment({
|
||||||
|
applicationId: app.applicationId as string,
|
||||||
|
branch: prBranch,
|
||||||
|
pullRequestId: prId,
|
||||||
|
pullRequestNumber: prNumber,
|
||||||
|
pullRequestTitle: prTitle,
|
||||||
|
pullRequestURL: prURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(previewDeployment);
|
||||||
|
previewDeploymentId = previewDeployment.previewDeploymentId;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobData: DeploymentJob = {
|
||||||
|
applicationId: app.applicationId as string,
|
||||||
|
titleLog: "Preview Deployment",
|
||||||
|
descriptionLog: `Hash: ${deploymentHash}`,
|
||||||
|
type: "deploy",
|
||||||
|
applicationType: "application-preview",
|
||||||
|
server: !!app.serverId,
|
||||||
|
previewDeploymentId,
|
||||||
|
isExternal: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (IS_CLOUD && app.serverId) {
|
||||||
|
jobData.serverId = app.serverId;
|
||||||
|
await deploy(jobData);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await myQueue.add(
|
||||||
|
"deployments",
|
||||||
|
{ ...jobData },
|
||||||
|
{
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Preview Deployments Created",
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -641,16 +641,10 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(
|
||||||
z
|
z.object({
|
||||||
.object({
|
start: z.string().optional(),
|
||||||
dateRange: z
|
end: z.string().optional(),
|
||||||
.object({
|
}),
|
||||||
start: z.string().optional(),
|
|
||||||
end: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const deploymentWorker = new Worker(
|
|||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
|
isExternal: job.data.isExternal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -107,6 +108,7 @@ export const deploymentWorker = new Worker(
|
|||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
|
isExternal: job.data.isExternal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type DeployJob =
|
|||||||
applicationType: "application-preview";
|
applicationType: "application-preview";
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
isExternal?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentJob = DeployJob;
|
export type DeploymentJob = DeployJob;
|
||||||
|
|||||||
@@ -131,10 +131,6 @@ export const applications = pgTable("application", {
|
|||||||
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
|
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
// Security: Require collaborator permissions for preview deployments
|
|
||||||
previewRequireCollaboratorPermissions: boolean(
|
|
||||||
"previewRequireCollaboratorPermissions",
|
|
||||||
).default(true),
|
|
||||||
rollbackActive: boolean("rollbackActive").default(false),
|
rollbackActive: boolean("rollbackActive").default(false),
|
||||||
buildArgs: text("buildArgs"),
|
buildArgs: text("buildArgs"),
|
||||||
memoryReservation: text("memoryReservation"),
|
memoryReservation: text("memoryReservation"),
|
||||||
@@ -432,7 +428,6 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
previewHttps: z.boolean().optional(),
|
previewHttps: z.boolean().optional(),
|
||||||
previewPath: z.string().optional(),
|
previewPath: z.string().optional(),
|
||||||
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
previewRequireCollaboratorPermissions: z.boolean().optional(),
|
|
||||||
watchPaths: z.array(z.string()).optional(),
|
watchPaths: z.array(z.string()).optional(),
|
||||||
cleanCache: z.boolean().optional(),
|
cleanCache: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export * from "./setup/traefik-setup";
|
|||||||
export * from "./setup/server-validate";
|
export * from "./setup/server-validate";
|
||||||
export * from "./setup/server-audit";
|
export * from "./setup/server-audit";
|
||||||
export * from "./utils/watch-paths/should-deploy";
|
export * from "./utils/watch-paths/should-deploy";
|
||||||
export * from "./utils/providers/github";
|
|
||||||
export * from "./utils/backups/index";
|
export * from "./utils/backups/index";
|
||||||
export * from "./utils/backups/mariadb";
|
export * from "./utils/backups/mariadb";
|
||||||
export * from "./utils/backups/mongo";
|
export * from "./utils/backups/mongo";
|
||||||
|
|||||||
@@ -404,11 +404,13 @@ export const deployPreviewApplication = async ({
|
|||||||
titleLog = "Preview Deployment",
|
titleLog = "Preview Deployment",
|
||||||
descriptionLog = "",
|
descriptionLog = "",
|
||||||
previewDeploymentId,
|
previewDeploymentId,
|
||||||
|
isExternal = false,
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
|
isExternal?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
|
|
||||||
@@ -434,36 +436,39 @@ export const deployPreviewApplication = async ({
|
|||||||
githubId: application?.githubId || "",
|
githubId: application?.githubId || "",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const commentExists = await issueCommentExists({
|
if (!isExternal) {
|
||||||
...issueParams,
|
const commentExists = await issueCommentExists({
|
||||||
});
|
|
||||||
if (!commentExists) {
|
|
||||||
const result = await createPreviewDeploymentComment({
|
|
||||||
...issueParams,
|
...issueParams,
|
||||||
previewDomain,
|
|
||||||
appName: previewDeployment.appName,
|
|
||||||
githubId: application?.githubId || "",
|
|
||||||
previewDeploymentId,
|
|
||||||
});
|
});
|
||||||
|
if (!commentExists) {
|
||||||
if (!result) {
|
const result = await createPreviewDeploymentComment({
|
||||||
throw new TRPCError({
|
...issueParams,
|
||||||
code: "NOT_FOUND",
|
previewDomain,
|
||||||
message: "Pull request comment not found",
|
appName: previewDeployment.appName,
|
||||||
|
githubId: application?.githubId || "",
|
||||||
|
previewDeploymentId,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
if (!result) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Pull request comment not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||||
|
}
|
||||||
|
const buildingComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"running",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const buildingComment = getIssueComment(
|
|
||||||
application.name,
|
|
||||||
"running",
|
|
||||||
previewDomain,
|
|
||||||
);
|
|
||||||
await updateIssueComment({
|
|
||||||
...issueParams,
|
|
||||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
|
||||||
});
|
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = application.previewBuildArgs;
|
||||||
@@ -477,25 +482,31 @@ export const deployPreviewApplication = async ({
|
|||||||
});
|
});
|
||||||
await buildApplication(application, deployment.logPath);
|
await buildApplication(application, deployment.logPath);
|
||||||
}
|
}
|
||||||
const successComment = getIssueComment(
|
|
||||||
application.name,
|
if (!isExternal) {
|
||||||
"success",
|
const successComment = getIssueComment(
|
||||||
previewDomain,
|
application.name,
|
||||||
);
|
"success",
|
||||||
await updateIssueComment({
|
previewDomain,
|
||||||
...issueParams,
|
);
|
||||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
await updateIssueComment({
|
||||||
});
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "done",
|
previewStatus: "done",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
if (!isExternal) {
|
||||||
await updateIssueComment({
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||||
...issueParams,
|
await updateIssueComment({
|
||||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
...issueParams,
|
||||||
});
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "error",
|
previewStatus: "error",
|
||||||
@@ -511,11 +522,13 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
titleLog = "Preview Deployment",
|
titleLog = "Preview Deployment",
|
||||||
descriptionLog = "",
|
descriptionLog = "",
|
||||||
previewDeploymentId,
|
previewDeploymentId,
|
||||||
|
isExternal = false,
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
|
isExternal?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
|
|
||||||
@@ -541,36 +554,39 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
githubId: application?.githubId || "",
|
githubId: application?.githubId || "",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const commentExists = await issueCommentExists({
|
if (!isExternal) {
|
||||||
...issueParams,
|
const commentExists = await issueCommentExists({
|
||||||
});
|
|
||||||
if (!commentExists) {
|
|
||||||
const result = await createPreviewDeploymentComment({
|
|
||||||
...issueParams,
|
...issueParams,
|
||||||
previewDomain,
|
|
||||||
appName: previewDeployment.appName,
|
|
||||||
githubId: application?.githubId || "",
|
|
||||||
previewDeploymentId,
|
|
||||||
});
|
});
|
||||||
|
if (!commentExists) {
|
||||||
if (!result) {
|
const result = await createPreviewDeploymentComment({
|
||||||
throw new TRPCError({
|
...issueParams,
|
||||||
code: "NOT_FOUND",
|
previewDomain,
|
||||||
message: "Pull request comment not found",
|
appName: previewDeployment.appName,
|
||||||
|
githubId: application?.githubId || "",
|
||||||
|
previewDeploymentId,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
if (!result) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Pull request comment not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
|
||||||
|
}
|
||||||
|
const buildingComment = getIssueComment(
|
||||||
|
application.name,
|
||||||
|
"running",
|
||||||
|
previewDomain,
|
||||||
|
);
|
||||||
|
await updateIssueComment({
|
||||||
|
...issueParams,
|
||||||
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const buildingComment = getIssueComment(
|
|
||||||
application.name,
|
|
||||||
"running",
|
|
||||||
previewDomain,
|
|
||||||
);
|
|
||||||
await updateIssueComment({
|
|
||||||
...issueParams,
|
|
||||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
|
||||||
});
|
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = application.previewBuildArgs;
|
||||||
@@ -592,25 +608,29 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
await mechanizeDockerContainer(application);
|
await mechanizeDockerContainer(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
const successComment = getIssueComment(
|
if (!isExternal) {
|
||||||
application.name,
|
const successComment = getIssueComment(
|
||||||
"success",
|
application.name,
|
||||||
previewDomain,
|
"success",
|
||||||
);
|
previewDomain,
|
||||||
await updateIssueComment({
|
);
|
||||||
...issueParams,
|
await updateIssueComment({
|
||||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
...issueParams,
|
||||||
});
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "done",
|
previewStatus: "done",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
if (!isExternal) {
|
||||||
await updateIssueComment({
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||||
...issueParams,
|
await updateIssueComment({
|
||||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
...issueParams,
|
||||||
});
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "error",
|
previewStatus: "error",
|
||||||
|
|||||||
@@ -192,156 +192,3 @@ export const createPreviewDeploymentComment = async ({
|
|||||||
pullRequestCommentId: `${issue.data.id}`,
|
pullRequestCommentId: `${issue.data.id}`,
|
||||||
}).then((response) => response[0]);
|
}).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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export const findPreviewDeploymentsByApplicationId = async (
|
|||||||
|
|
||||||
export const createPreviewDeployment = async (
|
export const createPreviewDeployment = async (
|
||||||
schema: typeof apiCreatePreviewDeployment._type,
|
schema: typeof apiCreatePreviewDeployment._type,
|
||||||
|
isExternal = false,
|
||||||
) => {
|
) => {
|
||||||
const application = await findApplicationById(schema.applicationId);
|
const application = await findApplicationById(schema.applicationId);
|
||||||
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
||||||
@@ -166,27 +167,32 @@ export const createPreviewDeployment = async (
|
|||||||
org?.ownerId || "",
|
org?.ownerId || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const octokit = authGithub(application?.github as Github);
|
let issueId = "";
|
||||||
|
if (!isExternal) {
|
||||||
|
const octokit = authGithub(application?.github as Github);
|
||||||
|
|
||||||
const runningComment = getIssueComment(
|
const runningComment = getIssueComment(
|
||||||
application.name,
|
application.name,
|
||||||
"initializing",
|
"initializing",
|
||||||
`${application.previewHttps ? "https" : "http"}://${generateDomain}`,
|
`${application.previewHttps ? "https" : "http"}://${generateDomain}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const issue = await octokit.rest.issues.createComment({
|
const issue = await octokit.rest.issues.createComment({
|
||||||
owner: application?.owner || "",
|
owner: application?.owner || "",
|
||||||
repo: application?.repository || "",
|
repo: application?.repository || "",
|
||||||
issue_number: Number.parseInt(schema.pullRequestNumber),
|
issue_number: Number.parseInt(schema.pullRequestNumber),
|
||||||
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
issueId = `${issue.data.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
const previewDeployment = await db
|
const previewDeployment = await db
|
||||||
.insert(previewDeployments)
|
.insert(previewDeployments)
|
||||||
.values({
|
.values({
|
||||||
...schema,
|
...schema,
|
||||||
appName: appName,
|
appName: appName,
|
||||||
pullRequestCommentId: `${issue.data.id}`,
|
pullRequestCommentId: issueId,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -233,6 +239,13 @@ export const findPreviewDeploymentsByPullRequestId = async (
|
|||||||
) => {
|
) => {
|
||||||
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
||||||
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
||||||
|
with: {
|
||||||
|
application: {
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return previewDeploymentResult;
|
return previewDeploymentResult;
|
||||||
|
|||||||
@@ -45,49 +45,6 @@ export const getGithubToken = async (
|
|||||||
return installation.token;
|
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) => {
|
export const haveGithubRequirements = (githubProvider: Github) => {
|
||||||
return !!(
|
return !!(
|
||||||
githubProvider?.githubAppId &&
|
githubProvider?.githubAppId &&
|
||||||
|
|||||||
Reference in New Issue
Block a user