Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
4c96d8559a Fix zombie processes from Docker cleanup by adding wait command
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-22 16:54:09 +00:00
copilot-swe-agent[bot]
39cf706053 Initial plan 2025-12-22 16:48:27 +00:00
87 changed files with 613 additions and 15730 deletions

View File

@@ -24,14 +24,14 @@ jobs:
- name: Install Nixpacks - name: Install Nixpacks
if: matrix.job == 'test' if: matrix.job == 'test'
run: | run: |
export NIXPACKS_VERSION=1.41.0 export NIXPACKS_VERSION=1.39.0
curl -sSL https://nixpacks.com/install.sh | bash curl -sSL https://nixpacks.com/install.sh | bash
echo "Nixpacks installed $NIXPACKS_VERSION" echo "Nixpacks installed $NIXPACKS_VERSION"
- name: Install Railpack - name: Install Railpack
if: matrix.job == 'test' if: matrix.job == 'test'
run: | run: |
export RAILPACK_VERSION=0.15.4 export RAILPACK_VERSION=0.15.0
curl -sSL https://railpack.com/install.sh | bash curl -sSL https://railpack.com/install.sh | bash
echo "Railpack installed $RAILPACK_VERSION" echo "Railpack installed $RAILPACK_VERSION"

3
.gitignore vendored
View File

@@ -44,6 +44,3 @@ yarn-error.log*
.db .db
# Development environment
.devcontainer

View File

@@ -148,7 +148,7 @@ curl -sSL https://railpack.com/install.sh | sh
```bash ```bash
# Install Buildpacks # Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.39.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
``` ```
## Pull Request ## Pull Request

View File

@@ -51,18 +51,18 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --ver
# 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.41.0 ARG NIXPACKS_VERSION=1.39.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 \
&& pnpm install -g tsx && pnpm install -g tsx
# Install Railpack # Install Railpack
ARG RAILPACK_VERSION=0.15.4 ARG RAILPACK_VERSION=0.2.2
RUN curl -sSL https://railpack.com/install.sh | bash RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks # Install buildpacks
COPY --from=buildpacksio/pack:0.39.1 /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

@@ -13,6 +13,7 @@
"@dokploy/server": "workspace:*", "@dokploy/server": "workspace:*",
"@hono/node-server": "^1.14.3", "@hono/node-server": "^1.14.3",
"@hono/zod-validator": "0.3.0", "@hono/zod-validator": "0.3.0",
"@nerimity/mimiqueue": "1.2.3",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"hono": "^4.7.10", "hono": "^4.7.10",
"pino": "9.4.0", "pino": "9.4.0",

View File

@@ -25,7 +25,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
titleLog: z.string().optional(), titleLog: z.string().optional(),
descriptionLog: z.string().optional(), descriptionLog: z.string().optional(),
server: z.boolean().optional(), server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]), type: z.enum(["deploy"]),
applicationType: z.literal("application-preview"), applicationType: z.literal("application-preview"),
serverId: z.string().min(1), serverId: z.string().min(1),
}), }),

View File

@@ -4,7 +4,6 @@ import {
deployPreviewApplication, deployPreviewApplication,
rebuildApplication, rebuildApplication,
rebuildCompose, rebuildCompose,
rebuildPreviewApplication,
updateApplicationStatus, updateApplicationStatus,
updateCompose, updateCompose,
updatePreviewDeployment, updatePreviewDeployment,
@@ -55,14 +54,7 @@ export const deploy = async (job: DeployJob) => {
previewStatus: "running", previewStatus: "running",
}); });
if (job.server) { if (job.server) {
if (job.type === "redeploy") { if (job.type === "deploy") {
await rebuildPreviewApplication({
applicationId: job.applicationId,
titleLog: job.titleLog || "Rebuild Preview Deployment",
descriptionLog: job.descriptionLog || "",
previewDeploymentId: job.previewDeploymentId,
});
} else if (job.type === "deploy") {
await deployPreviewApplication({ await deployPreviewApplication({
applicationId: job.applicationId, applicationId: job.applicationId,
titleLog: job.titleLog || "Preview Deployment", titleLog: job.titleLog || "Preview Deployment",

View File

@@ -25,7 +25,7 @@ if (typeof window === "undefined") {
} }
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.15.4", railpackVersion: "0.2.2",
applicationId: "", applicationId: "",
previewLabels: [], previewLabels: [],
createEnvFile: true, createEnvFile: true,

View File

@@ -5,27 +5,21 @@ vi.mock("node:fs", () => ({
default: fs, default: fs,
})); }));
import type { FileConfig } from "@dokploy/server"; import type { FileConfig, User } from "@dokploy/server";
import { import {
createDefaultServerTraefikConfig, createDefaultServerTraefikConfig,
loadOrCreateConfig, loadOrCreateConfig,
updateServerTraefik, updateServerTraefik,
} from "@dokploy/server"; } from "@dokploy/server";
import type { webServerSettings } from "@dokploy/server/db/schema";
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
type WebServerSettings = typeof webServerSettings.$inferSelect; const baseAdmin: User = {
const baseSettings: WebServerSettings = {
id: "",
https: false, https: false,
certificateType: "none", enablePaidFeatures: false,
host: null, allowImpersonation: false,
serverIp: null, role: "user",
letsEncryptEmail: null, firstName: "",
sshPrivateKey: null, lastName: "",
enableDockerCleanup: false,
logCleanupCron: null,
metricsConfig: { metricsConfig: {
containers: { containers: {
refreshRate: 20, refreshRate: 20,
@@ -51,8 +45,29 @@ const baseSettings: WebServerSettings = {
cleanupCacheApplications: false, cleanupCacheApplications: false,
cleanupCacheOnCompose: false, cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false, cleanupCacheOnPreviews: false,
createdAt: null, createdAt: new Date(),
serverIp: null,
certificateType: "none",
host: null,
letsEncryptEmail: null,
sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
banExpires: new Date(),
banned: true,
banReason: "",
email: "",
expirationDate: "",
id: "",
isRegistered: false,
createdAt2: new Date().toISOString(),
emailVerified: false,
image: "",
updatedAt: new Date(), updatedAt: new Date(),
twoFactorEnabled: false,
}; };
beforeEach(() => { beforeEach(() => {
@@ -70,7 +85,7 @@ test("Should read the configuration file", () => {
test("Should apply redirect-to-https", () => { test("Should apply redirect-to-https", () => {
updateServerTraefik( updateServerTraefik(
{ {
...baseSettings, ...baseAdmin,
https: true, https: true,
certificateType: "letsencrypt", certificateType: "letsencrypt",
}, },
@@ -85,7 +100,7 @@ test("Should apply redirect-to-https", () => {
}); });
test("Should change only host when no certificate", () => { test("Should change only host when no certificate", () => {
updateServerTraefik(baseSettings, "example.com"); updateServerTraefik(baseAdmin, "example.com");
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -95,7 +110,7 @@ test("Should change only host when no certificate", () => {
test("Should not touch config without host", () => { test("Should not touch config without host", () => {
const originalConfig: FileConfig = loadOrCreateConfig("dokploy"); const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
updateServerTraefik(baseSettings, null); updateServerTraefik(baseAdmin, null);
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -104,14 +119,11 @@ test("Should not touch config without host", () => {
test("Should remove websecure if https rollback to http", () => { test("Should remove websecure if https rollback to http", () => {
updateServerTraefik( updateServerTraefik(
{ ...baseSettings, certificateType: "letsencrypt" }, { ...baseAdmin, certificateType: "letsencrypt" },
"example.com", "example.com",
); );
updateServerTraefik( updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com");
{ ...baseSettings, certificateType: "none" },
"example.com",
);
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");

View File

@@ -3,7 +3,7 @@ import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.15.4", railpackVersion: "0.2.2",
rollbackActive: false, rollbackActive: false,
applicationId: "", applicationId: "",
previewLabels: [], previewLabels: [],

View File

@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react"; import { Cog } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -20,39 +20,8 @@ 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
// Railpack versions from https://github.com/railwayapp/railpack/releases
export const RAILPACK_VERSIONS = [
"0.15.4",
"0.15.3",
"0.15.2",
"0.15.1",
"0.15.0",
"0.14.0",
"0.13.0",
"0.12.0",
"0.11.0",
"0.10.0",
"0.9.2",
"0.9.1",
"0.9.0",
"0.8.0",
"0.7.0",
"0.6.0",
"0.5.0",
"0.4.0",
"0.3.0",
"0.2.2",
] as const;
export enum BuildType { export enum BuildType {
dockerfile = "dockerfile", dockerfile = "dockerfile",
heroku_buildpacks = "heroku_buildpacks", heroku_buildpacks = "heroku_buildpacks",
@@ -96,7 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.railpack), buildType: z.literal(BuildType.railpack),
railpackVersion: z.string().nullable().default("0.15.4"), railpackVersion: z.string().nullable().default("0.2.2"),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.static), buildType: z.literal(BuildType.static),
@@ -183,8 +152,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
}); });
const buildType = form.watch("buildType"); const buildType = form.watch("buildType");
const railpackVersion = form.watch("railpackVersion");
const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -196,14 +163,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
}; };
form.reset(resetData(typedData)); form.reset(resetData(typedData));
// Check if railpack version is manual (not in the predefined list)
if (
data.railpackVersion &&
!RAILPACK_VERSIONS.includes(data.railpackVersion as any)
) {
setIsManualRailpackVersion(true);
}
} }
}, [data, form]); }, [data, form]);
@@ -227,7 +186,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === BuildType.static ? data.isStaticSpa : null, data.buildType === BuildType.static ? data.isStaticSpa : null,
railpackVersion: railpackVersion:
data.buildType === BuildType.railpack data.buildType === BuildType.railpack
? data.railpackVersion || "0.15.4" ? data.railpackVersion || "0.2.2"
: null, : null,
}) })
.then(async () => { .then(async () => {
@@ -444,88 +403,23 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
/> />
)} )}
{buildType === BuildType.railpack && ( {buildType === BuildType.railpack && (
<> <FormField
<FormField control={form.control}
control={form.control} name="railpackVersion"
name="railpackVersion" render={({ field }) => (
render={({ field }) => ( <FormItem>
<FormItem> <FormLabel>Railpack Version</FormLabel>
<FormLabel>Railpack Version</FormLabel> <FormControl>
<FormControl> <Input
{isManualRailpackVersion ? ( placeholder="Railpack Version"
<div className="space-y-2"> {...field}
<Input value={field.value ?? ""}
placeholder="Enter custom version (e.g., 0.15.4)" />
{...field} </FormControl>
value={field.value ?? ""} <FormMessage />
/> </FormItem>
<Button )}
type="button" />
variant="outline"
size="sm"
onClick={() => {
setIsManualRailpackVersion(false);
field.onChange("0.15.4");
}}
>
Use predefined versions
</Button>
</div>
) : (
<Select
onValueChange={(value) => {
if (value === "manual") {
setIsManualRailpackVersion(true);
field.onChange("");
} else {
field.onChange(value);
}
}}
value={field.value ?? "0.15.4"}
>
<SelectTrigger>
<SelectValue placeholder="Select Railpack version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">
<span className="font-medium">
Manual (Custom Version)
</span>
</SelectItem>
{RAILPACK_VERSIONS.map((version) => (
<SelectItem key={version} value={version}>
v{version}
{version === "0.15.4" && (
<Badge
variant="secondary"
className="ml-2 px-1 text-xs"
>
Latest
</Badge>
)}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</FormControl>
<FormDescription>
Select a Railpack version or choose manual to enter a
custom version.{" "}
<a
href="https://github.com/railwayapp/railpack/releases"
target="_blank"
rel="noreferrer"
className="text-primary underline underline-offset-4"
>
View releases
</a>
</FormDescription>
<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">

View File

@@ -256,9 +256,9 @@ export const ShowDeployments = ({
return ( return (
<div <div
key={deployment.deploymentId} key={deployment.deploymentId}
className="flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between" className="flex items-center justify-between rounded-lg border p-4 gap-2"
> >
<div className="flex flex-1 flex-col min-w-0"> <div className="flex flex-col">
<span className="flex items-center gap-4 font-medium capitalize text-foreground"> <span className="flex items-center gap-4 font-medium capitalize text-foreground">
{index + 1}. {deployment.status} {index + 1}. {deployment.status}
<StatusTooltip <StatusTooltip
@@ -313,8 +313,8 @@ export const ShowDeployments = ({
)} )}
</div> </div>
</div> </div>
<div className="flex w-full flex-col items-start gap-2 sm:w-auto sm:max-w-[300px] sm:items-end sm:justify-start"> <div className="flex flex-col items-end gap-2 max-w-[300px] w-full justify-start">
<div className="text-sm capitalize text-muted-foreground flex flex-wrap items-center gap-2"> <div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
<DateTooltip date={deployment.createdAt} /> <DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && ( {deployment.startedAt && deployment.finishedAt && (
<Badge <Badge
@@ -333,7 +333,7 @@ export const ShowDeployments = ({
)} )}
</div> </div>
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:justify-end"> <div className="flex flex-row items-center gap-2">
{deployment.pid && deployment.status === "running" && ( {deployment.pid && deployment.status === "running" && (
<DialogAction <DialogAction
title="Kill Process" title="Kill Process"
@@ -355,7 +355,6 @@ export const ShowDeployments = ({
variant="destructive" variant="destructive"
size="sm" size="sm"
isLoading={isKillingProcess} isLoading={isKillingProcess}
className="w-full sm:w-auto"
> >
Kill Process Kill Process
</Button> </Button>
@@ -365,7 +364,6 @@ export const ShowDeployments = ({
onClick={() => { onClick={() => {
setActiveLog(deployment); setActiveLog(deployment);
}} }}
className="w-full sm:w-auto"
> >
View View
</Button> </Button>
@@ -407,7 +405,6 @@ export const ShowDeployments = ({
variant="secondary" variant="secondary"
size="sm" size="sm"
isLoading={isRollingBack} isLoading={isRollingBack}
className="w-full sm:w-auto"
> >
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" /> <RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
Rollback Rollback

View File

@@ -232,9 +232,9 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<FormItem className="md:col-span-2 flex flex-col"> <FormItem className="md:col-span-2 flex flex-col">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<FormLabel>Repository</FormLabel> <FormLabel>Repository</FormLabel>
{field.value.gitlabPathNamespace && ( {field.value.owner && field.value.repo && (
<Link <Link
href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`} href={`${gitlabUrl}/${field.value.owner}/${field.value.repo}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"

View File

@@ -2,7 +2,6 @@ import {
ExternalLink, ExternalLink,
FileText, FileText,
GitPullRequest, GitPullRequest,
Hammer,
Loader2, Loader2,
PenSquare, PenSquare,
RocketIcon, RocketIcon,
@@ -23,13 +22,6 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
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 { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
@@ -46,9 +38,6 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { mutateAsync: deletePreviewDeployment, isLoading } = const { mutateAsync: deletePreviewDeployment, isLoading } =
api.previewDeployment.delete.useMutation(); api.previewDeployment.delete.useMutation();
const { mutateAsync: redeployPreviewDeployment } =
api.previewDeployment.redeploy.useMutation();
const { const {
data: previewDeployments, data: previewDeployments,
refetch: refetchPreviewDeployments, refetch: refetchPreviewDeployments,
@@ -57,8 +46,6 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
{ applicationId }, { applicationId },
{ {
enabled: !!applicationId, enabled: !!applicationId,
refetchInterval: (data) =>
data?.some((d) => d.previewStatus === "running") ? 2000 : false,
}, },
); );
@@ -206,58 +193,6 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
</Button> </Button>
</ShowDeploymentsModal> </ShowDeploymentsModal>
<DialogAction
title="Rebuild Preview Deployment"
description="Are you sure you want to rebuild this preview deployment?"
type="default"
onClick={async () => {
await redeployPreviewDeployment({
previewDeploymentId:
deployment.previewDeploymentId,
})
.then(() => {
toast.success(
"Preview deployment rebuild started",
);
refetchPreviewDeployments();
})
.catch(() => {
toast.error(
"Error rebuilding preview deployment",
);
});
}}
>
<Button
variant="outline"
size="sm"
isLoading={status === "running"}
className="gap-2"
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<Hammer className="size-4" />
Rebuild
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent
sideOffset={5}
className="z-[60]"
>
<p>
Rebuild the preview deployment without
downloading new code
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</TooltipProvider>
</Button>
</DialogAction>
<AddPreviewDomain <AddPreviewDomain
previewDeploymentId={`${deployment.previewDeploymentId}`} previewDeploymentId={`${deployment.previewDeploymentId}`}
domainId={deployment.domain?.domainId} domainId={deployment.domain?.domainId}

View File

@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useMemo } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -97,16 +97,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository"); const repository = form.watch("repository");
const gitlabId = form.watch("gitlabId"); const gitlabId = form.watch("gitlabId");
const gitlabUrl = useMemo(() => {
const url = gitlabProviders?.find(
(provider) => provider.gitlabId === gitlabId,
)?.gitlabUrl;
const gitlabUrl = url?.replace(/\/$/, "");
return gitlabUrl || "https://gitlab.com";
}, [gitlabId, gitlabProviders]);
const { const {
data: repositories, data: repositories,
isLoading: isLoadingRepositories, isLoading: isLoadingRepositories,
@@ -234,9 +224,9 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormItem className="md:col-span-2 flex flex-col"> <FormItem className="md:col-span-2 flex flex-col">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<FormLabel>Repository</FormLabel> <FormLabel>Repository</FormLabel>
{field.value.gitlabPathNamespace && ( {field.value.owner && field.value.repo && (
<Link <Link
href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`} href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"

View File

@@ -559,7 +559,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
type="password" type="password"
placeholder="******************" placeholder="******************"
autoComplete="one-time-code" autoComplete="one-time-code"
enablePasswordGenerator={true}
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -579,7 +578,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
<Input <Input
type="password" type="password"
placeholder="******************" placeholder="******************"
enablePasswordGenerator={true}
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -1,74 +0,0 @@
import { CreditCard, FileText } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { ShowInvoices } from "./show-invoices";
const navigationItems = [
{
name: "Subscription",
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",
icon: FileText,
},
];
export const ShowBillingInvoices = () => {
const router = useRouter();
return (
<div className="w-full">
<Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<CardHeader>
<CardTitle className="text-xl flex flex-row gap-2">
<CreditCard className="size-6 text-muted-foreground self-center" />
Billing
</CardTitle>
<CardDescription>
Manage your subscription and invoices
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 py-4 border-t">
<nav className="flex space-x-2 border-b">
{navigationItems.map((item) => {
const Icon = item.icon;
const isActive = router.pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
isActive
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
)}
>
<Icon className="h-4 w-4" />
{item.name}
</Link>
);
})}
</nav>
<div className="mt-6">
<ShowInvoices />
</div>
</CardContent>
</div>
</Card>
</div>
);
};

View File

@@ -4,13 +4,11 @@ import {
AlertTriangle, AlertTriangle,
CheckIcon, CheckIcon,
CreditCard, CreditCard,
FileText,
Loader2, Loader2,
MinusIcon, MinusIcon,
PlusIcon, PlusIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -39,22 +37,7 @@ export const calculatePrice = (count: number, isAnnual = false) => {
if (count <= 1) return 4.5; if (count <= 1) return 4.5;
return count * 3.5; return count * 3.5;
}; };
const navigationItems = [
{
name: "Subscription",
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",
icon: FileText,
},
];
export const ShowBilling = () => { export const ShowBilling = () => {
const router = useRouter();
const { data: servers } = api.server.count.useQuery(); const { data: servers } = api.server.count.useQuery();
const { data: admin } = api.user.get.useQuery(); const { data: admin } = api.user.get.useQuery();
const { data, isLoading } = api.stripe.getProducts.useQuery(); const { data, isLoading } = api.stripe.getProducts.useQuery();
@@ -93,41 +76,17 @@ export const ShowBilling = () => {
return ( return (
<div className="w-full"> <div className="w-full">
<Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto"> <Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md"> <div className="rounded-xl bg-background shadow-md ">
<CardHeader> <CardHeader className="">
<CardTitle className="text-xl flex flex-row gap-2"> <CardTitle className="text-xl flex flex-row gap-2">
<CreditCard className="size-6 text-muted-foreground self-center" /> <CreditCard className="size-6 text-muted-foreground self-center" />
Billing Billing
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>Manage your subscription</CardDescription>
Manage your subscription and invoices
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 py-4 border-t"> <CardContent className="space-y-2 py-8 border-t">
<nav className="flex space-x-2 border-b"> <div className="flex flex-col gap-4 w-full">
{navigationItems.map((item) => {
const Icon = item.icon;
const isActive = router.pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
isActive
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
)}
>
<Icon className="h-4 w-4" />
{item.name}
</Link>
);
})}
</nav>
<div className="flex flex-col gap-4 w-full mt-6">
<Tabs <Tabs
defaultValue="monthly" defaultValue="monthly"
value={isAnnual ? "annual" : "monthly"} value={isAnnual ? "annual" : "monthly"}

View File

@@ -1,137 +0,0 @@
import { Download, ExternalLink, FileText, Loader2 } from "lucide-react";
import type Stripe from "stripe";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
const formatDate = (timestamp: number | null) => {
if (!timestamp) return "-";
return new Date(timestamp * 1000).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
};
const formatAmount = (amount: number, currency: string) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency.toUpperCase(),
}).format(amount / 100);
};
const getStatusBadge = (status: Stripe.Invoice.Status | null) => {
const statusConfig: Record<
Stripe.Invoice.Status,
{ label: string; variant: "default" | "secondary" | "destructive" }
> = {
paid: { label: "Paid", variant: "default" },
open: { label: "Open", variant: "secondary" },
draft: { label: "Draft", variant: "secondary" },
void: { label: "Void", variant: "destructive" },
uncollectible: { label: "Uncollectible", variant: "destructive" },
};
if (!status) {
return <Badge variant="secondary">Unknown</Badge>;
}
const config = statusConfig[status] || {
label: status,
variant: "secondary" as const,
};
return <Badge variant={config.variant}>{config.label}</Badge>;
};
export const ShowInvoices = () => {
const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery();
return (
<div className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center min-h-[20vh]">
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center">
Loading invoices...
<Loader2 className="animate-spin" />
</span>
</div>
) : invoices && invoices.length > 0 ? (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Date</TableHead>
<TableHead>Due Date</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell className="font-medium">
{invoice.number || invoice.id.slice(0, 12)}
</TableCell>
<TableCell>{formatDate(invoice.created)}</TableCell>
<TableCell>{formatDate(invoice.dueDate)}</TableCell>
<TableCell>
{formatAmount(invoice.amountDue, invoice.currency)}
</TableCell>
<TableCell>{getStatusBadge(invoice.status)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{invoice.hostedInvoiceUrl && (
<Button
variant="outline"
size="sm"
onClick={() =>
window.open(
invoice.hostedInvoiceUrl || "",
"_blank",
)
}
>
<ExternalLink className="h-4 w-4" />
</Button>
)}
{invoice.invoicePdf && (
<Button
variant="outline"
size="sm"
onClick={() =>
window.open(invoice.invoicePdf || "", "_blank")
}
>
<Download className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="flex flex-col items-center justify-center min-h-[20vh] gap-2">
<FileText className="size-12 text-muted-foreground" />
<p className="text-base text-muted-foreground">No invoices found</p>
<p className="text-sm text-muted-foreground">
Your invoices will appear here once you have a subscription
</p>
</div>
)}
</div>
);
};

View File

@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2, Palette, User } from "lucide-react"; import { Loader2, User } from "lucide-react";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -27,7 +27,6 @@ import {
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 { Switch } from "@/components/ui/switch";
import { getAvatarType, isSolidColorAvatar } from "@/lib/avatar-utils";
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils"; import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Configure2FA } from "./configure-2fa"; import { Configure2FA } from "./configure-2fa";
@@ -75,7 +74,6 @@ export const ProfileForm = () => {
} = api.user.update.useMutation(); } = api.user.update.useMutation();
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const [gravatarHash, setGravatarHash] = useState<string | null>(null); const [gravatarHash, setGravatarHash] = useState<string | null>(null);
const colorInputRef = useRef<HTMLInputElement>(null);
const availableAvatars = useMemo(() => { const availableAvatars = useMemo(() => {
if (gravatarHash === null) return randomImages; if (gravatarHash === null) return randomImages;
@@ -276,8 +274,16 @@ export const ProfileForm = () => {
onValueChange={(e) => { onValueChange={(e) => {
field.onChange(e); field.onChange(e);
}} }}
defaultValue={getAvatarType(field.value)} defaultValue={
value={getAvatarType(field.value)} field.value?.startsWith("data:")
? "upload"
: field.value
}
value={
field.value?.startsWith("data:")
? "upload"
: field.value
}
className="flex flex-row flex-wrap gap-2 max-xl:justify-center" className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
> >
<FormItem key="no-avatar"> <FormItem key="no-avatar">
@@ -364,40 +370,6 @@ export const ProfileForm = () => {
/> />
</FormLabel> </FormLabel>
</FormItem> </FormItem>
<FormItem key="color-avatar">
<FormLabel className="[&:has([data-state=checked])>.color-avatar]:border-primary [&:has([data-state=checked])>.color-avatar]:border-1 [&:has([data-state=checked])>.color-avatar]:p-px cursor-pointer relative">
<FormControl>
<RadioGroupItem
value="color"
className="sr-only"
/>
</FormControl>
<div
className="color-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-colors flex items-center justify-center overflow-hidden cursor-pointer"
style={{
backgroundColor: isSolidColorAvatar(
field.value,
)
? field.value
: undefined,
}}
onClick={() =>
colorInputRef.current?.click()
}
>
{!isSolidColorAvatar(field.value) && (
<Palette className="h-5 w-5 text-muted-foreground" />
)}
</div>
<input
ref={colorInputRef}
type="color"
className="absolute opacity-0 pointer-events-none w-12 h-12 top-0 left-0"
value={field.value}
onChange={field.onChange}
/>
</FormLabel>
</FormItem>
{availableAvatars.map((image) => ( {availableAvatars.map((image) => (
<FormItem key={image}> <FormItem key={image}>
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer"> <FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">

View File

@@ -7,12 +7,9 @@ interface Props {
serverId?: string; serverId?: string;
} }
export const ToggleDockerCleanup = ({ serverId }: Props) => { export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.settings.getWebServerSettings.useQuery( const { data, refetch } = api.user.get.useQuery(undefined, {
undefined, enabled: !serverId,
{ });
enabled: !serverId,
},
);
const { data: server, refetch: refetchServer } = api.server.one.useQuery( const { data: server, refetch: refetchServer } = api.server.one.useQuery(
{ {
@@ -25,7 +22,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
const enabled = serverId const enabled = serverId
? server?.enableDockerCleanup ? server?.enableDockerCleanup
: data?.enableDockerCleanup; : data?.user.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
@@ -33,10 +30,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
try { try {
await mutateAsync({ await mutateAsync({
enableDockerCleanup: checked, enableDockerCleanup: checked,
...(serverId && { serverId }), serverId: serverId,
} as {
enableDockerCleanup: boolean;
serverId?: string;
}); });
if (serverId) { if (serverId) {
await refetchServer(); await refetchServer();

View File

@@ -80,7 +80,7 @@ const Schema = z.object({
type Schema = z.infer<typeof Schema>; type Schema = z.infer<typeof Schema>;
export const SetupMonitoring = ({ serverId }: Props) => { export const SetupMonitoring = ({ serverId }: Props) => {
const { data: serverData } = serverId const { data } = serverId
? api.server.one.useQuery( ? api.server.one.useQuery(
{ {
serverId: serverId || "", serverId: serverId || "",
@@ -89,14 +89,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
enabled: !!serverId, enabled: !!serverId,
}, },
) )
: { data: null }; : api.user.getServerMetrics.useQuery();
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery(undefined, {
enabled: !serverId,
});
const data = serverId ? serverData : webServerSettings;
const url = useUrl(); const url = useUrl();

View File

@@ -22,6 +22,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -88,15 +89,15 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
) : ( ) : (
<Button <DropdownMenuItem
className="w-full cursor-pointer " className="w-full cursor-pointer "
size="sm" onSelect={(e) => {
onClick={() => { e.preventDefault();
setIsOpen(true); setIsOpen(true);
}} }}
> >
Setup Server <Settings className="size-4" /> Setup Server
</Button> </DropdownMenuItem>
)} )}
<DialogContent className="sm:max-w-4xl "> <DialogContent className="sm:max-w-4xl ">
<DialogHeader> <DialogHeader>

View File

@@ -6,7 +6,9 @@ import {
Loader2, Loader2,
MoreHorizontal, MoreHorizontal,
Network, Network,
Pencil,
ServerIcon, ServerIcon,
Settings,
Terminal, Terminal,
Trash2, Trash2,
User, User,
@@ -29,7 +31,9 @@ import {
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { import {
@@ -281,32 +285,7 @@ export const ShowServers = () => {
{/* Compact Actions */} {/* Compact Actions */}
{isActive && ( {isActive && (
<div className="flex items-center gap-2 pt-3 border-t mt-auto flex-wrap"> <div className="flex items-center gap-2 pt-3 border-t mt-auto">
<div className="flex items-center gap-2 w-full">
<Tooltip>
<TooltipTrigger asChild>
<SetupServer
serverId={server.serverId}
/>
</TooltipTrigger>
<TooltipContent
className="max-w-xs"
side="bottom"
>
<div className="space-y-1">
<p className="font-semibold">
Setup Server
</p>
<p className="text-xs text-muted-foreground">
Configure and initialize your
server with Docker, Traefik, and
other essential services
</p>
</div>
</TooltipContent>
</Tooltip>
</div>
<TooltipProvider> <TooltipProvider>
{server.sshKeyId && ( {server.sshKeyId && (
<Tooltip> <Tooltip>
@@ -332,6 +311,20 @@ export const ShowServers = () => {
</Tooltip> </Tooltip>
)} )}
<Tooltip>
<TooltipTrigger asChild>
<div>
<SetupServer
serverId={server.serverId}
asButton={true}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Setup Server</p>
</TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div> <div>

View File

@@ -67,7 +67,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => { export const WebDomain = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { data, refetch } = api.settings.getWebServerSettings.useQuery(); const { data, refetch } = api.user.get.useQuery();
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation(); api.settings.assignDomainServer.useMutation();
@@ -82,15 +82,15 @@ export const WebDomain = () => {
}); });
const https = form.watch("https"); const https = form.watch("https");
const domain = form.watch("domain") || ""; const domain = form.watch("domain") || "";
const host = data?.host || ""; const host = data?.user?.host || "";
const hasChanged = domain !== host; const hasChanged = domain !== host;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
domain: data?.host || "", domain: data?.user?.host || "",
certificateType: data?.certificateType || "none", certificateType: data?.user?.certificateType,
letsEncryptEmail: data?.letsEncryptEmail || "", letsEncryptEmail: data?.user?.letsEncryptEmail || "",
https: data?.https || false, https: data?.user?.https || false,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);

View File

@@ -16,8 +16,7 @@ import { UpdateServer } from "./web-server/update-server";
export const WebServer = () => { export const WebServer = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { data: webServerSettings } = const { data } = api.user.get.useQuery();
api.settings.getWebServerSettings.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
@@ -54,7 +53,7 @@ export const WebServer = () => {
<div className="flex items-center flex-wrap justify-between gap-4"> <div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Server IP: {webServerSettings?.serverIp} Server IP: {data?.user.serverIp}
</span> </span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Version: {dokployVersion} Version: {dokployVersion}

View File

@@ -46,15 +46,15 @@ interface Props {
export const UpdateServerIp = ({ children }: Props) => { export const UpdateServerIp = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { data, refetch } = api.settings.getWebServerSettings.useQuery(); const { data } = api.user.get.useQuery();
const { data: ip } = api.server.publicIp.useQuery(); const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.settings.updateServerIp.useMutation(); api.user.update.useMutation();
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
serverIp: data?.serverIp || "", serverIp: data?.user.serverIp || "",
}, },
resolver: zodResolver(schema), resolver: zodResolver(schema),
}); });
@@ -62,11 +62,13 @@ export const UpdateServerIp = ({ children }: Props) => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
serverIp: data.serverIp || "", serverIp: data.user.serverIp || "",
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
const utils = api.useUtils();
const setCurrentIp = () => { const setCurrentIp = () => {
if (!ip) return; if (!ip) return;
form.setValue("serverIp", ip); form.setValue("serverIp", ip);
@@ -78,7 +80,7 @@ export const UpdateServerIp = ({ children }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Server IP Updated"); toast.success("Server IP Updated");
await refetch(); await utils.user.get.invalidate();
setIsOpen(false); setIsOpen(false);
}) })
.catch(() => { .catch(() => {

View File

@@ -10,7 +10,7 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
return ( return (
<div className="flex w-full items-center space-x-2"> <div className="flex w-full items-center space-x-2">
<Input ref={inputRef} {...props} type="password" /> <Input ref={inputRef} type={"password"} {...props} />
<Button <Button
variant={"secondary"} variant={"secondary"}
onClick={() => { onClick={() => {

View File

@@ -1,6 +1,6 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar"; import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react"; import * as React from "react";
import { isSolidColorAvatar } from "@/lib/avatar-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
@@ -20,33 +20,14 @@ Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef< const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> & { React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
src?: string | null; >(({ className, ...props }, ref) => (
} <AvatarPrimitive.Image
>(({ className, src, ...props }, ref) => { ref={ref}
if (isSolidColorAvatar(src)) { className={cn("aspect-square h-full w-full", className)}
return ( {...props}
<div />
key={`solid-${src}`} ));
ref={ref}
className={cn("aspect-square h-full w-full rounded-full", className)}
style={{
backgroundColor: src,
}}
{...props}
/>
);
}
return (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
src={src ?? ""}
{...props}
/>
);
});
AvatarImage.displayName = AvatarPrimitive.Image.displayName; AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<

View File

@@ -1,75 +1,18 @@
import { EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react"; import { EyeIcon, EyeOffIcon } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { generateRandomPassword } from "@/lib/password-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { extends React.InputHTMLAttributes<HTMLInputElement> {
errorMessage?: string; errorMessage?: string;
enablePasswordGenerator?: boolean;
passwordGeneratorLength?: number;
} }
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
( ({ className, errorMessage, type, ...props }, ref) => {
{
className,
errorMessage,
type,
enablePasswordGenerator = false,
passwordGeneratorLength,
...props
},
ref,
) => {
const [showPassword, setShowPassword] = React.useState(false); const [showPassword, setShowPassword] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const isPassword = type === "password"; const isPassword = type === "password";
const shouldShowGenerator =
isPassword &&
enablePasswordGenerator !== false &&
!props.disabled &&
!props.readOnly;
const inputType = isPassword ? (showPassword ? "text" : "password") : type; const inputType = isPassword ? (showPassword ? "text" : "password") : type;
const setRefs = React.useCallback(
(node: HTMLInputElement | null) => {
// @ts-ignore
inputRef.current = node;
if (typeof ref === "function") {
ref(node);
} else if (ref) {
ref.current = node;
}
},
[ref],
);
const handleGeneratePassword = () => {
const nextValue =
typeof passwordGeneratorLength === "number" &&
passwordGeneratorLength > 0
? generateRandomPassword(Math.floor(passwordGeneratorLength))
: generateRandomPassword();
const input = inputRef.current;
if (!input) {
return;
}
const valueSetter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
)?.set;
if (valueSetter) {
valueSetter.call(input, nextValue);
} else {
input.value = nextValue;
}
input.dispatchEvent(new Event("input", { bubbles: true }));
};
return ( return (
<> <>
<div className="relative w-full"> <div className="relative w-full">
@@ -78,39 +21,25 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
className={cn( className={cn(
// bg-gray // bg-gray
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"), isPassword && "pr-10", // Add padding for the eye icon
className, className,
)} )}
ref={setRefs} ref={ref}
{...props} {...props}
/> />
{isPassword && ( {isPassword && (
<div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground"> <button
{shouldShowGenerator && ( type="button"
<button className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground focus:outline-none"
type="button" onClick={() => setShowPassword(!showPassword)}
className="hover:text-foreground focus:outline-none" tabIndex={-1}
onClick={handleGeneratePassword} >
aria-label="Generate password" {showPassword ? (
title="Generate password" <EyeOffIcon className="h-4 w-4" />
tabIndex={-1} ) : (
> <EyeIcon className="h-4 w-4" />
<RefreshCcw className="h-4 w-4" />
</button>
)} )}
<button </button>
type="button"
className="hover:text-foreground focus:outline-none"
onClick={() => setShowPassword(!showPassword)}
tabIndex={-1}
>
{showPassword ? (
<EyeOffIcon className="h-4 w-4" />
) : (
<EyeIcon className="h-4 w-4" />
)}
</button>
</div>
)} )}
</div> </div>
{errorMessage && ( {errorMessage && (

View File

@@ -1,114 +0,0 @@
CREATE TABLE "webServerSettings" (
"id" text PRIMARY KEY NOT NULL,
"serverIp" text,
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
"https" boolean DEFAULT false NOT NULL,
"host" text,
"letsEncryptEmail" text,
"sshPrivateKey" text,
"enableDockerCleanup" boolean DEFAULT true NOT NULL,
"logCleanupCron" text DEFAULT '0 0 * * *',
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL,
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now() NOT NULL
);
-- Migrate data from user table to webServerSettings
-- Get the owner user's data and insert into webServerSettings
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
u."serverIp",
COALESCE(u."certificateType", 'none') as "certificateType",
COALESCE(u."https", false) as "https",
u."host",
u."letsEncryptEmail",
u."sshPrivateKey",
COALESCE(u."enableDockerCleanup", true) as "enableDockerCleanup",
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
COALESCE(
u."metricsConfig",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb
) as "metricsConfig",
COALESCE(u."cleanupCacheApplications", false) as "cleanupCacheApplications",
COALESCE(u."cleanupCacheOnPreviews", false) as "cleanupCacheOnPreviews",
COALESCE(u."cleanupCacheOnCompose", false) as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
FROM "user" u
INNER JOIN "member" m ON u."id" = m."user_id"
WHERE m."role" = 'owner'
ORDER BY m."created_at" ASC
LIMIT 1;
-- If no owner found, create a default entry
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
NULL as "serverIp",
'none' as "certificateType",
false as "https",
NULL as "host",
NULL as "letsEncryptEmail",
NULL as "sshPrivateKey",
true as "enableDockerCleanup",
'0 0 * * *' as "logCleanupCron",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb as "metricsConfig",
false as "cleanupCacheApplications",
false as "cleanupCacheOnPreviews",
false as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
WHERE NOT EXISTS (
SELECT 1 FROM "webServerSettings"
);
--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "serverIp";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "certificateType";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "https";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "host";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "logCleanupCron";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "metricsConfig";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnCompose";

View File

@@ -1 +0,0 @@
ALTER TABLE "application" ALTER COLUMN "railpackVersion" SET DEFAULT '0.15.4';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -932,20 +932,6 @@
"when": 1765346573500, "when": 1765346573500,
"tag": "0132_clean_layla_miller", "tag": "0132_clean_layla_miller",
"breakpoints": true "breakpoints": true
},
{
"idx": 133,
"version": "7",
"when": 1766301478005,
"tag": "0133_striped_the_order",
"breakpoints": true
},
{
"idx": 134,
"version": "7",
"when": 1767871040249,
"tag": "0134_strong_hercules",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,30 +0,0 @@
/**
* Checks if the given avatar value represents a solid color in hexadecimal format.
*
* @param value Avatar value to check.
*
* @return True if the avatar is a solid color, false otherwise.
*/
export function isSolidColorAvatar(value?: string | null) {
return (
(value?.startsWith("#") && /^#[0-9A-Fa-f]{6}$/.test(value)) ||
value?.startsWith("color:") ||
false
);
}
/**
* Gets the avatar type for form selection (RadioGroup value).
*
* @param value Avatar value.
*
* @return "upload" for base64 images, "color" for solid colors, or the original value for other types.
*/
export function getAvatarType(value?: string | null) {
if (!value) return "";
if (value.startsWith("data:")) return "upload";
if (isSolidColorAvatar(value)) return "color";
return value;
}

View File

@@ -1,38 +0,0 @@
const DEFAULT_PASSWORD_LENGTH = 20;
const DEFAULT_PASSWORD_CHARSET =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
export const generateRandomPassword = (
length: number = DEFAULT_PASSWORD_LENGTH,
charset: string = DEFAULT_PASSWORD_CHARSET,
) => {
const safeLength =
Number.isFinite(length) && length > 0
? Math.floor(length)
: DEFAULT_PASSWORD_LENGTH;
if (safeLength <= 0 || charset.length === 0) {
return "";
}
const cryptoApi =
typeof globalThis !== "undefined" ? globalThis.crypto : undefined;
if (!cryptoApi?.getRandomValues) {
let fallback = "";
for (let i = 0; i < safeLength; i += 1) {
fallback += charset[Math.floor(Math.random() * charset.length)];
}
return fallback;
}
const values = new Uint32Array(safeLength);
cryptoApi.getRandomValues(values);
let result = "";
for (const value of values) {
result += charset[value % charset.length];
}
return result;
};

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.26.4", "version": "v0.26.3",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -109,6 +109,7 @@
"drizzle-orm": "^0.39.3", "drizzle-orm": "^0.39.3",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.5.1",
"fancy-ansi": "^0.1.3", "fancy-ansi": "^0.1.3",
"hi-base32": "^0.5.1",
"i18next": "^23.16.8", "i18next": "^23.16.8",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@@ -125,6 +126,7 @@
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
"octokit": "3.1.2", "octokit": "3.1.2",
"otpauth": "^9.4.0",
"pino": "9.4.0", "pino": "9.4.0",
"pino-pretty": "11.2.2", "pino-pretty": "11.2.2",
"postgres": "3.4.4", "postgres": "3.4.4",
@@ -138,6 +140,7 @@
"react-i18next": "^15.5.2", "react-i18next": "^15.5.2",
"react-markdown": "^9.1.0", "react-markdown": "^9.1.0",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"rotating-file-stream": "3.2.3",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"sonner": "^1.7.4", "sonner": "^1.7.4",
"ssh2": "1.15.0", "ssh2": "1.15.0",
@@ -153,11 +156,9 @@
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"yaml": "2.8.1", "yaml": "2.8.1",
"zod": "^3.25.32", "zod": "^3.25.32",
"zod-form-data": "^2.0.7", "zod-form-data": "^2.0.7"
"semver": "7.7.3"
}, },
"devDependencies": { "devDependencies": {
"@types/semver": "7.7.1",
"@types/shell-quote": "^1.7.5", "@types/shell-quote": "^1.7.5",
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",

View File

@@ -909,9 +909,7 @@ const EnvironmentPage = (
<ProjectEnvironment projectId={projectId}> <ProjectEnvironment projectId={projectId}>
<Button variant="outline">Project Environment</Button> <Button variant="outline">Project Environment</Button>
</ProjectEnvironment> </ProjectEnvironment>
{(auth?.role === "owner" || {(auth?.role === "owner" || auth?.canCreateServices) && (
auth?.role === "admin" ||
auth?.canCreateServices) && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button> <Button>
@@ -1034,7 +1032,6 @@ const EnvironmentPage = (
</Button> </Button>
</DialogAction> </DialogAction>
{(auth?.role === "owner" || {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && ( auth?.canDeleteServices) && (
<> <>
<DialogAction <DialogAction

View File

@@ -192,9 +192,7 @@ const Service = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateApplication applicationId={applicationId} /> <UpdateApplication applicationId={applicationId} />
{(auth?.role === "owner" || {(auth?.role === "owner" || auth?.canDeleteServices) && (
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={applicationId} type="application" /> <DeleteService id={applicationId} type="application" />
)} )}
</div> </div>

View File

@@ -182,9 +182,7 @@ const Service = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateCompose composeId={composeId} /> <UpdateCompose composeId={composeId} />
{(auth?.role === "owner" || {(auth?.role === "owner" || auth?.canDeleteServices) && (
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={composeId} type="compose" /> <DeleteService id={composeId} type="compose" />
)} )}
</div> </div>

View File

@@ -156,9 +156,7 @@ const Mariadb = (
</div> </div>
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateMariadb mariadbId={mariadbId} /> <UpdateMariadb mariadbId={mariadbId} />
{(auth?.role === "owner" || {(auth?.role === "owner" || auth?.canDeleteServices) && (
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={mariadbId} type="mariadb" /> <DeleteService id={mariadbId} type="mariadb" />
)} )}
</div> </div>

View File

@@ -155,9 +155,7 @@ const Mongo = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateMongo mongoId={mongoId} /> <UpdateMongo mongoId={mongoId} />
{(auth?.role === "owner" || {(auth?.role === "owner" || auth?.canDeleteServices) && (
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={mongoId} type="mongo" /> <DeleteService id={mongoId} type="mongo" />
)} )}
</div> </div>

View File

@@ -156,9 +156,7 @@ const MySql = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateMysql mysqlId={mysqlId} /> <UpdateMysql mysqlId={mysqlId} />
{(auth?.role === "owner" || {(auth?.role === "owner" || auth?.canDeleteServices) && (
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={mysqlId} type="mysql" /> <DeleteService id={mysqlId} type="mysql" />
)} )}
</div> </div>

View File

@@ -154,9 +154,7 @@ const Postgresql = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdatePostgres postgresId={postgresId} /> <UpdatePostgres postgresId={postgresId} />
{(auth?.role === "owner" || {(auth?.role === "owner" || auth?.canDeleteServices) && (
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={postgresId} type="postgres" /> <DeleteService id={postgresId} type="postgres" />
)} )}
</div> </div>

View File

@@ -154,9 +154,7 @@ const Redis = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateRedis redisId={redisId} /> <UpdateRedis redisId={redisId} />
{(auth?.role === "owner" || {(auth?.role === "owner" || auth?.canDeleteServices) && (
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={redisId} type="redis" /> <DeleteService id={redisId} type="redis" />
)} )}
</div> </div>

View File

@@ -1,63 +0,0 @@
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
const Page = () => {
return <ShowBillingInvoices />;
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Invoices">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { req, res } = ctx;
const { user, session } = await validateRequest(req);
if (!user || user.role !== "owner") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,8 +1,8 @@
import { import {
getWebServerSettings, findUserById,
IS_CLOUD, IS_CLOUD,
setupWebMonitoring, setupWebMonitoring,
updateWebServerSettings, updateUser,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { apiUpdateWebServerMonitoring } from "@/server/db/schema"; import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
@@ -11,7 +11,7 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
export const adminRouter = createTRPCRouter({ export const adminRouter = createTRPCRouter({
setupMonitoring: adminProcedure setupMonitoring: adminProcedure
.input(apiUpdateWebServerMonitoring) .input(apiUpdateWebServerMonitoring)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
try { try {
if (IS_CLOUD) { if (IS_CLOUD) {
throw new TRPCError({ throw new TRPCError({
@@ -19,8 +19,15 @@ export const adminRouter = createTRPCRouter({
message: "Feature disabled on cloud", message: "Feature disabled on cloud",
}); });
} }
const user = await findUserById(ctx.user.ownerId);
if (user.id !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to setup the monitoring",
});
}
await updateWebServerSettings({ await updateUser(user.id, {
metricsConfig: { metricsConfig: {
server: { server: {
type: "Dokploy", type: "Dokploy",
@@ -45,9 +52,8 @@ export const adminRouter = createTRPCRouter({
}, },
}); });
await setupWebMonitoring(); const currentServer = await setupWebMonitoring(user.id);
const settings = await getWebServerSettings(); return currentServer;
return settings;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -285,7 +285,6 @@ export const backupRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
await runWebServerBackup(backup); await runWebServerBackup(backup);
await keepLatestNBackups(backup);
return true; return true;
}), }),
listBackupFiles: protectedProcedure listBackupFiles: protectedProcedure

View File

@@ -17,8 +17,8 @@ import {
findGitProviderById, findGitProviderById,
findProjectById, findProjectById,
findServerById, findServerById,
findUserById,
getComposeContainer, getComposeContainer,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
loadServices, loadServices,
randomizeComposeFile, randomizeComposeFile,
@@ -430,11 +430,7 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true, removeOnFail: true,
}, },
); );
return { return { success: true, message: "Deployment queued" };
success: true,
message: "Deployment queued",
composeId: compose.composeId,
};
}), }),
redeploy: protectedProcedure redeploy: protectedProcedure
.input(apiRedeployCompose) .input(apiRedeployCompose)
@@ -472,11 +468,7 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true, removeOnFail: true,
}, },
); );
return { return { success: true, message: "Redeployment queued" };
success: true,
message: "Redeployment queued",
composeId: compose.composeId,
};
}), }),
stop: protectedProcedure stop: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
@@ -577,7 +569,8 @@ export const composeRouter = createTRPCRouter({
const template = await fetchTemplateFiles(input.id, input.baseUrl); const template = await fetchTemplateFiles(input.id, input.baseUrl);
let serverIp = "127.0.0.1"; const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
const project = await findProjectById(environment.projectId); const project = await findProjectById(environment.projectId);
@@ -586,9 +579,6 @@ export const composeRouter = createTRPCRouter({
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
@@ -813,16 +803,14 @@ export const composeRouter = createTRPCRouter({
const decodedData = Buffer.from(input.base64, "base64").toString( const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8", "utf-8",
); );
let serverIp = "127.0.0.1"; const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
if (compose.serverId) { if (compose.serverId) {
const server = await findServerById(compose.serverId); const server = await findServerById(compose.serverId);
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const templateData = JSON.parse(decodedData); const templateData = JSON.parse(decodedData);
const config = parse(templateData.config) as CompleteTemplate; const config = parse(templateData.config) as CompleteTemplate;
@@ -892,16 +880,14 @@ export const composeRouter = createTRPCRouter({
await removeDomainById(domain.domainId); await removeDomainById(domain.domainId);
} }
let serverIp = "127.0.0.1"; const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
if (compose.serverId) { if (compose.serverId) {
const server = await findServerById(compose.serverId); const server = await findServerById(compose.serverId);
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const templateData = JSON.parse(decodedData); const templateData = JSON.parse(decodedData);

View File

@@ -9,7 +9,6 @@ import {
findPreviewDeploymentById, findPreviewDeploymentById,
findServerById, findServerById,
generateTraefikMeDomain, generateTraefikMeDomain,
getWebServerSettings,
manageDomain, manageDomain,
removeDomain, removeDomain,
removeDomainById, removeDomainById,
@@ -108,13 +107,16 @@ export const domainRouter = createTRPCRouter({
}), }),
canGenerateTraefikMeDomains: protectedProcedure canGenerateTraefikMeDomains: protectedProcedure
.input(z.object({ serverId: z.string() })) .input(z.object({ serverId: z.string() }))
.query(async ({ input }) => { .query(async ({ input, ctx }) => {
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
if (input.serverId) { if (input.serverId) {
const server = await findServerById(input.serverId); const server = await findServerById(input.serverId);
return server.ipAddress; return server.ipAddress;
} }
const settings = await getWebServerSettings(); return organization?.owner.serverIp;
return settings?.serverIp || "";
}), }),
update: protectedProcedure update: protectedProcedure

View File

@@ -8,7 +8,6 @@ import {
createSlackNotification, createSlackNotification,
createTelegramNotification, createTelegramNotification,
findNotificationById, findNotificationById,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
removeNotificationById, removeNotificationById,
sendCustomNotification, sendCustomNotification,
@@ -67,6 +66,7 @@ import {
apiUpdateTelegram, apiUpdateTelegram,
notifications, notifications,
server, server,
user,
} from "@/server/db/schema"; } from "@/server/db/schema";
export const notificationRouter = createTRPCRouter({ export const notificationRouter = createTRPCRouter({
@@ -364,20 +364,21 @@ export const notificationRouter = createTRPCRouter({
let organizationId = ""; let organizationId = "";
let ServerName = ""; let ServerName = "";
if (input.ServerType === "Dokploy") { if (input.ServerType === "Dokploy") {
const settings = await getWebServerSettings(); const result = await db
if ( .select()
!settings?.metricsConfig?.server?.token || .from(user)
settings.metricsConfig.server.token !== input.Token .where(
) { sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
);
if (!result?.[0]?.id) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Token not found", message: "Token not found",
}); });
} }
// For Dokploy server type, we don't have a specific organizationId organizationId = result?.[0]?.id;
// This might need to be adjusted based on your business logic
organizationId = "";
ServerName = "Dokploy"; ServerName = "Dokploy";
} else { } else {
const result = await db const result = await db

View File

@@ -2,15 +2,11 @@ import {
findApplicationById, findApplicationById,
findPreviewDeploymentById, findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId, findPreviewDeploymentsByApplicationId,
IS_CLOUD,
removePreviewDeployment, removePreviewDeployment,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { apiFindAllByApplication } from "@/server/db/schema"; import { apiFindAllByApplication } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
export const previewDeploymentRouter = createTRPCRouter({ export const previewDeploymentRouter = createTRPCRouter({
@@ -64,55 +60,4 @@ export const previewDeploymentRouter = createTRPCRouter({
} }
return previewDeployment; return previewDeployment;
}), }),
redeploy: protectedProcedure
.input(
z.object({
previewDeploymentId: z.string(),
title: z.string().optional(),
description: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById(
input.previewDeploymentId,
);
if (
previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this preview deployment",
});
}
const application = await findApplicationById(
previewDeployment.applicationId,
);
const jobData: DeploymentJob = {
applicationId: previewDeployment.applicationId,
titleLog: input.title || "Rebuild Preview Deployment",
descriptionLog: input.description || "",
type: "redeploy",
applicationType: "application-preview",
previewDeploymentId: input.previewDeploymentId,
server: !!application.serverId,
};
if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
deploy(jobData).catch((error) => {
console.error("Background deployment failed:", error);
});
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
return true;
}),
}); });

View File

@@ -12,11 +12,11 @@ import {
DEFAULT_UPDATE_DATA, DEFAULT_UPDATE_DATA,
execAsync, execAsync,
findServerById, findServerById,
findUserById,
getDokployImage, getDokployImage,
getDokployImageTag, getDokployImageTag,
getLogCleanupStatus, getLogCleanupStatus,
getUpdateData, getUpdateData,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
parseRawConfig, parseRawConfig,
paths, paths,
@@ -40,7 +40,7 @@ import {
updateLetsEncryptEmail, updateLetsEncryptEmail,
updateServerById, updateServerById,
updateServerTraefik, updateServerTraefik,
updateWebServerSettings, updateUser,
writeConfig, writeConfig,
writeMainConfig, writeMainConfig,
writeTraefikConfigInPath, writeTraefikConfigInPath,
@@ -77,18 +77,11 @@ import {
} from "../trpc"; } from "../trpc";
export const settingsRouter = createTRPCRouter({ export const settingsRouter = createTRPCRouter({
getWebServerSettings: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return null;
}
const settings = await getWebServerSettings();
return settings;
}),
reloadServer: adminProcedure.mutation(async () => { reloadServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await reloadDockerResource("dokploy", undefined, packageInfo.version); await reloadDockerResource("dokploy");
return true; return true;
}), }),
cleanRedis: adminProcedure.mutation(async () => { cleanRedis: adminProcedure.mutation(async () => {
@@ -216,11 +209,11 @@ export const settingsRouter = createTRPCRouter({
}), }),
saveSSHPrivateKey: adminProcedure saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey) .input(apiSaveSSHKey)
.mutation(async ({ input }) => { .mutation(async ({ input, ctx }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await updateWebServerSettings({ await updateUser(ctx.user.ownerId, {
sshPrivateKey: input.sshPrivateKey, sshPrivateKey: input.sshPrivateKey,
}); });
@@ -228,36 +221,36 @@ export const settingsRouter = createTRPCRouter({
}), }),
assignDomainServer: adminProcedure assignDomainServer: adminProcedure
.input(apiAssignDomain) .input(apiAssignDomain)
.mutation(async ({ input }) => { .mutation(async ({ ctx, input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
const settings = await updateWebServerSettings({ const user = await updateUser(ctx.user.ownerId, {
host: input.host, host: input.host,
letsEncryptEmail: input.letsEncryptEmail, letsEncryptEmail: input.letsEncryptEmail,
certificateType: input.certificateType, certificateType: input.certificateType,
https: input.https, https: input.https,
}); });
if (!settings) { if (!user) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "Web server settings not found", message: "User not found",
}); });
} }
updateServerTraefik(settings, input.host); updateServerTraefik(user, input.host);
if (input.letsEncryptEmail) { if (input.letsEncryptEmail) {
updateLetsEncryptEmail(input.letsEncryptEmail); updateLetsEncryptEmail(input.letsEncryptEmail);
} }
return settings; return user;
}), }),
cleanSSHPrivateKey: adminProcedure.mutation(async () => { cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await updateWebServerSettings({ await updateUser(ctx.user.ownerId, {
sshPrivateKey: null, sshPrivateKey: null,
}); });
return true; return true;
@@ -317,11 +310,11 @@ export const settingsRouter = createTRPCRouter({
} }
} }
} else if (!IS_CLOUD) { } else if (!IS_CLOUD) {
const settingsUpdated = await updateWebServerSettings({ const userUpdated = await updateUser(ctx.user.ownerId, {
enableDockerCleanup: input.enableDockerCleanup, enableDockerCleanup: input.enableDockerCleanup,
}); });
if (settingsUpdated?.enableDockerCleanup) { if (userUpdated?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`, `Docker Cleanup ${new Date().toLocaleString()}] Running...`,
@@ -399,7 +392,7 @@ export const settingsRouter = createTRPCRouter({
return DEFAULT_UPDATE_DATA; return DEFAULT_UPDATE_DATA;
} }
return await getUpdateData(packageInfo.version); return await getUpdateData();
}), }),
updateServer: adminProcedure.mutation(async () => { updateServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
@@ -495,28 +488,13 @@ export const settingsRouter = createTRPCRouter({
return readConfigInPath(input.path, input.serverId); return readConfigInPath(input.path, input.serverId);
}), }),
getIp: protectedProcedure.query(async () => { getIp: protectedProcedure.query(async ({ ctx }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return ""; return true;
} }
const settings = await getWebServerSettings(); const user = await findUserById(ctx.user.ownerId);
return settings?.serverIp || ""; return user.serverIp;
}), }),
updateServerIp: adminProcedure
.input(
z.object({
serverIp: z.string(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const settings = await updateWebServerSettings({
serverIp: input.serverIp,
});
return settings;
}),
getOpenApiDocument: protectedProcedure.query( getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => { async ({ ctx }): Promise<unknown> => {

View File

@@ -81,7 +81,6 @@ export const stripeRouter = createTRPCRouter({
metadata: { metadata: {
adminId: owner.id, adminId: owner.id,
}, },
customer_email: owner.email,
allow_promotion_codes: true, allow_promotion_codes: true,
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`, success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`, cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
@@ -129,39 +128,4 @@ export const stripeRouter = createTRPCRouter({
return servers.length < user.serversQuantity; return servers.length < user.serversQuantity;
}), }),
getInvoices: adminProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const stripeCustomerId = user.stripeCustomerId;
if (!stripeCustomerId) {
return [];
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
try {
const invoices = await stripe.invoices.list({
customer: stripeCustomerId,
limit: 100,
});
return invoices.data.map((invoice) => ({
id: invoice.id,
number: invoice.number,
status: invoice.status,
amountDue: invoice.amount_due,
amountPaid: invoice.amount_paid,
currency: invoice.currency,
created: invoice.created,
dueDate: invoice.due_date,
hostedInvoiceUrl: invoice.hosted_invoice_url,
invoicePdf: invoice.invoice_pdf,
}));
} catch (_) {
return [];
}
}),
}); });

View File

@@ -5,7 +5,6 @@ import {
findUserById, findUserById,
getDokployUrl, getDokployUrl,
getUserByToken, getUserByToken,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
removeUserById, removeUserById,
sendEmailNotification, sendEmailNotification,
@@ -215,11 +214,10 @@ export const userRouter = createTRPCRouter({
}), }),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => { getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId); const user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings();
return { return {
serverIp: settings?.serverIp, serverIp: user.serverIp,
enabledFeatures: user.enablePaidFeatures, enabledFeatures: user.enablePaidFeatures,
metricsConfig: settings?.metricsConfig, metricsConfig: user?.metricsConfig,
}; };
}), }),
remove: protectedProcedure remove: protectedProcedure

View File

@@ -4,7 +4,6 @@ import {
deployPreviewApplication, deployPreviewApplication,
rebuildApplication, rebuildApplication,
rebuildCompose, rebuildCompose,
rebuildPreviewApplication,
updateApplicationStatus, updateApplicationStatus,
updateCompose, updateCompose,
updatePreviewDeployment, updatePreviewDeployment,
@@ -55,14 +54,7 @@ export const deploymentWorker = new Worker(
previewStatus: "running", previewStatus: "running",
}); });
if (job.data.type === "redeploy") { if (job.data.type === "deploy") {
await rebuildPreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
});
} else if (job.data.type === "deploy") {
await deployPreviewApplication({ await deployPreviewApplication({
applicationId: job.data.applicationId, applicationId: job.data.applicationId,
titleLog: job.data.titleLog, titleLog: job.data.titleLog,

View File

@@ -22,7 +22,7 @@ type DeployJob =
titleLog: string; titleLog: string;
descriptionLog: string; descriptionLog: string;
server?: boolean; server?: boolean;
type: "deploy" | "redeploy"; type: "deploy";
applicationType: "application-preview"; applicationType: "application-preview";
previewDeploymentId: string; previewDeploymentId: string;
serverId?: string; serverId?: string;

View File

@@ -58,7 +58,7 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta
WITH recent_metrics AS ( WITH recent_metrics AS (
SELECT metrics_json SELECT metrics_json
FROM container_metrics FROM container_metrics
WHERE container_name = ? WHERE container_name LIKE ? || '%'
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT ? LIMIT ?
) )
@@ -98,7 +98,7 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e
WITH recent_metrics AS ( WITH recent_metrics AS (
SELECT metrics_json SELECT metrics_json
FROM container_metrics FROM container_metrics
WHERE container_name = ? WHERE container_name LIKE ? || '%'
ORDER BY timestamp DESC ORDER BY timestamp DESC
) )
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC

View File

@@ -1,21 +1,32 @@
{ {
"name": "@dokploy/server", "name": "@dokploy/server",
"version": "1.0.0", "version": "1.0.0",
"main": "./src/index.ts", "main": "./dist/index.js",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts", ".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
"./db": { "./db": {
"import": "./src/db/index.ts", "import": "./dist/db/index.js",
"require": "./dist/db/index.cjs.js" "require": "./dist/db/index.cjs.js"
}, },
"./setup/*": { "./*": {
"import": "./src/setup/*.ts", "import": "./dist/*",
"require": "./dist/setup/index.cjs.js" "require": "./dist/*.cjs"
}, },
"./constants": { "./dist": {
"import": "./src/constants/index.ts", "import": "./dist/index.js",
"require": "./dist/constants.cjs.js" "require": "./dist/index.cjs.js"
},
"./dist/db": {
"import": "./dist/db/index.js",
"require": "./dist/db/index.cjs.js"
},
"./dist/db/schema": {
"import": "./dist/db/schema/index.js",
"require": "./dist/db/schema/index.cjs.js"
} }
}, },
"scripts": { "scripts": {
@@ -57,6 +68,7 @@
"drizzle-dbml-generator": "0.10.0", "drizzle-dbml-generator": "0.10.0",
"drizzle-orm": "^0.39.3", "drizzle-orm": "^0.39.3",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"yaml": "2.8.1", "yaml": "2.8.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"micromatch": "4.0.8", "micromatch": "4.0.8",
@@ -66,6 +78,7 @@
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
"octokit": "3.1.2", "octokit": "3.1.2",
"otpauth": "^9.4.0",
"pino": "9.4.0", "pino": "9.4.0",
"pino-pretty": "11.2.2", "pino-pretty": "11.2.2",
"postgres": "3.4.4", "postgres": "3.4.4",
@@ -73,16 +86,15 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"rotating-file-stream": "3.2.3",
"shell-quote": "^1.8.1", "shell-quote": "^1.8.1",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"ssh2": "1.15.0", "ssh2": "1.15.0",
"toml": "3.0.0", "toml": "3.0.0",
"ws": "8.16.0", "ws": "8.16.0",
"zod": "^3.25.32", "zod": "^3.25.32"
"semver": "7.7.3"
}, },
"devDependencies": { "devDependencies": {
"@types/semver": "7.7.1",
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23", "@types/dockerode": "3.3.23",

View File

@@ -277,7 +277,7 @@ table application {
replicas integer [not null, default: 1] replicas integer [not null, default: 1]
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
buildType buildType [not null, default: 'nixpacks'] buildType buildType [not null, default: 'nixpacks']
railpackVersion text [default: '0.15.4'] railpackVersion text [default: '0.2.2']
herokuVersion text [default: '24'] herokuVersion text [default: '24']
publishDirectory text publishDirectory text
isStaticSpa boolean isStaticSpa boolean

View File

@@ -177,7 +177,7 @@ export const applications = pgTable("application", {
.notNull() .notNull()
.default("idle"), .default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"), buildType: buildType("buildType").notNull().default("nixpacks"),
railpackVersion: text("railpackVersion").default("0.15.4"), railpackVersion: text("railpackVersion").default("0.2.2"),
herokuVersion: text("herokuVersion").default("24"), herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"), publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"), isStaticSpa: boolean("isStaticSpa"),

View File

@@ -35,4 +35,3 @@ export * from "./ssh-key";
export * from "./user"; export * from "./user";
export * from "./utils"; export * from "./utils";
export * from "./volume-backups"; export * from "./volume-backups";
export * from "./web-server-settings";

View File

@@ -3,6 +3,7 @@ import { relations } from "drizzle-orm";
import { import {
boolean, boolean,
integer, integer,
jsonb,
pgTable, pgTable,
text, text,
timestamp, timestamp,
@@ -14,6 +15,7 @@ import { account, apikey, organization } from "./account";
import { backups } from "./backups"; import { backups } from "./backups";
import { projects } from "./project"; import { projects } from "./project";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { certificateType } from "./shared";
/** /**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects. * database instance for multiple projects.
@@ -49,10 +51,73 @@ export const user = pgTable("user", {
banExpires: timestamp("ban_expires"), banExpires: timestamp("ban_expires"),
updatedAt: timestamp("updated_at").notNull(), updatedAt: timestamp("updated_at").notNull(),
// Admin // Admin
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
role: text("role").notNull().default("user"), role: text("role").notNull().default("user"),
// Metrics // Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false), enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
allowImpersonation: boolean("allowImpersonation").notNull().default(false), allowImpersonation: boolean("allowImpersonation").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
stripeCustomerId: text("stripeCustomerId"), stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"), stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0), serversQuantity: integer("serversQuantity").notNull().default(0),
@@ -138,6 +203,33 @@ export const apiFindOneUserByAuth = createSchema
// authId: true, // authId: true,
}) })
.required(); .required();
export const apiSaveSSHKey = createSchema
.pick({
sshPrivateKey: true,
})
.required();
export const apiAssignDomain = createSchema
.pick({
host: true,
certificateType: true,
letsEncryptEmail: true,
https: true,
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiUpdateDockerCleanup = createSchema
.pick({
enableDockerCleanup: true,
})
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTraefikConfig = z.object({ export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1), traefikConfig: z.string().min(1),
@@ -206,6 +298,32 @@ export const apiReadStatsLogs = z.object({
.optional(), .optional(),
}); });
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
email: z email: z
.string() .string()
@@ -216,4 +334,29 @@ export const apiUpdateUser = createSchema.partial().extend({
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(), name: z.string().optional(),
lastName: z.string().optional(), lastName: z.string().optional(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
logCleanupCron: z.string().optional().nullable(),
}); });

View File

@@ -1,178 +0,0 @@
import { relations } from "drizzle-orm";
import { boolean, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { certificateType } from "./shared";
export const webServerSettings = pgTable("webServerSettings", {
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
// Web Server Configuration
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
// Metrics Configuration
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
// Cache Cleanup Configuration
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const webServerSettingsRelations = relations(
webServerSettings,
() => ({}),
);
const createSchema = createInsertSchema(webServerSettings, {
id: z.string().min(1),
});
export const apiUpdateWebServerSettings = createSchema.partial().extend({
serverIp: z.string().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
https: z.boolean().optional(),
host: z.string().optional(),
letsEncryptEmail: z.string().email().optional().nullable(),
sshPrivateKey: z.string().optional(),
enableDockerCleanup: z.boolean().optional(),
logCleanupCron: z.string().optional().nullable(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
cleanupCacheApplications: z.boolean().optional(),
cleanupCacheOnPreviews: z.boolean().optional(),
cleanupCacheOnCompose: z.boolean().optional(),
});
export const apiAssignDomain = z
.object({
host: z.string(),
certificateType: z.enum(["letsencrypt", "none", "custom"]),
letsEncryptEmail: z.string().email().optional().nullable(),
https: z.boolean().optional(),
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiSaveSSHKey = z
.object({
sshPrivateKey: z.string(),
})
.required();
export const apiUpdateDockerCleanup = z.object({
enableDockerCleanup: z.boolean(),
serverId: z.string().optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});

View File

@@ -41,7 +41,6 @@ export * from "./services/settings";
export * from "./services/ssh-key"; export * from "./services/ssh-key";
export * from "./services/user"; export * from "./services/user";
export * from "./services/volume-backups"; export * from "./services/volume-backups";
export * from "./services/web-server-settings";
export * from "./setup/config-paths"; export * from "./setup/config-paths";
export * from "./setup/monitoring-setup"; export * from "./setup/monitoring-setup";
export * from "./setup/postgres-setup"; export * from "./setup/postgres-setup";

View File

@@ -9,10 +9,7 @@ import { IS_CLOUD } from "../constants";
import { db } from "../db"; import { db } from "../db";
import * as schema from "../db/schema"; import * as schema from "../db/schema";
import { getUserByToken } from "../services/admin"; import { getUserByToken } from "../services/admin";
import { import { updateUser } from "../services/user";
getWebServerSettings,
updateWebServerSettings,
} from "../services/web-server-settings";
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot"; import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
import { sendEmail } from "../verification/send-verification-email"; import { sendEmail } from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils"; import { getPublicIpWithFallback } from "../wss/utils";
@@ -38,20 +35,22 @@ const { handler, api } = betterAuth({
}, },
...(!IS_CLOUD && { ...(!IS_CLOUD && {
async trustedOrigins() { async trustedOrigins() {
const settings = await getWebServerSettings(); const admin = await db.query.member.findFirst({
if (!settings) { where: eq(schema.member.role, "owner"),
return []; with: {
user: true,
},
});
if (admin?.user) {
return [
...(admin.user.serverIp
? [`http://${admin.user.serverIp}:3000`]
: []),
...(admin.user.host ? [`https://${admin.user.host}`] : []),
];
} }
return [ return [];
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
...(settings?.host ? [`https://${settings?.host}`] : []),
...(process.env.NODE_ENV === "development"
? [
"http://localhost:3000",
"https://absolutely-handy-falcon.ngrok-free.app",
]
: []),
];
}, },
}), }),
emailVerification: { emailVerification: {
@@ -123,7 +122,7 @@ const { handler, api } = betterAuth({
}); });
if (!IS_CLOUD) { if (!IS_CLOUD) {
await updateWebServerSettings({ await updateUser(user.id, {
serverIp: await getPublicIpWithFallback(), serverIp: await getPublicIpWithFallback(),
}); });
} }

View File

@@ -8,7 +8,6 @@ import {
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { getWebServerSettings } from "./web-server-settings";
export const findUserById = async (userId: string) => { export const findUserById = async (userId: string) => {
const userResult = await db.query.user.findFirst({ const userResult = await db.query.user.findFirst({
@@ -108,11 +107,11 @@ export const getDokployUrl = async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return "https://app.dokploy.com"; return "https://app.dokploy.com";
} }
const settings = await getWebServerSettings(); const owner = await findOwner();
if (settings?.host) { if (owner.user.host) {
const protocol = settings?.https ? "https" : "http"; const protocol = owner.user.https ? "https" : "http";
return `${protocol}://${settings?.host}`; return `${protocol}://${owner.user.host}`;
} }
return `http://${settings?.serverIp}:${process.env.PORT}`; return `http://${owner.user.serverIp}:${process.env.PORT}`;
}; };

View File

@@ -6,8 +6,8 @@ import { generateObject } from "ai";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { findOrganizationById } from "./admin";
import { findServerById } from "./server"; import { findServerById } from "./server";
import { getWebServerSettings } from "./web-server-settings";
export const getAiSettingsByOrganizationId = async (organizationId: string) => { export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({ const aiSettings = await db.query.ai.findMany({
@@ -79,8 +79,8 @@ export const suggestVariants = async ({
let ip = ""; let ip = "";
if (!IS_CLOUD) { if (!IS_CLOUD) {
const settings = await getWebServerSettings(); const organization = await findOrganizationById(organizationId);
ip = settings?.serverIp || ""; ip = organization?.owner.serverIp || "";
} }
if (serverId) { if (serverId) {

View File

@@ -452,137 +452,6 @@ export const deployPreviewApplication = async ({
return true; return true;
}; };
export const rebuildPreviewApplication = async ({
applicationId,
titleLog = "Rebuild Preview Deployment",
descriptionLog = "",
previewDeploymentId,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
}) => {
const application = await findApplicationById(applicationId);
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
const deployment = await createDeploymentPreview({
title: titleLog,
description: descriptionLog,
previewDeploymentId: previewDeploymentId,
});
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
const issueParams = {
owner: application?.owner || "",
repository: application?.repository || "",
issue_number: previewDeployment.pullRequestNumber,
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
// Set application properties for preview deployment
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.rollbackActive = false;
application.buildRegistry = null;
application.rollbackRegistry = null;
application.registry = null;
const serverId = application.serverId;
let command = "set -e;";
// Only rebuild, don't clone repository
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
let command = "";
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
const serverId = application.buildServerId || application.serverId;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
});
throw error;
}
return true;
};
export const getApplicationStats = async (appName: string) => { export const getApplicationStats = async (appName: string) => {
if (appName === "dokploy") { if (appName === "dokploy") {
return await getAdvancedStats(appName); return await getAdvancedStats(appName);

View File

@@ -1,12 +1,12 @@
import dns from "node:dns"; import dns from "node:dns";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { generateRandomDomain } from "@dokploy/server/templates"; import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain"; import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema"; import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { detectCDNProvider } from "./cdn"; import { detectCDNProvider } from "./cdn";
import { findServerById } from "./server"; import { findServerById } from "./server";
@@ -61,9 +61,9 @@ export const generateTraefikMeDomain = async (
projectName: appName, projectName: appName,
}); });
} }
const settings = await getWebServerSettings(); const admin = await findUserById(userId);
return generateRandomDomain({ return generateRandomDomain({
serverIp: settings?.serverIp || "", serverIp: admin?.serverIp || "",
projectName: appName, projectName: appName,
}); });
}; };

View File

@@ -13,11 +13,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github"; import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application"; import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain"; import { manageDomain } from "../utils/traefik/domain";
import { findUserById } from "./admin";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment"; import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain"; import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github"; import { type Github, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect; export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -253,8 +253,8 @@ const generateWildcardDomain = async (
} }
if (!ip) { if (!ip) {
const settings = await getWebServerSettings(); const admin = await findUserById(userId);
ip = settings?.serverIp || ""; ip = admin?.serverIp || "";
} }
const slugIp = ip.replaceAll(".", "-"); const slugIp = ip.replaceAll(".", "-");

View File

@@ -5,12 +5,12 @@ import {
execAsync, execAsync,
execAsyncRemote, execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import semver from "semver";
import { import {
initializeStandaloneTraefik, initializeStandaloneTraefik,
initializeTraefikService, initializeTraefikService,
type TraefikOptions, type TraefikOptions,
} from "../setup/traefik-setup"; } from "../setup/traefik-setup";
export interface IUpdateData { export interface IUpdateData {
latestVersion: string | null; latestVersion: string | null;
updateAvailable: boolean; updateAvailable: boolean;
@@ -55,95 +55,56 @@ export const getServiceImageDigest = async () => {
}; };
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ /** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
export const getUpdateData = async ( export const getUpdateData = async (): Promise<IUpdateData> => {
currentVersion: string, let currentDigest: string;
): Promise<IUpdateData> => {
try { try {
const baseUrl = currentDigest = await getServiceImageDigest();
"https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
let url: string | null = `${baseUrl}?page_size=100`;
let allResults: { digest: string; name: string }[] = [];
// Fetch all tags from Docker Hub
while (url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
const data = (await response.json()) as {
next: string | null;
results: { digest: string; name: string }[];
};
allResults = allResults.concat(data.results);
url = data?.next;
}
const currentImageTag = getDokployImageTag();
// Special handling for canary and feature branches
// For development versions (canary/feature), don't perform update checks
// These are unstable versions that change frequently, and users on these
// branches are expected to manually manage updates
if (currentImageTag === "canary" || currentImageTag === "feature") {
const currentDigest = await getServiceImageDigest();
const latestDigest = allResults.find(
(t) => t.name === currentImageTag,
)?.digest;
if (!latestDigest) {
return DEFAULT_UPDATE_DATA;
}
if (currentDigest !== latestDigest) {
return {
latestVersion: currentImageTag,
updateAvailable: true,
};
}
return {
latestVersion: currentImageTag,
updateAvailable: false,
};
}
// For stable versions, use semver comparison
// Find the "latest" tag and get its digest
const latestTag = allResults.find((t) => t.name === "latest");
if (!latestTag) {
return DEFAULT_UPDATE_DATA;
}
// Find the versioned tag (v0.x.x) that has the same digest as "latest"
const latestVersionTag = allResults.find(
(t) => t.digest === latestTag.digest && t.name.startsWith("v"),
);
if (!latestVersionTag) {
return DEFAULT_UPDATE_DATA;
}
const latestVersion = latestVersionTag.name;
// Use semver to compare versions for stable releases
const cleanedCurrent = semver.clean(currentVersion);
const cleanedLatest = semver.clean(latestVersion);
if (!cleanedCurrent || !cleanedLatest) {
return DEFAULT_UPDATE_DATA;
}
// Check if the latest version is greater than the current version
const updateAvailable = semver.gt(cleanedLatest, cleanedCurrent);
return {
latestVersion,
updateAvailable,
};
} catch (error) { } catch (error) {
console.error("Error fetching update data:", error); // TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version.
return DEFAULT_UPDATE_DATA; return DEFAULT_UPDATE_DATA;
} }
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
let url: string | null = `${baseUrl}?page_size=100`;
let allResults: { digest: string; name: string }[] = [];
while (url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
const data = (await response.json()) as {
next: string | null;
results: { digest: string; name: string }[];
};
allResults = allResults.concat(data.results);
url = data?.next;
}
const imageTag = getDokployImageTag();
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
if (!searchedDigest) {
return DEFAULT_UPDATE_DATA;
}
if (imageTag === "latest") {
const versionedTag = allResults.find(
(t) => t.digest === searchedDigest && t.name.startsWith("v"),
);
if (!versionedTag) {
return DEFAULT_UPDATE_DATA;
}
const { name: latestVersion, digest } = versionedTag;
const updateAvailable = digest !== currentDigest;
return { latestVersion, updateAvailable };
}
const updateAvailable = searchedDigest !== currentDigest;
return { latestVersion: imageTag, updateAvailable };
}; };
interface TreeDataItem { interface TreeDataItem {
@@ -293,22 +254,11 @@ fi`;
export const reloadDockerResource = async ( export const reloadDockerResource = async (
resourceName: string, resourceName: string,
serverId?: string, serverId?: string,
version?: string,
) => { ) => {
const resourceType = await getDockerResourceType(resourceName, serverId); const resourceType = await getDockerResourceType(resourceName, serverId);
let command = ""; let command = "";
if (resourceType === "service") { if (resourceType === "service") {
if (resourceName === "dokploy") { command = `docker service update --force ${resourceName}`;
const currentImageTag = getDokployImageTag();
let imageTag = version;
if (currentImageTag === "canary" || currentImageTag === "feature") {
imageTag = currentImageTag;
}
command = `docker service update --force --image dokploy/dokploy:${imageTag} ${resourceName}`;
} else {
command = `docker service update --force ${resourceName}`;
}
} else if (resourceType === "standalone") { } else if (resourceType === "standalone") {
command = `docker restart ${resourceName}`; command = `docker restart ${resourceName}`;
} else { } else {

View File

@@ -1,44 +0,0 @@
import { db } from "@dokploy/server/db";
import { webServerSettings } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
/**
* Get the web server settings (singleton - only one row should exist)
*/
export const getWebServerSettings = async () => {
const settings = await db.query.webServerSettings.findFirst({
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
});
if (!settings) {
// Create default settings if none exist
const [newSettings] = await db
.insert(webServerSettings)
.values({})
.returning();
return newSettings;
}
return settings;
};
/**
* Update web server settings
*/
export const updateWebServerSettings = async (
updates: Partial<typeof webServerSettings.$inferInsert>,
) => {
const current = await getWebServerSettings();
const [updated] = await db
.update(webServerSettings)
.set({
...updates,
updatedAt: new Date(),
})
.where(eq(webServerSettings.id, current?.id ?? ""))
.returning();
return updated;
};

View File

@@ -1,7 +1,7 @@
import { findServerById } from "@dokploy/server/services/server"; import { findServerById } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import type { ContainerCreateOptions } from "dockerode"; import type { ContainerCreateOptions } from "dockerode";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { findUserById } from "../services/admin";
import { getDokployImageTag } from "../services/settings"; import { getDokployImageTag } from "../services/settings";
import { pullImage, pullRemoteImage } from "../utils/docker/utils"; import { pullImage, pullRemoteImage } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
} }
}; };
export const setupWebMonitoring = async () => { export const setupWebMonitoring = async (userId: string) => {
const webServerSettings = await getWebServerSettings(); const user = await findUserById(userId);
const containerName = "dokploy-monitoring"; const containerName = "dokploy-monitoring";
let imageName = "dokploy/monitoring:latest"; let imageName = "dokploy/monitoring:latest";
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async () => {
const settings: ContainerCreateOptions = { const settings: ContainerCreateOptions = {
name: containerName, name: containerName,
Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`], Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
Image: imageName, Image: imageName,
HostConfig: { HostConfig: {
// Memory: 100 * 1024 * 1024, // 100MB en bytes // Memory: 100 * 1024 * 1024, // 100MB en bytes
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async () => {
Name: "always", Name: "always",
}, },
PortBindings: { PortBindings: {
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [ [`${user?.metricsConfig?.server?.port}/tcp`]: [
{ {
HostPort: webServerSettings?.metricsConfig?.server?.port.toString(), HostPort: user?.metricsConfig?.server?.port.toString(),
}, },
], ],
}, },
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async () => {
// NetworkMode: "host", // NetworkMode: "host",
}, },
ExposedPorts: { ExposedPorts: {
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {}, [`${user?.metricsConfig?.server?.port}/tcp`]: {},
}, },
}; };
const docker = await getRemoteDocker(); const docker = await getRemoteDocker();

View File

@@ -629,7 +629,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then if command_exists nixpacks; then
echo "Nixpacks already installed ✅" echo "Nixpacks already installed ✅"
else else
export NIXPACKS_VERSION=1.41.0 export NIXPACKS_VERSION=1.39.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅" echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi fi
@@ -639,7 +639,7 @@ const installRailpack = () => `
if command_exists railpack; then if command_exists railpack; then
echo "Railpack already installed ✅" echo "Railpack already installed ✅"
else else
export RAILPACK_VERSION=0.15.4 export RAILPACK_VERSION=0.2.2
bash -c "$(curl -fsSL https://railpack.com/install.sh)" bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅" echo "Railpack version $RAILPACK_VERSION installed ✅"
fi fi
@@ -653,8 +653,8 @@ const installBuildpacks = () => `
if command_exists pack; then if command_exists pack; then
echo "Buildpacks already installed ✅" echo "Buildpacks already installed ✅"
else else
BUILDPACKS_VERSION=0.39.1 BUILDPACKS_VERSION=0.35.0
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅" echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
fi fi
`; `;

View File

@@ -1,8 +1,6 @@
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { import { findOwner } from "@dokploy/server/services/admin";
getWebServerSettings, import { updateUser } from "@dokploy/server/services/user";
updateWebServerSettings,
} from "@dokploy/server/services/web-server-settings";
import { scheduledJobs, scheduleJob } from "node-schedule"; import { scheduledJobs, scheduleJob } from "node-schedule";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
@@ -31,9 +29,12 @@ export const startLogCleanup = async (
} }
}); });
await updateWebServerSettings({ const owner = await findOwner();
logCleanupCron: cronExpression, if (owner) {
}); await updateUser(owner.user.id, {
logCleanupCron: cronExpression,
});
}
return true; return true;
} catch (error) { } catch (error) {
@@ -50,9 +51,12 @@ export const stopLogCleanup = async (): Promise<boolean> => {
} }
// Update database // Update database
await updateWebServerSettings({ const owner = await findOwner();
logCleanupCron: null, if (owner) {
}); await updateUser(owner.user.id, {
logCleanupCron: null,
});
}
return true; return true;
} catch (error) { } catch (error) {
@@ -65,8 +69,8 @@ export const getLogCleanupStatus = async (): Promise<{
enabled: boolean; enabled: boolean;
cronExpression: string | null; cronExpression: string | null;
}> => { }> => {
const settings = await getWebServerSettings(); const owner = await findOwner();
const cronExpression = settings?.logCleanupCron ?? null; const cronExpression = owner?.user.logCleanupCron ?? null;
return { return {
enabled: cronExpression !== null, enabled: cronExpression !== null,
cronExpression, cronExpression,

View File

@@ -71,9 +71,8 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
return createOpenAICompatible({ return createOpenAICompatible({
name: "gemini", name: "gemini",
baseURL: config.apiUrl, baseURL: config.apiUrl,
headers: { queryParams: { key: config.apiKey },
Authorization: `Bearer ${config.apiKey}`, headers: {},
},
}); });
case "custom": case "custom":
return createOpenAICompatible({ return createOpenAICompatible({

View File

@@ -2,7 +2,6 @@ import path from "node:path";
import { member } from "@dokploy/server/db/schema"; import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server"; import { getAllServers } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule"; import { scheduleJob } from "node-schedule";
import { db } from "../../db/index"; import { db } from "../../db/index";
@@ -26,9 +25,7 @@ export const initCronJobs = async () => {
return; return;
} }
const webServerSettings = await getWebServerSettings(); if (admin?.user?.enableDockerCleanup) {
if (webServerSettings?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`, `Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
@@ -85,12 +82,9 @@ export const initCronJobs = async () => {
} }
} }
if (webServerSettings?.logCleanupCron) { if (admin?.user?.logCleanupCron) {
console.log( console.log("Starting log requests cleanup", admin.user.logCleanupCron);
"Starting log requests cleanup", await startLogCleanup(admin.user.logCleanupCron);
webServerSettings.logCleanupCron,
);
await startLogCleanup(webServerSettings.logCleanupCron);
} }
}; };

View File

@@ -90,7 +90,7 @@ export const createCommand = (compose: ComposeNested) => {
if (composeType === "docker-compose") { if (composeType === "docker-compose") {
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`; command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
} else if (composeType === "stack") { } else if (composeType === "stack") {
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`; command = `stack deploy -c ${path} ${appName} --prune`;
} }
return command; return command;

View File

@@ -167,9 +167,15 @@ while true; do
fi fi
done done
# Execute command and capture exit code
${exec} ${exec}
EXIT_CODE=$?
echo "Execution completed." # Wait for all background processes to complete to prevent zombie processes
wait
echo "Execution completed with exit code: $EXIT_CODE"
exit $EXIT_CODE
`; `;
const cleanupCommands = { const cleanupCommands = {

View File

@@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { webServerSettings } from "@dokploy/server/db/schema/web-server-settings"; import type { User } from "@dokploy/server/services/user";
import { parse, stringify } from "yaml"; import { parse, stringify } from "yaml";
import { import {
loadOrCreateConfig, loadOrCreateConfig,
@@ -12,10 +12,10 @@ import type { FileConfig } from "./file-types";
import type { MainTraefikConfig } from "./types"; import type { MainTraefikConfig } from "./types";
export const updateServerTraefik = ( export const updateServerTraefik = (
settings: typeof webServerSettings.$inferSelect | null, user: User | null,
newHost: string | null, newHost: string | null,
) => { ) => {
const { https, certificateType } = settings || {}; const { https, certificateType } = user || {};
const appName = "dokploy"; const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName); const config: FileConfig = loadOrCreateConfig(appName);

99
pnpm-lock.yaml generated
View File

@@ -51,6 +51,9 @@ importers:
'@hono/zod-validator': '@hono/zod-validator':
specifier: 0.3.0 specifier: 0.3.0
version: 0.3.0(hono@4.7.10)(zod@3.25.32) version: 0.3.0(hono@4.7.10)(zod@3.25.32)
'@nerimity/mimiqueue':
specifier: 1.2.3
version: 1.2.3(redis@4.7.0)
dotenv: dotenv:
specifier: ^16.4.5 specifier: ^16.4.5
version: 16.4.5 version: 16.4.5
@@ -310,6 +313,9 @@ importers:
fancy-ansi: fancy-ansi:
specifier: ^0.1.3 specifier: ^0.1.3
version: 0.1.3 version: 0.1.3
hi-base32:
specifier: ^0.5.1
version: 0.5.1
i18next: i18next:
specifier: ^23.16.8 specifier: ^23.16.8
version: 23.16.8 version: 23.16.8
@@ -358,6 +364,9 @@ importers:
octokit: octokit:
specifier: 3.1.2 specifier: 3.1.2
version: 3.1.2 version: 3.1.2
otpauth:
specifier: ^9.4.0
version: 9.4.0
pino: pino:
specifier: 9.4.0 specifier: 9.4.0
version: 9.4.0 version: 9.4.0
@@ -397,9 +406,9 @@ importers:
recharts: recharts:
specifier: ^2.15.3 specifier: ^2.15.3
version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
semver: rotating-file-stream:
specifier: 7.7.3 specifier: 3.2.3
version: 7.7.3 version: 3.2.3
shell-quote: shell-quote:
specifier: ^1.8.1 specifier: ^1.8.1
version: 1.8.2 version: 1.8.2
@@ -485,9 +494,6 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: 18.3.0 specifier: 18.3.0
version: 18.3.0 version: 18.3.0
'@types/semver':
specifier: 7.7.1
version: 7.7.1
'@types/shell-quote': '@types/shell-quote':
specifier: ^1.7.5 specifier: ^1.7.5
version: 1.7.5 version: 1.7.5
@@ -675,6 +681,9 @@ importers:
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32) version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32)
hi-base32:
specifier: ^0.5.1
version: 0.5.1
lodash: lodash:
specifier: 4.17.21 specifier: 4.17.21
version: 4.17.21 version: 4.17.21
@@ -699,6 +708,9 @@ importers:
octokit: octokit:
specifier: 3.1.2 specifier: 3.1.2
version: 3.1.2 version: 3.1.2
otpauth:
specifier: ^9.4.0
version: 9.4.0
pino: pino:
specifier: 9.4.0 specifier: 9.4.0
version: 9.4.0 version: 9.4.0
@@ -720,9 +732,9 @@ importers:
react-dom: react-dom:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
semver: rotating-file-stream:
specifier: 7.7.3 specifier: 3.2.3
version: 7.7.3 version: 3.2.3
shell-quote: shell-quote:
specifier: ^1.8.1 specifier: ^1.8.1
version: 1.8.2 version: 1.8.2
@@ -778,9 +790,6 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: 18.3.0 specifier: 18.3.0
version: 18.3.0 version: 18.3.0
'@types/semver':
specifier: 7.7.1
version: 7.7.1
'@types/shell-quote': '@types/shell-quote':
specifier: ^1.7.5 specifier: ^1.7.5
version: 1.7.5 version: 1.7.5
@@ -1948,6 +1957,11 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@nerimity/mimiqueue@1.2.3':
resolution: {integrity: sha512-WPoGe417P+S0FLfl3psRBI5adcAWXb917vCF1qD2yGZ1ggBEnMH6UrUK464gzJEOpAlGt8BBbIp0tgCEazZ47A==}
peerDependencies:
redis: ^4.7.0
'@next/env@16.0.10': '@next/env@16.0.10':
resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==}
@@ -4052,9 +4066,6 @@ packages:
'@types/readable-stream@4.0.20': '@types/readable-stream@4.0.20':
resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==} resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
'@types/shell-quote@1.7.5': '@types/shell-quote@1.7.5':
resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==}
@@ -4297,6 +4308,9 @@ packages:
assertion-error@1.1.0: assertion-error@1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
async-await-queue@2.1.4:
resolution: {integrity: sha512-3DpDtxkKO0O/FPlWbk/CrbexjuSxWm1CH1bXlVNVyMBIkKHhT5D85gzHmGJokG3ibNGWQ7pHBmStxUW/z/0LYQ==}
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -5381,6 +5395,9 @@ packages:
help-me@5.0.0: help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
hi-base32@0.5.1:
resolution: {integrity: sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==}
highlight.js@10.7.3: highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
@@ -6415,6 +6432,9 @@ packages:
openapi-types@12.1.3: openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
otpauth@9.4.0:
resolution: {integrity: sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw==}
p-cancelable@3.0.0: p-cancelable@3.0.0:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
@@ -7044,6 +7064,10 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
rotating-file-stream@3.2.3:
resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==}
engines: {node: '>=14.0'}
rou3@0.5.1: rou3@0.5.1:
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
@@ -7073,6 +7097,11 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
semver@7.7.3: semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -8100,7 +8129,7 @@ snapshots:
'@commitlint/is-ignored@19.8.1': '@commitlint/is-ignored@19.8.1':
dependencies: dependencies:
'@commitlint/types': 19.8.1 '@commitlint/types': 19.8.1
semver: 7.7.3 semver: 7.7.2
'@commitlint/lint@19.8.1': '@commitlint/lint@19.8.1':
dependencies: dependencies:
@@ -8717,7 +8746,7 @@ snapshots:
nopt: 5.0.0 nopt: 5.0.0
npmlog: 5.0.1 npmlog: 5.0.1
rimraf: 3.0.2 rimraf: 3.0.2
semver: 7.7.3 semver: 7.7.2
tar: 6.2.1 tar: 6.2.1
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@@ -8743,6 +8772,11 @@ snapshots:
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
optional: true optional: true
'@nerimity/mimiqueue@1.2.3(redis@4.7.0)':
dependencies:
async-await-queue: 2.1.4
redis: 4.7.0
'@next/env@16.0.10': {} '@next/env@16.0.10': {}
'@next/swc-darwin-arm64@16.0.10': '@next/swc-darwin-arm64@16.0.10':
@@ -9303,7 +9337,7 @@ snapshots:
'@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.28.0 '@opentelemetry/semantic-conventions': 1.28.0
forwarded-parse: 2.1.2 forwarded-parse: 2.1.2
semver: 7.7.3 semver: 7.7.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -9504,7 +9538,7 @@ snapshots:
'@types/shimmer': 1.2.0 '@types/shimmer': 1.2.0
import-in-the-middle: 1.14.2 import-in-the-middle: 1.14.2
require-in-the-middle: 7.5.2 require-in-the-middle: 7.5.2
semver: 7.7.3 semver: 7.7.2
shimmer: 1.2.1 shimmer: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -9649,7 +9683,7 @@ snapshots:
'@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
semver: 7.7.3 semver: 7.7.2
'@opentelemetry/semantic-conventions@1.28.0': {} '@opentelemetry/semantic-conventions@1.28.0': {}
@@ -11397,8 +11431,6 @@ snapshots:
dependencies: dependencies:
'@types/node': 20.17.51 '@types/node': 20.17.51
'@types/semver@7.7.1': {}
'@types/shell-quote@1.7.5': {} '@types/shell-quote@1.7.5': {}
'@types/shimmer@1.2.0': {} '@types/shimmer@1.2.0': {}
@@ -11651,6 +11683,8 @@ snapshots:
assertion-error@1.1.0: {} assertion-error@1.1.0: {}
async-await-queue@2.1.4: {}
asynckit@0.4.0: {} asynckit@0.4.0: {}
atomic-sleep@1.0.0: {} atomic-sleep@1.0.0: {}
@@ -11796,7 +11830,7 @@ snapshots:
lodash: 4.17.21 lodash: 4.17.21
msgpackr: 1.11.4 msgpackr: 1.11.4
node-abort-controller: 3.1.1 node-abort-controller: 3.1.1
semver: 7.7.3 semver: 7.7.2
tslib: 2.8.1 tslib: 2.8.1
uuid: 9.0.1 uuid: 9.0.1
transitivePeerDependencies: transitivePeerDependencies:
@@ -12312,7 +12346,7 @@ snapshots:
'@one-ini/wasm': 0.1.1 '@one-ini/wasm': 0.1.1
commander: 10.0.1 commander: 10.0.1
minimatch: 9.0.1 minimatch: 9.0.1
semver: 7.7.3 semver: 7.7.2
electron-to-chromium@1.5.159: {} electron-to-chromium@1.5.159: {}
@@ -12626,7 +12660,7 @@ snapshots:
'@petamoriken/float16': 3.9.2 '@petamoriken/float16': 3.9.2
debug: 4.4.1 debug: 4.4.1
env-paths: 3.0.0 env-paths: 3.0.0
semver: 7.7.3 semver: 7.7.2
shell-quote: 1.8.2 shell-quote: 1.8.2
which: 4.0.0 which: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -12800,6 +12834,8 @@ snapshots:
help-me@5.0.0: {} help-me@5.0.0: {}
hi-base32@0.5.1: {}
highlight.js@10.7.3: {} highlight.js@10.7.3: {}
highlightjs-vue@1.0.0: {} highlightjs-vue@1.0.0: {}
@@ -13112,7 +13148,7 @@ snapshots:
lodash.isstring: 4.0.1 lodash.isstring: 4.0.1
lodash.once: 4.1.1 lodash.once: 4.1.1
ms: 2.1.3 ms: 2.1.3
semver: 7.7.3 semver: 7.7.2
jss-plugin-camel-case@10.10.0: jss-plugin-camel-case@10.10.0:
dependencies: dependencies:
@@ -13936,6 +13972,10 @@ snapshots:
openapi-types@12.1.3: {} openapi-types@12.1.3: {}
otpauth@9.4.0:
dependencies:
'@noble/hashes': 1.7.1
p-cancelable@3.0.0: {} p-cancelable@3.0.0: {}
p-limit@2.3.0: p-limit@2.3.0:
@@ -14620,6 +14660,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.41.1 '@rollup/rollup-win32-x64-msvc': 4.41.1
fsevents: 2.3.3 fsevents: 2.3.3
rotating-file-stream@3.2.3: {}
rou3@0.5.1: {} rou3@0.5.1: {}
run-parallel@1.2.0: run-parallel@1.2.0:
@@ -14644,7 +14686,10 @@ snapshots:
semver@6.3.1: {} semver@6.3.1: {}
semver@7.7.3: {} semver@7.7.2: {}
semver@7.7.3:
optional: true
serialize-error-cjs@0.1.4: {} serialize-error-cjs@0.1.4: {}

View File

@@ -276,7 +276,7 @@ table application {
replicas integer [not null, default: 1] replicas integer [not null, default: 1]
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
buildType buildType [not null, default: 'nixpacks'] buildType buildType [not null, default: 'nixpacks']
railpackVersion text [default: '0.15.4'] railpackVersion text [default: '0.2.2']
herokuVersion text [default: '24'] herokuVersion text [default: '24']
publishDirectory text publishDirectory text
isStaticSpa boolean isStaticSpa boolean