Compare commits

..

1 Commits

Author SHA1 Message Date
autofix-ci[bot]
04a10146ac [autofix.ci] apply automated fixes 2025-05-17 05:16:30 +00:00
161 changed files with 4134 additions and 40400 deletions

View File

@@ -12,7 +12,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.16.0 node-version: 20.9.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm run server:build
@@ -26,7 +26,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.16.0 node-version: 20.9.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm run server:build
@@ -39,7 +39,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 20.16.0 node-version: 20.9.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm run server:build

2
.nvmrc
View File

@@ -1 +1 @@
20.16.0 20.9.0

View File

@@ -52,7 +52,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory. We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
```bash ```bash
git clone https://github.com/dokploy/dokploy.git git clone https://github.com/dokploy/dokploy.git

View File

@@ -1,4 +1,3 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
@@ -30,7 +29,7 @@ WORKDIR /app
# Set production # Set production
ENV NODE_ENV=production ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files # Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next COPY --from=build /prod/dokploy/.next ./.next
@@ -50,7 +49,7 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx # Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash # | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.39.0 ARG NIXPACKS_VERSION=1.35.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \ && chmod +x install.sh \
&& ./install.sh \ && ./install.sh \
@@ -64,4 +63,4 @@ RUN curl -sSL https://railpack.com/install.sh | bash
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000 EXPOSE 3000
CMD [ "pnpm", "start" ] CMD [ "pnpm", "start" ]

View File

@@ -1,4 +1,3 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"

View File

@@ -1,4 +1,3 @@
# syntax=docker/dockerfile:1
# Build stage # Build stage
FROM golang:1.21-alpine3.19 AS builder FROM golang:1.21-alpine3.19 AS builder

View File

@@ -1,4 +1,3 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"

View File

@@ -1,4 +1,3 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"

View File

@@ -148,6 +148,19 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/> <img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
</a> </a>
<!-- ## Supported OS
- Ubuntu 24.04 LTS
- Ubuntu 23.10
- Ubuntu 22.04 LTS
- Ubuntu 20.04 LTS
- Ubuntu 18.04 LTS
- Debian 12
- Debian 11
- Fedora 40
- Centos 9
- Centos 8 -->
## Contributing ## Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information. Check out the [Contributing Guide](CONTRIBUTING.md) for more information.

View File

@@ -1,28 +0,0 @@
# 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.

View File

@@ -9,25 +9,25 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.12.1",
"@hono/zod-validator": "0.3.0",
"@nerimity/mimiqueue": "1.2.3",
"dotenv": "^16.3.1",
"hono": "^4.5.8",
"pino": "9.4.0", "pino": "9.4.0",
"pino-pretty": "11.2.2", "pino-pretty": "11.2.2",
"@hono/zod-validator": "0.3.0",
"zod": "^3.23.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.12.1",
"hono": "^4.5.8",
"dotenv": "^16.3.1",
"redis": "4.7.0", "redis": "4.7.0",
"zod": "^3.23.4" "@nerimity/mimiqueue": "1.2.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.17", "typescript": "^5.4.2",
"@types/react": "^18.2.37", "@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15", "@types/react-dom": "^18.2.15",
"tsx": "^4.7.1", "@types/node": "^20.11.17",
"typescript": "^5.4.2" "tsx": "^4.7.1"
}, },
"packageManager": "pnpm@9.5.0" "packageManager": "pnpm@9.5.0"
} }

View File

@@ -1 +1 @@
20.16.0 20.9.0

26
apps/dokploy/Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# Build only the dokploy app
RUN pnpm run dokploy:build
# Deploy only the dokploy app
RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
FROM base AS dokploy
COPY --from=build /prod/dokploy /prod/dokploy
WORKDIR /prod/dokploy
EXPOSE 3000
CMD [ "pnpm", "start" ]

View File

@@ -105,7 +105,6 @@ const baseApp: ApplicationNested = {
ports: [], ports: [],
projectId: "", projectId: "",
publishDirectory: null, publishDirectory: null,
isStaticSpa: null,
redirects: [], redirects: [],
refreshToken: "", refreshToken: "",
registry: null, registry: null,
@@ -121,7 +120,6 @@ const baseApp: ApplicationNested = {
updateConfigSwarm: null, updateConfigSwarm: null,
username: null, username: null,
dockerContextPath: null, dockerContextPath: null,
rollbackActive: false,
}; };
describe("unzipDrop using real zip files", () => { describe("unzipDrop using real zip files", () => {
@@ -151,68 +149,67 @@ describe("unzipDrop using real zip files", () => {
} finally { } finally {
} }
}); });
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
baseApp.appName = "folderwithfile";
// const appName = "folderwithfile";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
});
it("should correctly extract a zip with multiple root folders", async () => {
baseApp.appName = "two-folders";
// const appName = "two-folders";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "folder2")).toBe(true);
});
it("should correctly extract a zip with a single root with a file", async () => {
baseApp.appName = "nested";
// const appName = "nested";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "folder2")).toBe(true);
expect(files.some((f) => f.name === "folder3")).toBe(true);
});
it("should correctly extract a zip with a single root with a folder", async () => {
baseApp.appName = "folder-with-sibling-file";
// const appName = "folder-with-sibling-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "test.txt")).toBe(true);
});
}); });
// it("should correctly extract a zip with a single root folder and a subfolder", async () => {
// baseApp.appName = "folderwithfile";
// // const appName = "folderwithfile";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
// });
// it("should correctly extract a zip with multiple root folders", async () => {
// baseApp.appName = "two-folders";
// // const appName = "two-folders";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "folder2")).toBe(true);
// });
// it("should correctly extract a zip with a single root with a file", async () => {
// baseApp.appName = "nested";
// // const appName = "nested";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/nested.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "folder2")).toBe(true);
// expect(files.some((f) => f.name === "folder3")).toBe(true);
// });
// it("should correctly extract a zip with a single root with a folder", async () => {
// baseApp.appName = "folder-with-sibling-file";
// // const appName = "folder-with-sibling-file";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "test.txt")).toBe(true);
// });
// });

View File

@@ -5,7 +5,6 @@ import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
rollbackActive: false,
applicationId: "", applicationId: "",
herokuVersion: "", herokuVersion: "",
giteaRepository: "", giteaRepository: "",
@@ -86,7 +85,6 @@ const baseApp: ApplicationNested = {
ports: [], ports: [],
projectId: "", projectId: "",
publishDirectory: null, publishDirectory: null,
isStaticSpa: null,
redirects: [], redirects: [],
refreshToken: "", refreshToken: "",
registry: null, registry: null,

View File

@@ -1,5 +1,5 @@
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
import { describe, expect, test } from "vitest"; import { describe, expect, test } from "vitest";
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
describe("normalizeS3Path", () => { describe("normalizeS3Path", () => {
test("should handle empty and whitespace-only prefix", () => { test("should handle empty and whitespace-only prefix", () => {

View File

@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
{templateInfo.template.envs.map((env, index) => ( {templateInfo.template.envs.map((env, index) => (
<div <div
key={index} key={index}
className="rounded-lg truncate border bg-card p-2 font-mono text-sm" className="rounded-lg border bg-card p-2 font-mono text-sm"
> >
{env} {env}
</div> </div>
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
<DialogDescription>Mount File Content</DialogDescription> <DialogDescription>Mount File Content</DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="h-[45vh] pr-4"> <ScrollArea className="h-[25vh] pr-4">
<CodeEditor <CodeEditor
language="yaml" language="yaml"
value={selectedMount?.content || ""} value={selectedMount?.content || ""}

View File

@@ -247,7 +247,7 @@ export const UpdateVolume = ({
control={form.control} control={form.control}
name="content" name="content"
render={({ field }) => ( render={({ field }) => (
<FormItem className="max-w-full max-w-[45rem]"> <FormItem>
<FormLabel>Content</FormLabel> <FormLabel>Content</FormLabel>
<FormControl> <FormControl>
<FormControl> <FormControl>
@@ -256,7 +256,7 @@ export const UpdateVolume = ({
placeholder={`NODE_ENV=production placeholder={`NODE_ENV=production
PORT=3000 PORT=3000
`} `}
className="h-96 font-mono w-full" className="h-96 font-mono"
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -2,7 +2,6 @@ import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { import {
Form, Form,
FormControl, FormControl,
@@ -64,11 +63,10 @@ const mySchema = z.discriminatedUnion("buildType", [
publishDirectory: z.string().optional(), publishDirectory: z.string().optional(),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.railpack), buildType: z.literal(BuildType.static),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.static), buildType: z.literal(BuildType.railpack),
isStaticSpa: z.boolean().default(false),
}), }),
]); ]);
@@ -85,7 +83,6 @@ interface ApplicationData {
dockerBuildStage?: string | null; dockerBuildStage?: string | null;
herokuVersion?: string | null; herokuVersion?: string | null;
publishDirectory?: string | null; publishDirectory?: string | null;
isStaticSpa?: boolean | null;
} }
function isValidBuildType(value: string): value is BuildType { function isValidBuildType(value: string): value is BuildType {
@@ -118,18 +115,16 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.static: case BuildType.static:
return { return {
buildType: BuildType.static, buildType: BuildType.static,
isStaticSpa: data.isStaticSpa ?? false,
}; };
case BuildType.railpack: case BuildType.railpack:
return { return {
buildType: BuildType.railpack, buildType: BuildType.railpack,
}; };
default: { default:
const buildType = data.buildType as BuildType; const buildType = data.buildType as BuildType;
return { return {
buildType, buildType,
} as AddTemplate; } as AddTemplate;
}
} }
}; };
@@ -179,8 +174,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === BuildType.heroku_buildpacks data.buildType === BuildType.heroku_buildpacks
? data.herokuVersion ? data.herokuVersion
: null, : null,
isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null,
}) })
.then(async () => { .then(async () => {
toast.success("Build type saved"); toast.success("Build type saved");
@@ -371,30 +364,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)} )}
/> />
)} )}
{buildType === BuildType.static && (
<FormField
control={form.control}
name="isStaticSpa"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-x-2 p-2">
<Checkbox
id="checkboxIsStaticSpa"
value={String(field.value)}
checked={field.value}
onCheckedChange={field.onChange}
/>
<FormLabel htmlFor="checkboxIsStaticSpa">
Single Page Application (SPA)
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">
Save Save

View File

@@ -1,6 +1,5 @@
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -10,14 +9,12 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api"; import { type RouterOutputs, api } from "@/utils/api";
import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } from "lucide-react"; import { RocketIcon, Clock, Loader2 } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { CancelQueues } from "./cancel-queues"; import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token"; import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment"; import { ShowDeployment } from "./show-deployment";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; import { Badge } from "@/components/ui/badge";
import { DialogAction } from "@/components/shared/dialog-action";
import { toast } from "sonner";
interface Props { interface Props {
id: string; id: string;
@@ -60,9 +57,6 @@ export const ShowDeployments = ({
}, },
); );
const { mutateAsync: rollback, isLoading: isRollingBack } =
api.rollback.rollback.useMutation();
const [url, setUrl] = React.useState(""); const [url, setUrl] = React.useState("");
useEffect(() => { useEffect(() => {
setUrl(document.location.origin); setUrl(document.location.origin);
@@ -77,18 +71,9 @@ export const ShowDeployments = ({
See all the 10 last deployments for this {type} See all the 10 last deployments for this {type}
</CardDescription> </CardDescription>
</div> </div>
<div className="flex flex-row items-center gap-2"> {(type === "application" || type === "compose") && (
{(type === "application" || type === "compose") && ( <CancelQueues id={id} type={type} />
<CancelQueues id={id} type={type} /> )}
)}
{type === "application" && (
<ShowRollbackSettings applicationId={id}>
<Button variant="outline">
Configure Rollbacks <Settings className="size-4" />
</Button>
</ShowRollbackSettings>
)}
</div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
{refreshToken && ( {refreshToken && (
@@ -169,47 +154,13 @@ export const ShowDeployments = ({
)} )}
</div> </div>
<div className="flex flex-row items-center gap-2"> <Button
<Button onClick={() => {
onClick={() => { setActiveLog(deployment);
setActiveLog(deployment); }}
}} >
> View
View </Button>
</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>
</div> </div>
))} ))}

View File

@@ -1,5 +1,3 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -8,6 +6,8 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { Copy, HelpCircle, Server } from "lucide-react"; import { Copy, HelpCircle, Server } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";

View File

@@ -1,5 +1,4 @@
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -8,12 +7,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
CheckCircle2, CheckCircle2,
@@ -28,10 +21,17 @@ import {
XCircle, XCircle,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { DnsHelperModal } from "./dns-helper-modal";
import { AddDomain } from "./handle-domain"; import { AddDomain } from "./handle-domain";
import { useState } from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { DnsHelperModal } from "./dns-helper-modal";
import { Badge } from "@/components/ui/badge";
export type ValidationState = { export type ValidationState = {
isLoading: boolean; isLoading: boolean;
@@ -39,7 +39,6 @@ export type ValidationState = {
error?: string; error?: string;
resolvedIp?: string; resolvedIp?: string;
message?: string; message?: string;
cdnProvider?: string;
}; };
export type ValidationStates = Record<string, ValidationState>; export type ValidationStates = Record<string, ValidationState>;
@@ -120,7 +119,6 @@ export const ShowDomains = ({ id, type }: Props) => {
isValid: result.isValid, isValid: result.isValid,
error: result.error, error: result.error,
resolvedIp: result.resolvedIp, resolvedIp: result.resolvedIp,
cdnProvider: result.cdnProvider,
message: result.error && result.isValid ? result.error : undefined, message: result.error && result.isValid ? result.error : undefined,
}, },
})); }));
@@ -356,9 +354,8 @@ export const ShowDomains = ({ id, type }: Props) => {
) : validationState?.isValid ? ( ) : validationState?.isValid ? (
<> <>
<CheckCircle2 className="size-3 mr-1" /> <CheckCircle2 className="size-3 mr-1" />
{validationState.message && {validationState.message
validationState.cdnProvider ? "Behind Cloudflare"
? `Behind ${validationState.cdnProvider}`
: "DNS Valid"} : "DNS Valid"}
</> </>
) : validationState?.error ? ( ) : validationState?.error ? (

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false, enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data?.applicationId, form]); }, [form.reset, data, form]);
const onSubmit = async (data: BitbucketProvider) => { const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
registryURL: data.registryUrl || "", registryURL: data.registryUrl || "",
}); });
} }
}, [form.reset, data?.applicationId, form]); }, [form.reset, data, form]);
const onSubmit = async (values: DockerProvider) => { const onSubmit = async (values: DockerProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -17,13 +17,13 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react"; import { KeyRoundIcon, LockIcon, X } from "lucide-react";

View File

@@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false, enableSubmodules: data.enableSubmodules || false,
}); });
} }
}, [form.reset, data?.applicationId, form]); }, [form.reset, data, form]);
const onSubmit = async (data: GiteaProvider) => { const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data?.applicationId, form]); }, [form.reset, data, form]);
const onSubmit = async (data: GithubProvider) => { const onSubmit = async (data: GithubProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data?.applicationId, form]); }, [form.reset, data, form]);
const onSubmit = async (data: GitlabProvider) => { const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -16,11 +16,9 @@ import { api } from "@/utils/api";
import { GitBranch, Loader2, UploadCloud } from "lucide-react"; import { GitBranch, Loader2, UploadCloud } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
import { SaveBitbucketProvider } from "./save-bitbucket-provider"; import { SaveBitbucketProvider } from "./save-bitbucket-provider";
import { SaveDragNDrop } from "./save-drag-n-drop"; import { SaveDragNDrop } from "./save-drag-n-drop";
import { SaveGitlabProvider } from "./save-gitlab-provider"; import { SaveGitlabProvider } from "./save-gitlab-provider";
import { UnauthorizedGitProvider } from "./unauthorized-git-provider";
type TabState = type TabState =
| "github" | "github"
@@ -45,31 +43,12 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
const { data: giteaProviders, isLoading: isLoadingGitea } = const { data: giteaProviders, isLoading: isLoadingGitea } =
api.gitea.giteaProviders.useQuery(); api.gitea.giteaProviders.useQuery();
const { data: application, refetch } = api.application.one.useQuery({ const { data: application } = api.application.one.useQuery({ applicationId });
applicationId,
});
const { mutateAsync: disconnectGitProvider } =
api.application.disconnectGitProvider.useMutation();
const [tab, setSab] = useState<TabState>(application?.sourceType || "github"); const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
const isLoading = const isLoading =
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea; 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) { if (isLoading) {
return ( return (
<Card className="group relative w-full bg-transparent"> <Card className="group relative w-full bg-transparent">
@@ -98,38 +77,6 @@ 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 ( return (
<Card className="group relative w-full bg-transparent"> <Card className="group relative w-full bg-transparent">
<CardHeader> <CardHeader>

View File

@@ -1,149 +0,0 @@
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>
);
};

View File

@@ -24,9 +24,9 @@ import {
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { AddPreviewDomain } from "./add-preview-domain"; import { AddPreviewDomain } from "./add-preview-domain";
import { ShowPreviewSettings } from "./show-preview-settings"; import { ShowPreviewSettings } from "./show-preview-settings";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
interface Props { interface Props {
applicationId: string; applicationId: string;

View File

@@ -1,123 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
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>
<AlertBlock>
Having rollbacks enabled increases storage usage. Be careful with
this option. Note that manually cleaning the cache may delete
rollback images, making them unavailable for future rollbacks.
</AlertBlock>
</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>
);
};

View File

@@ -1,23 +1,25 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
FormDescription,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Info,
PlusCircle,
PenBoxIcon,
RefreshCw,
DatabaseZap,
} from "lucide-react";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -25,27 +27,25 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import {
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { cn } from "@/lib/utils";
import type { CacheType } from "../domains/handle-domain"; import type { CacheType } from "../domains/handle-domain";
export const commonCronExpressions = [ export const commonCronExpressions = [

View File

@@ -1,6 +1,14 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { HandleSchedules } from "./handle-schedules";
import {
Clock,
Play,
Terminal,
Trash2,
ClipboardList,
Loader2,
} from "lucide-react";
import { import {
Card, Card,
CardContent, CardContent,
@@ -8,24 +16,16 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { DialogAction } from "@/components/shared/dialog-action";
import {
ClipboardList,
Clock,
Loader2,
Play,
Terminal,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleSchedules } from "./handle-schedules";
interface Props { interface Props {
id: string; id: string;

View File

@@ -44,10 +44,8 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
resolver: zodResolver(AddComposeFile), resolver: zodResolver(AddComposeFile),
}); });
const composeFile = form.watch("composeFile");
useEffect(() => { useEffect(() => {
if (data && !composeFile) { if (data) {
form.reset({ form.reset({
composeFile: data.composeFile || "", composeFile: data.composeFile || "",
}); });

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data?.composeId, form]); }, [form.reset, data, form]);
const onSubmit = async (data: BitbucketProvider) => { const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data?.composeId, form]); }, [form.reset, data, form]);
const onSubmit = async (data: GiteaProvider) => { const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data?.composeId, form]); }, [form.reset, data, form]);
const onSubmit = async (data: GithubProvider) => { const onSubmit = async (data: GithubProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false, enableSubmodules: data.enableSubmodules ?? false,
}); });
} }
}, [form.reset, data?.composeId, form]); }, [form.reset, data, form]);
const onSubmit = async (data: GitlabProvider) => { const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({ await mutateAsync({

View File

@@ -18,8 +18,6 @@ import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose"; import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose"; import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-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"; type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
interface Props { interface Props {
@@ -36,29 +34,12 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
const { data: giteaProviders, isLoading: isLoadingGitea } = const { data: giteaProviders, isLoading: isLoadingGitea } =
api.gitea.giteaProviders.useQuery(); api.gitea.giteaProviders.useQuery();
const { mutateAsync: disconnectGitProvider } = const { data: compose } = api.compose.one.useQuery({ composeId });
api.compose.disconnectGitProvider.useMutation();
const { data: compose, refetch } = api.compose.one.useQuery({ composeId });
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github"); const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
const isLoading = const isLoading =
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea; 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) { if (isLoading) {
return ( return (
<Card className="group relative w-full bg-transparent"> <Card className="group relative w-full bg-transparent">
@@ -87,37 +68,6 @@ 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 ( return (
<Card className="group relative w-full bg-transparent"> <Card className="group relative w-full bg-transparent">
<CardHeader> <CardHeader>

View File

@@ -71,8 +71,8 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
isolatedDeployment: formData?.isolatedDeployment || false, isolatedDeployment: formData?.isolatedDeployment || false,
}) })
.then(async (_data) => { .then(async (_data) => {
await randomizeCompose(); randomizeCompose();
await refetch(); refetch();
toast.success("Compose updated"); toast.success("Compose updated");
}) })
.catch(() => { .catch(() => {
@@ -84,10 +84,15 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
await mutateAsync({ await mutateAsync({
composeId, composeId,
suffix: data?.appName || "", suffix: data?.appName || "",
}).then(async (data) => { })
await utils.project.all.invalidate(); .then(async (data) => {
setCompose(data); await utils.project.all.invalidate();
}); setCompose(data);
toast.success("Compose Isolated");
})
.catch(() => {
toast.error("Error isolating the compose");
});
}; };
return ( return (

View File

@@ -77,8 +77,8 @@ export const RandomizeCompose = ({ composeId }: Props) => {
randomize: formData?.randomize || false, randomize: formData?.randomize || false,
}) })
.then(async (_data) => { .then(async (_data) => {
await randomizeCompose(); randomizeCompose();
await refetch(); refetch();
toast.success("Compose updated"); toast.success("Compose updated");
}) })
.catch(() => { .catch(() => {
@@ -90,10 +90,15 @@ export const RandomizeCompose = ({ composeId }: Props) => {
await mutateAsync({ await mutateAsync({
composeId, composeId,
suffix, suffix,
}).then(async (data) => { })
await utils.project.all.invalidate(); .then(async (data) => {
setCompose(data); await utils.project.all.invalidate();
}); setCompose(data);
toast.success("Compose randomized");
})
.catch(() => {
toast.error("Error randomizing the compose");
});
}; };
return ( return (

View File

@@ -39,12 +39,6 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -54,9 +48,9 @@ import {
CheckIcon, CheckIcon,
ChevronsUpDown, ChevronsUpDown,
Copy, Copy,
DatabaseZap,
RefreshCw,
RotateCcw, RotateCcw,
RefreshCw,
DatabaseZap,
} from "lucide-react"; } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -64,6 +58,12 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import type { ServiceType } from "../../application/advanced/show-resources"; import type { ServiceType } from "../../application/advanced/show-resources";
import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type DatabaseType = type DatabaseType =
| Exclude<ServiceType, "application" | "redis"> | Exclude<ServiceType, "application" | "redis">

View File

@@ -1,10 +1,3 @@
import {
MariadbIcon,
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -20,7 +13,6 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
ClipboardList, ClipboardList,
@@ -33,9 +25,17 @@ import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources"; import type { ServiceType } from "../../application/advanced/show-resources";
import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
import { HandleBackup } from "./handle-backup";
import { RestoreBackup } from "./restore-backup"; import { RestoreBackup } from "./restore-backup";
import { HandleBackup } from "./handle-backup";
import { cn } from "@/lib/utils";
import {
MariadbIcon,
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
interface Props { interface Props {
id: string; id: string;

View File

@@ -1,9 +1,24 @@
"use client"; "use client";
import { Logo } from "@/components/shared/logo"; import { authClient } from "@/lib/auth-client";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
CheckIcon,
ChevronsUpDown,
Settings2,
UserIcon,
XIcon,
Shield,
Calendar,
Key,
Copy,
Fingerprint,
Building2,
CreditCard,
Server,
} from "lucide-react";
import { toast } from "sonner";
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
@@ -17,34 +32,19 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover"; } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Logo } from "@/components/shared/logo";
import { Badge } from "@/components/ui/badge";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider,
TooltipTrigger, TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { authClient } from "@/lib/auth-client"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { format } from "date-fns"; import { format } from "date-fns";
import { import copy from "copy-to-clipboard";
Building2, import { api } from "@/utils/api";
Calendar,
CheckIcon,
ChevronsUpDown,
Copy,
CreditCard,
Fingerprint,
Key,
Server,
Settings2,
Shield,
UserIcon,
XIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
type User = typeof authClient.$Infer.Session.user; type User = typeof authClient.$Infer.Session.user;

View File

@@ -10,7 +10,6 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Copy, Loader2 } from "lucide-react"; import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -49,7 +48,6 @@ export const DuplicateProject = ({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
const utils = api.useUtils(); const utils = api.useUtils();
const router = useRouter(); const router = useRouter();
@@ -61,15 +59,9 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({ api.project.duplicate.useMutation({
onSuccess: async (newProject) => { onSuccess: async (newProject) => {
await utils.project.all.invalidate(); await utils.project.all.invalidate();
toast.success( toast.success("Project duplicated successfully");
duplicateType === "new-project"
? "Project duplicated successfully"
: "Services duplicated successfully",
);
setOpen(false); setOpen(false);
if (duplicateType === "new-project") { router.push(`/dashboard/project/${newProject.projectId}`);
router.push(`/dashboard/project/${newProject.projectId}`);
}
}, },
onError: (error) => { onError: (error) => {
toast.error(error.message); toast.error(error.message);
@@ -77,7 +69,7 @@ export const DuplicateProject = ({
}); });
const handleDuplicate = async () => { const handleDuplicate = async () => {
if (duplicateType === "new-project" && !name) { if (!name) {
toast.error("Project name is required"); toast.error("Project name is required");
return; return;
} }
@@ -91,7 +83,6 @@ export const DuplicateProject = ({
id: service.id, id: service.id,
type: service.type, type: service.type,
})), })),
duplicateInSameProject: duplicateType === "same-project",
}); });
}; };
@@ -104,7 +95,6 @@ export const DuplicateProject = ({
// Reset form when closing // Reset form when closing
setName(""); setName("");
setDescription(""); setDescription("");
setDuplicateType("new-project");
} }
}} }}
> >
@@ -116,54 +106,32 @@ export const DuplicateProject = ({
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Duplicate Services</DialogTitle> <DialogTitle>Duplicate Project</DialogTitle>
<DialogDescription> <DialogDescription>
Choose where to duplicate the selected services Create a new project with the selected services
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Duplicate to</Label> <Label htmlFor="name">Name</Label>
<RadioGroup <Input
value={duplicateType} id="name"
onValueChange={setDuplicateType} value={name}
className="grid gap-2" onChange={(e) => setName(e.target.value)}
> placeholder="New project name"
<div className="flex items-center space-x-2"> />
<RadioGroupItem value="new-project" id="new-project" />
<Label htmlFor="new-project">New project</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="same-project" id="same-project" />
<Label htmlFor="same-project">Same project</Label>
</div>
</RadioGroup>
</div> </div>
{duplicateType === "new-project" && ( <div className="grid gap-2">
<> <Label htmlFor="description">Description</Label>
<div className="grid gap-2"> <Input
<Label htmlFor="name">Name</Label> id="description"
<Input value={description}
id="name" onChange={(e) => setDescription(e.target.value)}
value={name} placeholder="Project description (optional)"
onChange={(e) => setName(e.target.value)} />
placeholder="New project name" </div>
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
</>
)}
<div className="grid gap-2"> <div className="grid gap-2">
<Label>Selected services to duplicate</Label> <Label>Selected services to duplicate</Label>
@@ -191,14 +159,10 @@ export const DuplicateProject = ({
{isLoading ? ( {isLoading ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
{duplicateType === "new-project" Duplicating...
? "Duplicating project..."
: "Duplicating services..."}
</> </>
) : duplicateType === "new-project" ? (
"Duplicate project"
) : ( ) : (
"Duplicate services" "Duplicate"
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@@ -38,7 +38,7 @@ const AddProjectSchema = z.object({
(name) => { (name) => {
const trimmedName = name.trim(); const trimmedName = name.trim();
const validNameRegex = 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); return validNameRegex.test(trimmedName);
}, },
{ {

View File

@@ -1,93 +1,80 @@
// @ts-nocheck
export const extractExpirationDate = (certData: string): Date | null => { export const extractExpirationDate = (certData: string): Date | null => {
try { try {
// Decode PEM base64 to DER binary const match = certData.match(
const b64 = certData.replace(/-----[^-]+-----/g, "").replace(/\s+/g, ""); /-----BEGIN CERTIFICATE-----\s*([^-]+)\s*-----END CERTIFICATE-----/,
const binStr = atob(b64); );
const der = new Uint8Array(binStr.length); if (!match?.[1]) return null;
for (let i = 0; i < binStr.length; i++) {
der[i] = binStr.charCodeAt(i); 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);
} }
let offset = 0; // 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;
// Helper: read ASN.1 length field if (!dateFound) {
function readLength(pos: number): { length: number; offset: number } { // Skip "not before" date
// biome-ignore lint/style/noParameterAssign: <explanation> dateFound = true;
let len = der[pos++]; j += dateLength + 2;
if (len & 0x80) { continue;
const bytes = len & 0x7f; }
len = 0;
for (let i = 0; i < bytes; i++) { // Found "not after" date
// biome-ignore lint/style/noParameterAssign: <explanation> let dateStr = "";
len = (len << 8) + der[pos++]; 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++;
} }
} }
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) { } catch (error) {
console.error("Error parsing certificate:", error); console.error("Error parsing certificate:", error);
return null; return null;

View File

@@ -18,7 +18,6 @@ import { useEffect, useState } from "react";
export const AddGithubProvider = () => { export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { data: activeOrganization } = authClient.useActiveOrganization(); const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: session } = authClient.useSession();
const { data } = api.user.get.useQuery(); const { data } = api.user.get.useQuery();
const [manifest, setManifest] = useState(""); const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false); const [isOrganization, setIsOrganization] = useState(false);
@@ -28,7 +27,7 @@ export const AddGithubProvider = () => {
const url = document.location.origin; const url = document.location.origin;
const manifest = JSON.stringify( const manifest = JSON.stringify(
{ {
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`, redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`, name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin, url: origin,
hook_attributes: { hook_attributes: {

View File

@@ -18,7 +18,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { generateSHA256Hash } from "@/lib/utils"; import { generateSHA256Hash } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@@ -30,6 +29,7 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Disable2FA } from "./disable-2fa"; import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa"; import { Enable2FA } from "./enable-2fa";
import { Switch } from "@/components/ui/switch";
const profileSchema = z.object({ const profileSchema = z.object({
email: z.string(), email: z.string(),

View File

@@ -156,67 +156,6 @@ export const HandleServers = ({ serverId }: Props) => {
remotely. remotely.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div>
<p className="text-primary text-sm font-medium">
You will need to purchase or rent a Virtual Private Server (VPS) to
proceed, we recommend to use one of these providers since has been
heavily tested.
</p>
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
<li>
<a
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
className="text-link underline"
>
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"
className="text-link underline"
>
DigitalOcean - Get $200 Credits
</a>
</li>
<li>
<a
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
className="text-link underline"
>
Hetzner - Get 20 Credits
</a>
</li>
<li>
<a
href="https://www.vultr.com/?ref=9679828"
className="text-link underline"
>
Vultr
</a>
</li>
<li>
<a
href="https://www.linode.com/es/pricing/#compute-shared"
className="text-link underline"
>
Linode
</a>
</li>
</ul>
<AlertBlock className="mt-4 px-4">
You are free to use whatever provider, but we recommend to use one
of the above, to avoid issues.
</AlertBlock>
</div>
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You cannot create more servers,{" "} You cannot create more servers,{" "}

View File

@@ -1,7 +1,7 @@
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules"; import { useState } from "react";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react"; import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
interface Props { interface Props {
serverId: string; serverId: string;

View File

@@ -40,10 +40,10 @@ import { HandleServers } from "./handle-servers";
import { SetupServer } from "./setup-server"; import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal"; import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowMonitoringModal } from "./show-monitoring-modal"; import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSchedulesModal } from "./show-schedules-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowSchedulesModal } from "./show-schedules-modal";
export const ShowServers = () => { export const ShowServers = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");

View File

@@ -177,14 +177,6 @@ export const WelcomeSuscription = () => {
Hostinger - Get 20% Discount Hostinger - Get 20% Discount
</a> </a>
</li> </li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li> <li>
<a <a
href="https://m.do.co/c/db24efd43f35" href="https://m.do.co/c/db24efd43f35"

View File

@@ -41,7 +41,6 @@ const addInvitation = z.object({
.min(1, "Email is required") .min(1, "Email is required")
.email({ message: "Invalid email" }), .email({ message: "Invalid email" }),
role: z.enum(["member", "admin"]), role: z.enum(["member", "admin"]),
notificationId: z.string().optional(),
}); });
type AddInvitation = z.infer<typeof addInvitation>; type AddInvitation = z.infer<typeof addInvitation>;
@@ -50,10 +49,6 @@ export const AddInvitation = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const [isLoading, setIsLoading] = useState(false); 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 [error, setError] = useState<string | null>(null);
const { data: activeOrganization } = authClient.useActiveOrganization(); const { data: activeOrganization } = authClient.useActiveOrganization();
@@ -61,7 +56,6 @@ export const AddInvitation = () => {
defaultValues: { defaultValues: {
email: "", email: "",
role: "member", role: "member",
notificationId: "",
}, },
resolver: zodResolver(addInvitation), resolver: zodResolver(addInvitation),
}); });
@@ -80,20 +74,7 @@ export const AddInvitation = () => {
if (result.error) { if (result.error) {
setError(result.error.message || ""); setError(result.error.message || "");
} else { } else {
if (!isCloud && data.notificationId) { toast.success("Invitation created");
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); setError(null);
setOpen(false); setOpen(false);
} }
@@ -168,47 +149,6 @@ 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"> <DialogFooter className="flex w-full flex-row">
<Button <Button
isLoading={isLoading} isLoading={isLoading}

View File

@@ -1,7 +1,6 @@
import { api } from "@/utils/api";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { ChatwootWidget } from "../shared/ChatwootWidget";
import Page from "./side"; import Page from "./side";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { api } from "@/utils/api";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@@ -10,15 +9,10 @@ interface Props {
export const DashboardLayout = ({ children }: Props) => { export const DashboardLayout = ({ children }: Props) => {
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery(); const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return ( return (
<> <>
<Page>{children}</Page> <Page>{children}</Page>
{isCloud === true && (
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
)}
{haveRootAccess === true && <ImpersonationBar />} {haveRootAccess === true && <ImpersonationBar />}
</> </>
); );

View File

@@ -0,0 +1,9 @@
import Page from "./side";
interface Props {
children: React.ReactNode;
}
export const ProjectLayout = ({ children }: Props) => {
return <Page>{children}</Page>;
};

View File

@@ -1,69 +0,0 @@
import Script from "next/script";
import { useEffect } from "react";
interface ChatwootWidgetProps {
websiteToken: string;
baseUrl?: string;
settings?: {
position?: "left" | "right";
type?: "standard" | "expanded_bubble";
launcherTitle?: string;
darkMode?: boolean;
hideMessageBubble?: boolean;
placement?: "right" | "left";
showPopoutButton?: boolean;
widgetStyle?: "standard" | "bubble";
};
user?: {
identifier: string;
name?: string;
email?: string;
phoneNumber?: string;
avatarUrl?: string;
customAttributes?: Record<string, any>;
identifierHash?: string;
};
}
export const ChatwootWidget = ({
websiteToken,
baseUrl = "https://app.chatwoot.com",
settings = {
position: "right",
type: "standard",
launcherTitle: "Chat with us",
},
user,
}: ChatwootWidgetProps) => {
useEffect(() => {
// Configurar los settings de Chatwoot
window.chatwootSettings = {
position: "right",
};
(window as any).chatwootSDKReady = () => {
window.chatwootSDK?.run({ websiteToken, baseUrl });
const trySetUser = () => {
if (window.$chatwoot && user) {
window.$chatwoot.setUser(user.identifier, {
email: user.email,
name: user.name,
avatar_url: user.avatarUrl,
phone_number: user.phoneNumber,
});
}
};
trySetUser();
};
}, [websiteToken, baseUrl, user, settings]);
return (
<Script
src={`${baseUrl}/packs/js/sdk.js`}
strategy="lazyOnload"
onLoad={() => (window as any).chatwootSDKReady?.()}
/>
);
};

View File

@@ -1 +0,0 @@
ALTER TABLE "application" ADD COLUMN "isStaticSpa" boolean;

View File

@@ -1,3 +0,0 @@
ALTER TABLE "user_temp" ALTER COLUMN "logCleanupCron" SET DEFAULT '0 0 * * *';
UPDATE "user_temp" SET "logCleanupCron" = '0 0 * * *' WHERE "logCleanupCron" IS NULL;

View File

@@ -1,14 +0,0 @@
ALTER TABLE "git_provider" ADD COLUMN "userId" text;--> statement-breakpoint
-- Update existing git providers to be owned by the organization owner
-- We can get the owner_id directly from the organization table
UPDATE "git_provider"
SET "userId" = (
SELECT o."owner_id"
FROM "organization" o
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;

View File

@@ -1,13 +0,0 @@
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;

View File

@@ -1,3 +0,0 @@
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;

View File

@@ -1,3 +0,0 @@
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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -645,48 +645,6 @@
"when": 1746518402168, "when": 1746518402168,
"tag": "0091_spotty_kulan_gath", "tag": "0091_spotty_kulan_gath",
"breakpoints": true "breakpoints": true
},
{
"idx": 92,
"version": "7",
"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
} }
] ]
} }

View File

@@ -21,7 +21,6 @@ try {
entryPoints: { entryPoints: {
server: "server/server.ts", server: "server/server.ts",
"reset-password": "reset-password.ts", "reset-password": "reset-password.ts",
"reset-2fa": "reset-2fa.ts",
}, },
bundle: true, bundle: true,
platform: "node", platform: "node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.23.3", "version": "v0.22.4",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -11,7 +11,6 @@
"build-next": "next build", "build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run", "setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"reset-password": "node -r dotenv/config dist/reset-password.mjs", "reset-password": "node -r dotenv/config dist/reset-password.mjs",
"reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ", "dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"dev-turbopack": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json", "dev-turbopack": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
@@ -126,8 +125,6 @@
"octokit": "3.1.2", "octokit": "3.1.2",
"ollama-ai-provider": "^1.1.0", "ollama-ai-provider": "^1.1.0",
"otpauth": "^9.2.3", "otpauth": "^9.2.3",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"postgres": "3.4.4", "postgres": "3.4.4",
"public-ip": "6.0.2", "public-ip": "6.0.2",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
@@ -148,13 +145,13 @@
"swagger-ui-react": "^5.17.14", "swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"toml": "3.0.0",
"undici": "^6.19.2", "undici": "^6.19.2",
"use-resize-observer": "9.1.0", "use-resize-observer": "9.1.0",
"ws": "8.16.0", "ws": "8.16.0",
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4", "zod": "^3.23.4",
"zod-form-data": "^2.0.2" "zod-form-data": "^2.0.2",
"toml": "3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/adm-zip": "^0.5.5", "@types/adm-zip": "^0.5.5",
@@ -189,7 +186,7 @@
}, },
"packageManager": "pnpm@9.5.0", "packageManager": "pnpm@9.5.0",
"engines": { "engines": {
"node": "^20.16.0", "node": "^20.9.0",
"pnpm": ">=9.5.0" "pnpm": ">=9.5.0"
}, },
"lint-staged": { "lint-staged": {

View File

@@ -17,6 +17,7 @@ const inter = Inter({ subsets: ["latin"] });
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & { export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode; getLayout?: (page: ReactElement) => ReactNode;
// session: Session | null;
theme?: string; theme?: string;
}; };
@@ -32,13 +33,11 @@ const MyApp = ({
return ( return (
<> <>
<style jsx global> <style jsx global>{`
{` :root {
:root { --font-inter: ${inter.style.fontFamily};
--font-inter: ${inter.style.fontFamily}; }
} `}</style>
`}
</style>
<Head> <Head>
<title>Dokploy</title> <title>Dokploy</title>
</Head> </Head>

View File

@@ -10,14 +10,13 @@ type Query = {
state: string; state: string;
installation_id: string; installation_id: string;
setup_action: string; setup_action: string;
userId: string;
}; };
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
) { ) {
const { code, state, installation_id, userId }: Query = req.query as Query; const { code, state, installation_id }: Query = req.query as Query;
if (!code) { if (!code) {
return res.status(400).json({ error: "Missing code parameter" }); return res.status(400).json({ error: "Missing code parameter" });
@@ -45,7 +44,6 @@ export default async function handler(
githubPrivateKey: data.pem, githubPrivateKey: data.pem,
}, },
value as string, value as string,
userId,
); );
} else if (action === "gh_setup") { } else if (action === "gh_setup") {
await db await db

View File

@@ -10,7 +10,7 @@ import {
PostgresqlIcon, PostgresqlIcon,
RedisIcon, RedisIcon,
} from "@/components/icons/data-tools-icons"; } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
@@ -18,7 +18,6 @@ import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant"; import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
import { import {
Card, Card,
CardContent, CardContent,
@@ -94,6 +93,7 @@ import { useRouter } from "next/router";
import { type ReactElement, useEffect, useMemo, useState } from "react"; import { type ReactElement, useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import superjson from "superjson"; import superjson from "superjson";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
export type Services = { export type Services = {
appName: string; appName: string;
@@ -1064,7 +1064,7 @@ const Project = (
export default Project; export default Project;
Project.getLayout = (page: ReactElement) => { Project.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>; return <ProjectLayout>{page}</ProjectLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -17,7 +17,7 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring"; import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring"; import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -363,7 +363,7 @@ const Service = (
export default Service; export default Service;
Service.getLayout = (page: ReactElement) => { Service.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>; return <ProjectLayout>{page}</ProjectLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -13,7 +13,7 @@ import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups"; import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring"; import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring"; import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -74,7 +74,12 @@ const Service = (
} }
}, [router.query.tab]); }, [router.query.tab]);
const { data } = api.compose.one.useQuery({ composeId }); const { data } = api.compose.one.useQuery(
{ composeId },
{
refetchInterval: 5000,
},
);
const { data: auth } = api.user.get.useQuery(); const { data: auth } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
@@ -361,7 +366,7 @@ const Service = (
export default Service; export default Service;
Service.getLayout = (page: ReactElement) => { Service.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>; return <ProjectLayout>{page}</ProjectLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring"; import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MariadbIcon } from "@/components/icons/data-tools-icons"; import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -294,7 +294,7 @@ const Mariadb = (
export default Mariadb; export default Mariadb;
Mariadb.getLayout = (page: ReactElement) => { Mariadb.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>; return <ProjectLayout>{page}</ProjectLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring"; import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MongodbIcon } from "@/components/icons/data-tools-icons"; import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -296,7 +296,7 @@ const Mongo = (
export default Mongo; export default Mongo;
Mongo.getLayout = (page: ReactElement) => { Mongo.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>; return <ProjectLayout>{page}</ProjectLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/gener
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql"; import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MysqlIcon } from "@/components/icons/data-tools-icons"; import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -280,7 +280,7 @@ const MySql = (
export default MySql; export default MySql;
MySql.getLayout = (page: ReactElement) => { MySql.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>; return <ProjectLayout>{page}</ProjectLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres"; import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { PostgresqlIcon } from "@/components/icons/data-tools-icons"; import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -278,7 +278,7 @@ const Postgresql = (
export default Postgresql; export default Postgresql;
Postgresql.getLayout = (page: ReactElement) => { Postgresql.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>; return <ProjectLayout>{page}</ProjectLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -9,7 +9,7 @@ import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/gener
import { UpdateRedis } from "@/components/dashboard/redis/update-redis"; import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { RedisIcon } from "@/components/icons/data-tools-icons"; import { RedisIcon } from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { ProjectLayout } from "@/components/layouts/project-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -285,7 +285,7 @@ const Redis = (
export default Redis; export default Redis;
Redis.getLayout = (page: ReactElement) => { Redis.getLayout = (page: ReactElement) => {
return <DashboardLayout>{page}</DashboardLayout>; return <ProjectLayout>{page}</ProjectLayout>;
}; };
export async function getServerSideProps( export async function getServerSideProps(

View File

@@ -1,11 +1,11 @@
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import type { GetServerSidePropsContext } from "next";
import { validateRequest } from "@dokploy/server/lib/auth";
import { IS_CLOUD } from "@dokploy/server/constants";
import { api } from "@/utils/api";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { Card } from "@/components/ui/card";
function SchedulesPage() { function SchedulesPage() {
const { data: user } = api.user.get.useQuery(); const { data: user } = api.user.get.useQuery();
return ( return (

View File

@@ -1,16 +1,16 @@
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { WebDomain } from "@/components/dashboard/settings/web-domain"; import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server"; import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { getLocale, serverSideTranslations } from "@/utils/i18n"; import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { IS_CLOUD, validateRequest } from "@dokploy/server"; import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import superjson from "superjson"; import superjson from "superjson";
import { api } from "@/utils/api";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { Card } from "@/components/ui/card";
const Page = () => { const Page = () => {
const { data: user } = api.user.get.useQuery(); const { data: user } = api.user.get.useQuery();
return ( return (

View File

@@ -1,27 +0,0 @@
import { findAdmin } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { users_temp } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
(async () => {
try {
const result = await findAdmin();
const update = await db
.update(users_temp)
.set({
twoFactorEnabled: false,
})
.where(eq(users_temp.id, result.userId));
if (update) {
console.log("2FA reset successful");
} else {
console.log("Password reset failed");
}
process.exit(0);
} catch (error) {
console.log("Error resetting 2FA", error);
}
})();

View File

@@ -36,7 +36,6 @@ import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm"; import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user"; import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule"; import { scheduleRouter } from "./routers/schedule";
import { rollbackRouter } from "./routers/rollbacks";
/** /**
* This is the primary router for your server. * This is the primary router for your server.
* *
@@ -81,7 +80,6 @@ export const appRouter = createTRPCRouter({
ai: aiRouter, ai: aiRouter,
organization: organizationRouter, organization: organizationRouter,
schedule: scheduleRouter, schedule: scheduleRouter,
rollback: rollbackRouter,
}); });
// export type definition of API // export type definition of API

View File

@@ -31,7 +31,6 @@ import {
createApplication, createApplication,
deleteAllMiddlewares, deleteAllMiddlewares,
findApplicationById, findApplicationById,
findGitProviderById,
findProjectById, findProjectById,
getApplicationStats, getApplicationStats,
mechanizeDockerContainer, mechanizeDockerContainer,
@@ -127,45 +126,7 @@ export const applicationRouter = createTRPCRouter({
message: "You are not authorized to access this application", 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 reload: protectedProcedure
@@ -369,7 +330,6 @@ export const applicationRouter = createTRPCRouter({
dockerContextPath: input.dockerContextPath, dockerContextPath: input.dockerContextPath,
dockerBuildStage: input.dockerBuildStage, dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion, herokuVersion: input.herokuVersion,
isStaticSpa: input.isStaticSpa,
}); });
return true; return true;
@@ -527,67 +487,6 @@ export const applicationRouter = createTRPCRouter({
enableSubmodules: input.enableSubmodules, 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; return true;
}), }),
markRunning: protectedProcedure markRunning: protectedProcedure

View File

@@ -22,7 +22,6 @@ import {
findPostgresByBackupId, findPostgresByBackupId,
findPostgresById, findPostgresById,
findServerById, findServerById,
keepLatestNBackups,
removeBackupById, removeBackupById,
removeScheduleBackup, removeScheduleBackup,
runMariadbBackup, runMariadbBackup,
@@ -198,8 +197,6 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
const postgres = await findPostgresByBackupId(backup.backupId); const postgres = await findPostgresByBackupId(backup.backupId);
await runPostgresBackup(postgres, backup); await runPostgresBackup(postgres, backup);
await keepLatestNBackups(backup, postgres?.serverId);
return true; return true;
} catch (error) { } catch (error) {
const message = const message =
@@ -220,7 +217,6 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
const mysql = await findMySqlByBackupId(backup.backupId); const mysql = await findMySqlByBackupId(backup.backupId);
await runMySqlBackup(mysql, backup); await runMySqlBackup(mysql, backup);
await keepLatestNBackups(backup, mysql?.serverId);
return true; return true;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -237,7 +233,6 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
const mariadb = await findMariadbByBackupId(backup.backupId); const mariadb = await findMariadbByBackupId(backup.backupId);
await runMariadbBackup(mariadb, backup); await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, mariadb?.serverId);
return true; return true;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -254,7 +249,6 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
const compose = await findComposeByBackupId(backup.backupId); const compose = await findComposeByBackupId(backup.backupId);
await runComposeBackup(compose, backup); await runComposeBackup(compose, backup);
await keepLatestNBackups(backup, compose?.serverId);
return true; return true;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
@@ -271,7 +265,6 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
const mongo = await findMongoByBackupId(backup.backupId); const mongo = await findMongoByBackupId(backup.backupId);
await runMongoBackup(mongo, backup); await runMongoBackup(mongo, backup);
await keepLatestNBackups(backup, mongo?.serverId);
return true; return true;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({

View File

@@ -22,11 +22,7 @@ export const bitbucketRouter = createTRPCRouter({
.input(apiCreateBitbucket) .input(apiCreateBitbucket)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
return await createBitbucket( return await createBitbucket(input, ctx.session.activeOrganizationId);
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
@@ -41,8 +37,7 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId); const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if ( if (
bitbucketProvider.gitProvider.organizationId !== bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -61,13 +56,11 @@ export const bitbucketRouter = createTRPCRouter({
}, },
}); });
result = result.filter((provider) => { result = result.filter(
return ( (provider) =>
provider.gitProvider.organizationId === provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId,
provider.gitProvider.userId === ctx.session.userId );
);
});
return result; return result;
}), }),
@@ -77,8 +70,7 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId); const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if ( if (
bitbucketProvider.gitProvider.organizationId !== bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -95,8 +87,7 @@ export const bitbucketRouter = createTRPCRouter({
); );
if ( if (
bitbucketProvider.gitProvider.organizationId !== bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -112,8 +103,7 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId); const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if ( if (
bitbucketProvider.gitProvider.organizationId !== bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -136,8 +126,7 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId); const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if ( if (
bitbucketProvider.gitProvider.organizationId !== bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",

View File

@@ -28,7 +28,6 @@ import {
deleteMount, deleteMount,
findComposeById, findComposeById,
findDomainsByComposeId, findDomainsByComposeId,
findGitProviderById,
findProjectById, findProjectById,
findServerById, findServerById,
findUserById, findUserById,
@@ -52,9 +51,9 @@ import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { parse } from "toml";
import _ from "lodash"; import _ from "lodash";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { parse } from "toml";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
@@ -120,45 +119,7 @@ export const composeRouter = createTRPCRouter({
message: "You are not authorized to access this compose", 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 update: protectedProcedure
@@ -478,15 +439,7 @@ export const composeRouter = createTRPCRouter({
} }
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
const appName = `${projectName}-${generatePassword(6)}`; const generate = processTemplate(template.config, {
const config = {
...template.config,
variables: {
APP_NAME: appName,
...template.config.variables,
},
};
const generate = processTemplate(config, {
serverIp: serverIp, serverIp: serverIp,
projectName: projectName, projectName: projectName,
}); });
@@ -498,7 +451,7 @@ export const composeRouter = createTRPCRouter({
serverId: input.serverId, serverId: input.serverId,
name: input.id, name: input.id,
sourceType: "raw", sourceType: "raw",
appName: appName, appName: `${projectName}-${generatePassword(6)}`,
isolatedDeployment: true, isolatedDeployment: true,
}); });
@@ -535,7 +488,7 @@ export const composeRouter = createTRPCRouter({
} }
} }
return compose; return null;
}), }),
templates: publicProcedure templates: publicProcedure
@@ -565,61 +518,6 @@ export const composeRouter = createTRPCRouter({
const uniqueTags = _.uniq(allTags); const uniqueTags = _.uniq(allTags);
return uniqueTags; 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 move: protectedProcedure
.input( .input(
@@ -707,15 +605,7 @@ export const composeRouter = createTRPCRouter({
}); });
} }
const configModified = { const processedTemplate = processTemplate(config, {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp, serverIp: serverIp,
projectName: compose.appName, projectName: compose.appName,
}); });
@@ -785,15 +675,7 @@ export const composeRouter = createTRPCRouter({
}); });
} }
const configModified = { const processedTemplate = processTemplate(config, {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp, serverIp: serverIp,
projectName: compose.appName, projectName: compose.appName,
}); });

View File

@@ -1,4 +1,3 @@
import { db } from "@/server/db";
import { import {
apiFindAllByApplication, apiFindAllByApplication,
apiFindAllByCompose, apiFindAllByCompose,
@@ -17,6 +16,7 @@ import {
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@/server/db";
export const deploymentRouter = createTRPCRouter({ export const deploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
@@ -65,11 +65,7 @@ export const deploymentRouter = createTRPCRouter({
const deploymentsList = await db.query.deployments.findMany({ const deploymentsList = await db.query.deployments.findMany({
where: eq(deployments[`${input.type}Id`], input.id), where: eq(deployments[`${input.type}Id`], input.id),
orderBy: desc(deployments.createdAt), orderBy: desc(deployments.createdAt),
with: {
rollback: true,
},
}); });
return deploymentsList; return deploymentsList;
}), }),
}); });

View File

@@ -3,7 +3,7 @@ import { db } from "@/server/db";
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema"; import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
import { findGitProviderById, removeGitProvider } from "@dokploy/server"; import { findGitProviderById, removeGitProvider } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
export const gitProviderRouter = createTRPCRouter({ export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => { getAll: protectedProcedure.query(async ({ ctx }) => {
@@ -15,10 +15,7 @@ export const gitProviderRouter = createTRPCRouter({
gitea: true, gitea: true,
}, },
orderBy: desc(gitProvider.createdAt), orderBy: desc(gitProvider.createdAt),
where: and( where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
eq(gitProvider.userId, ctx.session.userId),
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
),
}); });
}), }),
remove: protectedProcedure remove: protectedProcedure

View File

@@ -26,11 +26,7 @@ export const giteaRouter = createTRPCRouter({
.input(apiCreateGitea) .input(apiCreateGitea)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
return await createGitea( return await createGitea(input, ctx.session.activeOrganizationId);
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
@@ -46,8 +42,7 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(input.giteaId); const giteaProvider = await findGiteaById(input.giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
giteaProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -67,8 +62,7 @@ export const giteaRouter = createTRPCRouter({
result = result.filter( result = result.filter(
(provider) => (provider) =>
provider.gitProvider.organizationId === provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId,
provider.gitProvider.userId === ctx.session.userId,
); );
const filtered = result const filtered = result
@@ -100,8 +94,7 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
giteaProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -137,8 +130,7 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
giteaProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -170,8 +162,7 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
giteaProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -199,8 +190,7 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(input.giteaId); const giteaProvider = await findGiteaById(input.giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
giteaProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -241,8 +231,7 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
if ( if (
giteaProvider.gitProvider.organizationId !== giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
giteaProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",

View File

@@ -21,8 +21,7 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId); const githubProvider = await findGithubById(input.githubId);
if ( if (
githubProvider.gitProvider.organizationId !== githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
githubProvider.gitProvider.userId === ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -37,8 +36,7 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId); const githubProvider = await findGithubById(input.githubId);
if ( if (
githubProvider.gitProvider.organizationId !== githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
githubProvider.gitProvider.userId === ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -53,8 +51,7 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId || ""); const githubProvider = await findGithubById(input.githubId || "");
if ( if (
githubProvider.gitProvider.organizationId !== 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 //TODO: Remove this line when the cloud version is ready
throw new TRPCError({ throw new TRPCError({
@@ -74,8 +71,7 @@ export const githubRouter = createTRPCRouter({
result = result.filter( result = result.filter(
(provider) => (provider) =>
provider.gitProvider.organizationId === provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId,
provider.gitProvider.userId === ctx.session.userId,
); );
const filtered = result const filtered = result
@@ -99,8 +95,7 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId); const githubProvider = await findGithubById(input.githubId);
if ( if (
githubProvider.gitProvider.organizationId !== githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
githubProvider.gitProvider.userId === ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -122,8 +117,7 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId); const githubProvider = await findGithubById(input.githubId);
if ( if (
githubProvider.gitProvider.organizationId !== githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
githubProvider.gitProvider.userId === ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",

View File

@@ -25,11 +25,7 @@ export const gitlabRouter = createTRPCRouter({
.input(apiCreateGitlab) .input(apiCreateGitlab)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
return await createGitlab( return await createGitlab(input, ctx.session.activeOrganizationId);
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
@@ -44,8 +40,7 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId); const gitlabProvider = await findGitlabById(input.gitlabId);
if ( if (
gitlabProvider.gitProvider.organizationId !== gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
gitlabProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -61,13 +56,11 @@ export const gitlabRouter = createTRPCRouter({
}, },
}); });
result = result.filter((provider) => { result = result.filter(
return ( (provider) =>
provider.gitProvider.organizationId === provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId,
provider.gitProvider.userId === ctx.session.userId );
);
});
const filtered = result const filtered = result
.filter((provider) => haveGitlabRequirements(provider)) .filter((provider) => haveGitlabRequirements(provider))
.map((provider) => { .map((provider) => {
@@ -87,8 +80,7 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId); const gitlabProvider = await findGitlabById(input.gitlabId);
if ( if (
gitlabProvider.gitProvider.organizationId !== gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
gitlabProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -104,8 +96,7 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId || ""); const gitlabProvider = await findGitlabById(input.gitlabId || "");
if ( if (
gitlabProvider.gitProvider.organizationId !== gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
gitlabProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -121,8 +112,7 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId || ""); const gitlabProvider = await findGitlabById(input.gitlabId || "");
if ( if (
gitlabProvider.gitProvider.organizationId !== gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
gitlabProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -145,8 +135,7 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId); const gitlabProvider = await findGitlabById(input.gitlabId);
if ( if (
gitlabProvider.gitProvider.organizationId !== gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId && ctx.session.activeOrganizationId
gitlabProvider.gitProvider.userId !== ctx.session.userId
) { ) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",

View File

@@ -446,12 +446,4 @@ 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,
},
});
}),
}); });

View File

@@ -21,32 +21,32 @@ import {
addNewProject, addNewProject,
checkProjectAccess, checkProjectAccess,
createApplication, createApplication,
createBackup,
createCompose, createCompose,
createDomain,
createMariadb, createMariadb,
createMongo, createMongo,
createMount,
createMysql, createMysql,
createPort,
createPostgres, createPostgres,
createPreviewDeployment,
createProject, createProject,
createRedirect,
createRedis, createRedis,
createSecurity,
deleteProject, deleteProject,
findApplicationById, findApplicationById,
findComposeById, findComposeById,
findMariadbById,
findMemberById,
findMongoById, findMongoById,
findMySqlById, findMemberById,
findPostgresById,
findProjectById,
findRedisById, findRedisById,
findProjectById,
findUserById, findUserById,
updateProjectById, updateProjectById,
findPostgresById,
findMariadbById,
findMySqlById,
createDomain,
createPort,
createMount,
createRedirect,
createPreviewDeployment,
createBackup,
createSecurity,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, desc, eq, sql } from "drizzle-orm"; import { and, desc, eq, sql } from "drizzle-orm";
@@ -309,7 +309,6 @@ export const projectRouter = createTRPCRouter({
}), }),
) )
.optional(), .optional(),
duplicateInSameProject: z.boolean().default(false),
}), }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -332,17 +331,15 @@ export const projectRouter = createTRPCRouter({
}); });
} }
// Create new project or use existing one // Create new project
const targetProject = input.duplicateInSameProject const newProject = await createProject(
? sourceProject {
: await createProject( name: input.name,
{ description: input.description,
name: input.name, env: sourceProject.env,
description: input.description, },
env: sourceProject.env, ctx.session.activeOrganizationId,
}, );
ctx.session.activeOrganizationId,
);
if (input.includeServices) { if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || []; const servicesToDuplicate = input.selectedServices || [];
@@ -365,10 +362,7 @@ export const projectRouter = createTRPCRouter({
const newApplication = await createApplication({ const newApplication = await createApplication({
...application, ...application,
name: input.duplicateInSameProject projectId: newProject.projectId,
? `${application.name} (copy)`
: application.name,
projectId: targetProject.projectId,
}); });
for (const domain of domains) { for (const domain of domains) {
@@ -429,10 +423,7 @@ export const projectRouter = createTRPCRouter({
const newPostgres = await createPostgres({ const newPostgres = await createPostgres({
...postgres, ...postgres,
name: input.duplicateInSameProject projectId: newProject.projectId,
? `${postgres.name} (copy)`
: postgres.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -458,10 +449,7 @@ export const projectRouter = createTRPCRouter({
await findMariadbById(id); await findMariadbById(id);
const newMariadb = await createMariadb({ const newMariadb = await createMariadb({
...mariadb, ...mariadb,
name: input.duplicateInSameProject projectId: newProject.projectId,
? `${mariadb.name} (copy)`
: mariadb.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -487,10 +475,7 @@ export const projectRouter = createTRPCRouter({
await findMongoById(id); await findMongoById(id);
const newMongo = await createMongo({ const newMongo = await createMongo({
...mongo, ...mongo,
name: input.duplicateInSameProject projectId: newProject.projectId,
? `${mongo.name} (copy)`
: mongo.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -516,10 +501,7 @@ export const projectRouter = createTRPCRouter({
await findMySqlById(id); await findMySqlById(id);
const newMysql = await createMysql({ const newMysql = await createMysql({
...mysql, ...mysql,
name: input.duplicateInSameProject projectId: newProject.projectId,
? `${mysql.name} (copy)`
: mysql.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -544,10 +526,7 @@ export const projectRouter = createTRPCRouter({
const { redisId, mounts, ...redis } = await findRedisById(id); const { redisId, mounts, ...redis } = await findRedisById(id);
const newRedis = await createRedis({ const newRedis = await createRedis({
...redis, ...redis,
name: input.duplicateInSameProject projectId: newProject.projectId,
? `${redis.name} (copy)`
: redis.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -566,10 +545,7 @@ export const projectRouter = createTRPCRouter({
await findComposeById(id); await findComposeById(id);
const newCompose = await createCompose({ const newCompose = await createCompose({
...compose, ...compose,
name: input.duplicateInSameProject projectId: newProject.projectId,
? `${compose.name} (copy)`
: compose.name,
projectId: targetProject.projectId,
}); });
for (const mount of mounts) { for (const mount of mounts) {
@@ -596,20 +572,21 @@ export const projectRouter = createTRPCRouter({
}; };
// Duplicate selected services // Duplicate selected services
for (const service of servicesToDuplicate) { for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type); await duplicateService(service.id, service.type);
} }
} }
if (!input.duplicateInSameProject && ctx.user.role === "member") { if (ctx.user.role === "member") {
await addNewProject( await addNewProject(
ctx.user.id, ctx.user.id,
targetProject.projectId, newProject.projectId,
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
); );
} }
return targetProject; return newProject;
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",

View File

@@ -1,37 +0,0 @@
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,
});
}
}),
});

View File

@@ -1,24 +1,24 @@
import { removeJob, schedule } from "@/server/utils/backup"; import { TRPCError } from "@trpc/server";
import { IS_CLOUD, scheduleJob } from "@dokploy/server"; import { z } from "zod";
import { removeScheduleJob } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { deployments } from "@dokploy/server/db/schema/deployment";
import { import {
createScheduleSchema, createScheduleSchema,
schedules, schedules,
updateScheduleSchema, updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule"; } from "@dokploy/server/db/schema/schedule";
import { desc, eq } from "drizzle-orm";
import { db } from "@dokploy/server/db";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { runCommand } from "@dokploy/server/index"; import { runCommand } from "@dokploy/server/index";
import { deployments } from "@dokploy/server/db/schema/deployment";
import { import {
createSchedule,
deleteSchedule, deleteSchedule,
findScheduleById, findScheduleById,
createSchedule,
updateSchedule, updateSchedule,
} from "@dokploy/server/services/schedule"; } from "@dokploy/server/services/schedule";
import { TRPCError } from "@trpc/server"; import { IS_CLOUD, scheduleJob } from "@dokploy/server";
import { desc, eq } from "drizzle-orm"; import { removeJob, schedule } from "@/server/utils/backup";
import { z } from "zod"; import { removeScheduleJob } from "@dokploy/server";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const scheduleRouter = createTRPCRouter({ export const scheduleRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure
.input(createScheduleSchema) .input(createScheduleSchema)

Some files were not shown because too many files have changed in this diff Show More