mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-15 00:15:17 +00:00
Merge branch 'canary' into Move-environment-variables-icon-outside-dropdown-#2755
This commit is contained in:
@@ -77,6 +77,10 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<div>
|
<div>
|
||||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
||||||
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
||||||
|
<a href="https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor" target="_blank">
|
||||||
|
<img src="https://www.lambdatest.com/blue-logo.png" width="450" height="100" />
|
||||||
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Premium Supporters 🥇 -->
|
<!-- Premium Supporters 🥇 -->
|
||||||
|
|||||||
@@ -228,5 +228,58 @@ describe("helpers functions", () => {
|
|||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
|
"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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -150,7 +150,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
<FormLabel>Memory Limit</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
@@ -182,7 +185,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
name="memoryReservation"
|
name="memoryReservation"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
<FormLabel>Memory Reservation</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
@@ -215,7 +221,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<FormLabel>CPU Limit</FormLabel>
|
<FormLabel>CPU Limit</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
@@ -249,7 +258,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<FormLabel>CPU Reservation</FormLabel>
|
<FormLabel>CPU Reservation</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
|
|||||||
+1
-1
@@ -150,7 +150,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules || false,
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
+1
-1
@@ -149,7 +149,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules,
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
+1
-1
@@ -167,7 +167,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules,
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -48,10 +48,8 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
||||||
api.schedule.delete.useMutation();
|
api.schedule.delete.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: runManually, isLoading } =
|
const { mutateAsync: runManually, isLoading } =
|
||||||
api.schedule.runManually.useMutation();
|
api.schedule.runManually.useMutation();
|
||||||
|
|
||||||
@@ -67,7 +65,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
Schedule tasks to run automatically at specified intervals.
|
Schedule tasks to run automatically at specified intervals.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{schedules && schedules.length > 0 && (
|
{schedules && schedules.length > 0 && (
|
||||||
<HandleSchedules id={id} scheduleType={scheduleType} />
|
<HandleSchedules id={id} scheduleType={scheduleType} />
|
||||||
)}
|
)}
|
||||||
@@ -75,7 +72,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-0">
|
<CardContent className="px-0">
|
||||||
{isLoadingSchedules ? (
|
{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" />
|
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||||
<span className="text-sm text-muted-foreground/70">
|
<span className="text-sm text-muted-foreground/70">
|
||||||
Loading scheduled tasks...
|
Loading scheduled tasks...
|
||||||
@@ -91,13 +88,13 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={schedule.scheduleId}
|
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">
|
<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" />
|
<Clock className="size-4 text-primary/70" />
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
||||||
{schedule.name}
|
{schedule.name}
|
||||||
@@ -132,27 +129,25 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{schedule.command && (
|
{schedule.command && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-start gap-2 max-w-full">
|
||||||
<Terminal className="size-3.5 text-muted-foreground/70" />
|
<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">
|
<code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]">
|
||||||
{schedule.command}
|
{schedule.command}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-0.5 md:gap-1.5 mt-2 sm:mt-0 sm:ml-3">
|
||||||
<div className="flex items-center gap-0.5 md:gap-1.5">
|
|
||||||
<ShowDeploymentsModal
|
<ShowDeploymentsModal
|
||||||
id={schedule.scheduleId}
|
id={schedule.scheduleId}
|
||||||
type="schedule"
|
type="schedule"
|
||||||
serverId={serverId || undefined}
|
serverId={serverId || undefined}
|
||||||
>
|
>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<ClipboardList className="size-4 transition-colors " />
|
<ClipboardList className="size-4 transition-colors" />
|
||||||
</Button>
|
</Button>
|
||||||
</ShowDeploymentsModal>
|
</ShowDeploymentsModal>
|
||||||
|
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -163,7 +158,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
toast.success("Schedule run successfully");
|
toast.success("Schedule run successfully");
|
||||||
|
|
||||||
await runManually({
|
await runManually({
|
||||||
scheduleId: schedule.scheduleId,
|
scheduleId: schedule.scheduleId,
|
||||||
})
|
})
|
||||||
@@ -178,19 +172,17 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Play className="size-4 transition-colors" />
|
<Play className="size-4 transition-colors" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Run Manual Schedule</TooltipContent>
|
<TooltipContent>Run Manual Schedule</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<HandleSchedules
|
<HandleSchedules
|
||||||
scheduleId={schedule.scheduleId}
|
scheduleId={schedule.scheduleId}
|
||||||
id={id}
|
id={id}
|
||||||
scheduleType={scheduleType}
|
scheduleType={scheduleType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Schedule"
|
title="Delete Schedule"
|
||||||
description="Are you sure you want to delete this schedule?"
|
description="Are you sure you want to delete this schedule?"
|
||||||
@@ -214,7 +206,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-red-500/10 "
|
className="group hover:bg-red-500/10"
|
||||||
isLoading={isDeleting}
|
isLoading={isDeleting}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
push(
|
push(
|
||||||
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
|
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
|
||||||
);
|
);
|
||||||
toast.success("deleted successfully");
|
toast.success("Service deleted successfully");
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
+1
-1
@@ -152,7 +152,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules,
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
triggerType: data.triggerType,
|
triggerType: data.triggerType,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
+1
-1
@@ -160,7 +160,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules,
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ export const ImpersonationBar = () => {
|
|||||||
<div className="flex items-center gap-4 flex-1 flex-wrap">
|
<div className="flex items-center gap-4 flex-1 flex-wrap">
|
||||||
<Avatar className="h-10 w-10">
|
<Avatar className="h-10 w-10">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
|
className="object-cover"
|
||||||
src={data?.user?.image || ""}
|
src={data?.user?.image || ""}
|
||||||
alt={data?.user?.name || ""}
|
alt={data?.user?.name || ""}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import { api } from "@/utils/api";
|
|||||||
type DbType = typeof mySchema._type.type;
|
type DbType = typeof mySchema._type.type;
|
||||||
|
|
||||||
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||||
mongo: "mongo:6",
|
mongo: "mongo:7",
|
||||||
mariadb: "mariadb:11",
|
mariadb: "mariadb:11",
|
||||||
mysql: "mysql:8",
|
mysql: "mysql:8",
|
||||||
postgres: "postgres:15",
|
postgres: "postgres:15",
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{(isError || isErrorConnection) && (
|
{(isError || isErrorConnection) && (
|
||||||
<AlertBlock type="error" className="break-words">
|
<AlertBlock type="error" className="w-full">
|
||||||
{connectionError?.message || error?.message}
|
{connectionError?.message || error?.message}
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -208,10 +208,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === "email") {
|
if (type === "email" && fields.length === 0) {
|
||||||
append("");
|
append("");
|
||||||
}
|
}
|
||||||
}, [type, append]);
|
}, [type, append, fields.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (notification) {
|
if (notification) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 QRCode from "qrcode";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -29,6 +30,12 @@ import {
|
|||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/components/ui/input-otp";
|
} from "@/components/ui/input-otp";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
@@ -54,6 +61,26 @@ type TwoFactorSetupData = {
|
|||||||
type PasswordForm = z.infer<typeof PasswordSchema>;
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
type PinForm = z.infer<typeof PinSchema>;
|
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 = () => {
|
export const Enable2FA = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
||||||
@@ -62,6 +89,7 @@ export const Enable2FA = () => {
|
|||||||
const [step, setStep] = useState<"password" | "verify">("password");
|
const [step, setStep] = useState<"password" | "verify">("password");
|
||||||
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
const [otpValue, setOtpValue] = useState("");
|
const [otpValue, setOtpValue] = useState("");
|
||||||
|
const { data: currentUser } = api.user.get.useQuery();
|
||||||
|
|
||||||
const handleVerifySubmit = async (e: React.FormEvent) => {
|
const handleVerifySubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
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 (
|
return (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -264,6 +340,7 @@ export const Enable2FA = () => {
|
|||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Scan this QR code with your authenticator app
|
Scan this QR code with your authenticator app
|
||||||
</span>
|
</span>
|
||||||
|
{/** biome-ignore lint/performance/noImgElement: This is a valid use case for an img element */}
|
||||||
<img
|
<img
|
||||||
src={data.qrCodeUrl}
|
src={data.qrCodeUrl}
|
||||||
alt="2FA QR Code"
|
alt="2FA QR Code"
|
||||||
@@ -281,7 +358,46 @@ export const Enable2FA = () => {
|
|||||||
|
|
||||||
{backupCodes && backupCodes.length > 0 && (
|
{backupCodes && backupCodes.length > 0 && (
|
||||||
<div className="w-full space-y-3 border rounded-lg p-4">
|
<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">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{backupCodes.map((code, index) => (
|
{backupCodes.map((code, index) => (
|
||||||
<code
|
<code
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
|
className="object-cover"
|
||||||
src={data?.user?.image || ""}
|
src={data?.user?.image || ""}
|
||||||
alt={data?.user?.image || ""}
|
alt={data?.user?.image || ""}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,13 +39,19 @@ export function AlertBlock({
|
|||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
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,
|
iconClassName,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{icon || <Icon className="text-current" />}
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
<span className="text-sm text-current">{children}</span>
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot : "button";
|
||||||
|
const type = props.type ?? undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Comp
|
<Comp
|
||||||
@@ -65,6 +67,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
disabled={isLoading || props.disabled}
|
disabled={isLoading || props.disabled}
|
||||||
|
type={type}
|
||||||
>
|
>
|
||||||
{isLoading && <Loader2 className="animate-spin" />}
|
{isLoading && <Loader2 className="animate-spin" />}
|
||||||
<Slottable>{children}</Slottable>
|
<Slottable>{children}</Slottable>
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
|
SquareTerminal,
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
Terminal,
|
|
||||||
SquareTerminal,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
@@ -35,8 +34,8 @@ import { AddDatabase } from "@/components/dashboard/project/add-database";
|
|||||||
import { AddTemplate } from "@/components/dashboard/project/add-template";
|
import { AddTemplate } from "@/components/dashboard/project/add-template";
|
||||||
import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector";
|
import { AdvancedEnvironmentSelector } from "@/components/dashboard/project/advanced-environment-selector";
|
||||||
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
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 { ProjectEnvironment } from "@/components/dashboard/projects/project-environment";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MariadbIcon,
|
MariadbIcon,
|
||||||
MongodbIcon,
|
MongodbIcon,
|
||||||
@@ -49,6 +48,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
|
|||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -98,8 +98,6 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
|
||||||
import { EnvironmentVariables } from "@/components/dashboard/project/environment-variables";
|
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -781,7 +779,7 @@ const EnvironmentPage = (
|
|||||||
currentEnvironmentId={environmentId}
|
currentEnvironmentId={environmentId}
|
||||||
/>
|
/>
|
||||||
<EnvironmentVariables environmentId={environmentId}>
|
<EnvironmentVariables environmentId={environmentId}>
|
||||||
<SquareTerminal className="h-5 w-5 text-muted-foreground" />
|
<SquareTerminal className="h-5 w-5 text-muted-foreground cursor-pointer hover:text-primary transition-colors duration-200 hover:bg-primary/10 rounded-md p-px" />
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
setIsError(true);
|
setIsError(true);
|
||||||
setError(error.message || "An error occurred");
|
setError(error.message || "An error occurred");
|
||||||
} else {
|
} else {
|
||||||
toast.success("User registered successfuly", {
|
toast.success("User registered successfully", {
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
if (!isCloud) {
|
if (!isCloud) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const getGiteaOAuthUrl = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = `${baseUrl}/api/providers/gitea/callback`;
|
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(
|
return `${giteaUrl}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||||
redirectUri,
|
redirectUri,
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ export function processValue(
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
typeof payload === "string" &&
|
typeof payload === "string" &&
|
||||||
payload.startsWith("{") &&
|
payload.trimStart().startsWith("{") &&
|
||||||
payload.endsWith("}")
|
payload.trimEnd().endsWith("}")
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(payload);
|
payload = JSON.parse(payload);
|
||||||
|
|||||||
@@ -46,8 +46,14 @@ export const deleteMiddleware = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAllMiddlewares = async (application: ApplicationNested) => {
|
export const deleteAllMiddlewares = async (application: ApplicationNested) => {
|
||||||
const config = loadMiddlewares<FileConfig>();
|
const { security, appName, redirects, serverId } = application;
|
||||||
const { security, appName, redirects } = application;
|
let config: FileConfig;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
config = await loadRemoteMiddlewares(serverId);
|
||||||
|
} else {
|
||||||
|
config = loadMiddlewares<FileConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
if (config.http?.middlewares) {
|
if (config.http?.middlewares) {
|
||||||
if (security.length > 0) {
|
if (security.length > 0) {
|
||||||
@@ -62,8 +68,8 @@ export const deleteAllMiddlewares = async (application: ApplicationNested) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (application.serverId) {
|
if (serverId) {
|
||||||
await writeTraefikConfigRemote(config, "middlewares", application.serverId);
|
await writeTraefikConfigRemote(config, "middlewares", serverId);
|
||||||
} else {
|
} else {
|
||||||
writeMiddleware(config);
|
writeMiddleware(config);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user