mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-20 06:35:22 +02:00
Merge branch 'canary' into feature/copy-logs-to-clipboard
This commit is contained in:
@@ -48,6 +48,7 @@ const baseApp: ApplicationNested = {
|
||||
dockerBuildStage: "",
|
||||
isPreviewDeploymentsActive: false,
|
||||
previewBuildArgs: null,
|
||||
previewBuildSecrets: null,
|
||||
previewCertificateType: "none",
|
||||
previewCustomCertResolver: null,
|
||||
previewEnv: null,
|
||||
@@ -73,6 +74,7 @@ const baseApp: ApplicationNested = {
|
||||
},
|
||||
},
|
||||
buildArgs: null,
|
||||
buildSecrets: null,
|
||||
buildPath: "/",
|
||||
gitlabPathNamespace: "",
|
||||
buildType: "nixpacks",
|
||||
|
||||
@@ -228,5 +228,58 @@ describe("helpers functions", () => {
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle JWT payload with newlines and whitespace by trimming them", () => {
|
||||
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
|
||||
const expiry = iat + 3600;
|
||||
const payloadWithNewlines = `{
|
||||
"role": "anon",
|
||||
"iss": "supabase",
|
||||
"exp": ${expiry}
|
||||
}
|
||||
`;
|
||||
const jwt = processValue(
|
||||
"${jwt:secret:payload}",
|
||||
{
|
||||
secret: "mysecret",
|
||||
payload: payloadWithNewlines,
|
||||
},
|
||||
mockSchema,
|
||||
);
|
||||
expect(jwt).toMatch(jwtMatchExp);
|
||||
const parts = jwt.split(".") as JWTParts;
|
||||
jwtCheckHeader(parts[0]);
|
||||
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||
expect(decodedPayload).toHaveProperty("role");
|
||||
expect(decodedPayload.role).toEqual("anon");
|
||||
expect(decodedPayload).toHaveProperty("iss");
|
||||
expect(decodedPayload.iss).toEqual("supabase");
|
||||
expect(decodedPayload).toHaveProperty("exp");
|
||||
expect(decodedPayload.exp).toEqual(expiry);
|
||||
});
|
||||
|
||||
it("should handle JWT payload with leading and trailing whitespace", () => {
|
||||
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
|
||||
const expiry = iat + 3600;
|
||||
const payloadWithWhitespace = ` {"role": "service_role", "iss": "supabase", "exp": ${expiry}} `;
|
||||
const jwt = processValue(
|
||||
"${jwt:secret:payload}",
|
||||
{
|
||||
secret: "mysecret",
|
||||
payload: payloadWithWhitespace,
|
||||
},
|
||||
mockSchema,
|
||||
);
|
||||
expect(jwt).toMatch(jwtMatchExp);
|
||||
const parts = jwt.split(".") as JWTParts;
|
||||
jwtCheckHeader(parts[0]);
|
||||
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||
expect(decodedPayload).toHaveProperty("role");
|
||||
expect(decodedPayload.role).toEqual("service_role");
|
||||
expect(decodedPayload).toHaveProperty("iss");
|
||||
expect(decodedPayload.iss).toEqual("supabase");
|
||||
expect(decodedPayload).toHaveProperty("exp");
|
||||
expect(decodedPayload.exp).toEqual(expiry);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,8 +25,10 @@ const baseApp: ApplicationNested = {
|
||||
registryUrl: "",
|
||||
watchPaths: [],
|
||||
buildArgs: null,
|
||||
buildSecrets: null,
|
||||
isPreviewDeploymentsActive: false,
|
||||
previewBuildArgs: null,
|
||||
previewBuildSecrets: null,
|
||||
triggerType: "push",
|
||||
previewCertificateType: "none",
|
||||
previewEnv: null,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { api } from "@/utils/api";
|
||||
const addEnvironmentSchema = z.object({
|
||||
env: z.string(),
|
||||
buildArgs: z.string(),
|
||||
buildSecrets: z.string(),
|
||||
});
|
||||
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
@@ -37,6 +38,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
defaultValues: {
|
||||
env: "",
|
||||
buildArgs: "",
|
||||
buildSecrets: "",
|
||||
},
|
||||
resolver: zodResolver(addEnvironmentSchema),
|
||||
});
|
||||
@@ -44,15 +46,18 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
// Watch form values
|
||||
const currentEnv = form.watch("env");
|
||||
const currentBuildArgs = form.watch("buildArgs");
|
||||
const currentBuildSecrets = form.watch("buildSecrets");
|
||||
const hasChanges =
|
||||
currentEnv !== (data?.env || "") ||
|
||||
currentBuildArgs !== (data?.buildArgs || "");
|
||||
currentBuildArgs !== (data?.buildArgs || "") ||
|
||||
currentBuildSecrets !== (data?.buildSecrets || "");
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
env: data.env || "",
|
||||
buildArgs: data.buildArgs || "",
|
||||
buildSecrets: data.buildSecrets || "",
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
@@ -61,6 +66,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
mutateAsync({
|
||||
env: formData.env,
|
||||
buildArgs: formData.buildArgs,
|
||||
buildSecrets: formData.buildSecrets,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -76,6 +82,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
form.reset({
|
||||
env: data?.env || "",
|
||||
buildArgs: data?.buildArgs || "",
|
||||
buildSecrets: data?.buildSecrets || "",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -104,13 +111,36 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildArgs"
|
||||
title="Build-time Variables"
|
||||
title="Build-time Arguments"
|
||||
description={
|
||||
<span>
|
||||
Available only at build-time. See documentation
|
||||
Arguments are available only at build-time. See
|
||||
documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/guide/build-args/"
|
||||
href="https://docs.docker.com/build/building/variables/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildSecrets"
|
||||
title="Build-time Secrets"
|
||||
description={
|
||||
<span>
|
||||
Secrets are specially designed for sensitive information and
|
||||
are only available at build-time. See documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/building/secrets/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -46,6 +46,7 @@ const schema = z
|
||||
.object({
|
||||
env: z.string(),
|
||||
buildArgs: z.string(),
|
||||
buildSecrets: z.string(),
|
||||
wildcardDomain: z.string(),
|
||||
port: z.number(),
|
||||
previewLimit: z.number(),
|
||||
@@ -109,6 +110,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
form.reset({
|
||||
env: data.previewEnv || "",
|
||||
buildArgs: data.previewBuildArgs || "",
|
||||
buildSecrets: data.previewBuildSecrets || "",
|
||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||
port: data.previewPort || 3000,
|
||||
previewLabels: data.previewLabels || [],
|
||||
@@ -127,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
updateApplication({
|
||||
previewEnv: formData.env,
|
||||
previewBuildArgs: formData.buildArgs,
|
||||
previewBuildSecrets: formData.buildSecrets,
|
||||
previewWildcard: formData.wildcardDomain,
|
||||
previewPort: formData.port,
|
||||
previewLabels: formData.previewLabels,
|
||||
@@ -467,13 +470,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildArgs"
|
||||
title="Build-time Variables"
|
||||
title="Build-time Arguments"
|
||||
description={
|
||||
<span>
|
||||
Available only at build-time. See documentation
|
||||
Arguments are available only at build-time. See
|
||||
documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/guide/build-args/"
|
||||
href="https://docs.docker.com/build/building/variables/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
}
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<Secrets
|
||||
name="buildSecrets"
|
||||
title="Build-time Secrets"
|
||||
description={
|
||||
<span>
|
||||
Secrets are specially designed for sensitive information
|
||||
and are only available at build-time. See
|
||||
documentation
|
||||
<a
|
||||
className="text-primary"
|
||||
href="https://docs.docker.com/build/building/secrets/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Terminal,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -33,6 +34,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
const [runningSchedules, setRunningSchedules] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const {
|
||||
data: schedules,
|
||||
isLoading: isLoadingSchedules,
|
||||
@@ -46,14 +50,27 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
enabled: !!id,
|
||||
},
|
||||
);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
||||
api.schedule.delete.useMutation();
|
||||
const { mutateAsync: runManually } = api.schedule.runManually.useMutation();
|
||||
|
||||
const { mutateAsync: runManually, isLoading } =
|
||||
api.schedule.runManually.useMutation();
|
||||
const handleRunManually = async (scheduleId: string) => {
|
||||
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
||||
try {
|
||||
await runManually({ scheduleId });
|
||||
toast.success("Schedule run successfully");
|
||||
await refetchSchedules();
|
||||
} catch {
|
||||
toast.error("Error running schedule");
|
||||
} finally {
|
||||
setRunningSchedules((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(scheduleId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||
@@ -67,7 +84,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
Schedule tasks to run automatically at specified intervals.
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
{schedules && schedules.length > 0 && (
|
||||
<HandleSchedules id={id} scheduleType={scheduleType} />
|
||||
)}
|
||||
@@ -75,7 +91,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
</CardHeader>
|
||||
<CardContent className="px-0">
|
||||
{isLoadingSchedules ? (
|
||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||
<span className="text-sm text-muted-foreground/70">
|
||||
Loading scheduled tasks...
|
||||
@@ -91,13 +107,13 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
return (
|
||||
<div
|
||||
key={schedule.scheduleId}
|
||||
className="flex items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||
className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-start gap-3 w-full sm:w-auto">
|
||||
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<Clock className="size-4 text-primary/70" />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-1.5 w-full sm:w-auto">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
||||
{schedule.name}
|
||||
@@ -132,16 +148,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
)}
|
||||
</div>
|
||||
{schedule.command && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="size-3.5 text-muted-foreground/70" />
|
||||
<code className="font-mono text-[10px] text-muted-foreground/70">
|
||||
<div className="flex items-start gap-2 max-w-full">
|
||||
<Terminal className="size-3.5 text-muted-foreground/70 flex-shrink-0 mt-0.5" />
|
||||
<code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]">
|
||||
{schedule.command}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-0.5 md:gap-1.5">
|
||||
<ShowDeploymentsModal
|
||||
id={schedule.scheduleId}
|
||||
@@ -149,10 +164,9 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
serverId={serverId || undefined}
|
||||
>
|
||||
<Button variant="ghost" size="icon">
|
||||
<ClipboardList className="size-4 transition-colors " />
|
||||
<ClipboardList className="size-4 transition-colors" />
|
||||
</Button>
|
||||
</ShowDeploymentsModal>
|
||||
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -160,37 +174,26 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
isLoading={isLoading}
|
||||
onClick={async () => {
|
||||
toast.success("Schedule run successfully");
|
||||
|
||||
await runManually({
|
||||
scheduleId: schedule.scheduleId,
|
||||
})
|
||||
.then(async () => {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 1500),
|
||||
);
|
||||
refetchSchedules();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error running schedule");
|
||||
});
|
||||
}}
|
||||
disabled={runningSchedules.has(schedule.scheduleId)}
|
||||
onClick={() =>
|
||||
handleRunManually(schedule.scheduleId)
|
||||
}
|
||||
>
|
||||
<Play className="size-4 transition-colors" />
|
||||
{runningSchedules.has(schedule.scheduleId) ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="size-4 transition-colors" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Run Manual Schedule</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<HandleSchedules
|
||||
scheduleId={schedule.scheduleId}
|
||||
id={id}
|
||||
scheduleType={scheduleType}
|
||||
/>
|
||||
|
||||
<DialogAction
|
||||
title="Delete Schedule"
|
||||
description="Are you sure you want to delete this schedule?"
|
||||
@@ -214,8 +217,8 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isDeleting}
|
||||
className="group hover:bg-red-500/10"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
|
||||
@@ -248,7 +248,7 @@ export const AdvancedEnvironmentSelector = ({
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Action buttons for non-production environments */}
|
||||
<EnvironmentVariables environmentId={environment.environmentId}>
|
||||
{/* <EnvironmentVariables environmentId={environment.environmentId}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -259,7 +259,7 @@ export const AdvancedEnvironmentSelector = ({
|
||||
>
|
||||
<Terminal className="h-3 w-3" />
|
||||
</Button>
|
||||
</EnvironmentVariables>
|
||||
</EnvironmentVariables> */}
|
||||
{environment.name !== "production" && (
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<Button
|
||||
|
||||
@@ -217,7 +217,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{(isError || isErrorConnection) && (
|
||||
<AlertBlock type="error" className="break-words">
|
||||
<AlertBlock type="error" className="w-full">
|
||||
{connectionError?.message || error?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Fingerprint, QrCode } from "lucide-react";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon, DownloadIcon, Fingerprint, QrCode } from "lucide-react";
|
||||
import QRCode from "qrcode";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -29,6 +30,12 @@ import {
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
@@ -54,6 +61,26 @@ type TwoFactorSetupData = {
|
||||
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||
type PinForm = z.infer<typeof PinSchema>;
|
||||
|
||||
const USERNAME_PLACEHOLDER = "%username%";
|
||||
const DATE_PLACEHOLDER = "%date%";
|
||||
const BACKUP_CODES_PLACEHOLDER = "%backupCodes%";
|
||||
|
||||
const backupCodeTemplate = `Dokploy - BACKUP VERIFICATION CODES
|
||||
|
||||
Points to note
|
||||
--------------
|
||||
# Each code can be used only once.
|
||||
# Do not share these codes with anyone.
|
||||
|
||||
Generated codes
|
||||
---------------
|
||||
Username: ${USERNAME_PLACEHOLDER}
|
||||
Generated on: ${DATE_PLACEHOLDER}
|
||||
|
||||
|
||||
${BACKUP_CODES_PLACEHOLDER}
|
||||
`;
|
||||
|
||||
export const Enable2FA = () => {
|
||||
const utils = api.useUtils();
|
||||
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
||||
@@ -62,6 +89,7 @@ export const Enable2FA = () => {
|
||||
const [step, setStep] = useState<"password" | "verify">("password");
|
||||
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||
const [otpValue, setOtpValue] = useState("");
|
||||
const { data: currentUser } = api.user.get.useQuery();
|
||||
|
||||
const handleVerifySubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -178,6 +206,54 @@ export const Enable2FA = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadBackupCodes = () => {
|
||||
if (!backupCodes || backupCodes.length === 0) {
|
||||
toast.error("No backup codes to download.");
|
||||
return;
|
||||
}
|
||||
|
||||
const backupCodesFormatted = backupCodes
|
||||
.map((code, index) => ` ${index + 1}. ${code}`)
|
||||
.join("\n");
|
||||
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
const filename = `dokploy-2fa-backup-codes-${year}${month}${day}.txt`;
|
||||
|
||||
const backupCodesText = backupCodeTemplate
|
||||
.replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown")
|
||||
.replace(DATE_PLACEHOLDER, date.toLocaleString())
|
||||
.replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted);
|
||||
|
||||
const blob = new Blob([backupCodesText], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleCopyBackupCodes = () => {
|
||||
const date = new Date();
|
||||
|
||||
const backupCodesFormatted = backupCodes
|
||||
.map((code, index) => ` ${index + 1}. ${code}`)
|
||||
.join("\n");
|
||||
|
||||
const backupCodesText = backupCodeTemplate
|
||||
.replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown")
|
||||
.replace(DATE_PLACEHOLDER, date.toLocaleString())
|
||||
.replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted);
|
||||
|
||||
copy(backupCodesText);
|
||||
toast.success("Backup codes copied to clipboard");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -264,6 +340,7 @@ export const Enable2FA = () => {
|
||||
<span className="text-sm font-medium">
|
||||
Scan this QR code with your authenticator app
|
||||
</span>
|
||||
{/** biome-ignore lint/performance/noImgElement: This is a valid use case for an img element */}
|
||||
<img
|
||||
src={data.qrCodeUrl}
|
||||
alt="2FA QR Code"
|
||||
@@ -281,7 +358,46 @@ export const Enable2FA = () => {
|
||||
|
||||
{backupCodes && backupCodes.length > 0 && (
|
||||
<div className="w-full space-y-3 border rounded-lg p-4">
|
||||
<h4 className="font-medium">Backup Codes</h4>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-medium">Backup Codes</h4>
|
||||
<div className="flex items-center gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleCopyBackupCodes}
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Copy</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleDownloadBackupCodes}
|
||||
>
|
||||
<DownloadIcon className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Download</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{backupCodes.map((code, index) => (
|
||||
<code
|
||||
|
||||
@@ -39,13 +39,19 @@ export function AlertBlock({
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex items-center flex-row gap-4 rounded-lg p-2",
|
||||
"flex items-start flex-row gap-4 rounded-lg p-2",
|
||||
iconClassName,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{icon || <Icon className="text-current" />}
|
||||
<span className="text-sm text-current">{children}</span>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{icon || <Icon className="text-current" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm text-current break-words overflow-wrap-anywhere whitespace-pre-wrap">
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const type = props.type ?? undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Comp
|
||||
@@ -65,6 +67,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
ref={ref}
|
||||
{...props}
|
||||
disabled={isLoading || props.disabled}
|
||||
type={type}
|
||||
>
|
||||
{isLoading && <Loader2 className="animate-spin" />}
|
||||
<Slottable>{children}</Slottable>
|
||||
|
||||
2
apps/dokploy/drizzle/0117_lumpy_nuke.sql
Normal file
2
apps/dokploy/drizzle/0117_lumpy_nuke.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "application" ADD COLUMN "previewBuildSecrets" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD COLUMN "buildSecrets" text;
|
||||
6634
apps/dokploy/drizzle/meta/0117_snapshot.json
Normal file
6634
apps/dokploy/drizzle/meta/0117_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -820,6 +820,13 @@
|
||||
"when": 1759645163834,
|
||||
"tag": "0116_amusing_firedrake",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 117,
|
||||
"version": "7",
|
||||
"when": 1761370953274,
|
||||
"tag": "0117_lumpy_nuke",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PlusIcon,
|
||||
Search,
|
||||
ServerIcon,
|
||||
SquareTerminal,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
@@ -33,6 +34,7 @@ import { AddDatabase } from "@/components/dashboard/project/add-database";
|
||||
import { AddTemplate } from "@/components/dashboard/project/add-template";
|
||||
import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector";
|
||||
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
||||
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
|
||||
import { ProjectEnvironment } from "@/components/dashboard/projects/project-environment";
|
||||
import {
|
||||
MariadbIcon,
|
||||
@@ -46,6 +48,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -95,7 +98,6 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
@@ -776,6 +778,11 @@ const EnvironmentPage = (
|
||||
projectId={projectId}
|
||||
currentEnvironmentId={environmentId}
|
||||
/>
|
||||
<EnvironmentVariables environmentId={environmentId}>
|
||||
<Button variant="ghost" size="icon">
|
||||
<SquareTerminal className="size-5 text-muted-foreground cursor-pointer" />
|
||||
</Button>
|
||||
</EnvironmentVariables>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{currentEnvironment.description || "No description provided"}
|
||||
|
||||
@@ -360,6 +360,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
await updateApplication(input.applicationId, {
|
||||
env: input.env,
|
||||
buildArgs: input.buildArgs,
|
||||
buildSecrets: input.buildSecrets,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
|
||||
@@ -22,7 +22,7 @@ export const getGiteaOAuthUrl = (
|
||||
}
|
||||
|
||||
const redirectUri = `${baseUrl}/api/providers/gitea/callback`;
|
||||
const scopes = "repo repo:status read:user read:org";
|
||||
const scopes = "read:repository read:user read:organization";
|
||||
|
||||
return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||
redirectUri,
|
||||
|
||||
@@ -80,6 +80,7 @@ export const applications = pgTable("application", {
|
||||
previewEnv: text("previewEnv"),
|
||||
watchPaths: text("watchPaths").array(),
|
||||
previewBuildArgs: text("previewBuildArgs"),
|
||||
previewBuildSecrets: text("previewBuildSecrets"),
|
||||
previewLabels: text("previewLabels").array(),
|
||||
previewWildcard: text("previewWildcard"),
|
||||
previewPort: integer("previewPort").default(3000),
|
||||
@@ -99,6 +100,7 @@ export const applications = pgTable("application", {
|
||||
).default(true),
|
||||
rollbackActive: boolean("rollbackActive").default(false),
|
||||
buildArgs: text("buildArgs"),
|
||||
buildSecrets: text("buildSecrets"),
|
||||
memoryReservation: text("memoryReservation"),
|
||||
memoryLimit: text("memoryLimit"),
|
||||
cpuReservation: text("cpuReservation"),
|
||||
@@ -253,6 +255,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
autoDeploy: z.boolean(),
|
||||
env: z.string().optional(),
|
||||
buildArgs: z.string().optional(),
|
||||
buildSecrets: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
memoryReservation: z.string().optional(),
|
||||
@@ -304,6 +307,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
previewPort: z.number().optional(),
|
||||
previewEnv: z.string().optional(),
|
||||
previewBuildArgs: z.string().optional(),
|
||||
previewBuildSecrets: z.string().optional(),
|
||||
previewWildcard: z.string().optional(),
|
||||
previewLimit: z.number().optional(),
|
||||
previewHttps: z.boolean().optional(),
|
||||
@@ -458,6 +462,7 @@ export const apiSaveEnvironmentVariables = createSchema
|
||||
applicationId: true,
|
||||
env: true,
|
||||
buildArgs: true,
|
||||
buildSecrets: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
@@ -473,6 +473,7 @@ export const deployPreviewApplication = async ({
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
application.buildSecrets = application.previewBuildSecrets;
|
||||
|
||||
if (application.sourceType === "github") {
|
||||
await cloneGithubRepository({
|
||||
@@ -580,6 +581,7 @@ export const deployRemotePreviewApplication = async ({
|
||||
application.appName = previewDeployment.appName;
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = application.previewBuildArgs;
|
||||
application.buildSecrets = application.previewBuildSecrets;
|
||||
|
||||
if (application.serverId) {
|
||||
let command = "set -e;";
|
||||
|
||||
@@ -141,8 +141,8 @@ export function processValue(
|
||||
}
|
||||
if (
|
||||
typeof payload === "string" &&
|
||||
payload.startsWith("{") &&
|
||||
payload.endsWith("}")
|
||||
payload.trimStart().startsWith("{") &&
|
||||
payload.trimEnd().endsWith("}")
|
||||
) {
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { prepareEnvironmentVariables } from "@dokploy/server/utils/docker/utils";
|
||||
import {
|
||||
getEnviromentVariablesObject,
|
||||
prepareEnvironmentVariables,
|
||||
} from "@dokploy/server/utils/docker/utils";
|
||||
import {
|
||||
getBuildAppDirectory,
|
||||
getDockerContextPath,
|
||||
@@ -17,6 +20,7 @@ export const buildCustomDocker = async (
|
||||
env,
|
||||
publishDirectory,
|
||||
buildArgs,
|
||||
buildSecrets,
|
||||
dockerBuildStage,
|
||||
cleanCache,
|
||||
} = application;
|
||||
@@ -26,11 +30,6 @@ export const buildCustomDocker = async (
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
const dockerContextPath = getDockerContextPath(application);
|
||||
|
||||
@@ -44,9 +43,29 @@ export const buildCustomDocker = async (
|
||||
commandArgs.push("--target", dockerBuildStage);
|
||||
}
|
||||
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
for (const arg of args) {
|
||||
commandArgs.push("--build-arg", arg);
|
||||
}
|
||||
|
||||
const secrets = getEnviromentVariablesObject(
|
||||
buildSecrets,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
for (const key in secrets) {
|
||||
// Although buildx is smart enough to know we may be referring to an environment variable name,
|
||||
// we still make sure it doesn't fall back to type=file.
|
||||
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
|
||||
commandArgs.push("--secret", `type=env,id=${key}`);
|
||||
}
|
||||
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
@@ -70,6 +89,10 @@ export const buildCustomDocker = async (
|
||||
},
|
||||
{
|
||||
cwd: dockerContextPath || defaultContextPath,
|
||||
env: {
|
||||
...process.env,
|
||||
...secrets,
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -86,6 +109,7 @@ export const getDockerCommand = (
|
||||
env,
|
||||
publishDirectory,
|
||||
buildArgs,
|
||||
buildSecrets,
|
||||
dockerBuildStage,
|
||||
cleanCache,
|
||||
} = application;
|
||||
@@ -96,11 +120,6 @@ export const getDockerCommand = (
|
||||
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
const dockerContextPath =
|
||||
getDockerContextPath(application) || defaultContextPath;
|
||||
@@ -115,10 +134,33 @@ export const getDockerCommand = (
|
||||
commandArgs.push("--no-cache");
|
||||
}
|
||||
|
||||
const args = prepareEnvironmentVariables(
|
||||
buildArgs,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
for (const arg of args) {
|
||||
commandArgs.push("--build-arg", `'${arg}'`);
|
||||
}
|
||||
|
||||
const secrets = getEnviromentVariablesObject(
|
||||
buildSecrets,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
|
||||
const joinedSecrets = Object.entries(secrets)
|
||||
.map(([key, value]) => `${key}='${value.replace(/'/g, "'\"'\"'")}'`)
|
||||
.join(" ");
|
||||
|
||||
for (const key in secrets) {
|
||||
// Although buildx is smart enough to know we may be referring to an environment variable name,
|
||||
// we still make sure it doesn't fall back to `type=file`.
|
||||
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
|
||||
commandArgs.push("--secret", `type=env,id=${key}`);
|
||||
}
|
||||
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
@@ -140,7 +182,7 @@ cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
|
||||
exit 1;
|
||||
}
|
||||
|
||||
docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||
${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||
echo "❌ Docker build failed" >> ${logPath};
|
||||
exit 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user