mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build stage
|
||||
FROM golang:1.21-alpine3.19 AS builder
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20.9-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
28
SECURITY.md
Normal file
28
SECURITY.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Dokploy Security Policy
|
||||
|
||||
At Dokploy, security is a top priority. We appreciate the help of security researchers and the community in identifying and reporting vulnerabilities.
|
||||
|
||||
## How to Report a Vulnerability
|
||||
|
||||
If you have discovered a security vulnerability in Dokploy, we ask that you report it responsibly by following these guidelines:
|
||||
|
||||
1. **Contact us:** Send an email to [contact@dokploy.com](mailto:contact@dokploy.com).
|
||||
2. **Provide clear details:** Include as much information as possible to help us understand and reproduce the vulnerability. This should include:
|
||||
* A clear description of the vulnerability.
|
||||
* Steps to reproduce the vulnerability.
|
||||
* Any sample code, screenshots, or videos that might be helpful.
|
||||
* The potential impact of the vulnerability.
|
||||
3. **Do not make the vulnerability public:** Please refrain from publicly disclosing the vulnerability until we have had the opportunity to investigate and address it. This is crucial for protecting our users.
|
||||
4. **Allow us time:** We will endeavor to acknowledge receipt of your report as soon as possible and keep you informed of our progress. The time to resolve the vulnerability may vary depending on its complexity and severity.
|
||||
|
||||
## What We Expect From You
|
||||
|
||||
* Do not access user data or systems beyond what is necessary to demonstrate the vulnerability.
|
||||
* Do not perform denial-of-service (DoS) attacks, spamming, or social engineering.
|
||||
* Do not modify or destroy data that does not belong to you.
|
||||
|
||||
## Our Commitment
|
||||
|
||||
We are committed to working with you quickly and responsibly to address any legitimate security vulnerability.
|
||||
|
||||
Thank you for helping us keep Dokploy secure for everyone.
|
||||
@@ -9,25 +9,25 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"zod": "^3.23.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"hono": "^4.5.8",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"@nerimity/mimiqueue": "1.2.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"hono": "^4.5.8",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"redis": "4.7.0",
|
||||
"@nerimity/mimiqueue": "1.2.3"
|
||||
"zod": "^3.23.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.2",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1"
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0"
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ const baseApp: ApplicationNested = {
|
||||
updateConfigSwarm: null,
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
rollbackActive: false,
|
||||
};
|
||||
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createRouterConfig } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
rollbackActive: false,
|
||||
applicationId: "",
|
||||
herokuVersion: "",
|
||||
giteaRepository: "",
|
||||
|
||||
@@ -247,7 +247,7 @@ export const UpdateVolume = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormItem className="max-w-full max-w-[45rem]">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
@@ -256,7 +256,7 @@ export const UpdateVolume = ({
|
||||
placeholder={`NODE_ENV=production
|
||||
PORT=3000
|
||||
`}
|
||||
className="h-96 font-mono"
|
||||
className="h-96 font-mono w-full"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -10,11 +10,14 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { type RouterOutputs, api } from "@/utils/api";
|
||||
import { Clock, Loader2, RocketIcon } from "lucide-react";
|
||||
import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CancelQueues } from "./cancel-queues";
|
||||
import { RefreshToken } from "./refresh-token";
|
||||
import { ShowDeployment } from "./show-deployment";
|
||||
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -57,6 +60,9 @@ export const ShowDeployments = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: rollback, isLoading: isRollingBack } =
|
||||
api.rollback.rollback.useMutation();
|
||||
|
||||
const [url, setUrl] = React.useState("");
|
||||
useEffect(() => {
|
||||
setUrl(document.location.origin);
|
||||
@@ -71,9 +77,18 @@ export const ShowDeployments = ({
|
||||
See all the 10 last deployments for this {type}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{(type === "application" || type === "compose") && (
|
||||
<CancelQueues id={id} type={type} />
|
||||
)}
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{(type === "application" || type === "compose") && (
|
||||
<CancelQueues id={id} type={type} />
|
||||
)}
|
||||
{type === "application" && (
|
||||
<ShowRollbackSettings applicationId={id}>
|
||||
<Button variant="outline">
|
||||
Configure Rollbacks <Settings className="size-4" />
|
||||
</Button>
|
||||
</ShowRollbackSettings>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{refreshToken && (
|
||||
@@ -154,13 +169,47 @@ export const ShowDeployments = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveLog(deployment);
|
||||
}}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setActiveLog(deployment);
|
||||
}}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
|
||||
{deployment?.rollback &&
|
||||
deployment.status === "done" &&
|
||||
type === "application" && (
|
||||
<DialogAction
|
||||
title="Rollback to this deployment"
|
||||
description="Are you sure you want to rollback to this deployment?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await rollback({
|
||||
rollbackId: deployment.rollback.rollbackId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"Rollback initiated successfully",
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error initiating rollback");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
isLoading={isRollingBack}
|
||||
>
|
||||
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
|
||||
Rollback
|
||||
</Button>
|
||||
</DialogAction>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -39,6 +39,7 @@ export type ValidationState = {
|
||||
error?: string;
|
||||
resolvedIp?: string;
|
||||
message?: string;
|
||||
cdnProvider?: string;
|
||||
};
|
||||
|
||||
export type ValidationStates = Record<string, ValidationState>;
|
||||
@@ -119,6 +120,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
isValid: result.isValid,
|
||||
error: result.error,
|
||||
resolvedIp: result.resolvedIp,
|
||||
cdnProvider: result.cdnProvider,
|
||||
message: result.error && result.isValid ? result.error : undefined,
|
||||
},
|
||||
}));
|
||||
@@ -354,8 +356,9 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
) : validationState?.isValid ? (
|
||||
<>
|
||||
<CheckCircle2 className="size-3 mr-1" />
|
||||
{validationState.message
|
||||
? "Behind Cloudflare"
|
||||
{validationState.message &&
|
||||
validationState.cdnProvider
|
||||
? `Behind ${validationState.cdnProvider}`
|
||||
: "DNS Valid"}
|
||||
</>
|
||||
) : validationState?.error ? (
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
currentBuildArgs !== (data?.buildArgs || "");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && !hasChanges) {
|
||||
form.reset({
|
||||
env: data.env || "",
|
||||
buildArgs: data.buildArgs || "",
|
||||
|
||||
@@ -16,9 +16,11 @@ import { api } from "@/utils/api";
|
||||
import { GitBranch, Loader2, UploadCloud } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||
import { SaveDragNDrop } from "./save-drag-n-drop";
|
||||
import { SaveGitlabProvider } from "./save-gitlab-provider";
|
||||
import { UnauthorizedGitProvider } from "./unauthorized-git-provider";
|
||||
|
||||
type TabState =
|
||||
| "github"
|
||||
@@ -43,12 +45,31 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
||||
api.gitea.giteaProviders.useQuery();
|
||||
|
||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||
const { data: application, refetch } = api.application.one.useQuery({
|
||||
applicationId,
|
||||
});
|
||||
const { mutateAsync: disconnectGitProvider } =
|
||||
api.application.disconnectGitProvider.useMutation();
|
||||
|
||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||
|
||||
const isLoading =
|
||||
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
try {
|
||||
await disconnectGitProvider({ applicationId });
|
||||
toast.success("Repository disconnected successfully");
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to disconnect repository: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
@@ -77,6 +98,38 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user doesn't have access to the current git provider
|
||||
if (
|
||||
application &&
|
||||
!application.hasGitProviderAccess &&
|
||||
application.sourceType !== "docker" &&
|
||||
application.sourceType !== "drop"
|
||||
) {
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Repository connection through unauthorized provider
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UnauthorizedGitProvider
|
||||
service={application}
|
||||
onDisconnect={handleDisconnect}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
BitbucketIcon,
|
||||
GitIcon,
|
||||
GiteaIcon,
|
||||
GithubIcon,
|
||||
GitlabIcon,
|
||||
} from "@/components/icons/data-tools-icons";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { RouterOutputs } from "@/utils/api";
|
||||
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
service:
|
||||
| RouterOutputs["application"]["one"]
|
||||
| RouterOutputs["compose"]["one"];
|
||||
onDisconnect: () => void;
|
||||
}
|
||||
|
||||
export const UnauthorizedGitProvider = ({ service, onDisconnect }: Props) => {
|
||||
const getProviderIcon = (sourceType: string) => {
|
||||
switch (sourceType) {
|
||||
case "github":
|
||||
return <GithubIcon className="size-5 text-muted-foreground" />;
|
||||
case "gitlab":
|
||||
return <GitlabIcon className="size-5 text-muted-foreground" />;
|
||||
case "bitbucket":
|
||||
return <BitbucketIcon className="size-5 text-muted-foreground" />;
|
||||
case "gitea":
|
||||
return <GiteaIcon className="size-5 text-muted-foreground" />;
|
||||
case "git":
|
||||
return <GitIcon className="size-5 text-muted-foreground" />;
|
||||
default:
|
||||
return <GitBranch className="size-5 text-muted-foreground" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getRepositoryInfo = () => {
|
||||
switch (service.sourceType) {
|
||||
case "github":
|
||||
return {
|
||||
repo: service.repository,
|
||||
branch: service.branch,
|
||||
owner: service.owner,
|
||||
};
|
||||
case "gitlab":
|
||||
return {
|
||||
repo: service.gitlabRepository,
|
||||
branch: service.gitlabBranch,
|
||||
owner: service.gitlabOwner,
|
||||
};
|
||||
case "bitbucket":
|
||||
return {
|
||||
repo: service.bitbucketRepository,
|
||||
branch: service.bitbucketBranch,
|
||||
owner: service.bitbucketOwner,
|
||||
};
|
||||
case "gitea":
|
||||
return {
|
||||
repo: service.giteaRepository,
|
||||
branch: service.giteaBranch,
|
||||
owner: service.giteaOwner,
|
||||
};
|
||||
case "git":
|
||||
return {
|
||||
repo: service.customGitUrl,
|
||||
branch: service.customGitBranch,
|
||||
owner: null,
|
||||
};
|
||||
default:
|
||||
return { repo: null, branch: null, owner: null };
|
||||
}
|
||||
};
|
||||
|
||||
const { repo, branch, owner } = getRepositoryInfo();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
This application is connected to a {service.sourceType} repository
|
||||
through a git provider that you don't have access to. You can see
|
||||
basic repository information below, but cannot modify the
|
||||
configuration.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Card className="border-dashed border-2 border-muted-foreground/20 bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{getProviderIcon(service.sourceType)}
|
||||
<span className="capitalize text-sm font-medium">
|
||||
{service.sourceType} Repository
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{owner && (
|
||||
<div>
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
Owner:
|
||||
</span>
|
||||
<p className="text-sm">{owner}</p>
|
||||
</div>
|
||||
)}
|
||||
{repo && (
|
||||
<div>
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
Repository:
|
||||
</span>
|
||||
<p className="text-sm">{repo}</p>
|
||||
</div>
|
||||
)}
|
||||
{branch && (
|
||||
<div>
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
Branch:
|
||||
</span>
|
||||
<p className="text-sm">{branch}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 border-t">
|
||||
<DialogAction
|
||||
title="Disconnect Repository"
|
||||
description="Are you sure you want to disconnect this repository?"
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
onDisconnect();
|
||||
}}
|
||||
>
|
||||
<Button variant="secondary" className="w-full">
|
||||
<Unlink className="size-4 mr-2" />
|
||||
Disconnect Repository
|
||||
</Button>
|
||||
</DialogAction>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Disconnecting will allow you to configure a new repository with
|
||||
your own git providers.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
rollbackActive: z.boolean(),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: application, refetch } = api.application.one.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync: updateApplication, isLoading } =
|
||||
api.application.update.useMutation();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
rollbackActive: application?.rollbackActive ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
rollbackActive: data.rollbackActive,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Rollback settings updated");
|
||||
setIsOpen(false);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Failed to update rollback settings");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rollback Settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure how rollbacks work for this application
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rollbackActive"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Enable Rollbacks
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Allow rolling back to previous deployments
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full" isLoading={isLoading}>
|
||||
Save Settings
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -44,8 +44,10 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
resolver: zodResolver(AddComposeFile),
|
||||
});
|
||||
|
||||
const composeFile = form.watch("composeFile");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data && !composeFile) {
|
||||
form.reset({
|
||||
composeFile: data.composeFile || "",
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ import { SaveGitProviderCompose } from "./save-git-provider-compose";
|
||||
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
|
||||
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
|
||||
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
|
||||
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
|
||||
interface Props {
|
||||
@@ -34,12 +36,29 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
||||
api.gitea.giteaProviders.useQuery();
|
||||
|
||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||
const { mutateAsync: disconnectGitProvider } =
|
||||
api.compose.disconnectGitProvider.useMutation();
|
||||
|
||||
const { data: compose, refetch } = api.compose.one.useQuery({ composeId });
|
||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||
|
||||
const isLoading =
|
||||
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
try {
|
||||
await disconnectGitProvider({ composeId });
|
||||
toast.success("Repository disconnected successfully");
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to disconnect repository: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
@@ -68,6 +87,37 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user doesn't have access to the current git provider
|
||||
if (
|
||||
compose &&
|
||||
!compose.hasGitProviderAccess &&
|
||||
compose.sourceType !== "raw"
|
||||
) {
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-start justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||
Repository connection through unauthorized provider
|
||||
</p>
|
||||
</div>
|
||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||
<GitBranch className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UnauthorizedGitProvider
|
||||
service={compose}
|
||||
onDisconnect={handleDisconnect}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="group relative w-full bg-transparent">
|
||||
<CardHeader>
|
||||
|
||||
@@ -38,7 +38,7 @@ const AddProjectSchema = z.object({
|
||||
(name) => {
|
||||
const trimmedName = name.trim();
|
||||
const validNameRegex =
|
||||
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_-]*[\p{L}\p{N}_-]$/u;
|
||||
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_.-]*[\p{L}\p{N}_-]$/u;
|
||||
return validNameRegex.test(trimmedName);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,80 +1,93 @@
|
||||
// @ts-nocheck
|
||||
|
||||
export const extractExpirationDate = (certData: string): Date | null => {
|
||||
try {
|
||||
const match = certData.match(
|
||||
/-----BEGIN CERTIFICATE-----\s*([^-]+)\s*-----END CERTIFICATE-----/,
|
||||
);
|
||||
if (!match?.[1]) return null;
|
||||
|
||||
const base64Cert = match[1].replace(/\s/g, "");
|
||||
const binaryStr = window.atob(base64Cert);
|
||||
const bytes = new Uint8Array(binaryStr.length);
|
||||
|
||||
for (let i = 0; i < binaryStr.length; i++) {
|
||||
bytes[i] = binaryStr.charCodeAt(i);
|
||||
// Decode PEM base64 to DER binary
|
||||
const b64 = certData.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
||||
const binStr = atob(b64);
|
||||
const der = new Uint8Array(binStr.length);
|
||||
for (let i = 0; i < binStr.length; i++) {
|
||||
der[i] = binStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
// ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18
|
||||
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
|
||||
let dateFound = false;
|
||||
for (let i = 0; i < bytes.length - 2; i++) {
|
||||
// Look for sequence containing validity period (0x30)
|
||||
if (bytes[i] === 0x30) {
|
||||
// Check next bytes for UTCTime or GeneralizedTime
|
||||
let j = i + 1;
|
||||
while (j < bytes.length - 2) {
|
||||
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
|
||||
const dateType = bytes[j];
|
||||
const dateLength = bytes[j + 1];
|
||||
if (typeof dateLength === "undefined") break;
|
||||
let offset = 0;
|
||||
|
||||
if (!dateFound) {
|
||||
// Skip "not before" date
|
||||
dateFound = true;
|
||||
j += dateLength + 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found "not after" date
|
||||
let dateStr = "";
|
||||
for (let k = 0; k < dateLength; k++) {
|
||||
const charCode = bytes[j + 2 + k];
|
||||
if (typeof charCode === "undefined") continue;
|
||||
dateStr += String.fromCharCode(charCode);
|
||||
}
|
||||
|
||||
if (dateType === 0x17) {
|
||||
// UTCTime (YYMMDDhhmmssZ)
|
||||
const year = Number.parseInt(dateStr.slice(0, 2));
|
||||
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
fullYear,
|
||||
Number.parseInt(dateStr.slice(2, 4)) - 1,
|
||||
Number.parseInt(dateStr.slice(4, 6)),
|
||||
Number.parseInt(dateStr.slice(6, 8)),
|
||||
Number.parseInt(dateStr.slice(8, 10)),
|
||||
Number.parseInt(dateStr.slice(10, 12)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// GeneralizedTime (YYYYMMDDhhmmssZ)
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
Number.parseInt(dateStr.slice(0, 4)),
|
||||
Number.parseInt(dateStr.slice(4, 6)) - 1,
|
||||
Number.parseInt(dateStr.slice(6, 8)),
|
||||
Number.parseInt(dateStr.slice(8, 10)),
|
||||
Number.parseInt(dateStr.slice(10, 12)),
|
||||
Number.parseInt(dateStr.slice(12, 14)),
|
||||
),
|
||||
);
|
||||
}
|
||||
j++;
|
||||
// Helper: read ASN.1 length field
|
||||
function readLength(pos: number): { length: number; offset: number } {
|
||||
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||
let len = der[pos++];
|
||||
if (len & 0x80) {
|
||||
const bytes = len & 0x7f;
|
||||
len = 0;
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
// biome-ignore lint/style/noParameterAssign: <explanation>
|
||||
len = (len << 8) + der[pos++];
|
||||
}
|
||||
}
|
||||
return { length: len, offset: pos };
|
||||
}
|
||||
return null;
|
||||
|
||||
// Skip the outer certificate sequence
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected sequence");
|
||||
({ offset } = readLength(offset));
|
||||
|
||||
// Skip tbsCertificate sequence
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected tbsCertificate");
|
||||
({ offset } = readLength(offset));
|
||||
|
||||
// Check for optional version field (context-specific tag [0])
|
||||
if (der[offset] === 0xa0) {
|
||||
offset++;
|
||||
const versionLen = readLength(offset);
|
||||
offset = versionLen.offset + versionLen.length;
|
||||
}
|
||||
|
||||
// Skip serialNumber, signature, issuer
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (der[offset] !== 0x30 && der[offset] !== 0x02)
|
||||
throw new Error("Unexpected structure");
|
||||
offset++;
|
||||
const fieldLen = readLength(offset);
|
||||
offset = fieldLen.offset + fieldLen.length;
|
||||
}
|
||||
|
||||
// Validity sequence (notBefore and notAfter)
|
||||
if (der[offset++] !== 0x30) throw new Error("Expected validity sequence");
|
||||
const validityLen = readLength(offset);
|
||||
offset = validityLen.offset;
|
||||
|
||||
// notBefore
|
||||
offset++;
|
||||
const notBeforeLen = readLength(offset);
|
||||
offset = notBeforeLen.offset + notBeforeLen.length;
|
||||
|
||||
// notAfter
|
||||
offset++;
|
||||
const notAfterLen = readLength(offset);
|
||||
const notAfterStr = new TextDecoder().decode(
|
||||
der.slice(notAfterLen.offset, notAfterLen.offset + notAfterLen.length),
|
||||
);
|
||||
|
||||
// Parse GeneralizedTime (15 chars) or UTCTime (13 chars)
|
||||
function parseTime(str: string): Date {
|
||||
if (str.length === 13) {
|
||||
// UTCTime YYMMDDhhmmssZ
|
||||
const year = Number.parseInt(str.slice(0, 2), 10);
|
||||
const fullYear = year < 50 ? 2000 + year : 1900 + year;
|
||||
return new Date(
|
||||
`${fullYear}-${str.slice(2, 4)}-${str.slice(4, 6)}T${str.slice(6, 8)}:${str.slice(8, 10)}:${str.slice(10, 12)}Z`,
|
||||
);
|
||||
}
|
||||
if (str.length === 15) {
|
||||
// GeneralizedTime YYYYMMDDhhmmssZ
|
||||
return new Date(
|
||||
`${str.slice(0, 4)}-${str.slice(4, 6)}-${str.slice(6, 8)}T${str.slice(8, 10)}:${str.slice(10, 12)}:${str.slice(12, 14)}Z`,
|
||||
);
|
||||
}
|
||||
throw new Error("Invalid ASN.1 time format");
|
||||
}
|
||||
|
||||
return parseTime(notAfterStr);
|
||||
} catch (error) {
|
||||
console.error("Error parsing certificate:", error);
|
||||
return null;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useEffect, useState } from "react";
|
||||
export const AddGithubProvider = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
const { data: session } = authClient.useSession();
|
||||
const { data } = api.user.get.useQuery();
|
||||
const [manifest, setManifest] = useState("");
|
||||
const [isOrganization, setIsOrganization] = useState(false);
|
||||
@@ -27,7 +28,7 @@ export const AddGithubProvider = () => {
|
||||
const url = document.location.origin;
|
||||
const manifest = JSON.stringify(
|
||||
{
|
||||
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}`,
|
||||
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`,
|
||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
|
||||
@@ -41,6 +41,7 @@ const addInvitation = z.object({
|
||||
.min(1, "Email is required")
|
||||
.email({ message: "Invalid email" }),
|
||||
role: z.enum(["member", "admin"]),
|
||||
notificationId: z.string().optional(),
|
||||
});
|
||||
|
||||
type AddInvitation = z.infer<typeof addInvitation>;
|
||||
@@ -49,6 +50,10 @@ export const AddInvitation = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: emailProviders } =
|
||||
api.notification.getEmailProviders.useQuery();
|
||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data: activeOrganization } = authClient.useActiveOrganization();
|
||||
|
||||
@@ -56,6 +61,7 @@ export const AddInvitation = () => {
|
||||
defaultValues: {
|
||||
email: "",
|
||||
role: "member",
|
||||
notificationId: "",
|
||||
},
|
||||
resolver: zodResolver(addInvitation),
|
||||
});
|
||||
@@ -74,7 +80,20 @@ export const AddInvitation = () => {
|
||||
if (result.error) {
|
||||
setError(result.error.message || "");
|
||||
} else {
|
||||
toast.success("Invitation created");
|
||||
if (!isCloud && data.notificationId) {
|
||||
await sendInvitation({
|
||||
invitationId: result.data.id,
|
||||
notificationId: data.notificationId || "",
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Invitation created and email sent");
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
} else {
|
||||
toast.success("Invitation created");
|
||||
}
|
||||
setError(null);
|
||||
setOpen(false);
|
||||
}
|
||||
@@ -149,6 +168,47 @@ export const AddInvitation = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isCloud && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notificationId"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Email Provider</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select an email provider" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{emailProviders?.map((provider) => (
|
||||
<SelectItem
|
||||
key={provider.notificationId}
|
||||
value={provider.notificationId}
|
||||
>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectItem value="none" disabled>
|
||||
None
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Select the email provider to send the invitation
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<DialogFooter className="flex w-full flex-row">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
|
||||
import Page from "./side";
|
||||
import { ChatwootWidget } from "../shared/ChatwootWidget";
|
||||
import Page from "./side";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
|
||||
3
apps/dokploy/drizzle/0093_nice_gorilla_man.sql
Normal file
3
apps/dokploy/drizzle/0093_nice_gorilla_man.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "user_temp" ALTER COLUMN "logCleanupCron" SET DEFAULT '0 0 * * *';
|
||||
|
||||
UPDATE "user_temp" SET "logCleanupCron" = '0 0 * * *' WHERE "logCleanupCron" IS NULL;
|
||||
15
apps/dokploy/drizzle/0094_numerous_carmella_unuscione.sql
Normal file
15
apps/dokploy/drizzle/0094_numerous_carmella_unuscione.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE "git_provider" ADD COLUMN "userId" text;--> statement-breakpoint
|
||||
|
||||
-- Update existing git providers to be owned by the organization owner
|
||||
-- We need to get the account.user_id for the organization owner
|
||||
UPDATE "git_provider"
|
||||
SET "userId" = (
|
||||
SELECT a.user_id
|
||||
FROM "organization" o
|
||||
JOIN "account" a ON o."owner_id" = a.user_id
|
||||
WHERE o.id = "git_provider"."organizationId"
|
||||
);--> statement-breakpoint
|
||||
|
||||
-- Now make the column NOT NULL since all rows should have values
|
||||
ALTER TABLE "git_provider" ALTER COLUMN "userId" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;
|
||||
13
apps/dokploy/drizzle/0095_curly_justice.sql
Normal file
13
apps/dokploy/drizzle/0095_curly_justice.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE "rollback" (
|
||||
"rollbackId" text PRIMARY KEY NOT NULL,
|
||||
"deploymentId" text NOT NULL,
|
||||
"version" serial NOT NULL,
|
||||
"image" text,
|
||||
"createdAt" text NOT NULL,
|
||||
"fullContext" jsonb
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "rollbackActive" boolean DEFAULT false;--> statement-breakpoint
|
||||
ALTER TABLE "deployment" ADD COLUMN "rollbackId" text;--> statement-breakpoint
|
||||
ALTER TABLE "rollback" ADD CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk" FOREIGN KEY ("deploymentId") REFERENCES "public"."deployment"("deploymentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_rollbackId_rollback_rollbackId_fk" FOREIGN KEY ("rollbackId") REFERENCES "public"."rollback"("rollbackId") ON DELETE cascade ON UPDATE no action;
|
||||
3
apps/dokploy/drizzle/0096_small_shaman.sql
Normal file
3
apps/dokploy/drizzle/0096_small_shaman.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "rollback" DROP CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "rollback" ADD CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk" FOREIGN KEY ("deploymentId") REFERENCES "public"."deployment"("deploymentId") ON DELETE set null ON UPDATE no action;
|
||||
3
apps/dokploy/drizzle/0097_hard_lizard.sql
Normal file
3
apps/dokploy/drizzle/0097_hard_lizard.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "rollback" DROP CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "rollback" ADD CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk" FOREIGN KEY ("deploymentId") REFERENCES "public"."deployment"("deploymentId") ON DELETE cascade ON UPDATE no action;
|
||||
5718
apps/dokploy/drizzle/meta/0093_snapshot.json
Normal file
5718
apps/dokploy/drizzle/meta/0093_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5737
apps/dokploy/drizzle/meta/0094_snapshot.json
Normal file
5737
apps/dokploy/drizzle/meta/0094_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5826
apps/dokploy/drizzle/meta/0095_snapshot.json
Normal file
5826
apps/dokploy/drizzle/meta/0095_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5826
apps/dokploy/drizzle/meta/0096_snapshot.json
Normal file
5826
apps/dokploy/drizzle/meta/0096_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
5826
apps/dokploy/drizzle/meta/0097_snapshot.json
Normal file
5826
apps/dokploy/drizzle/meta/0097_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -652,6 +652,41 @@
|
||||
"when": 1747713229160,
|
||||
"tag": "0092_stiff_the_watchers",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 93,
|
||||
"version": "7",
|
||||
"when": 1750397258622,
|
||||
"tag": "0093_nice_gorilla_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 94,
|
||||
"version": "7",
|
||||
"when": 1750559214977,
|
||||
"tag": "0094_numerous_carmella_unuscione",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 95,
|
||||
"version": "7",
|
||||
"when": 1750562292392,
|
||||
"tag": "0095_curly_justice",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 96,
|
||||
"version": "7",
|
||||
"when": 1750566830268,
|
||||
"tag": "0096_small_shaman",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 97,
|
||||
"version": "7",
|
||||
"when": 1750567641441,
|
||||
"tag": "0097_hard_lizard",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.22.7",
|
||||
"version": "v0.23.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -36,8 +36,6 @@
|
||||
"test": "vitest --config __test__/vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"@ai-sdk/anthropic": "^1.0.6",
|
||||
"@ai-sdk/azure": "^1.0.15",
|
||||
"@ai-sdk/cohere": "^1.0.6",
|
||||
@@ -127,6 +125,8 @@
|
||||
"octokit": "3.1.2",
|
||||
"ollama-ai-provider": "^1.1.0",
|
||||
"otpauth": "^9.2.3",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"postgres": "3.4.4",
|
||||
"public-ip": "6.0.2",
|
||||
"qrcode": "^1.5.3",
|
||||
@@ -147,13 +147,13 @@
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwind-merge": "^2.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"toml": "3.0.0",
|
||||
"undici": "^6.19.2",
|
||||
"use-resize-observer": "9.1.0",
|
||||
"ws": "8.16.0",
|
||||
"xterm-addon-fit": "^0.8.0",
|
||||
"zod": "^3.23.4",
|
||||
"zod-form-data": "^2.0.2",
|
||||
"toml": "3.0.0"
|
||||
"zod-form-data": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
|
||||
@@ -10,13 +10,14 @@ type Query = {
|
||||
state: string;
|
||||
installation_id: string;
|
||||
setup_action: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
const { code, state, installation_id }: Query = req.query as Query;
|
||||
const { code, state, installation_id, userId }: Query = req.query as Query;
|
||||
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: "Missing code parameter" });
|
||||
@@ -44,6 +45,7 @@ export default async function handler(
|
||||
githubPrivateKey: data.pem,
|
||||
},
|
||||
value as string,
|
||||
userId,
|
||||
);
|
||||
} else if (action === "gh_setup") {
|
||||
await db
|
||||
|
||||
@@ -74,12 +74,7 @@ const Service = (
|
||||
}
|
||||
}, [router.query.tab]);
|
||||
|
||||
const { data } = api.compose.one.useQuery(
|
||||
{ composeId },
|
||||
{
|
||||
refetchInterval: 5000,
|
||||
},
|
||||
);
|
||||
const { data } = api.compose.one.useQuery({ composeId });
|
||||
|
||||
const { data: auth } = api.user.get.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
|
||||
@@ -28,7 +28,6 @@ import { projectRouter } from "./routers/project";
|
||||
import { redirectsRouter } from "./routers/redirects";
|
||||
import { redisRouter } from "./routers/redis";
|
||||
import { registryRouter } from "./routers/registry";
|
||||
import { scheduleRouter } from "./routers/schedule";
|
||||
import { securityRouter } from "./routers/security";
|
||||
import { serverRouter } from "./routers/server";
|
||||
import { settingsRouter } from "./routers/settings";
|
||||
@@ -36,6 +35,8 @@ import { sshRouter } from "./routers/ssh-key";
|
||||
import { stripeRouter } from "./routers/stripe";
|
||||
import { swarmRouter } from "./routers/swarm";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { scheduleRouter } from "./routers/schedule";
|
||||
import { rollbackRouter } from "./routers/rollbacks";
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
*
|
||||
@@ -80,6 +81,7 @@ export const appRouter = createTRPCRouter({
|
||||
ai: aiRouter,
|
||||
organization: organizationRouter,
|
||||
schedule: scheduleRouter,
|
||||
rollback: rollbackRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
createApplication,
|
||||
deleteAllMiddlewares,
|
||||
findApplicationById,
|
||||
findGitProviderById,
|
||||
findProjectById,
|
||||
getApplicationStats,
|
||||
mechanizeDockerContainer,
|
||||
@@ -126,7 +127,45 @@ export const applicationRouter = createTRPCRouter({
|
||||
message: "You are not authorized to access this application",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
|
||||
let hasGitProviderAccess = true;
|
||||
let unauthorizedProvider: string | null = null;
|
||||
|
||||
const getGitProviderId = () => {
|
||||
switch (application.sourceType) {
|
||||
case "github":
|
||||
return application.github?.gitProviderId;
|
||||
case "gitlab":
|
||||
return application.gitlab?.gitProviderId;
|
||||
case "bitbucket":
|
||||
return application.bitbucket?.gitProviderId;
|
||||
case "gitea":
|
||||
return application.gitea?.gitProviderId;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const gitProviderId = getGitProviderId();
|
||||
|
||||
if (gitProviderId) {
|
||||
try {
|
||||
const gitProvider = await findGitProviderById(gitProviderId);
|
||||
if (gitProvider.userId !== ctx.session.userId) {
|
||||
hasGitProviderAccess = false;
|
||||
unauthorizedProvider = application.sourceType;
|
||||
}
|
||||
} catch {
|
||||
hasGitProviderAccess = false;
|
||||
unauthorizedProvider = application.sourceType;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...application,
|
||||
hasGitProviderAccess,
|
||||
unauthorizedProvider,
|
||||
};
|
||||
}),
|
||||
|
||||
reload: protectedProcedure
|
||||
@@ -488,6 +527,67 @@ export const applicationRouter = createTRPCRouter({
|
||||
enableSubmodules: input.enableSubmodules,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.input(apiFindOneApplication)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const application = await findApplicationById(input.applicationId);
|
||||
if (
|
||||
application.project.organizationId !== ctx.session.activeOrganizationId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to disconnect this git provider",
|
||||
});
|
||||
}
|
||||
|
||||
// Reset all git provider related fields
|
||||
await updateApplication(input.applicationId, {
|
||||
// GitHub fields
|
||||
repository: null,
|
||||
branch: null,
|
||||
owner: null,
|
||||
buildPath: "/",
|
||||
githubId: null,
|
||||
triggerType: "push",
|
||||
|
||||
// GitLab fields
|
||||
gitlabRepository: null,
|
||||
gitlabOwner: null,
|
||||
gitlabBranch: null,
|
||||
gitlabBuildPath: null,
|
||||
gitlabId: null,
|
||||
gitlabProjectId: null,
|
||||
gitlabPathNamespace: null,
|
||||
|
||||
// Bitbucket fields
|
||||
bitbucketRepository: null,
|
||||
bitbucketOwner: null,
|
||||
bitbucketBranch: null,
|
||||
bitbucketBuildPath: null,
|
||||
bitbucketId: null,
|
||||
|
||||
// Gitea fields
|
||||
giteaRepository: null,
|
||||
giteaOwner: null,
|
||||
giteaBranch: null,
|
||||
giteaBuildPath: null,
|
||||
giteaId: null,
|
||||
|
||||
// Custom Git fields
|
||||
customGitBranch: null,
|
||||
customGitBuildPath: null,
|
||||
customGitUrl: null,
|
||||
customGitSSHKeyId: null,
|
||||
|
||||
// Common fields
|
||||
sourceType: "github", // Reset to default
|
||||
applicationStatus: "idle",
|
||||
watchPaths: null,
|
||||
enableSubmodules: false,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
markRunning: protectedProcedure
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
findPostgresByBackupId,
|
||||
findPostgresById,
|
||||
findServerById,
|
||||
keepLatestNBackups,
|
||||
removeBackupById,
|
||||
removeScheduleBackup,
|
||||
runMariadbBackup,
|
||||
@@ -197,6 +198,8 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||
await runPostgresBackup(postgres, backup);
|
||||
|
||||
await keepLatestNBackups(backup, postgres?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
const message =
|
||||
@@ -217,6 +220,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const mysql = await findMySqlByBackupId(backup.backupId);
|
||||
await runMySqlBackup(mysql, backup);
|
||||
await keepLatestNBackups(backup, mysql?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -233,6 +237,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const mariadb = await findMariadbByBackupId(backup.backupId);
|
||||
await runMariadbBackup(mariadb, backup);
|
||||
await keepLatestNBackups(backup, mariadb?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -249,6 +254,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const compose = await findComposeByBackupId(backup.backupId);
|
||||
await runComposeBackup(compose, backup);
|
||||
await keepLatestNBackups(backup, compose?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
@@ -265,6 +271,7 @@ export const backupRouter = createTRPCRouter({
|
||||
const backup = await findBackupById(input.backupId);
|
||||
const mongo = await findMongoByBackupId(backup.backupId);
|
||||
await runMongoBackup(mongo, backup);
|
||||
await keepLatestNBackups(backup, mongo?.serverId);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -22,7 +22,11 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
.input(apiCreateBitbucket)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createBitbucket(input, ctx.session.activeOrganizationId);
|
||||
return await createBitbucket(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session.userId,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -37,7 +41,8 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -56,11 +61,13 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
result = result.filter(
|
||||
(provider) =>
|
||||
result = result.filter((provider) => {
|
||||
return (
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId
|
||||
);
|
||||
});
|
||||
return result;
|
||||
}),
|
||||
|
||||
@@ -70,7 +77,8 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -87,7 +95,8 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -103,7 +112,8 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -126,7 +136,8 @@ export const bitbucketRouter = createTRPCRouter({
|
||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
||||
if (
|
||||
bitbucketProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
deleteMount,
|
||||
findComposeById,
|
||||
findDomainsByComposeId,
|
||||
findGitProviderById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
findUserById,
|
||||
@@ -119,7 +120,45 @@ export const composeRouter = createTRPCRouter({
|
||||
message: "You are not authorized to access this compose",
|
||||
});
|
||||
}
|
||||
return compose;
|
||||
|
||||
let hasGitProviderAccess = true;
|
||||
let unauthorizedProvider: string | null = null;
|
||||
|
||||
const getGitProviderId = () => {
|
||||
switch (compose.sourceType) {
|
||||
case "github":
|
||||
return compose.github?.gitProviderId;
|
||||
case "gitlab":
|
||||
return compose.gitlab?.gitProviderId;
|
||||
case "bitbucket":
|
||||
return compose.bitbucket?.gitProviderId;
|
||||
case "gitea":
|
||||
return compose.gitea?.gitProviderId;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const gitProviderId = getGitProviderId();
|
||||
|
||||
if (gitProviderId) {
|
||||
try {
|
||||
const gitProvider = await findGitProviderById(gitProviderId);
|
||||
if (gitProvider.userId !== ctx.session.userId) {
|
||||
hasGitProviderAccess = false;
|
||||
unauthorizedProvider = compose.sourceType;
|
||||
}
|
||||
} catch {
|
||||
hasGitProviderAccess = false;
|
||||
unauthorizedProvider = compose.sourceType;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...compose,
|
||||
hasGitProviderAccess,
|
||||
unauthorizedProvider,
|
||||
};
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
@@ -496,7 +535,7 @@ export const composeRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return compose;
|
||||
}),
|
||||
|
||||
templates: publicProcedure
|
||||
@@ -526,6 +565,61 @@ export const composeRouter = createTRPCRouter({
|
||||
const uniqueTags = _.uniq(allTags);
|
||||
return uniqueTags;
|
||||
}),
|
||||
disconnectGitProvider: protectedProcedure
|
||||
.input(apiFindCompose)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const compose = await findComposeById(input.composeId);
|
||||
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to disconnect this git provider",
|
||||
});
|
||||
}
|
||||
|
||||
// Reset all git provider related fields
|
||||
await updateCompose(input.composeId, {
|
||||
// GitHub fields
|
||||
repository: null,
|
||||
branch: null,
|
||||
owner: null,
|
||||
composePath: undefined,
|
||||
githubId: null,
|
||||
triggerType: "push",
|
||||
|
||||
// GitLab fields
|
||||
gitlabRepository: null,
|
||||
gitlabOwner: null,
|
||||
gitlabBranch: null,
|
||||
gitlabId: null,
|
||||
gitlabProjectId: null,
|
||||
gitlabPathNamespace: null,
|
||||
|
||||
// Bitbucket fields
|
||||
bitbucketRepository: null,
|
||||
bitbucketOwner: null,
|
||||
bitbucketBranch: null,
|
||||
bitbucketId: null,
|
||||
|
||||
// Gitea fields
|
||||
giteaRepository: null,
|
||||
giteaOwner: null,
|
||||
giteaBranch: null,
|
||||
giteaId: null,
|
||||
|
||||
// Custom Git fields
|
||||
customGitBranch: null,
|
||||
customGitUrl: null,
|
||||
customGitSSHKeyId: null,
|
||||
|
||||
// Common fields
|
||||
sourceType: "github", // Reset to default
|
||||
composeStatus: "idle",
|
||||
watchPaths: null,
|
||||
enableSubmodules: false,
|
||||
});
|
||||
|
||||
return true;
|
||||
}),
|
||||
|
||||
move: protectedProcedure
|
||||
.input(
|
||||
|
||||
@@ -65,7 +65,11 @@ export const deploymentRouter = createTRPCRouter({
|
||||
const deploymentsList = await db.query.deployments.findMany({
|
||||
where: eq(deployments[`${input.type}Id`], input.id),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
with: {
|
||||
rollback: true,
|
||||
},
|
||||
});
|
||||
|
||||
return deploymentsList;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { db } from "@/server/db";
|
||||
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
|
||||
import { findGitProviderById, removeGitProvider } from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
|
||||
export const gitProviderRouter = createTRPCRouter({
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
@@ -15,7 +15,10 @@ export const gitProviderRouter = createTRPCRouter({
|
||||
gitea: true,
|
||||
},
|
||||
orderBy: desc(gitProvider.createdAt),
|
||||
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
where: and(
|
||||
eq(gitProvider.userId, ctx.session.userId),
|
||||
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
|
||||
),
|
||||
});
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
|
||||
@@ -26,7 +26,11 @@ export const giteaRouter = createTRPCRouter({
|
||||
.input(apiCreateGitea)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createGitea(input, ctx.session.activeOrganizationId);
|
||||
return await createGitea(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session.userId,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -42,7 +46,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -62,7 +67,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
result = result.filter(
|
||||
(provider) =>
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId,
|
||||
);
|
||||
|
||||
const filtered = result
|
||||
@@ -94,7 +100,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -130,7 +137,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -162,7 +170,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -190,7 +199,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
const giteaProvider = await findGiteaById(input.giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -231,7 +241,8 @@ export const giteaRouter = createTRPCRouter({
|
||||
const giteaProvider = await findGiteaById(giteaId);
|
||||
if (
|
||||
giteaProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
giteaProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
|
||||
@@ -21,7 +21,8 @@ export const githubRouter = createTRPCRouter({
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -36,7 +37,8 @@ export const githubRouter = createTRPCRouter({
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -51,7 +53,8 @@ export const githubRouter = createTRPCRouter({
|
||||
const githubProvider = await findGithubById(input.githubId || "");
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
//TODO: Remove this line when the cloud version is ready
|
||||
throw new TRPCError({
|
||||
@@ -71,7 +74,8 @@ export const githubRouter = createTRPCRouter({
|
||||
result = result.filter(
|
||||
(provider) =>
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId,
|
||||
);
|
||||
|
||||
const filtered = result
|
||||
@@ -95,7 +99,8 @@ export const githubRouter = createTRPCRouter({
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -117,7 +122,8 @@ export const githubRouter = createTRPCRouter({
|
||||
const githubProvider = await findGithubById(input.githubId);
|
||||
if (
|
||||
githubProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
githubProvider.gitProvider.userId === ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
|
||||
@@ -25,7 +25,11 @@ export const gitlabRouter = createTRPCRouter({
|
||||
.input(apiCreateGitlab)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
return await createGitlab(input, ctx.session.activeOrganizationId);
|
||||
return await createGitlab(
|
||||
input,
|
||||
ctx.session.activeOrganizationId,
|
||||
ctx.session.userId,
|
||||
);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
@@ -40,7 +44,8 @@ export const gitlabRouter = createTRPCRouter({
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -56,11 +61,13 @@ export const gitlabRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
result = result.filter(
|
||||
(provider) =>
|
||||
result = result.filter((provider) => {
|
||||
return (
|
||||
provider.gitProvider.organizationId ===
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
ctx.session.activeOrganizationId &&
|
||||
provider.gitProvider.userId === ctx.session.userId
|
||||
);
|
||||
});
|
||||
const filtered = result
|
||||
.filter((provider) => haveGitlabRequirements(provider))
|
||||
.map((provider) => {
|
||||
@@ -80,7 +87,8 @@ export const gitlabRouter = createTRPCRouter({
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -96,7 +104,8 @@ export const gitlabRouter = createTRPCRouter({
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId || "");
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -112,7 +121,8 @@ export const gitlabRouter = createTRPCRouter({
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId || "");
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
@@ -135,7 +145,8 @@ export const gitlabRouter = createTRPCRouter({
|
||||
const gitlabProvider = await findGitlabById(input.gitlabId);
|
||||
if (
|
||||
gitlabProvider.gitProvider.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
ctx.session.activeOrganizationId &&
|
||||
gitlabProvider.gitProvider.userId !== ctx.session.userId
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
|
||||
@@ -446,4 +446,12 @@ export const notificationRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.notifications.findMany({
|
||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||
with: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
37
apps/dokploy/server/api/routers/rollbacks.ts
Normal file
37
apps/dokploy/server/api/routers/rollbacks.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { apiFindOneRollback } from "@/server/db/schema";
|
||||
import { removeRollbackById, rollback } from "@dokploy/server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const rollbackRouter = createTRPCRouter({
|
||||
delete: protectedProcedure
|
||||
.input(apiFindOneRollback)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return removeRollbackById(input.rollbackId);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error input: Deleting rollback";
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message,
|
||||
});
|
||||
}
|
||||
}),
|
||||
rollback: protectedProcedure
|
||||
.input(apiFindOneRollback)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await rollback(input.rollbackId);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error input: Rolling back",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -825,6 +825,9 @@ export const settingsRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
if (input.cronExpression) {
|
||||
return startLogCleanup(input.cronExpression);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
IS_CLOUD,
|
||||
createApiKey,
|
||||
findAdmin,
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
getUserByToken,
|
||||
removeUserById,
|
||||
sendEmailNotification,
|
||||
updateUser,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
@@ -362,4 +365,59 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
return organizations.length;
|
||||
}),
|
||||
sendInvitation: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
invitationId: z.string().min(1),
|
||||
notificationId: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = await findNotificationById(input.notificationId);
|
||||
|
||||
const email = notification.email;
|
||||
|
||||
const currentInvitation = await db.query.invitation.findFirst({
|
||||
where: eq(invitation.id, input.invitationId),
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Email notification not found",
|
||||
});
|
||||
}
|
||||
|
||||
const admin = await findAdmin();
|
||||
const host =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: admin.user.host;
|
||||
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
|
||||
|
||||
const organization = await findOrganizationById(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
|
||||
try {
|
||||
await sendEmailNotification(
|
||||
{
|
||||
...email,
|
||||
toAddresses: [currentInvitation?.email || ""],
|
||||
},
|
||||
"Invitation to join organization",
|
||||
`
|
||||
<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
|
||||
`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
return inviteLink;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ const config = {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
"2xl": "87.5rem",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
|
||||
@@ -8,26 +8,26 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"ioredis": "5.4.1",
|
||||
"bullmq": "5.4.2",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"zod": "^3.23.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"hono": "^4.5.8",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"bullmq": "5.4.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"hono": "^4.5.8",
|
||||
"ioredis": "5.4.1",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2"
|
||||
"pino-pretty": "11.2.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"zod": "^3.23.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.2",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1"
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0"
|
||||
}
|
||||
|
||||
12
package.json
12
package.json
@@ -20,15 +20,15 @@
|
||||
"format-and-lint:fix": "biome check . --write"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "16.4.5",
|
||||
"esbuild": "0.20.2",
|
||||
"tsx": "4.16.2",
|
||||
"lint-staged": "^15.2.7",
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"lefthook": "1.8.4",
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"@types/node": "^18.17.0"
|
||||
"@types/node": "^18.17.0",
|
||||
"dotenv": "16.4.5",
|
||||
"esbuild": "0.20.2",
|
||||
"lefthook": "1.8.4",
|
||||
"lint-staged": "^15.2.7",
|
||||
"tsx": "4.16.2"
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0",
|
||||
"engines": {
|
||||
|
||||
@@ -28,9 +28,6 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"micromatch": "4.0.8",
|
||||
"@ai-sdk/anthropic": "^1.0.6",
|
||||
"@ai-sdk/azure": "^1.0.15",
|
||||
"@ai-sdk/cohere": "^1.0.6",
|
||||
@@ -39,27 +36,29 @@
|
||||
"@ai-sdk/openai": "^1.0.12",
|
||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
||||
"@better-auth/utils": "0.2.4",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"better-auth": "v1.2.8-beta.7",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@octokit/auth-app": "^6.0.4",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@react-email/components": "^0.0.21",
|
||||
"@trpc/server": "^10.43.6",
|
||||
"adm-zip": "^0.5.14",
|
||||
"ai": "^4.0.23",
|
||||
"bcrypt": "5.1.1",
|
||||
"better-auth": "v1.2.8-beta.7",
|
||||
"bl": "6.0.11",
|
||||
"boxen": "^7.1.1",
|
||||
"date-fns": "3.6.0",
|
||||
"dockerode": "4.0.2",
|
||||
"dotenv": "16.4.5",
|
||||
"drizzle-dbml-generator": "0.10.0",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"drizzle-zod": "0.5.1",
|
||||
"hi-base32": "^0.5.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3",
|
||||
"node-os-utils": "1.3.7",
|
||||
"node-pty": "1.0.0",
|
||||
@@ -68,6 +67,8 @@
|
||||
"octokit": "3.1.2",
|
||||
"ollama-ai-provider": "^1.1.0",
|
||||
"otpauth": "^9.2.3",
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"postgres": "3.4.4",
|
||||
"public-ip": "6.0.2",
|
||||
"qrcode": "^1.5.3",
|
||||
@@ -75,19 +76,18 @@
|
||||
"react-dom": "18.2.0",
|
||||
"rotating-file-stream": "3.2.3",
|
||||
"slugify": "^1.6.6",
|
||||
"ws": "8.16.0",
|
||||
"zod": "^3.23.4",
|
||||
"ssh2": "1.15.0",
|
||||
"@octokit/rest": "^20.0.2",
|
||||
"toml": "3.0.0"
|
||||
"toml": "3.0.0",
|
||||
"ws": "8.16.0",
|
||||
"zod": "^3.23.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/dockerode": "3.3.23",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash": "4.17.4",
|
||||
"@types/micromatch": "4.0.9",
|
||||
"@types/node": "^18.17.0",
|
||||
"@types/node-os-utils": "1.3.4",
|
||||
"@types/node-schedule": "2.1.6",
|
||||
|
||||
@@ -27,7 +27,6 @@ import { server } from "./server";
|
||||
import { applicationStatus, certificateType, triggerType } from "./shared";
|
||||
import { sshKeys } from "./ssh-key";
|
||||
import { generateAppName } from "./utils";
|
||||
|
||||
export const sourceType = pgEnum("sourceType", [
|
||||
"docker",
|
||||
"git",
|
||||
@@ -132,6 +131,7 @@ export const applications = pgTable("application", {
|
||||
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
|
||||
false,
|
||||
),
|
||||
rollbackActive: boolean("rollbackActive").default(false),
|
||||
buildArgs: text("buildArgs"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
|
||||
@@ -15,6 +15,7 @@ import { compose } from "./compose";
|
||||
import { previewDeployments } from "./preview-deployments";
|
||||
import { schedules } from "./schedule";
|
||||
import { server } from "./server";
|
||||
import { rollbacks } from "./rollbacks";
|
||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||
"running",
|
||||
"done",
|
||||
@@ -58,6 +59,10 @@ export const deployments = pgTable("deployment", {
|
||||
backupId: text("backupId").references((): AnyPgColumn => backups.backupId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
rollbackId: text("rollbackId").references(
|
||||
(): AnyPgColumn => rollbacks.rollbackId,
|
||||
{ onDelete: "cascade" },
|
||||
),
|
||||
});
|
||||
|
||||
export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
@@ -85,6 +90,10 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
|
||||
fields: [deployments.backupId],
|
||||
references: [backups.backupId],
|
||||
}),
|
||||
rollback: one(rollbacks, {
|
||||
fields: [deployments.deploymentId],
|
||||
references: [rollbacks.deploymentId],
|
||||
}),
|
||||
}));
|
||||
|
||||
const schema = createInsertSchema(deployments, {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { bitbucket } from "./bitbucket";
|
||||
import { gitea } from "./gitea";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { users_temp } from "./user";
|
||||
|
||||
export const gitProviderType = pgEnum("gitProviderType", [
|
||||
"github",
|
||||
@@ -29,6 +30,9 @@ export const gitProvider = pgTable("git_provider", {
|
||||
organizationId: text("organizationId")
|
||||
.notNull()
|
||||
.references(() => organization.id, { onDelete: "cascade" }),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users_temp.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
@@ -52,6 +56,10 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
|
||||
fields: [gitProvider.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
user: one(users_temp, {
|
||||
fields: [gitProvider.userId],
|
||||
references: [users_temp.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(gitProvider);
|
||||
|
||||
@@ -32,3 +32,4 @@ export * from "./preview-deployments";
|
||||
export * from "./ai";
|
||||
export * from "./account";
|
||||
export * from "./schedule";
|
||||
export * from "./rollbacks";
|
||||
|
||||
45
packages/server/src/db/schema/rollbacks.ts
Normal file
45
packages/server/src/db/schema/rollbacks.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { deployments } from "./deployment";
|
||||
|
||||
export const rollbacks = pgTable("rollback", {
|
||||
rollbackId: text("rollbackId")
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
deploymentId: text("deploymentId")
|
||||
.notNull()
|
||||
.references(() => deployments.deploymentId, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
version: serial(),
|
||||
image: text("image"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
fullContext: jsonb("fullContext"),
|
||||
});
|
||||
|
||||
export type Rollback = typeof rollbacks.$inferSelect;
|
||||
|
||||
export const rollbacksRelations = relations(rollbacks, ({ one }) => ({
|
||||
deployment: one(deployments, {
|
||||
fields: [rollbacks.deploymentId],
|
||||
references: [deployments.deploymentId],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const createRollbackSchema = createInsertSchema(rollbacks).extend({
|
||||
appName: z.string().min(1),
|
||||
});
|
||||
|
||||
export const updateRollbackSchema = createRollbackSchema.extend({
|
||||
rollbackId: z.string().min(1),
|
||||
});
|
||||
|
||||
export const apiFindOneRollback = z.object({
|
||||
rollbackId: z.string().min(1),
|
||||
});
|
||||
@@ -56,7 +56,7 @@ export const users_temp = pgTable("user_temp", {
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
logCleanupCron: text("logCleanupCron"),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
role: text("role").notNull().default("user"),
|
||||
// Metrics
|
||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||
|
||||
@@ -32,6 +32,7 @@ export * from "./services/gitea";
|
||||
export * from "./services/server";
|
||||
export * from "./services/schedule";
|
||||
export * from "./services/application";
|
||||
export * from "./services/rollbacks";
|
||||
export * from "./utils/databases/rebuild";
|
||||
export * from "./setup/config-paths";
|
||||
export * from "./setup/postgres-setup";
|
||||
|
||||
@@ -18,9 +18,6 @@ const { handler, api } = betterAuth({
|
||||
provider: "pg",
|
||||
schema: schema,
|
||||
}),
|
||||
logger: {
|
||||
disabled: process.env.NODE_ENV === "production",
|
||||
},
|
||||
appName: "Dokploy",
|
||||
socialProviders: {
|
||||
github: {
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { createRollback } from "./rollbacks";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
@@ -214,6 +215,17 @@ export const deployApplication = async ({
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
@@ -338,6 +350,17 @@ export const deployRemoteApplication = async ({
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.project.name,
|
||||
applicationName: application.name,
|
||||
|
||||
@@ -13,6 +13,7 @@ export type Bitbucket = typeof bitbucket.$inferSelect;
|
||||
export const createBitbucket = async (
|
||||
input: typeof apiCreateBitbucket._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
@@ -21,6 +22,7 @@ export const createBitbucket = async (
|
||||
providerType: "bitbucket",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
634
packages/server/src/services/cdn.ts
Normal file
634
packages/server/src/services/cdn.ts
Normal file
@@ -0,0 +1,634 @@
|
||||
// CDN Provider Interface
|
||||
export interface CDNProvider {
|
||||
name: string;
|
||||
displayName: string;
|
||||
checkIp: (ip: string) => boolean;
|
||||
warningMessage: string;
|
||||
}
|
||||
|
||||
const isIPInCIDR = (ip: string, cidr: string): boolean => {
|
||||
const [network, prefixLength] = cidr.split("/");
|
||||
if (!network || !prefixLength) return false;
|
||||
const prefix = Number.parseInt(prefixLength, 10);
|
||||
|
||||
// Convert IP addresses to 32-bit integers
|
||||
const ipToInt = (ipStr: string): number => {
|
||||
return (
|
||||
ipStr
|
||||
.split(".")
|
||||
.reduce((acc, octet) => (acc << 8) + Number.parseInt(octet, 10), 0) >>>
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
const ipInt = ipToInt(ip);
|
||||
const networkInt = ipToInt(network);
|
||||
const mask = (0xffffffff << (32 - prefix)) >>> 0;
|
||||
|
||||
return (ipInt & mask) === (networkInt & mask);
|
||||
};
|
||||
|
||||
// Cloudflare IP ranges
|
||||
// https://www.cloudflare.com/ips-v4
|
||||
const CLOUDFLARE_IP_RANGES = [
|
||||
"173.245.48.0/20",
|
||||
"103.21.244.0/22",
|
||||
"103.22.200.0/22",
|
||||
"103.31.4.0/22",
|
||||
"141.101.64.0/18",
|
||||
"108.162.192.0/18",
|
||||
"190.93.240.0/20",
|
||||
"188.114.96.0/20",
|
||||
"197.234.240.0/22",
|
||||
"198.41.128.0/17",
|
||||
"162.158.0.0/15",
|
||||
"104.16.0.0/13",
|
||||
"104.24.0.0/14",
|
||||
"172.64.0.0/13",
|
||||
"131.0.72.0/22",
|
||||
];
|
||||
|
||||
// Fastly IP ranges
|
||||
// https://api.fastly.com/public-ip-list
|
||||
const FASTLY_IP_RANGES = [
|
||||
"23.235.32.0/20",
|
||||
"43.249.72.0/22",
|
||||
"103.244.50.0/24",
|
||||
"103.245.222.0/23",
|
||||
"103.245.224.0/24",
|
||||
"104.156.80.0/20",
|
||||
"140.248.64.0/18",
|
||||
"140.248.128.0/17",
|
||||
"146.75.0.0/17",
|
||||
"151.101.0.0/16",
|
||||
"157.52.64.0/18",
|
||||
"167.82.0.0/17",
|
||||
"167.82.128.0/20",
|
||||
"167.82.160.0/20",
|
||||
"167.82.224.0/20",
|
||||
"172.111.64.0/18",
|
||||
"185.31.16.0/22",
|
||||
"199.27.72.0/21",
|
||||
"199.232.0.0/16",
|
||||
];
|
||||
|
||||
// Bunny CDN IP addresses
|
||||
// https://bunnycdn.com/api/system/edgeserverlist
|
||||
const BUNNY_CDN_IPS = new Set([
|
||||
"89.187.188.227",
|
||||
"89.187.188.228",
|
||||
"139.180.134.196",
|
||||
"89.38.96.158",
|
||||
"89.187.162.249",
|
||||
"89.187.162.242",
|
||||
"185.102.217.65",
|
||||
"185.93.1.243",
|
||||
"156.146.40.49",
|
||||
"185.59.220.199",
|
||||
"185.59.220.198",
|
||||
"195.181.166.158",
|
||||
"185.180.12.68",
|
||||
"138.199.24.209",
|
||||
"138.199.24.211",
|
||||
"79.127.216.111",
|
||||
"79.127.216.112",
|
||||
"89.187.169.47",
|
||||
"138.199.24.218",
|
||||
"185.40.106.117",
|
||||
"200.25.45.4",
|
||||
"200.25.57.5",
|
||||
"193.162.131.1",
|
||||
"200.25.11.8",
|
||||
"200.25.53.5",
|
||||
"200.25.13.98",
|
||||
"41.242.2.18",
|
||||
"200.25.62.5",
|
||||
"200.25.38.69",
|
||||
"200.25.42.70",
|
||||
"200.25.36.166",
|
||||
"195.206.229.106",
|
||||
"194.242.11.186",
|
||||
"185.164.35.8",
|
||||
"94.20.154.22",
|
||||
"185.93.1.244",
|
||||
"156.59.145.154",
|
||||
"143.244.49.177",
|
||||
"138.199.46.66",
|
||||
"138.199.37.227",
|
||||
"138.199.37.231",
|
||||
"138.199.37.230",
|
||||
"138.199.37.229",
|
||||
"138.199.46.69",
|
||||
"138.199.46.68",
|
||||
"138.199.46.67",
|
||||
"185.93.1.246",
|
||||
"138.199.37.232",
|
||||
"195.181.163.196",
|
||||
"107.182.163.162",
|
||||
"195.181.163.195",
|
||||
"84.17.46.53",
|
||||
"212.102.40.114",
|
||||
"84.17.46.54",
|
||||
"138.199.40.58",
|
||||
"143.244.38.134",
|
||||
"143.244.38.136",
|
||||
"185.152.64.17",
|
||||
"84.17.59.115",
|
||||
"89.187.165.194",
|
||||
"138.199.15.193",
|
||||
"89.35.237.170",
|
||||
"37.19.216.130",
|
||||
"185.93.1.247",
|
||||
"185.93.3.244",
|
||||
"143.244.49.179",
|
||||
"143.244.49.180",
|
||||
"138.199.9.104",
|
||||
"185.152.66.243",
|
||||
"143.244.49.178",
|
||||
"169.150.221.147",
|
||||
"200.25.18.73",
|
||||
"84.17.63.178",
|
||||
"200.25.32.131",
|
||||
"37.19.207.34",
|
||||
"192.189.65.146",
|
||||
"143.244.45.177",
|
||||
"185.93.1.249",
|
||||
"185.93.1.250",
|
||||
"169.150.215.115",
|
||||
"209.177.87.197",
|
||||
"156.146.56.162",
|
||||
"156.146.56.161",
|
||||
"185.93.2.246",
|
||||
"185.93.2.245",
|
||||
"212.102.40.113",
|
||||
"185.93.2.244",
|
||||
"143.244.50.82",
|
||||
"143.244.50.83",
|
||||
"156.146.56.163",
|
||||
"129.227.217.178",
|
||||
"129.227.217.179",
|
||||
"200.25.69.94",
|
||||
"128.1.52.179",
|
||||
"200.25.16.103",
|
||||
"15.235.54.226",
|
||||
"102.67.138.155",
|
||||
"156.146.43.65",
|
||||
"195.181.163.203",
|
||||
"195.181.163.202",
|
||||
"156.146.56.169",
|
||||
"156.146.56.170",
|
||||
"156.146.56.166",
|
||||
"156.146.56.171",
|
||||
"169.150.207.210",
|
||||
"156.146.56.167",
|
||||
"143.244.50.84",
|
||||
"143.244.50.85",
|
||||
"143.244.50.86",
|
||||
"143.244.50.87",
|
||||
"156.146.56.168",
|
||||
"169.150.207.211",
|
||||
"212.102.50.59",
|
||||
"146.185.248.15",
|
||||
"143.244.50.90",
|
||||
"143.244.50.91",
|
||||
"143.244.50.88",
|
||||
"143.244.50.209",
|
||||
"143.244.50.213",
|
||||
"143.244.50.214",
|
||||
"143.244.49.183",
|
||||
"143.244.50.89",
|
||||
"143.244.50.210",
|
||||
"143.244.50.211",
|
||||
"143.244.50.212",
|
||||
"5.42.206.66",
|
||||
"94.46.27.186",
|
||||
"169.150.207.213",
|
||||
"169.150.207.214",
|
||||
"169.150.207.215",
|
||||
"169.150.207.212",
|
||||
"169.150.219.114",
|
||||
"169.150.202.210",
|
||||
"169.150.242.193",
|
||||
"185.93.1.251",
|
||||
"169.150.207.216",
|
||||
"169.150.207.217",
|
||||
"169.150.238.19",
|
||||
"102.219.126.20",
|
||||
"156.59.66.182",
|
||||
"122.10.251.130",
|
||||
"185.24.11.18",
|
||||
"138.199.36.7",
|
||||
"138.199.36.8",
|
||||
"138.199.36.9",
|
||||
"138.199.36.10",
|
||||
"138.199.36.11",
|
||||
"138.199.37.225",
|
||||
"84.17.46.49",
|
||||
"84.17.37.217",
|
||||
"169.150.225.35",
|
||||
"169.150.225.36",
|
||||
"169.150.225.37",
|
||||
"169.150.225.38",
|
||||
"169.150.225.39",
|
||||
"169.150.225.34",
|
||||
"169.150.236.97",
|
||||
"169.150.236.98",
|
||||
"169.150.236.99",
|
||||
"169.150.236.100",
|
||||
"93.189.63.149",
|
||||
"143.244.56.49",
|
||||
"143.244.56.50",
|
||||
"143.244.56.51",
|
||||
"169.150.247.40",
|
||||
"169.150.247.33",
|
||||
"169.150.247.34",
|
||||
"169.150.247.35",
|
||||
"169.150.247.36",
|
||||
"169.150.247.37",
|
||||
"169.150.247.38",
|
||||
"169.150.247.39",
|
||||
"38.142.94.218",
|
||||
"87.249.137.52",
|
||||
"38.104.169.186",
|
||||
"66.181.163.74",
|
||||
"84.17.38.227",
|
||||
"84.17.38.228",
|
||||
"84.17.38.229",
|
||||
"84.17.38.230",
|
||||
"84.17.38.231",
|
||||
"84.17.38.232",
|
||||
"169.150.225.41",
|
||||
"169.150.225.42",
|
||||
"169.150.249.162",
|
||||
"169.150.249.163",
|
||||
"169.150.249.164",
|
||||
"169.150.249.165",
|
||||
"169.150.249.166",
|
||||
"169.150.249.167",
|
||||
"169.150.249.168",
|
||||
"169.150.249.169",
|
||||
"185.131.64.124",
|
||||
"103.112.0.22",
|
||||
"37.236.234.2",
|
||||
"169.150.252.209",
|
||||
"212.102.46.118",
|
||||
"192.169.120.162",
|
||||
"93.180.217.214",
|
||||
"37.19.203.178",
|
||||
"107.155.47.146",
|
||||
"193.201.190.174",
|
||||
"156.59.95.218",
|
||||
"213.170.143.139",
|
||||
"129.227.186.154",
|
||||
"195.238.127.98",
|
||||
"200.25.22.6",
|
||||
"204.16.244.92",
|
||||
"200.25.70.101",
|
||||
"200.25.66.100",
|
||||
"139.180.209.182",
|
||||
"103.108.231.41",
|
||||
"103.108.229.5",
|
||||
"103.216.220.9",
|
||||
"169.150.225.40",
|
||||
"212.102.50.49",
|
||||
"212.102.50.52",
|
||||
"109.61.83.242",
|
||||
"109.61.83.243",
|
||||
"212.102.50.50",
|
||||
"169.150.225.43",
|
||||
"45.125.247.57",
|
||||
"103.235.199.170",
|
||||
"128.1.35.170",
|
||||
"38.32.110.58",
|
||||
"169.150.220.228",
|
||||
"169.150.220.229",
|
||||
"169.150.220.230",
|
||||
"169.150.220.231",
|
||||
"138.199.4.179",
|
||||
"207.211.214.145",
|
||||
"109.61.86.193",
|
||||
"103.214.20.95",
|
||||
"178.175.134.51",
|
||||
"138.199.4.178",
|
||||
"172.255.253.140",
|
||||
"185.24.11.19",
|
||||
"109.61.83.244",
|
||||
"109.61.83.245",
|
||||
"84.17.38.250",
|
||||
"84.17.38.251",
|
||||
"146.59.69.202",
|
||||
"146.70.80.218",
|
||||
"200.25.80.74",
|
||||
"79.127.213.214",
|
||||
"79.127.213.215",
|
||||
"79.127.213.216",
|
||||
"79.127.213.217",
|
||||
"195.69.140.112",
|
||||
"109.61.83.247",
|
||||
"109.61.83.246",
|
||||
"185.93.2.248",
|
||||
"109.61.83.249",
|
||||
"109.61.83.250",
|
||||
"109.61.83.251",
|
||||
"46.199.75.115",
|
||||
"141.164.35.160",
|
||||
"109.61.83.97",
|
||||
"109.61.83.98",
|
||||
"109.61.83.99",
|
||||
"129.227.179.18",
|
||||
"185.180.14.250",
|
||||
"152.89.160.26",
|
||||
"5.189.202.62",
|
||||
"98.98.242.142",
|
||||
"156.59.92.126",
|
||||
"84.17.59.117",
|
||||
"79.127.216.66",
|
||||
"79.127.204.113",
|
||||
"79.127.237.132",
|
||||
"169.150.236.104",
|
||||
"169.150.236.105",
|
||||
"37.27.135.61",
|
||||
"158.51.123.205",
|
||||
"156.146.43.70",
|
||||
"156.146.43.71",
|
||||
"156.146.43.72",
|
||||
"180.149.231.175",
|
||||
"185.93.2.243",
|
||||
"143.244.56.52",
|
||||
"143.244.56.53",
|
||||
"143.244.56.54",
|
||||
"143.244.56.55",
|
||||
"143.244.56.56",
|
||||
"143.244.56.57",
|
||||
"143.244.56.58",
|
||||
"144.76.236.44",
|
||||
"88.198.57.50",
|
||||
"78.46.69.199",
|
||||
"136.243.16.49",
|
||||
"138.201.86.122",
|
||||
"136.243.42.90",
|
||||
"88.99.95.221",
|
||||
"178.63.2.112",
|
||||
"5.9.98.45",
|
||||
"136.243.42.10",
|
||||
"169.150.236.106",
|
||||
"169.150.236.107",
|
||||
"185.93.1.242",
|
||||
"185.93.1.245",
|
||||
"143.244.60.193",
|
||||
"195.181.163.194",
|
||||
"79.127.188.193",
|
||||
"79.127.188.196",
|
||||
"79.127.188.194",
|
||||
"79.127.188.195",
|
||||
"104.166.144.106",
|
||||
"156.59.126.78",
|
||||
"185.135.85.154",
|
||||
"38.54.5.37",
|
||||
"38.54.3.92",
|
||||
"185.165.170.74",
|
||||
"207.121.80.118",
|
||||
"207.121.46.228",
|
||||
"207.121.46.236",
|
||||
"207.121.46.244",
|
||||
"207.121.46.252",
|
||||
"216.202.235.164",
|
||||
"207.121.46.220",
|
||||
"207.121.75.132",
|
||||
"207.121.80.12",
|
||||
"207.121.80.172",
|
||||
"207.121.90.60",
|
||||
"207.121.90.68",
|
||||
"207.121.97.204",
|
||||
"207.121.90.252",
|
||||
"207.121.97.236",
|
||||
"207.121.99.12",
|
||||
"138.199.24.219",
|
||||
"185.93.2.251",
|
||||
"138.199.46.65",
|
||||
"207.121.41.196",
|
||||
"207.121.99.20",
|
||||
"207.121.99.36",
|
||||
"207.121.99.44",
|
||||
"207.121.99.52",
|
||||
"207.121.99.60",
|
||||
"207.121.23.68",
|
||||
"207.121.23.124",
|
||||
"207.121.23.244",
|
||||
"207.121.23.180",
|
||||
"207.121.23.188",
|
||||
"207.121.23.196",
|
||||
"207.121.23.204",
|
||||
"207.121.24.52",
|
||||
"207.121.24.60",
|
||||
"207.121.24.68",
|
||||
"207.121.24.76",
|
||||
"207.121.24.92",
|
||||
"207.121.24.100",
|
||||
"207.121.24.108",
|
||||
"207.121.24.116",
|
||||
"154.95.86.76",
|
||||
"5.9.99.73",
|
||||
"78.46.92.118",
|
||||
"144.76.65.213",
|
||||
"78.46.156.89",
|
||||
"88.198.9.155",
|
||||
"144.76.79.22",
|
||||
"103.1.215.93",
|
||||
"103.137.12.33",
|
||||
"103.107.196.31",
|
||||
"116.90.72.155",
|
||||
"103.137.14.5",
|
||||
"116.90.75.65",
|
||||
"37.19.207.37",
|
||||
"208.83.234.224",
|
||||
"116.202.155.146",
|
||||
"116.202.193.178",
|
||||
"116.202.224.168",
|
||||
"188.40.126.227",
|
||||
"88.99.26.189",
|
||||
"168.119.39.238",
|
||||
"88.99.26.97",
|
||||
"168.119.12.188",
|
||||
"176.9.139.55",
|
||||
"142.132.223.79",
|
||||
"142.132.223.80",
|
||||
"142.132.223.81",
|
||||
"46.4.116.17",
|
||||
"46.4.119.81",
|
||||
"167.235.114.167",
|
||||
"159.69.68.171",
|
||||
"178.63.21.52",
|
||||
"46.4.120.152",
|
||||
"116.202.80.247",
|
||||
"5.9.71.119",
|
||||
"195.201.11.156",
|
||||
"78.46.123.17",
|
||||
"46.4.113.143",
|
||||
"136.243.2.236",
|
||||
"195.201.81.217",
|
||||
"148.251.42.123",
|
||||
"94.130.68.122",
|
||||
"88.198.22.103",
|
||||
"46.4.102.90",
|
||||
"157.90.180.205",
|
||||
"162.55.135.11",
|
||||
"195.201.109.59",
|
||||
"148.251.41.244",
|
||||
"116.202.235.16",
|
||||
"128.140.70.141",
|
||||
"78.46.74.86",
|
||||
"78.46.74.85",
|
||||
"178.63.41.242",
|
||||
"178.63.41.247",
|
||||
"178.63.41.234",
|
||||
"104.237.53.74",
|
||||
"104.237.54.154",
|
||||
"104.237.51.58",
|
||||
"64.185.235.90",
|
||||
"64.185.234.114",
|
||||
"64.185.232.194",
|
||||
"64.185.232.178",
|
||||
"64.185.232.82",
|
||||
"103.60.15.169",
|
||||
"103.60.15.170",
|
||||
"103.60.15.171",
|
||||
"103.60.15.172",
|
||||
"103.60.15.173",
|
||||
"103.60.15.162",
|
||||
"103.60.15.163",
|
||||
"103.60.15.164",
|
||||
"103.60.15.165",
|
||||
"103.60.15.166",
|
||||
"103.60.15.167",
|
||||
"103.60.15.168",
|
||||
"109.248.43.116",
|
||||
"109.248.43.117",
|
||||
"109.248.43.162",
|
||||
"109.248.43.163",
|
||||
"109.248.43.164",
|
||||
"109.248.43.165",
|
||||
"49.12.71.27",
|
||||
"49.12.0.158",
|
||||
"78.47.94.156",
|
||||
"109.248.43.159",
|
||||
"109.248.43.160",
|
||||
"109.248.43.208",
|
||||
"109.248.43.179",
|
||||
"109.248.43.232",
|
||||
"109.248.43.231",
|
||||
"109.248.43.241",
|
||||
"109.248.43.236",
|
||||
"109.248.43.240",
|
||||
"109.248.43.103",
|
||||
"116.202.118.194",
|
||||
"116.202.80.29",
|
||||
"159.69.57.80",
|
||||
"139.180.129.216",
|
||||
"139.99.174.7",
|
||||
"89.187.169.18",
|
||||
"89.187.179.7",
|
||||
"143.244.62.213",
|
||||
"185.93.3.246",
|
||||
"195.181.163.198",
|
||||
"185.152.64.19",
|
||||
"84.17.37.211",
|
||||
"212.102.50.54",
|
||||
"212.102.46.115",
|
||||
"143.244.38.135",
|
||||
"169.150.238.21",
|
||||
"169.150.207.51",
|
||||
"169.150.207.49",
|
||||
"84.17.38.226",
|
||||
"84.17.38.225",
|
||||
"37.19.222.248",
|
||||
"37.19.222.249",
|
||||
"169.150.247.139",
|
||||
"169.150.247.177",
|
||||
"169.150.247.178",
|
||||
"169.150.213.49",
|
||||
"212.102.46.119",
|
||||
"84.17.38.234",
|
||||
"84.17.38.233",
|
||||
"169.150.247.179",
|
||||
"169.150.247.180",
|
||||
"169.150.247.181",
|
||||
"169.150.247.182",
|
||||
"169.150.247.183",
|
||||
"169.150.247.138",
|
||||
"169.150.247.184",
|
||||
"169.150.247.185",
|
||||
"156.146.58.83",
|
||||
"212.102.43.88",
|
||||
"89.187.169.26",
|
||||
"109.61.89.57",
|
||||
"109.61.89.58",
|
||||
"109.61.83.241",
|
||||
"84.17.38.243",
|
||||
"84.17.38.244",
|
||||
"84.17.38.246",
|
||||
"84.17.38.247",
|
||||
"84.17.38.245",
|
||||
"143.244.38.129",
|
||||
"84.17.38.248",
|
||||
"89.187.176.34",
|
||||
"185.152.64.23",
|
||||
"79.127.213.209",
|
||||
"79.127.213.210",
|
||||
"84.17.37.209",
|
||||
"156.146.43.68",
|
||||
"185.93.3.243",
|
||||
"79.127.219.198",
|
||||
"138.199.33.57",
|
||||
"79.127.242.89",
|
||||
"138.199.4.136",
|
||||
"169.150.220.235",
|
||||
"138.199.4.129",
|
||||
"138.199.4.177",
|
||||
"37.19.222.34",
|
||||
"46.151.193.85",
|
||||
"212.104.158.17",
|
||||
"212.104.158.18",
|
||||
"212.104.158.19",
|
||||
"212.104.158.20",
|
||||
"212.104.158.21",
|
||||
"212.104.158.22",
|
||||
"212.104.158.24",
|
||||
"212.104.158.26",
|
||||
"79.127.237.134",
|
||||
"89.187.184.177",
|
||||
"89.187.184.179",
|
||||
"89.187.184.173",
|
||||
"89.187.184.178",
|
||||
"89.187.184.176",
|
||||
]);
|
||||
|
||||
const CDN_PROVIDERS: CDNProvider[] = [
|
||||
{
|
||||
name: "cloudflare",
|
||||
displayName: "Cloudflare",
|
||||
checkIp: (ip: string) =>
|
||||
CLOUDFLARE_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
|
||||
},
|
||||
{
|
||||
name: "bunnycdn",
|
||||
displayName: "Bunny CDN",
|
||||
checkIp: (ip: string) => BUNNY_CDN_IPS.has(ip),
|
||||
warningMessage:
|
||||
"Domain is behind Bunny CDN - actual IP is masked by CDN proxy",
|
||||
},
|
||||
{
|
||||
name: "fastly",
|
||||
displayName: "Fastly",
|
||||
checkIp: (ip: string) =>
|
||||
FASTLY_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
|
||||
warningMessage:
|
||||
"Domain is behind Fastly - actual IP is masked by CDN proxy",
|
||||
},
|
||||
];
|
||||
|
||||
export const detectCDNProvider = (ip: string): CDNProvider | null => {
|
||||
return CDN_PROVIDERS.find((provider) => provider.checkIp(ip)) || null;
|
||||
};
|
||||
@@ -31,23 +31,38 @@ import {
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { findScheduleById } from "./schedule";
|
||||
import { removeRollbackById } from "./rollbacks";
|
||||
|
||||
export type Deployment = typeof deployments.$inferSelect;
|
||||
|
||||
export const findDeploymentById = async (applicationId: string) => {
|
||||
const application = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
export const findDeploymentById = async (deploymentId: string) => {
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.deploymentId, deploymentId),
|
||||
with: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
if (!deployment) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Deployment not found",
|
||||
});
|
||||
}
|
||||
return application;
|
||||
return deployment;
|
||||
};
|
||||
|
||||
export const findDeploymentByApplicationId = async (applicationId: string) => {
|
||||
const deployment = await db.query.deployments.findFirst({
|
||||
where: eq(deployments.applicationId, applicationId),
|
||||
});
|
||||
|
||||
if (!deployment) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Deployment not found",
|
||||
});
|
||||
}
|
||||
return deployment;
|
||||
};
|
||||
|
||||
export const createDeployment = async (
|
||||
@@ -481,6 +496,9 @@ const getDeploymentsByType = async (
|
||||
const deploymentList = await db.query.deployments.findMany({
|
||||
where: eq(deployments[`${type}Id`], id),
|
||||
orderBy: desc(deployments.createdAt),
|
||||
with: {
|
||||
rollback: true,
|
||||
},
|
||||
});
|
||||
return deploymentList;
|
||||
};
|
||||
@@ -515,6 +533,9 @@ const removeLastTenDeployments = async (
|
||||
let command = "";
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
|
||||
command += `
|
||||
rm -rf ${logPath};
|
||||
@@ -525,8 +546,11 @@ const removeLastTenDeployments = async (
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
for (const oldDeployment of deploymentsToDelete) {
|
||||
if (oldDeployment.rollbackId) {
|
||||
await removeRollbackById(oldDeployment.rollbackId);
|
||||
}
|
||||
const logPath = path.join(oldDeployment.logPath);
|
||||
if (existsSync(logPath)) {
|
||||
if (existsSync(logPath) && !oldDeployment.errorMessage) {
|
||||
await fsPromises.unlink(logPath);
|
||||
}
|
||||
await removeDeployment(oldDeployment.deploymentId);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { eq } from "drizzle-orm";
|
||||
import { type apiCreateDomain, domains } from "../db/schema";
|
||||
import { findUserById } from "./admin";
|
||||
import { findApplicationById } from "./application";
|
||||
import { detectCDNProvider } from "./cdn";
|
||||
import { findServerById } from "./server";
|
||||
|
||||
export type Domain = typeof domains.$inferSelect;
|
||||
@@ -142,28 +143,6 @@ export const getDomainHost = (domain: Domain) => {
|
||||
|
||||
const resolveDns = promisify(dns.resolve4);
|
||||
|
||||
// Cloudflare IP ranges (simplified - these are some common ones)
|
||||
const CLOUDFLARE_IPS = [
|
||||
"172.67.",
|
||||
"104.21.",
|
||||
"104.16.",
|
||||
"104.17.",
|
||||
"104.18.",
|
||||
"104.19.",
|
||||
"104.20.",
|
||||
"104.22.",
|
||||
"104.23.",
|
||||
"104.24.",
|
||||
"104.25.",
|
||||
"104.26.",
|
||||
"104.27.",
|
||||
"104.28.",
|
||||
];
|
||||
|
||||
const isCloudflareIp = (ip: string) => {
|
||||
return CLOUDFLARE_IPS.some((range) => ip.startsWith(range));
|
||||
};
|
||||
|
||||
export const validateDomain = async (
|
||||
domain: string,
|
||||
expectedIp?: string,
|
||||
@@ -172,6 +151,7 @@ export const validateDomain = async (
|
||||
resolvedIp?: string;
|
||||
error?: string;
|
||||
isCloudflare?: boolean;
|
||||
cdnProvider?: string;
|
||||
}> => {
|
||||
try {
|
||||
// Remove protocol and path if present
|
||||
@@ -182,17 +162,18 @@ export const validateDomain = async (
|
||||
|
||||
const resolvedIps = ips.map((ip) => ip.toString());
|
||||
|
||||
// Check if it's a Cloudflare IP
|
||||
const behindCloudflare = ips.some((ip) => isCloudflareIp(ip));
|
||||
// Check if any IP belongs to a CDN provider
|
||||
const cdnProvider = ips
|
||||
.map((ip) => detectCDNProvider(ip))
|
||||
.find((provider) => provider !== null);
|
||||
|
||||
// If behind Cloudflare, we consider it valid but inform the user
|
||||
if (behindCloudflare) {
|
||||
// If behind a CDN, we consider it valid but inform the user
|
||||
if (cdnProvider) {
|
||||
return {
|
||||
isValid: true,
|
||||
resolvedIp: resolvedIps.join(", "),
|
||||
isCloudflare: true,
|
||||
error:
|
||||
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
|
||||
cdnProvider: cdnProvider.displayName,
|
||||
error: cdnProvider.warningMessage,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export type Gitea = typeof gitea.$inferSelect;
|
||||
export const createGitea = async (
|
||||
input: typeof apiCreateGitea._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
@@ -20,6 +21,7 @@ export const createGitea = async (
|
||||
providerType: "gitea",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
@@ -13,6 +13,7 @@ export type Github = typeof github.$inferSelect;
|
||||
export const createGithub = async (
|
||||
input: typeof apiCreateGithub._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
@@ -21,6 +22,7 @@ export const createGithub = async (
|
||||
providerType: "github",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
@@ -12,6 +12,7 @@ export type Gitlab = typeof gitlab.$inferSelect;
|
||||
export const createGitlab = async (
|
||||
input: typeof apiCreateGitlab._type,
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const newGitProvider = await tx
|
||||
@@ -20,6 +21,7 @@ export const createGitlab = async (
|
||||
providerType: "gitlab",
|
||||
organizationId: organizationId,
|
||||
name: input.name,
|
||||
userId: userId,
|
||||
})
|
||||
.returning()
|
||||
.then((response) => response[0]);
|
||||
|
||||
191
packages/server/src/services/rollbacks.ts
Normal file
191
packages/server/src/services/rollbacks.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../db";
|
||||
import {
|
||||
type createRollbackSchema,
|
||||
rollbacks,
|
||||
deployments as deploymentsSchema,
|
||||
} from "../db/schema";
|
||||
import type { z } from "zod";
|
||||
import { findApplicationById } from "./application";
|
||||
import { getRemoteDocker } from "../utils/servers/remote-docker";
|
||||
import type { ApplicationNested } from "../utils/builders";
|
||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { findDeploymentById } from "./deployment";
|
||||
|
||||
export const createRollback = async (
|
||||
input: z.infer<typeof createRollbackSchema>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
const rollback = await tx
|
||||
.insert(rollbacks)
|
||||
.values(input)
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
|
||||
if (!rollback) {
|
||||
throw new Error("Failed to create rollback");
|
||||
}
|
||||
|
||||
const tagImage = `${input.appName}:v${rollback.version}`;
|
||||
const deployment = await findDeploymentById(rollback.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const {
|
||||
deployments: _,
|
||||
bitbucket,
|
||||
github,
|
||||
gitlab,
|
||||
gitea,
|
||||
...rest
|
||||
} = await findApplicationById(deployment.applicationId);
|
||||
|
||||
await tx
|
||||
.update(rollbacks)
|
||||
.set({
|
||||
image: tagImage,
|
||||
fullContext: JSON.stringify(rest),
|
||||
})
|
||||
.where(eq(rollbacks.rollbackId, rollback.rollbackId));
|
||||
|
||||
// Update the deployment to reference this rollback
|
||||
await tx
|
||||
.update(deploymentsSchema)
|
||||
.set({
|
||||
rollbackId: rollback.rollbackId,
|
||||
})
|
||||
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
|
||||
|
||||
await createRollbackImage(rest, tagImage);
|
||||
|
||||
return rollback;
|
||||
});
|
||||
};
|
||||
|
||||
const findRollbackById = async (rollbackId: string) => {
|
||||
const result = await db.query.rollbacks.findFirst({
|
||||
where: eq(rollbacks.rollbackId, rollbackId),
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw new Error("Rollback not found");
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const createRollbackImage = async (
|
||||
application: ApplicationNested,
|
||||
tagImage: string,
|
||||
) => {
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
const appTagName =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: `${application.appName}:latest`;
|
||||
|
||||
const result = docker.getImage(appTagName || "");
|
||||
|
||||
const [repo, version] = tagImage.split(":");
|
||||
|
||||
await result.tag({
|
||||
repo,
|
||||
tag: version,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
|
||||
const command = `docker image rm ${image} --force`;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(command, serverId);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeRollbackById = async (rollbackId: string) => {
|
||||
const rollback = await findRollbackById(rollbackId);
|
||||
|
||||
if (!rollback) {
|
||||
throw new Error("Rollback not found");
|
||||
}
|
||||
|
||||
if (rollback?.image) {
|
||||
try {
|
||||
const deployment = await findDeploymentById(rollback.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
await deleteRollbackImage(rollback.image, application.serverId);
|
||||
|
||||
await db
|
||||
.delete(rollbacks)
|
||||
.where(eq(rollbacks.rollbackId, rollbackId))
|
||||
.returning()
|
||||
.then((res) => res[0]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return rollback;
|
||||
};
|
||||
|
||||
export const rollback = async (rollbackId: string) => {
|
||||
const result = await findRollbackById(rollbackId);
|
||||
|
||||
const deployment = await findDeploymentById(result.deploymentId);
|
||||
|
||||
if (!deployment?.applicationId) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
|
||||
const application = await findApplicationById(deployment.applicationId);
|
||||
|
||||
await rollbackApplication(
|
||||
application.appName,
|
||||
result.image || "",
|
||||
application.serverId,
|
||||
);
|
||||
};
|
||||
|
||||
const rollbackApplication = async (
|
||||
appName: string,
|
||||
image: string,
|
||||
serverId?: string | null,
|
||||
) => {
|
||||
const docker = await getRemoteDocker(serverId);
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: image,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const service = docker.getService(appName);
|
||||
const inspect = await service.inspect();
|
||||
|
||||
await service.update({
|
||||
version: Number.parseInt(inspect.Version.Index),
|
||||
...settings,
|
||||
TaskTemplate: {
|
||||
...settings.TaskTemplate,
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch (_error: unknown) {
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
@@ -116,7 +116,7 @@ if [ "$OS_TYPE" = 'amzn' ]; then
|
||||
fi
|
||||
|
||||
case "$OS_TYPE" in
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | opencloudos | amzn | alpine) ;;
|
||||
*)
|
||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||
exit
|
||||
@@ -367,7 +367,7 @@ const installUtilities = () => `
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
|
||||
;;
|
||||
centos | fedora | rhel | ol | rocky | almalinux | amzn)
|
||||
centos | fedora | rhel | ol | rocky | almalinux | opencloudos | amzn)
|
||||
if [ "$OS_TYPE" = "amzn" ]; then
|
||||
dnf install -y wget git git-lfs jq openssl >/dev/null
|
||||
else
|
||||
@@ -418,6 +418,16 @@ if ! [ -x "$(command -v docker)" ]; then
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"opencloudos")
|
||||
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
|
||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
|
||||
if ! [ -x "$(command -v docker)" ]; then
|
||||
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
|
||||
exit 1
|
||||
fi
|
||||
systemctl start docker >/dev/null 2>&1
|
||||
systemctl enable docker >/dev/null 2>&1
|
||||
;;
|
||||
"alpine")
|
||||
apk add docker docker-cli-compose >/dev/null 2>&1
|
||||
rc-update add docker default >/dev/null 2>&1
|
||||
|
||||
@@ -139,6 +139,8 @@ export const initializeTraefik = async ({
|
||||
const newContainer = docker.getContainer(containerName);
|
||||
await newContainer.start();
|
||||
console.log("Traefik container started successfully after retry");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -37,7 +37,8 @@ export const startLogCleanup = async (
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
} catch (error) {
|
||||
console.error("Error starting log cleanup:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,6 +88,7 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
|
||||
if (admin?.user.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
||||
await startLogCleanup(admin.user.logCleanupCron);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,10 @@ import { createHash } from "node:crypto";
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import {
|
||||
parseEnvironmentKeyValuePair,
|
||||
prepareEnvironmentVariables,
|
||||
} from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
@@ -81,10 +84,10 @@ export const buildRailpack = async (
|
||||
|
||||
// Add secrets properly formatted
|
||||
const env: { [key: string]: string } = {};
|
||||
for (const envVar of envVariables) {
|
||||
const [key, value] = envVar.split("=");
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -161,11 +164,11 @@ export const getRailpackCommand = (
|
||||
|
||||
// Add secrets properly formatted
|
||||
const exportEnvs = [];
|
||||
for (const envVar of envVariables) {
|
||||
const [key, value] = envVar.split("=");
|
||||
for (const pair of envVariables) {
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||
exportEnvs.push(`export ${key}=${value}`);
|
||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
||||
exportEnvs.push(`export ${key}='${value}'`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ export const cleanUpDockerBuilder = async (serverId?: string) => {
|
||||
};
|
||||
|
||||
export const cleanUpSystemPrune = async (serverId?: string) => {
|
||||
const command = "docker system prune --all --force --volumes";
|
||||
const command = "docker system prune --force --volumes";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
@@ -279,6 +279,17 @@ export const prepareEnvironmentVariables = (
|
||||
return resolvedVars;
|
||||
};
|
||||
|
||||
export const parseEnvironmentKeyValuePair = (
|
||||
pair: string,
|
||||
): [string, string] => {
|
||||
const [key, ...valueParts] = pair.split("=");
|
||||
if (!key || !valueParts.length) {
|
||||
throw new Error(`Invalid environment variable pair: ${pair}`);
|
||||
}
|
||||
|
||||
return [key, valueParts.join("")];
|
||||
};
|
||||
|
||||
export const getEnviromentVariablesObject = (
|
||||
input: string | null,
|
||||
projectEnv?: string | null,
|
||||
@@ -288,7 +299,7 @@ export const getEnviromentVariablesObject = (
|
||||
const jsonObject: Record<string, string> = {};
|
||||
|
||||
for (const pair of envs) {
|
||||
const [key, value] = pair.split("=");
|
||||
const [key, value] = parseEnvironmentKeyValuePair(pair);
|
||||
if (key && value) {
|
||||
jsonObject[key] = value;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export const cloneBitbucketRepository = async (
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -155,7 +155,7 @@ export const getGiteaCloneCommand = async (
|
||||
const cloneCommand = `
|
||||
rm -rf ${outputPath};
|
||||
mkdir -p ${outputPath};
|
||||
|
||||
|
||||
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
|
||||
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
|
||||
exit 1;
|
||||
@@ -232,7 +232,7 @@ export const cloneGiteaRepository = async (
|
||||
);
|
||||
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -149,7 +149,7 @@ export const cloneGithubRepository = async ({
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
@@ -132,7 +132,7 @@ export const cloneGitlabRepository = async (
|
||||
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
|
||||
|
||||
try {
|
||||
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
|
||||
const cloneArgs = [
|
||||
"clone",
|
||||
"--branch",
|
||||
@@ -152,7 +152,7 @@ export const cloneGitlabRepository = async (
|
||||
});
|
||||
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
|
||||
} catch (error) {
|
||||
writeStream.write(`ERROR Clonning: ${error}: ❌`);
|
||||
writeStream.write(`ERROR Cloning: ${error}: ❌`);
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
||||
Reference in New Issue
Block a user