mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-24 15:25:23 +02:00
Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dadef000d5 | ||
|
|
2cda9821a5 | ||
|
|
a0868ad57c | ||
|
|
d4f574aa3f | ||
|
|
07368ff8c6 | ||
|
|
102a7a00b8 | ||
|
|
25a6a5bec6 | ||
|
|
011792e26b | ||
|
|
a527bafad8 | ||
|
|
14e154bece | ||
|
|
e5aeff6106 | ||
|
|
f6ff90eed9 | ||
|
|
f34a65cf14 | ||
|
|
8c0db75e1e | ||
|
|
b3c6645b35 | ||
|
|
0ff0695b7f | ||
|
|
6a4ef1153f | ||
|
|
a65262b45e | ||
|
|
75a66826f2 | ||
|
|
a5eeb74831 | ||
|
|
3dad8b4a54 | ||
|
|
fd94a14d85 | ||
|
|
d7e0413ed9 | ||
|
|
f4748bdd11 | ||
|
|
5a50d4bfc7 | ||
|
|
d1130c4554 | ||
|
|
fd2775e32a | ||
|
|
51003276bc | ||
|
|
6fb3584283 | ||
|
|
997dd784a5 | ||
|
|
60d1bc4a6d | ||
|
|
daa8184c30 | ||
|
|
68be333b04 | ||
|
|
2723703f60 | ||
|
|
d83783c620 | ||
|
|
ea9c76c1df | ||
|
|
32c302e9ce | ||
|
|
9f99185628 | ||
|
|
05b20193c2 | ||
|
|
88a8c060db | ||
|
|
71c01ff30f | ||
|
|
babc1c033e | ||
|
|
b34334701b | ||
|
|
0b1e79a8e1 | ||
|
|
66e0bcc4c6 | ||
|
|
962d405436 | ||
|
|
6b547dbc32 | ||
|
|
ba7a325e8f | ||
|
|
24df8e79fa | ||
|
|
6c3f72858b | ||
|
|
ba4626c7da | ||
|
|
166b58b70e | ||
|
|
74e17b4de6 | ||
|
|
7bddc6f46b | ||
|
|
036eaa3c2d | ||
|
|
be80148310 | ||
|
|
b662629075 | ||
|
|
0077954c78 | ||
|
|
622bb3ff4e | ||
|
|
7817a3c2fb | ||
|
|
71152b664b | ||
|
|
63f3bb8cf2 | ||
|
|
8510bcbd40 | ||
|
|
7fe59ba51b | ||
|
|
d858acbaaa | ||
|
|
9925470663 | ||
|
|
342b1d676e | ||
|
|
6d52ab13a6 | ||
|
|
819bb6ca14 | ||
|
|
8338b27ab8 | ||
|
|
901013ccd1 | ||
|
|
ceb4cc453e | ||
|
|
2b19632cdc | ||
|
|
a99ac01eba | ||
|
|
8b82832204 | ||
|
|
5fd39843f7 | ||
|
|
557923c011 | ||
|
|
3aaef9cc3e | ||
|
|
7a777550a0 | ||
|
|
b3cec533f9 | ||
|
|
78a9fe9dc5 | ||
|
|
68be6f451f | ||
|
|
b6e6705de8 | ||
|
|
d0fd8e7c72 | ||
|
|
b200ed6a73 | ||
|
|
8537b6fbbf | ||
|
|
883e9f0fd1 | ||
|
|
7988de64c8 | ||
|
|
fd5fa32964 | ||
|
|
ca6a93fdf6 | ||
|
|
0c37d7b3ee | ||
|
|
8de5001471 | ||
|
|
9b81d15b0c | ||
|
|
2cc9855ed2 | ||
|
|
571e97f247 | ||
|
|
c4519256cf | ||
|
|
cd06b55a0c | ||
|
|
b4a3cbdff4 | ||
|
|
e645b31b32 |
@@ -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 🥇 -->
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const baseApp: ApplicationNested = {
|
|||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
isPreviewDeploymentsActive: false,
|
isPreviewDeploymentsActive: false,
|
||||||
previewBuildArgs: null,
|
previewBuildArgs: null,
|
||||||
|
previewBuildSecrets: null,
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
previewCustomCertResolver: null,
|
previewCustomCertResolver: null,
|
||||||
previewEnv: null,
|
previewEnv: null,
|
||||||
@@ -73,6 +74,7 @@ const baseApp: ApplicationNested = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
buildSecrets: null,
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
buildType: "nixpacks",
|
buildType: "nixpacks",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ const baseApp: ApplicationNested = {
|
|||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
buildSecrets: null,
|
||||||
isPreviewDeploymentsActive: false,
|
isPreviewDeploymentsActive: false,
|
||||||
previewBuildArgs: null,
|
previewBuildArgs: null,
|
||||||
|
previewBuildSecrets: null,
|
||||||
triggerType: "push",
|
triggerType: "push",
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
previewEnv: null,
|
previewEnv: null,
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ export const AddVolumes = ({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="content"
|
name="content"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="max-w-full max-w-[45rem]">
|
||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -327,7 +327,7 @@ export const AddVolumes = ({
|
|||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
className="h-96 font-mono"
|
className="h-96 font-mono "
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Loader2 } from "lucide-react";
|
import copy from "copy-to-clipboard";
|
||||||
|
import { Check, Copy, Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -29,9 +31,10 @@ export const ShowDeployment = ({
|
|||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (autoScroll && scrollRef.current) {
|
if (autoScroll && scrollRef.current) {
|
||||||
@@ -106,6 +109,20 @@ export const ShowDeployment = ({
|
|||||||
}
|
}
|
||||||
}, [filteredLogs, autoScroll]);
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
const logContent = filteredLogs
|
||||||
|
.map(({ timestamp, message }: LogLine) =>
|
||||||
|
`${timestamp?.toISOString() || ""} ${message}`.trim(),
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const success = copy(logContent);
|
||||||
|
if (success) {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const optionalErrors = parseLogs(errorMessage || "");
|
const optionalErrors = parseLogs(errorMessage || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -128,13 +145,27 @@ export const ShowDeployment = ({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription className="flex items-center gap-2">
|
<DialogDescription className="flex items-center gap-2">
|
||||||
<span>
|
<span className="flex items-center gap-2">
|
||||||
See all the details of this deployment |{" "}
|
See all the details of this deployment |{" "}
|
||||||
<Badge variant="blank" className="text-xs">
|
<Badge variant="blank" className="text-xs">
|
||||||
{filteredLogs.length} lines
|
{filteredLogs.length} lines
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={filteredLogs.length === 0}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{serverId && (
|
{serverId && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -108,6 +108,21 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { api } from "@/utils/api";
|
|||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
buildArgs: z.string(),
|
buildArgs: z.string(),
|
||||||
|
buildSecrets: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||||
@@ -37,6 +38,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
env: "",
|
env: "",
|
||||||
buildArgs: "",
|
buildArgs: "",
|
||||||
|
buildSecrets: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addEnvironmentSchema),
|
resolver: zodResolver(addEnvironmentSchema),
|
||||||
});
|
});
|
||||||
@@ -44,15 +46,18 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
// Watch form values
|
// Watch form values
|
||||||
const currentEnv = form.watch("env");
|
const currentEnv = form.watch("env");
|
||||||
const currentBuildArgs = form.watch("buildArgs");
|
const currentBuildArgs = form.watch("buildArgs");
|
||||||
|
const currentBuildSecrets = form.watch("buildSecrets");
|
||||||
const hasChanges =
|
const hasChanges =
|
||||||
currentEnv !== (data?.env || "") ||
|
currentEnv !== (data?.env || "") ||
|
||||||
currentBuildArgs !== (data?.buildArgs || "");
|
currentBuildArgs !== (data?.buildArgs || "") ||
|
||||||
|
currentBuildSecrets !== (data?.buildSecrets || "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
env: data.env || "",
|
env: data.env || "",
|
||||||
buildArgs: data.buildArgs || "",
|
buildArgs: data.buildArgs || "",
|
||||||
|
buildSecrets: data.buildSecrets || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
@@ -61,6 +66,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
mutateAsync({
|
mutateAsync({
|
||||||
env: formData.env,
|
env: formData.env,
|
||||||
buildArgs: formData.buildArgs,
|
buildArgs: formData.buildArgs,
|
||||||
|
buildSecrets: formData.buildSecrets,
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -76,9 +82,25 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
env: data?.env || "",
|
env: data?.env || "",
|
||||||
buildArgs: data?.buildArgs || "",
|
buildArgs: data?.buildArgs || "",
|
||||||
|
buildSecrets: data?.buildSecrets || "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background px-6 pb-6">
|
<Card className="bg-background px-6 pb-6">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -104,13 +126,36 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
{data?.buildType === "dockerfile" && (
|
{data?.buildType === "dockerfile" && (
|
||||||
<Secrets
|
<Secrets
|
||||||
name="buildArgs"
|
name="buildArgs"
|
||||||
title="Build-time Variables"
|
title="Build-time Arguments"
|
||||||
description={
|
description={
|
||||||
<span>
|
<span>
|
||||||
Available only at build-time. See documentation
|
Arguments are available only at build-time. See
|
||||||
|
documentation
|
||||||
<a
|
<a
|
||||||
className="text-primary"
|
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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const schema = z
|
|||||||
.object({
|
.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
buildArgs: z.string(),
|
buildArgs: z.string(),
|
||||||
|
buildSecrets: z.string(),
|
||||||
wildcardDomain: z.string(),
|
wildcardDomain: z.string(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
previewLimit: z.number(),
|
previewLimit: z.number(),
|
||||||
@@ -109,6 +110,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
env: data.previewEnv || "",
|
env: data.previewEnv || "",
|
||||||
buildArgs: data.previewBuildArgs || "",
|
buildArgs: data.previewBuildArgs || "",
|
||||||
|
buildSecrets: data.previewBuildSecrets || "",
|
||||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||||
port: data.previewPort || 3000,
|
port: data.previewPort || 3000,
|
||||||
previewLabels: data.previewLabels || [],
|
previewLabels: data.previewLabels || [],
|
||||||
@@ -127,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
updateApplication({
|
updateApplication({
|
||||||
previewEnv: formData.env,
|
previewEnv: formData.env,
|
||||||
previewBuildArgs: formData.buildArgs,
|
previewBuildArgs: formData.buildArgs,
|
||||||
|
previewBuildSecrets: formData.buildSecrets,
|
||||||
previewWildcard: formData.wildcardDomain,
|
previewWildcard: formData.wildcardDomain,
|
||||||
previewPort: formData.port,
|
previewPort: formData.port,
|
||||||
previewLabels: formData.previewLabels,
|
previewLabels: formData.previewLabels,
|
||||||
@@ -467,13 +470,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
{data?.buildType === "dockerfile" && (
|
{data?.buildType === "dockerfile" && (
|
||||||
<Secrets
|
<Secrets
|
||||||
name="buildArgs"
|
name="buildArgs"
|
||||||
title="Build-time Variables"
|
title="Build-time Arguments"
|
||||||
description={
|
description={
|
||||||
<span>
|
<span>
|
||||||
Available only at build-time. See documentation
|
Arguments are available only at build-time. See
|
||||||
|
documentation
|
||||||
<a
|
<a
|
||||||
className="text-primary"
|
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"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -33,6 +34,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||||
|
const [runningSchedules, setRunningSchedules] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
data: schedules,
|
data: schedules,
|
||||||
isLoading: isLoadingSchedules,
|
isLoading: isLoadingSchedules,
|
||||||
@@ -46,14 +50,27 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
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 } = api.schedule.runManually.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: runManually, isLoading } =
|
const handleRunManually = async (scheduleId: string) => {
|
||||||
api.schedule.runManually.useMutation();
|
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 (
|
return (
|
||||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
<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.
|
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 +91,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 +107,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,16 +148,15 @@ 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">
|
<div className="flex items-center gap-0.5 md:gap-1.5">
|
||||||
<ShowDeploymentsModal
|
<ShowDeploymentsModal
|
||||||
id={schedule.scheduleId}
|
id={schedule.scheduleId}
|
||||||
@@ -149,10 +164,9 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
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>
|
||||||
@@ -160,37 +174,26 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
isLoading={isLoading}
|
disabled={runningSchedules.has(schedule.scheduleId)}
|
||||||
onClick={async () => {
|
onClick={() =>
|
||||||
toast.success("Schedule run successfully");
|
handleRunManually(schedule.scheduleId)
|
||||||
|
}
|
||||||
await runManually({
|
|
||||||
scheduleId: schedule.scheduleId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, 1500),
|
|
||||||
);
|
|
||||||
refetchSchedules();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error running schedule");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Play className="size-4 transition-colors" />
|
{runningSchedules.has(schedule.scheduleId) ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<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,8 +217,8 @@ 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}
|
disabled={isDeleting}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -38,6 +39,7 @@ export const ShowVolumeBackups = ({
|
|||||||
type = "application",
|
type = "application",
|
||||||
serverId,
|
serverId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [runningBackups, setRunningBackups] = useState<Set<string>>(new Set());
|
||||||
const {
|
const {
|
||||||
data: volumeBackups,
|
data: volumeBackups,
|
||||||
isLoading: isLoadingVolumeBackups,
|
isLoading: isLoadingVolumeBackups,
|
||||||
@@ -51,19 +53,33 @@ export const ShowVolumeBackups = ({
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
|
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
|
||||||
api.volumeBackups.delete.useMutation();
|
api.volumeBackups.delete.useMutation();
|
||||||
|
const { mutateAsync: runManually } =
|
||||||
const { mutateAsync: runManually, isLoading } =
|
|
||||||
api.volumeBackups.runManually.useMutation();
|
api.volumeBackups.runManually.useMutation();
|
||||||
|
|
||||||
|
const handleRunManually = async (volumeBackupId: string) => {
|
||||||
|
setRunningBackups((prev) => new Set(prev).add(volumeBackupId));
|
||||||
|
try {
|
||||||
|
await runManually({ volumeBackupId });
|
||||||
|
toast.success("Volume backup run successfully");
|
||||||
|
await refetchVolumeBackups();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error running volume backup");
|
||||||
|
} finally {
|
||||||
|
setRunningBackups((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(volumeBackupId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||||
<CardHeader className="px-0">
|
<CardHeader className="px-0">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center flex-wrap gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||||
Volume Backups
|
Volume Backups
|
||||||
@@ -73,12 +89,10 @@ export const ShowVolumeBackups = ({
|
|||||||
intervals.
|
intervals.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{volumeBackups && volumeBackups.length > 0 && (
|
{volumeBackups && volumeBackups.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RestoreVolumeBackups
|
<RestoreVolumeBackups
|
||||||
id={id}
|
id={id}
|
||||||
@@ -93,7 +107,7 @@ export const ShowVolumeBackups = ({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-0">
|
<CardContent className="px-0">
|
||||||
{isLoadingVolumeBackups ? (
|
{isLoadingVolumeBackups ? (
|
||||||
<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 volume backups...
|
Loading volume backups...
|
||||||
@@ -113,13 +127,13 @@ export const ShowVolumeBackups = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={volumeBackup.volumeBackupId}
|
key={volumeBackup.volumeBackupId}
|
||||||
className="flex items-center 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 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||||
<DatabaseBackup className="size-4 text-primary/70" />
|
<DatabaseBackup 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">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-sm font-medium leading-none">
|
<h3 className="text-sm font-medium leading-none">
|
||||||
{volumeBackup.name}
|
{volumeBackup.name}
|
||||||
@@ -143,18 +157,16 @@ export const ShowVolumeBackups = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 mt-2 sm:mt-0 sm:ml-3">
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<ShowDeploymentsModal
|
<ShowDeploymentsModal
|
||||||
id={volumeBackup.volumeBackupId}
|
id={volumeBackup.volumeBackupId}
|
||||||
type="volumeBackup"
|
type="volumeBackup"
|
||||||
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>
|
||||||
@@ -162,25 +174,18 @@ export const ShowVolumeBackups = ({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
isLoading={isLoading}
|
disabled={runningBackups.has(
|
||||||
onClick={async () => {
|
volumeBackup.volumeBackupId,
|
||||||
toast.success("Volume backup run successfully");
|
)}
|
||||||
|
onClick={() =>
|
||||||
await runManually({
|
handleRunManually(volumeBackup.volumeBackupId)
|
||||||
volumeBackupId: volumeBackup.volumeBackupId,
|
}
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, 1500),
|
|
||||||
);
|
|
||||||
refetchVolumeBackups();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error running volume backup");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Play className="size-4 transition-colors" />
|
{runningBackups.has(volumeBackup.volumeBackupId) ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Play className="size-4 transition-colors" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -188,13 +193,11 @@ export const ShowVolumeBackups = ({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<HandleVolumeBackups
|
<HandleVolumeBackups
|
||||||
volumeBackupId={volumeBackup.volumeBackupId}
|
volumeBackupId={volumeBackup.volumeBackupId}
|
||||||
id={id}
|
id={id}
|
||||||
volumeBackupType={type}
|
volumeBackupType={type}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Volume Backup"
|
title="Delete Volume Backup"
|
||||||
description="Are you sure you want to delete this volume backup?"
|
description="Are you sure you want to delete this volume backup?"
|
||||||
@@ -218,7 +221,7 @@ export const ShowVolumeBackups = ({
|
|||||||
<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" />
|
||||||
@@ -230,7 +233,7 @@ export const ShowVolumeBackups = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||||
<DatabaseBackup className="size-8 mb-4 text-muted-foreground" />
|
<DatabaseBackup className="size-8 mb-4 text-muted-foreground" />
|
||||||
<p className="text-lg font-medium text-muted-foreground">
|
<p className="text-lg font-medium text-muted-foreground">
|
||||||
No volume backups
|
No volume backups
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
composeId,
|
composeId,
|
||||||
composeFile: data.composeFile,
|
composeFile: data.composeFile,
|
||||||
|
composePath: "./docker-compose.yml",
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Download as DownloadIcon, Loader2, Pause, Play } from "lucide-react";
|
import copy from "copy-to-clipboard";
|
||||||
|
import {
|
||||||
|
Check,
|
||||||
|
Copy,
|
||||||
|
Download as DownloadIcon,
|
||||||
|
Loader2,
|
||||||
|
Pause,
|
||||||
|
Play,
|
||||||
|
} from "lucide-react";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -67,6 +75,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
const isPausedRef = useRef(false);
|
const isPausedRef = useRef(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (autoScroll && scrollRef.current) {
|
if (autoScroll && scrollRef.current) {
|
||||||
@@ -237,6 +246,29 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
const logContent = filteredLogs
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
timestamp,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
timestamp: Date | null;
|
||||||
|
message: string;
|
||||||
|
}) =>
|
||||||
|
showTimestamp
|
||||||
|
? `${timestamp?.toISOString() || "No timestamp"} ${message}`
|
||||||
|
: message,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const success = copy(logContent);
|
||||||
|
if (success) {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFilter = (logs: LogLine[]) => {
|
const handleFilter = (logs: LogLine[]) => {
|
||||||
return logs.filter((log) => {
|
return logs.filter((log) => {
|
||||||
const logType = getLogType(log.message).type;
|
const logType = getLogType(log.message).type;
|
||||||
@@ -320,6 +352,21 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{isPaused ? "Resume" : "Pause"}
|
{isPaused ? "Resume" : "Pause"}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={filteredLogs.length === 0}
|
||||||
|
title="Copy logs to clipboard"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{/* Action buttons for non-production environments */}
|
{/* Action buttons for non-production environments */}
|
||||||
<EnvironmentVariables environmentId={environment.environmentId}>
|
{/* <EnvironmentVariables environmentId={environment.environmentId}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -259,7 +259,7 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
>
|
>
|
||||||
<Terminal className="h-3 w-3" />
|
<Terminal className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables> */}
|
||||||
{environment.name !== "production" && (
|
{environment.name !== "production" && (
|
||||||
<div className="flex items-center gap-1 px-2">
|
<div className="flex items-center gap-1 px-2">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -82,6 +82,21 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
|||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && isOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading, isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -81,6 +81,21 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && isOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading, isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import {
|
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
AlertTriangle,
|
|
||||||
Mail,
|
|
||||||
MessageCircleMore,
|
|
||||||
PenBoxIcon,
|
|
||||||
PlusIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -13,6 +7,7 @@ import { z } from "zod";
|
|||||||
import {
|
import {
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
GotifyIcon,
|
GotifyIcon,
|
||||||
|
LarkIcon,
|
||||||
NtfyIcon,
|
NtfyIcon,
|
||||||
SlackIcon,
|
SlackIcon,
|
||||||
TelegramIcon,
|
TelegramIcon,
|
||||||
@@ -112,6 +107,12 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
priority: z.number().min(1).max(5).default(3),
|
priority: z.number().min(1).max(5).default(3),
|
||||||
})
|
})
|
||||||
.merge(notificationBaseSchema),
|
.merge(notificationBaseSchema),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("lark"),
|
||||||
|
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||||
|
})
|
||||||
|
.merge(notificationBaseSchema),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const notificationsMap = {
|
export const notificationsMap = {
|
||||||
@@ -127,6 +128,10 @@ export const notificationsMap = {
|
|||||||
icon: <DiscordIcon />,
|
icon: <DiscordIcon />,
|
||||||
label: "Discord",
|
label: "Discord",
|
||||||
},
|
},
|
||||||
|
lark: {
|
||||||
|
icon: <LarkIcon className="text-muted-foreground" />,
|
||||||
|
label: "Lark",
|
||||||
|
},
|
||||||
email: {
|
email: {
|
||||||
icon: <Mail size={29} className="text-muted-foreground" />,
|
icon: <Mail size={29} className="text-muted-foreground" />,
|
||||||
label: "Email",
|
label: "Email",
|
||||||
@@ -172,6 +177,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
api.notification.testGotifyConnection.useMutation();
|
api.notification.testGotifyConnection.useMutation();
|
||||||
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
|
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
|
||||||
api.notification.testNtfyConnection.useMutation();
|
api.notification.testNtfyConnection.useMutation();
|
||||||
|
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
|
||||||
|
api.notification.testLarkConnection.useMutation();
|
||||||
const slackMutation = notificationId
|
const slackMutation = notificationId
|
||||||
? api.notification.updateSlack.useMutation()
|
? api.notification.updateSlack.useMutation()
|
||||||
: api.notification.createSlack.useMutation();
|
: api.notification.createSlack.useMutation();
|
||||||
@@ -190,6 +197,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
const ntfyMutation = notificationId
|
const ntfyMutation = notificationId
|
||||||
? api.notification.updateNtfy.useMutation()
|
? api.notification.updateNtfy.useMutation()
|
||||||
: api.notification.createNtfy.useMutation();
|
: api.notification.createNtfy.useMutation();
|
||||||
|
const larkMutation = notificationId
|
||||||
|
? api.notification.updateLark.useMutation()
|
||||||
|
: api.notification.createLark.useMutation();
|
||||||
|
|
||||||
const form = useForm<NotificationSchema>({
|
const form = useForm<NotificationSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -208,10 +218,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) {
|
||||||
@@ -299,6 +309,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
serverUrl: notification.ntfy?.serverUrl,
|
serverUrl: notification.ntfy?.serverUrl,
|
||||||
name: notification.name,
|
name: notification.name,
|
||||||
dockerCleanup: notification.dockerCleanup,
|
dockerCleanup: notification.dockerCleanup,
|
||||||
|
serverThreshold: notification.serverThreshold,
|
||||||
|
});
|
||||||
|
} else if (notification.notificationType === "lark") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: notification.appBuildError,
|
||||||
|
appDeploy: notification.appDeploy,
|
||||||
|
dokployRestart: notification.dokployRestart,
|
||||||
|
databaseBackup: notification.databaseBackup,
|
||||||
|
type: notification.notificationType,
|
||||||
|
webhookUrl: notification.lark?.webhookUrl,
|
||||||
|
name: notification.name,
|
||||||
|
dockerCleanup: notification.dockerCleanup,
|
||||||
|
serverThreshold: notification.serverThreshold,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -313,6 +336,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
email: emailMutation,
|
email: emailMutation,
|
||||||
gotify: gotifyMutation,
|
gotify: gotifyMutation,
|
||||||
ntfy: ntfyMutation,
|
ntfy: ntfyMutation,
|
||||||
|
lark: larkMutation,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: NotificationSchema) => {
|
const onSubmit = async (data: NotificationSchema) => {
|
||||||
@@ -416,6 +440,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
notificationId: notificationId || "",
|
notificationId: notificationId || "",
|
||||||
ntfyId: notification?.ntfyId || "",
|
ntfyId: notification?.ntfyId || "",
|
||||||
});
|
});
|
||||||
|
} else if (data.type === "lark") {
|
||||||
|
promise = larkMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
webhookUrl: data.webhookUrl,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
notificationId: notificationId || "",
|
||||||
|
larkId: notification?.larkId || "",
|
||||||
|
serverThreshold: serverThreshold,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
@@ -504,7 +541,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={key}
|
htmlFor={key}
|
||||||
className="flex flex-col gap-2 items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
className="h-24 flex flex-col gap-2 items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||||
>
|
>
|
||||||
{value.icon}
|
{value.icon}
|
||||||
{value.label}
|
{value.label}
|
||||||
@@ -1002,6 +1039,27 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{type === "lark" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -1152,7 +1210,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
isLoadingDiscord ||
|
isLoadingDiscord ||
|
||||||
isLoadingEmail ||
|
isLoadingEmail ||
|
||||||
isLoadingGotify ||
|
isLoadingGotify ||
|
||||||
isLoadingNtfy
|
isLoadingNtfy ||
|
||||||
|
isLoadingLark
|
||||||
}
|
}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -1196,6 +1255,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
accessToken: form.getValues("accessToken"),
|
accessToken: form.getValues("accessToken"),
|
||||||
priority: form.getValues("priority"),
|
priority: form.getValues("priority"),
|
||||||
});
|
});
|
||||||
|
} else if (type === "lark") {
|
||||||
|
await testLarkConnection({
|
||||||
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
toast.success("Connection Success");
|
toast.success("Connection Success");
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
|
import { Bell, Loader2, Mail, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
GotifyIcon,
|
GotifyIcon,
|
||||||
|
LarkIcon,
|
||||||
NtfyIcon,
|
NtfyIcon,
|
||||||
SlackIcon,
|
SlackIcon,
|
||||||
TelegramIcon,
|
TelegramIcon,
|
||||||
@@ -35,7 +36,7 @@ export const ShowNotifications = () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add your providers to receive notifications, like Discord, Slack,
|
Add your providers to receive notifications, like Discord, Slack,
|
||||||
Telegram, Email.
|
Telegram, Email, Lark.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
@@ -95,6 +96,11 @@ export const ShowNotifications = () => {
|
|||||||
<NtfyIcon className="size-6" />
|
<NtfyIcon className="size-6" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{notification.notificationType === "lark" && (
|
||||||
|
<div className="flex items-center justify-center rounded-lg">
|
||||||
|
<LarkIcon className="size-7 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{notification.name}
|
{notification.name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import {
|
||||||
|
CopyIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
KeyRound,
|
||||||
|
RefreshCw,
|
||||||
|
ShieldOff,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
BACKUP_CODES_PLACEHOLDER,
|
||||||
|
backupCodeTemplate,
|
||||||
|
DATE_PLACEHOLDER,
|
||||||
|
USERNAME_PLACEHOLDER,
|
||||||
|
} from "./enable-2fa";
|
||||||
|
|
||||||
|
const PasswordSchema = z.object({
|
||||||
|
password: z.string().min(8, {
|
||||||
|
message: "Password is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
|
type Step = "password" | "actions" | "backup-codes";
|
||||||
|
|
||||||
|
export const Configure2FA = () => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data: currentUser } = api.user.get.useQuery();
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [step, setStep] = useState<Step>("password");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||||
|
const [showDisableConfirm, setShowDisableConfirm] = useState(false);
|
||||||
|
const [isDisabling, setIsDisabling] = useState(false);
|
||||||
|
const [isRegenerating, setIsRegenerating] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<PasswordForm>({
|
||||||
|
resolver: zodResolver(PasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDialogOpen) {
|
||||||
|
setStep("password");
|
||||||
|
setPassword("");
|
||||||
|
setBackupCodes([]);
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}, [isDialogOpen, form]);
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||||
|
setIsRegenerating(true);
|
||||||
|
try {
|
||||||
|
// Verify password by attempting to generate backup codes
|
||||||
|
// This validates the password and checks if 2FA is enabled
|
||||||
|
const result = await authClient.twoFactor.generateBackupCodes({
|
||||||
|
password: formData.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
form.setError("password", { message: result.error.message });
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, password is correct
|
||||||
|
setPassword(formData.password);
|
||||||
|
setStep("actions");
|
||||||
|
} catch (error) {
|
||||||
|
form.setError("password", {
|
||||||
|
message: error instanceof Error ? error.message : "Incorrect password",
|
||||||
|
});
|
||||||
|
toast.error("Incorrect password");
|
||||||
|
} finally {
|
||||||
|
setIsRegenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegenerateBackupCodes = async () => {
|
||||||
|
setIsRegenerating(true);
|
||||||
|
try {
|
||||||
|
const result = await authClient.twoFactor.generateBackupCodes({
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data?.backupCodes) {
|
||||||
|
setBackupCodes(result.data.backupCodes);
|
||||||
|
setStep("backup-codes");
|
||||||
|
toast.success("Backup codes regenerated successfully");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to regenerate backup codes",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsRegenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisable2FA = async () => {
|
||||||
|
setIsDisabling(true);
|
||||||
|
try {
|
||||||
|
const result = await authClient.twoFactor.disable({
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("2FA disabled successfully");
|
||||||
|
utils.user.get.invalidate();
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setShowDisableConfirm(false);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to disable 2FA. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsDisabling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDialog = () => {
|
||||||
|
if (step === "backup-codes") {
|
||||||
|
setStep("actions");
|
||||||
|
} else {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
|
<Button variant="secondary">
|
||||||
|
<KeyRound className="size-4 text-muted-foreground" />
|
||||||
|
Manage 2FA
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{step === "password" && "Verify Your Identity"}
|
||||||
|
{step === "actions" && "2FA Configuration"}
|
||||||
|
{step === "backup-codes" && "New Backup Codes"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{step === "password" &&
|
||||||
|
"Enter your password to manage your 2FA settings"}
|
||||||
|
{step === "actions" &&
|
||||||
|
"Choose an action to manage your two-factor authentication"}
|
||||||
|
{step === "backup-codes" &&
|
||||||
|
"Save these backup codes in a secure place"}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{step === "password" && (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(handlePasswordSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter your password to continue
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isRegenerating}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "actions" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div className="flex flex-col gap-2 p-4 border rounded-lg hover:bg-muted/50 transition-colors">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium flex items-center gap-2">
|
||||||
|
<RefreshCw className="size-4" />
|
||||||
|
Regenerate Backup Codes
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Generate new backup codes to replace your existing ones.
|
||||||
|
This will invalidate all previous backup codes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleRegenerateBackupCodes}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-2"
|
||||||
|
isLoading={isRegenerating}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 mr-2" />
|
||||||
|
Regenerate Backup Codes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 p-4 border border-destructive/50 rounded-lg hover:bg-destructive/5 transition-colors">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium flex items-center gap-2 text-destructive">
|
||||||
|
<ShieldOff className="size-4" />
|
||||||
|
Disable 2FA
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Completely disable two-factor authentication for your
|
||||||
|
account. This will make your account less secure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowDisableConfirm(true)}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full mt-2"
|
||||||
|
>
|
||||||
|
<ShieldOff className="size-4 mr-2" />
|
||||||
|
Disable 2FA
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "backup-codes" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="w-full space-y-3 border rounded-lg p-4 bg-muted/50">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{backupCodes.map((code, index) => (
|
||||||
|
<code
|
||||||
|
key={index}
|
||||||
|
className="bg-background p-2 rounded text-sm font-mono text-center"
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Save these backup codes in a secure place. You can use them to
|
||||||
|
access your account if you lose access to your authenticator
|
||||||
|
device. Each code can only be used once.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDownloadBackupCodes}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<DownloadIcon className="size-4 mr-2" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCopyBackupCodes}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4 mr-2" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button variant="outline" onClick={handleCloseDialog}>
|
||||||
|
Back to Actions
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setIsDialogOpen(false)}>Done</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<AlertDialog
|
||||||
|
open={showDisableConfirm}
|
||||||
|
onOpenChange={setShowDisableConfirm}
|
||||||
|
>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will permanently disable Two-Factor Authentication for your
|
||||||
|
account. Your account will be less secure without 2FA enabled.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleDisable2FA}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
disabled={isDisabling}
|
||||||
|
>
|
||||||
|
{isDisabling ? "Disabling..." : "Disable 2FA"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
const PasswordSchema = z.object({
|
|
||||||
password: z.string().min(8, {
|
|
||||||
message: "Password is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type PasswordForm = z.infer<typeof PasswordSchema>;
|
|
||||||
|
|
||||||
export const Disable2FA = () => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const form = useForm<PasswordForm>({
|
|
||||||
resolver: zodResolver(PasswordSchema),
|
|
||||||
defaultValues: {
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = async (formData: PasswordForm) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await authClient.twoFactor.disable({
|
|
||||||
password: formData.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
form.setError("password", {
|
|
||||||
message: result.error.message,
|
|
||||||
});
|
|
||||||
toast.error(result.error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("2FA disabled successfully");
|
|
||||||
utils.user.get.invalidate();
|
|
||||||
setIsOpen(false);
|
|
||||||
} catch {
|
|
||||||
form.setError("password", {
|
|
||||||
message: "Connection error. Please try again.",
|
|
||||||
});
|
|
||||||
toast.error("Connection error. Please try again.");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button variant="destructive">Disable 2FA</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This action cannot be undone. This will permanently disable
|
|
||||||
Two-Factor Authentication for your account.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Enter your password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Enter your password to disable 2FA
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-end gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
form.reset();
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" variant="destructive" isLoading={isLoading}>
|
|
||||||
Disable 2FA
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>;
|
||||||
|
|
||||||
|
export const USERNAME_PLACEHOLDER = "%username%";
|
||||||
|
export const DATE_PLACEHOLDER = "%date%";
|
||||||
|
export const BACKUP_CODES_PLACEHOLDER = "%backupCodes%";
|
||||||
|
|
||||||
|
export 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
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
|
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Disable2FA } from "./disable-2fa";
|
import { Configure2FA } from "./configure-2fa";
|
||||||
import { Enable2FA } from "./enable-2fa";
|
import { Enable2FA } from "./enable-2fa";
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
@@ -62,7 +62,6 @@ const randomImages = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const _utils = api.useUtils();
|
|
||||||
const { data, refetch, isLoading } = api.user.get.useQuery();
|
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -120,28 +119,27 @@ export const ProfileForm = () => {
|
|||||||
}, [form, data]);
|
}, [form, data]);
|
||||||
|
|
||||||
const onSubmit = async (values: Profile) => {
|
const onSubmit = async (values: Profile) => {
|
||||||
await mutateAsync({
|
try {
|
||||||
email: values.email.toLowerCase(),
|
await mutateAsync({
|
||||||
password: values.password || undefined,
|
email: values.email.toLowerCase(),
|
||||||
image: values.image,
|
password: values.password || undefined,
|
||||||
currentPassword: values.currentPassword || undefined,
|
image: values.image,
|
||||||
allowImpersonation: values.allowImpersonation,
|
currentPassword: values.currentPassword || undefined,
|
||||||
name: values.name || undefined,
|
allowImpersonation: values.allowImpersonation,
|
||||||
})
|
name: values.name || undefined,
|
||||||
.then(async () => {
|
|
||||||
await refetch();
|
|
||||||
toast.success("Profile Updated");
|
|
||||||
form.reset({
|
|
||||||
email: values.email,
|
|
||||||
password: "",
|
|
||||||
image: values.image,
|
|
||||||
currentPassword: "",
|
|
||||||
name: values.name || "",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the profile");
|
|
||||||
});
|
});
|
||||||
|
await refetch();
|
||||||
|
toast.success("Profile Updated");
|
||||||
|
form.reset({
|
||||||
|
email: values.email,
|
||||||
|
password: "",
|
||||||
|
image: values.image,
|
||||||
|
currentPassword: "",
|
||||||
|
name: values.name || "",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Error updating the profile");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -158,7 +156,8 @@ export const ProfileForm = () => {
|
|||||||
{t("settings.profile.description")}
|
{t("settings.profile.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Disable2FA />}
|
|
||||||
|
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Configure2FA />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
@@ -304,6 +303,7 @@ export const ProfileForm = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{field.value?.startsWith("data:") ? (
|
{field.value?.startsWith("data:") ? (
|
||||||
|
// biome-ignore lint/performance/noImgElement: this is an justified use of img element
|
||||||
<img
|
<img
|
||||||
src={field.value}
|
src={field.value}
|
||||||
alt="Custom avatar"
|
alt="Custom avatar"
|
||||||
@@ -362,6 +362,7 @@ export const ProfileForm = () => {
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
{/* biome-ignore lint/performance/noImgElement: this is an justified use of img element */}
|
||||||
<img
|
<img
|
||||||
key={image}
|
key={image}
|
||||||
src={image}
|
src={image}
|
||||||
|
|||||||
@@ -75,6 +75,21 @@ export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && !canEdit) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading, canEdit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|||||||
@@ -88,7 +88,32 @@ export const DiscordIcon = ({ className }: Props) => {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export const LarkIcon = ({ className }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
data-icon="LarkLogoColorful"
|
||||||
|
className={cn("size-9", className)}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m12.924 12.803.056-.054c.038-.034.076-.072.11-.11l.077-.076.23-.227 1.334-1.319.335-.331c.063-.063.13-.123.195-.183a7.777 7.777 0 0 1 1.823-1.24 7.607 7.607 0 0 1 1.014-.4 13.177 13.177 0 0 0-2.5-5.013 1.203 1.203 0 0 0-.94-.448h-9.65c-.173 0-.246.224-.107.325a28.23 28.23 0 0 1 8 9.098c.007-.006.016-.013.023-.022Z"
|
||||||
|
fill="#00D6B9"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9.097 21.299a13.258 13.258 0 0 0 11.82-7.247 5.576 5.576 0 0 1-.731 1.076 5.315 5.315 0 0 1-.745.7 5.117 5.117 0 0 1-.615.404 4.626 4.626 0 0 1-.726.331 5.312 5.312 0 0 1-1.883.312 5.892 5.892 0 0 1-.524-.031 6.509 6.509 0 0 1-.729-.126c-.06-.016-.12-.029-.18-.044-.166-.044-.33-.092-.494-.14-.082-.024-.164-.046-.246-.072-.123-.038-.247-.072-.366-.11l-.3-.095-.284-.094-.192-.067c-.08-.025-.155-.053-.234-.082a3.49 3.49 0 0 1-.167-.06c-.11-.04-.221-.079-.328-.12-.063-.025-.126-.047-.19-.072l-.252-.098c-.088-.035-.18-.07-.268-.107l-.174-.07c-.072-.028-.141-.06-.214-.088l-.164-.07c-.057-.024-.114-.05-.17-.075l-.149-.066-.135-.06-.14-.063a90.183 90.183 0 0 1-.141-.066 4.808 4.808 0 0 0-.18-.083c-.063-.028-.123-.06-.186-.088a5.697 5.697 0 0 1-.199-.098 27.762 27.762 0 0 1-8.067-5.969.18.18 0 0 0-.312.123l.006 9.21c0 .4.199.779.533 1a13.177 13.177 0 0 0 7.326 2.205Z"
|
||||||
|
fill="#3370FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M23.732 9.295a7.55 7.55 0 0 0-3.35-.776 7.521 7.521 0 0 0-2.284.35c-.054.016-.107.035-.158.05a8.297 8.297 0 0 0-.855.35 7.14 7.14 0 0 0-.552.297 6.716 6.716 0 0 0-.533.347c-.123.089-.243.18-.363.275-.13.104-.252.211-.375.321-.067.06-.13.123-.196.184l-.334.328-1.338 1.321-.23.228-.076.075c-.038.038-.076.073-.11.11l-.057.054a1.914 1.914 0 0 1-.085.08c-.032.028-.063.06-.095.088a13.286 13.286 0 0 1-2.748 1.946c.06.028.12.057.18.082l.142.066c.044.022.091.041.139.063l.135.06.149.067.17.075.164.07c.073.031.142.06.215.088.056.025.116.047.173.07.088.034.177.072.268.107.085.031.168.066.253.098l.189.072c.11.041.218.082.328.12.057.019.11.041.167.06.08.028.155.053.234.082l.192.066.284.095.3.095c.123.037.243.075.366.11l.246.072c.164.048.331.095.495.14.06.015.12.03.18.043.114.029.227.05.34.07.13.022.26.04.389.057a5.815 5.815 0 0 0 .994.019 5.172 5.172 0 0 0 1.413-.3 5.405 5.405 0 0 0 .726-.334c.06-.035.122-.07.182-.108a7.96 7.96 0 0 0 .432-.297 5.362 5.362 0 0 0 .577-.517 5.285 5.285 0 0 0 .37-.429 5.797 5.797 0 0 0 .527-.827l.13-.258 1.166-2.325-.003.006a7.391 7.391 0 0 1 1.527-2.186Z"
|
||||||
|
fill="#133C9A"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
export const GotifyIcon = ({ className }: Props) => {
|
export const GotifyIcon = ({ className }: Props) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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;
|
||||||
8
apps/dokploy/drizzle/0118_loose_anita_blake.sql
Normal file
8
apps/dokploy/drizzle/0118_loose_anita_blake.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TYPE "public"."notificationType" ADD VALUE 'lark';--> statement-breakpoint
|
||||||
|
CREATE TABLE "lark" (
|
||||||
|
"larkId" text PRIMARY KEY NOT NULL,
|
||||||
|
"webhookUrl" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "notification" ADD COLUMN "larkId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "notification" ADD CONSTRAINT "notification_larkId_lark_larkId_fk" FOREIGN KEY ("larkId") REFERENCES "public"."lark"("larkId") ON DELETE cascade ON UPDATE no action;
|
||||||
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
6679
apps/dokploy/drizzle/meta/0118_snapshot.json
Normal file
6679
apps/dokploy/drizzle/meta/0118_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -820,6 +820,20 @@
|
|||||||
"when": 1759645163834,
|
"when": 1759645163834,
|
||||||
"tag": "0116_amusing_firedrake",
|
"tag": "0116_amusing_firedrake",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 117,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1761370953274,
|
||||||
|
"tag": "0117_lumpy_nuke",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 118,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1761415824484,
|
||||||
|
"tag": "0118_loose_anita_blake",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.25.5",
|
"version": "v0.25.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -368,14 +368,14 @@ export const extractCommitedPaths = async (
|
|||||||
.map((change: any) => change.new?.target?.hash)
|
.map((change: any) => change.new?.target?.hash)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const commitedPaths: string[] = [];
|
const commitedPaths: string[] = [];
|
||||||
|
const username =
|
||||||
|
bitbucket?.bitbucketWorkspaceName || bitbucket?.bitbucketUsername || "";
|
||||||
for (const commit of commitHashes) {
|
for (const commit of commitHashes) {
|
||||||
const url = `https://api.bitbucket.org/2.0/repositories/${bitbucket?.bitbucketUsername}/${repository}/diffstat/${commit}`;
|
const url = `https://api.bitbucket.org/2.0/repositories/${username}/${repository}/diffstat/${commit}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: getBitbucketHeaders(bitbucket!),
|
headers: getBitbucketHeaders(bitbucket!),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
for (const value of data.values) {
|
for (const value of data.values) {
|
||||||
if (value?.new?.path) commitedPaths.push(value.new.path);
|
if (value?.new?.path) commitedPaths.push(value.new.path);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
|
SquareTerminal,
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -33,6 +34,7 @@ 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,
|
||||||
@@ -46,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 {
|
||||||
@@ -95,7 +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";
|
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -776,6 +778,11 @@ const EnvironmentPage = (
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
currentEnvironmentId={environmentId}
|
currentEnvironmentId={environmentId}
|
||||||
/>
|
/>
|
||||||
|
<EnvironmentVariables environmentId={environmentId}>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<SquareTerminal className="size-5 text-muted-foreground cursor-pointer" />
|
||||||
|
</Button>
|
||||||
|
</EnvironmentVariables>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{currentEnvironment.description || "No description provided"}
|
{currentEnvironment.description || "No description provided"}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ export const aiRouter = createTRPCRouter({
|
|||||||
case "ollama":
|
case "ollama":
|
||||||
response = await fetch(`${input.apiUrl}/api/tags`, { headers });
|
response = await fetch(`${input.apiUrl}/api/tags`, { headers });
|
||||||
break;
|
break;
|
||||||
|
case "gemini":
|
||||||
|
response = await fetch(
|
||||||
|
`${input.apiUrl}/models?key=${encodeURIComponent(input.apiKey)}`,
|
||||||
|
{ headers: {} },
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
if (!input.apiKey)
|
if (!input.apiKey)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -360,6 +360,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
await updateApplication(input.applicationId, {
|
await updateApplication(input.applicationId, {
|
||||||
env: input.env,
|
env: input.env,
|
||||||
buildArgs: input.buildArgs,
|
buildArgs: input.buildArgs,
|
||||||
|
buildSecrets: input.buildSecrets,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
createDiscordNotification,
|
createDiscordNotification,
|
||||||
createEmailNotification,
|
createEmailNotification,
|
||||||
|
createLarkNotification,
|
||||||
createGotifyNotification,
|
createGotifyNotification,
|
||||||
createNtfyNotification,
|
createNtfyNotification,
|
||||||
createSlackNotification,
|
createSlackNotification,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
removeNotificationById,
|
removeNotificationById,
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendServerThresholdNotifications,
|
sendServerThresholdNotifications,
|
||||||
@@ -17,6 +19,7 @@ import {
|
|||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
updateDiscordNotification,
|
updateDiscordNotification,
|
||||||
updateEmailNotification,
|
updateEmailNotification,
|
||||||
|
updateLarkNotification,
|
||||||
updateGotifyNotification,
|
updateGotifyNotification,
|
||||||
updateNtfyNotification,
|
updateNtfyNotification,
|
||||||
updateSlackNotification,
|
updateSlackNotification,
|
||||||
@@ -35,6 +38,7 @@ import { db } from "@/server/db";
|
|||||||
import {
|
import {
|
||||||
apiCreateDiscord,
|
apiCreateDiscord,
|
||||||
apiCreateEmail,
|
apiCreateEmail,
|
||||||
|
apiCreateLark,
|
||||||
apiCreateGotify,
|
apiCreateGotify,
|
||||||
apiCreateNtfy,
|
apiCreateNtfy,
|
||||||
apiCreateSlack,
|
apiCreateSlack,
|
||||||
@@ -42,12 +46,14 @@ import {
|
|||||||
apiFindOneNotification,
|
apiFindOneNotification,
|
||||||
apiTestDiscordConnection,
|
apiTestDiscordConnection,
|
||||||
apiTestEmailConnection,
|
apiTestEmailConnection,
|
||||||
|
apiTestLarkConnection,
|
||||||
apiTestGotifyConnection,
|
apiTestGotifyConnection,
|
||||||
apiTestNtfyConnection,
|
apiTestNtfyConnection,
|
||||||
apiTestSlackConnection,
|
apiTestSlackConnection,
|
||||||
apiTestTelegramConnection,
|
apiTestTelegramConnection,
|
||||||
apiUpdateDiscord,
|
apiUpdateDiscord,
|
||||||
apiUpdateEmail,
|
apiUpdateEmail,
|
||||||
|
apiUpdateLark,
|
||||||
apiUpdateGotify,
|
apiUpdateGotify,
|
||||||
apiUpdateNtfy,
|
apiUpdateNtfy,
|
||||||
apiUpdateSlack,
|
apiUpdateSlack,
|
||||||
@@ -328,6 +334,7 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
email: true,
|
email: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
lark: true,
|
||||||
},
|
},
|
||||||
orderBy: desc(notifications.createdAt),
|
orderBy: desc(notifications.createdAt),
|
||||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||||
@@ -511,6 +518,63 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
createLark: adminProcedure
|
||||||
|
.input(apiCreateLark)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
return await createLarkNotification(
|
||||||
|
input,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error creating the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
updateLark: adminProcedure
|
||||||
|
.input(apiUpdateLark)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
const notification = await findNotificationById(input.notificationId);
|
||||||
|
if (
|
||||||
|
IS_CLOUD &&
|
||||||
|
notification.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to update this notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await updateLarkNotification({
|
||||||
|
...input,
|
||||||
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
testLarkConnection: adminProcedure
|
||||||
|
.input(apiTestLarkConnection)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await sendLarkNotification(input, {
|
||||||
|
msg_type: "text",
|
||||||
|
content: {
|
||||||
|
text: "Hi, From Dokploy 👋",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error testing the notification",
|
||||||
|
cause: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.notifications.findMany({
|
return await db.query.notifications.findMany({
|
||||||
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func getRealOS() string {
|
|||||||
|
|
||||||
func GetServerMetrics() database.ServerMetric {
|
func GetServerMetrics() database.ServerMetric {
|
||||||
v, _ := mem.VirtualMemory()
|
v, _ := mem.VirtualMemory()
|
||||||
c, _ := cpu.Percent(0, false)
|
c, _ := cpu.Percent(time.Second, false)
|
||||||
cpuInfo, _ := cpu.Info()
|
cpuInfo, _ := cpu.Info()
|
||||||
diskInfo, _ := disk.Usage("/")
|
diskInfo, _ := disk.Usage("/")
|
||||||
netInfo, _ := net.IOCounters(false)
|
netInfo, _ := net.IOCounters(false)
|
||||||
@@ -130,7 +130,8 @@ func GetServerMetrics() database.ServerMetric {
|
|||||||
}
|
}
|
||||||
|
|
||||||
memTotalGB := float64(v.Total) / 1024 / 1024 / 1024
|
memTotalGB := float64(v.Total) / 1024 / 1024 / 1024
|
||||||
memUsedGB := float64(v.Used) / 1024 / 1024 / 1024
|
memAvailableGB := float64(v.Available) / 1024 / 1024 / 1024
|
||||||
|
memUsedGB := memTotalGB - memAvailableGB
|
||||||
memUsedPercent := (memUsedGB / memTotalGB) * 100
|
memUsedPercent := (memUsedGB / memTotalGB) * 100
|
||||||
|
|
||||||
var networkIn, networkOut float64
|
var networkIn, networkOut float64
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const applications = pgTable("application", {
|
|||||||
previewEnv: text("previewEnv"),
|
previewEnv: text("previewEnv"),
|
||||||
watchPaths: text("watchPaths").array(),
|
watchPaths: text("watchPaths").array(),
|
||||||
previewBuildArgs: text("previewBuildArgs"),
|
previewBuildArgs: text("previewBuildArgs"),
|
||||||
|
previewBuildSecrets: text("previewBuildSecrets"),
|
||||||
previewLabels: text("previewLabels").array(),
|
previewLabels: text("previewLabels").array(),
|
||||||
previewWildcard: text("previewWildcard"),
|
previewWildcard: text("previewWildcard"),
|
||||||
previewPort: integer("previewPort").default(3000),
|
previewPort: integer("previewPort").default(3000),
|
||||||
@@ -99,6 +100,7 @@ export const applications = pgTable("application", {
|
|||||||
).default(true),
|
).default(true),
|
||||||
rollbackActive: boolean("rollbackActive").default(false),
|
rollbackActive: boolean("rollbackActive").default(false),
|
||||||
buildArgs: text("buildArgs"),
|
buildArgs: text("buildArgs"),
|
||||||
|
buildSecrets: text("buildSecrets"),
|
||||||
memoryReservation: text("memoryReservation"),
|
memoryReservation: text("memoryReservation"),
|
||||||
memoryLimit: text("memoryLimit"),
|
memoryLimit: text("memoryLimit"),
|
||||||
cpuReservation: text("cpuReservation"),
|
cpuReservation: text("cpuReservation"),
|
||||||
@@ -253,6 +255,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
autoDeploy: z.boolean(),
|
autoDeploy: z.boolean(),
|
||||||
env: z.string().optional(),
|
env: z.string().optional(),
|
||||||
buildArgs: z.string().optional(),
|
buildArgs: z.string().optional(),
|
||||||
|
buildSecrets: z.string().optional(),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
memoryReservation: z.string().optional(),
|
memoryReservation: z.string().optional(),
|
||||||
@@ -304,6 +307,7 @@ const createSchema = createInsertSchema(applications, {
|
|||||||
previewPort: z.number().optional(),
|
previewPort: z.number().optional(),
|
||||||
previewEnv: z.string().optional(),
|
previewEnv: z.string().optional(),
|
||||||
previewBuildArgs: z.string().optional(),
|
previewBuildArgs: z.string().optional(),
|
||||||
|
previewBuildSecrets: z.string().optional(),
|
||||||
previewWildcard: z.string().optional(),
|
previewWildcard: z.string().optional(),
|
||||||
previewLimit: z.number().optional(),
|
previewLimit: z.number().optional(),
|
||||||
previewHttps: z.boolean().optional(),
|
previewHttps: z.boolean().optional(),
|
||||||
@@ -458,6 +462,7 @@ export const apiSaveEnvironmentVariables = createSchema
|
|||||||
applicationId: true,
|
applicationId: true,
|
||||||
env: true,
|
env: true,
|
||||||
buildArgs: true,
|
buildArgs: true,
|
||||||
|
buildSecrets: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { gitea } from "./gitea";
|
|||||||
import { github } from "./github";
|
import { github } from "./github";
|
||||||
import { gitlab } from "./gitlab";
|
import { gitlab } from "./gitlab";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { projects } from "./project";
|
|
||||||
import { schedules } from "./schedule";
|
import { schedules } from "./schedule";
|
||||||
import { server } from "./server";
|
import { server } from "./server";
|
||||||
import { applicationStatus, triggerType } from "./shared";
|
import { applicationStatus, triggerType } from "./shared";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const notificationType = pgEnum("notificationType", [
|
|||||||
"email",
|
"email",
|
||||||
"gotify",
|
"gotify",
|
||||||
"ntfy",
|
"ntfy",
|
||||||
|
"lark",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const notifications = pgTable("notification", {
|
export const notifications = pgTable("notification", {
|
||||||
@@ -48,6 +49,9 @@ export const notifications = pgTable("notification", {
|
|||||||
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
ntfyId: text("ntfyId").references(() => ntfy.ntfyId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
|
larkId: text("larkId").references(() => lark.larkId, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
}),
|
||||||
organizationId: text("organizationId")
|
organizationId: text("organizationId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => organization.id, { onDelete: "cascade" }),
|
.references(() => organization.id, { onDelete: "cascade" }),
|
||||||
@@ -116,6 +120,14 @@ export const ntfy = pgTable("ntfy", {
|
|||||||
priority: integer("priority").notNull().default(3),
|
priority: integer("priority").notNull().default(3),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const lark = pgTable("lark", {
|
||||||
|
larkId: text("larkId")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
webhookUrl: text("webhookUrl").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
||||||
slack: one(slack, {
|
slack: one(slack, {
|
||||||
fields: [notifications.slackId],
|
fields: [notifications.slackId],
|
||||||
@@ -141,6 +153,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
|
|||||||
fields: [notifications.ntfyId],
|
fields: [notifications.ntfyId],
|
||||||
references: [ntfy.ntfyId],
|
references: [ntfy.ntfyId],
|
||||||
}),
|
}),
|
||||||
|
lark: one(lark, {
|
||||||
|
fields: [notifications.larkId],
|
||||||
|
references: [lark.larkId],
|
||||||
|
}),
|
||||||
organization: one(organization, {
|
organization: one(organization, {
|
||||||
fields: [notifications.organizationId],
|
fields: [notifications.organizationId],
|
||||||
references: [organization.id],
|
references: [organization.id],
|
||||||
@@ -339,6 +355,31 @@ export const apiFindOneNotification = notificationsSchema
|
|||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
export const apiCreateLark = notificationsSchema
|
||||||
|
.pick({
|
||||||
|
appBuildError: true,
|
||||||
|
databaseBackup: true,
|
||||||
|
dokployRestart: true,
|
||||||
|
name: true,
|
||||||
|
appDeploy: true,
|
||||||
|
dockerCleanup: true,
|
||||||
|
serverThreshold: true,
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
webhookUrl: z.string().min(1),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const apiUpdateLark = apiCreateLark.partial().extend({
|
||||||
|
notificationId: z.string().min(1),
|
||||||
|
larkId: z.string().min(1),
|
||||||
|
organizationId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiTestLarkConnection = apiCreateLark.pick({
|
||||||
|
webhookUrl: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const apiSendTest = notificationsSchema
|
export const apiSendTest = notificationsSchema
|
||||||
.extend({
|
.extend({
|
||||||
botToken: z.string(),
|
botToken: z.string(),
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export * from "./utils/backups/postgres";
|
|||||||
export * from "./utils/backups/utils";
|
export * from "./utils/backups/utils";
|
||||||
export * from "./utils/backups/web-server";
|
export * from "./utils/backups/web-server";
|
||||||
export * from "./utils/builders/compose";
|
export * from "./utils/builders/compose";
|
||||||
export * from "./utils/startup/cancell-deployments";
|
|
||||||
export * from "./utils/builders/docker-file";
|
export * from "./utils/builders/docker-file";
|
||||||
export * from "./utils/builders/drop";
|
export * from "./utils/builders/drop";
|
||||||
export * from "./utils/builders/heroku";
|
export * from "./utils/builders/heroku";
|
||||||
@@ -77,7 +76,6 @@ export * from "./utils/builders/nixpacks";
|
|||||||
export * from "./utils/builders/paketo";
|
export * from "./utils/builders/paketo";
|
||||||
export * from "./utils/builders/static";
|
export * from "./utils/builders/static";
|
||||||
export * from "./utils/builders/utils";
|
export * from "./utils/builders/utils";
|
||||||
|
|
||||||
export * from "./utils/cluster/upload";
|
export * from "./utils/cluster/upload";
|
||||||
export * from "./utils/databases/rebuild";
|
export * from "./utils/databases/rebuild";
|
||||||
export * from "./utils/docker/collision";
|
export * from "./utils/docker/collision";
|
||||||
@@ -113,6 +111,8 @@ export * from "./utils/providers/raw";
|
|||||||
export * from "./utils/schedules/index";
|
export * from "./utils/schedules/index";
|
||||||
export * from "./utils/schedules/utils";
|
export * from "./utils/schedules/utils";
|
||||||
export * from "./utils/servers/remote-docker";
|
export * from "./utils/servers/remote-docker";
|
||||||
|
export * from "./utils/startup/cancell-deployments";
|
||||||
|
export * from "./utils/tracking/hubspot";
|
||||||
export * from "./utils/traefik/application";
|
export * from "./utils/traefik/application";
|
||||||
export * from "./utils/traefik/domain";
|
export * from "./utils/traefik/domain";
|
||||||
export * from "./utils/traefik/file-types";
|
export * from "./utils/traefik/file-types";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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 { updateUser } from "../services/user";
|
import { updateUser } from "../services/user";
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ const { handler, api } = betterAuth({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
after: async (user) => {
|
after: async (user, context) => {
|
||||||
const isAdminPresent = await db.query.member.findFirst({
|
const isAdminPresent = await db.query.member.findFirst({
|
||||||
where: eq(schema.member.role, "owner"),
|
where: eq(schema.member.role, "owner"),
|
||||||
});
|
});
|
||||||
@@ -126,6 +127,27 @@ const { handler, api } = betterAuth({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
try {
|
||||||
|
const hutk = getHubSpotUTK(
|
||||||
|
context?.request?.headers?.get("cookie") || undefined,
|
||||||
|
);
|
||||||
|
const hubspotSuccess = await submitToHubSpot(
|
||||||
|
{
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.name,
|
||||||
|
lastName: user.name,
|
||||||
|
},
|
||||||
|
hutk,
|
||||||
|
);
|
||||||
|
if (!hubspotSuccess) {
|
||||||
|
console.error("Failed to submit to HubSpot");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting to HubSpot", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (IS_CLOUD || !isAdminPresent) {
|
if (IS_CLOUD || !isAdminPresent) {
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
const organization = await tx
|
const organization = await tx
|
||||||
|
|||||||
@@ -472,7 +472,8 @@ export const deployPreviewApplication = async ({
|
|||||||
});
|
});
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
|
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
|
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
await cloneGithubRepository({
|
||||||
@@ -579,7 +580,8 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
});
|
});
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
|
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||||
|
|
||||||
if (application.serverId) {
|
if (application.serverId) {
|
||||||
let command = "set -e;";
|
let command = "set -e;";
|
||||||
@@ -666,7 +668,6 @@ export const rebuildRemoteApplication = async ({
|
|||||||
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
echo "Error occurred ❌, check the logs for details." >> ${deployment.logPath};
|
||||||
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
echo "${encodedContent}" | base64 -d >> "${deployment.logPath}";`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updateApplicationStatus(applicationId, "error");
|
await updateApplicationStatus(applicationId, "error");
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ import { db } from "@dokploy/server/db";
|
|||||||
import {
|
import {
|
||||||
type apiCreateDiscord,
|
type apiCreateDiscord,
|
||||||
type apiCreateEmail,
|
type apiCreateEmail,
|
||||||
|
type apiCreateLark,
|
||||||
type apiCreateGotify,
|
type apiCreateGotify,
|
||||||
type apiCreateNtfy,
|
type apiCreateNtfy,
|
||||||
type apiCreateSlack,
|
type apiCreateSlack,
|
||||||
type apiCreateTelegram,
|
type apiCreateTelegram,
|
||||||
type apiUpdateDiscord,
|
type apiUpdateDiscord,
|
||||||
type apiUpdateEmail,
|
type apiUpdateEmail,
|
||||||
|
type apiUpdateLark,
|
||||||
type apiUpdateGotify,
|
type apiUpdateGotify,
|
||||||
type apiUpdateNtfy,
|
type apiUpdateNtfy,
|
||||||
type apiUpdateSlack,
|
type apiUpdateSlack,
|
||||||
type apiUpdateTelegram,
|
type apiUpdateTelegram,
|
||||||
discord,
|
discord,
|
||||||
email,
|
email,
|
||||||
|
lark,
|
||||||
gotify,
|
gotify,
|
||||||
notifications,
|
notifications,
|
||||||
ntfy,
|
ntfy,
|
||||||
@@ -585,6 +588,7 @@ export const findNotificationById = async (notificationId: string) => {
|
|||||||
email: true,
|
email: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!notification) {
|
if (!notification) {
|
||||||
@@ -605,6 +609,94 @@ export const removeNotificationById = async (notificationId: string) => {
|
|||||||
return result[0];
|
return result[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createLarkNotification = async (
|
||||||
|
input: typeof apiCreateLark._type,
|
||||||
|
organizationId: string,
|
||||||
|
) => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const newLark = await tx
|
||||||
|
.insert(lark)
|
||||||
|
.values({
|
||||||
|
webhookUrl: input.webhookUrl,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newLark) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error input: Inserting lark",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDestination = await tx
|
||||||
|
.insert(notifications)
|
||||||
|
.values({
|
||||||
|
larkId: newLark.larkId,
|
||||||
|
name: input.name,
|
||||||
|
appDeploy: input.appDeploy,
|
||||||
|
appBuildError: input.appBuildError,
|
||||||
|
databaseBackup: input.databaseBackup,
|
||||||
|
dokployRestart: input.dokployRestart,
|
||||||
|
dockerCleanup: input.dockerCleanup,
|
||||||
|
notificationType: "lark",
|
||||||
|
organizationId: organizationId,
|
||||||
|
serverThreshold: input.serverThreshold,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newDestination) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error input: Inserting notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDestination;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateLarkNotification = async (
|
||||||
|
input: typeof apiUpdateLark._type,
|
||||||
|
) => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const newDestination = await tx
|
||||||
|
.update(notifications)
|
||||||
|
.set({
|
||||||
|
name: input.name,
|
||||||
|
appDeploy: input.appDeploy,
|
||||||
|
appBuildError: input.appBuildError,
|
||||||
|
databaseBackup: input.databaseBackup,
|
||||||
|
dokployRestart: input.dokployRestart,
|
||||||
|
dockerCleanup: input.dockerCleanup,
|
||||||
|
organizationId: input.organizationId,
|
||||||
|
serverThreshold: input.serverThreshold,
|
||||||
|
})
|
||||||
|
.where(eq(notifications.notificationId, input.notificationId))
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
if (!newDestination) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Error Updating notification",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(lark)
|
||||||
|
.set({
|
||||||
|
webhookUrl: input.webhookUrl,
|
||||||
|
})
|
||||||
|
.where(eq(lark.larkId, input.larkId))
|
||||||
|
.returning()
|
||||||
|
.then((value) => value[0]);
|
||||||
|
|
||||||
|
return newDestination;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const updateNotificationById = async (
|
export const updateNotificationById = async (
|
||||||
notificationId: string,
|
notificationId: string,
|
||||||
notificationData: Partial<Notification>,
|
notificationData: Partial<Notification>,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function getProviderName(apiUrl: string) {
|
|||||||
if (apiUrl.includes("api.mistral.ai")) return "mistral";
|
if (apiUrl.includes("api.mistral.ai")) return "mistral";
|
||||||
if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
|
if (apiUrl.includes(":11434") || apiUrl.includes("ollama")) return "ollama";
|
||||||
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
if (apiUrl.includes("api.deepinfra.com")) return "deepinfra";
|
||||||
|
if (apiUrl.includes("generativelanguage.googleapis.com")) return "gemini";
|
||||||
return "custom";
|
return "custom";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +67,13 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
|
|||||||
baseURL: config.apiUrl,
|
baseURL: config.apiUrl,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
});
|
});
|
||||||
|
case "gemini":
|
||||||
|
return createOpenAICompatible({
|
||||||
|
name: "gemini",
|
||||||
|
baseURL: config.apiUrl,
|
||||||
|
queryParams: { key: config.apiKey },
|
||||||
|
headers: {},
|
||||||
|
});
|
||||||
case "custom":
|
case "custom":
|
||||||
return createOpenAICompatible({
|
return createOpenAICompatible({
|
||||||
name: "custom",
|
name: "custom",
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { WriteStream } from "node:fs";
|
import type { WriteStream } from "node:fs";
|
||||||
import { prepareEnvironmentVariables } from "@dokploy/server/utils/docker/utils";
|
import {
|
||||||
|
getEnviromentVariablesObject,
|
||||||
|
prepareEnvironmentVariables,
|
||||||
|
} from "@dokploy/server/utils/docker/utils";
|
||||||
import {
|
import {
|
||||||
getBuildAppDirectory,
|
getBuildAppDirectory,
|
||||||
getDockerContextPath,
|
getDockerContextPath,
|
||||||
@@ -17,6 +20,7 @@ export const buildCustomDocker = async (
|
|||||||
env,
|
env,
|
||||||
publishDirectory,
|
publishDirectory,
|
||||||
buildArgs,
|
buildArgs,
|
||||||
|
buildSecrets,
|
||||||
dockerBuildStage,
|
dockerBuildStage,
|
||||||
cleanCache,
|
cleanCache,
|
||||||
} = application;
|
} = application;
|
||||||
@@ -26,11 +30,6 @@ export const buildCustomDocker = async (
|
|||||||
|
|
||||||
const defaultContextPath =
|
const defaultContextPath =
|
||||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||||
const args = prepareEnvironmentVariables(
|
|
||||||
buildArgs,
|
|
||||||
application.environment.project.env,
|
|
||||||
application.environment.env,
|
|
||||||
);
|
|
||||||
|
|
||||||
const dockerContextPath = getDockerContextPath(application);
|
const dockerContextPath = getDockerContextPath(application);
|
||||||
|
|
||||||
@@ -44,9 +43,29 @@ export const buildCustomDocker = async (
|
|||||||
commandArgs.push("--target", dockerBuildStage);
|
commandArgs.push("--target", dockerBuildStage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const args = prepareEnvironmentVariables(
|
||||||
|
buildArgs,
|
||||||
|
application.environment.project.env,
|
||||||
|
application.environment.env,
|
||||||
|
);
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
commandArgs.push("--build-arg", arg);
|
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,
|
Do not generate an environment file when publishDirectory is specified,
|
||||||
as it could be publicly exposed.
|
as it could be publicly exposed.
|
||||||
@@ -70,6 +89,10 @@ export const buildCustomDocker = async (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
cwd: dockerContextPath || defaultContextPath,
|
cwd: dockerContextPath || defaultContextPath,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...secrets,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -86,6 +109,7 @@ export const getDockerCommand = (
|
|||||||
env,
|
env,
|
||||||
publishDirectory,
|
publishDirectory,
|
||||||
buildArgs,
|
buildArgs,
|
||||||
|
buildSecrets,
|
||||||
dockerBuildStage,
|
dockerBuildStage,
|
||||||
cleanCache,
|
cleanCache,
|
||||||
} = application;
|
} = application;
|
||||||
@@ -96,11 +120,6 @@ export const getDockerCommand = (
|
|||||||
|
|
||||||
const defaultContextPath =
|
const defaultContextPath =
|
||||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||||
const args = prepareEnvironmentVariables(
|
|
||||||
buildArgs,
|
|
||||||
application.environment.project.env,
|
|
||||||
application.environment.env,
|
|
||||||
);
|
|
||||||
|
|
||||||
const dockerContextPath =
|
const dockerContextPath =
|
||||||
getDockerContextPath(application) || defaultContextPath;
|
getDockerContextPath(application) || defaultContextPath;
|
||||||
@@ -115,10 +134,33 @@ export const getDockerCommand = (
|
|||||||
commandArgs.push("--no-cache");
|
commandArgs.push("--no-cache");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const args = prepareEnvironmentVariables(
|
||||||
|
buildArgs,
|
||||||
|
application.environment.project.env,
|
||||||
|
application.environment.env,
|
||||||
|
);
|
||||||
|
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
commandArgs.push("--build-arg", `'${arg}'`);
|
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,
|
Do not generate an environment file when publishDirectory is specified,
|
||||||
as it could be publicly exposed.
|
as it could be publicly exposed.
|
||||||
@@ -140,7 +182,7 @@ cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
|
|||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
|
||||||
echo "❌ Docker build failed" >> ${logPath};
|
echo "❌ Docker build failed" >> ${logPath};
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
@@ -44,11 +45,13 @@ export const sendBuildErrorNotifications = async ({
|
|||||||
slack: true,
|
slack: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy } = notification;
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
|
notification;
|
||||||
if (email) {
|
if (email) {
|
||||||
const template = await renderAsync(
|
const template = await renderAsync(
|
||||||
BuildFailedEmail({
|
BuildFailedEmail({
|
||||||
@@ -211,5 +214,117 @@ export const sendBuildErrorNotifications = async ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lark) {
|
||||||
|
const limitCharacter = 800;
|
||||||
|
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
|
||||||
|
await sendLarkNotification(lark, {
|
||||||
|
msg_type: "interactive",
|
||||||
|
card: {
|
||||||
|
schema: "2.0",
|
||||||
|
config: {
|
||||||
|
update_multi: true,
|
||||||
|
style: {
|
||||||
|
text_size: {
|
||||||
|
normal_v2: {
|
||||||
|
default: "normal",
|
||||||
|
pc: "normal",
|
||||||
|
mobile: "heading",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "⚠️ Build Failed",
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
template: "red",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Project:**\n${projectName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Type:**\n${applicationType}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Application:**\n${applicationName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "button",
|
||||||
|
text: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "View Build Details",
|
||||||
|
},
|
||||||
|
type: "danger",
|
||||||
|
width: "default",
|
||||||
|
size: "medium",
|
||||||
|
behaviors: [
|
||||||
|
{
|
||||||
|
type: "open_url",
|
||||||
|
default_url: buildLink,
|
||||||
|
pc_url: "",
|
||||||
|
ios_url: "",
|
||||||
|
android_url: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
margin: "0px 0px 0px 0px",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
@@ -45,11 +46,13 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
slack: true,
|
slack: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy } = notification;
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
|
notification;
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
const template = await renderAsync(
|
const template = await renderAsync(
|
||||||
@@ -210,5 +213,109 @@ export const sendBuildSuccessNotifications = async ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lark) {
|
||||||
|
await sendLarkNotification(lark, {
|
||||||
|
msg_type: "interactive",
|
||||||
|
card: {
|
||||||
|
schema: "2.0",
|
||||||
|
config: {
|
||||||
|
update_multi: true,
|
||||||
|
style: {
|
||||||
|
text_size: {
|
||||||
|
normal_v2: {
|
||||||
|
default: "normal",
|
||||||
|
pc: "normal",
|
||||||
|
mobile: "heading",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "✅ Build Success",
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
template: "green",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Project:**\n${projectName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Type:**\n${applicationType}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Application:**\n${applicationName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "button",
|
||||||
|
text: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "View Build Details",
|
||||||
|
},
|
||||||
|
type: "primary",
|
||||||
|
width: "default",
|
||||||
|
size: "medium",
|
||||||
|
behaviors: [
|
||||||
|
{
|
||||||
|
type: "open_url",
|
||||||
|
default_url: buildLink,
|
||||||
|
pc_url: "",
|
||||||
|
ios_url: "",
|
||||||
|
android_url: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
margin: "0px 0px 0px 0px",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
@@ -44,11 +45,13 @@ export const sendDatabaseBackupNotifications = async ({
|
|||||||
slack: true,
|
slack: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy } = notification;
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
|
notification;
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
const template = await renderAsync(
|
const template = await renderAsync(
|
||||||
@@ -238,5 +241,120 @@ export const sendDatabaseBackupNotifications = async ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lark) {
|
||||||
|
const limitCharacter = 800;
|
||||||
|
const truncatedErrorMessage =
|
||||||
|
errorMessage && errorMessage.length > limitCharacter
|
||||||
|
? errorMessage.substring(0, limitCharacter)
|
||||||
|
: errorMessage;
|
||||||
|
|
||||||
|
await sendLarkNotification(lark, {
|
||||||
|
msg_type: "interactive",
|
||||||
|
card: {
|
||||||
|
schema: "2.0",
|
||||||
|
config: {
|
||||||
|
update_multi: true,
|
||||||
|
style: {
|
||||||
|
text_size: {
|
||||||
|
normal_v2: {
|
||||||
|
default: "normal",
|
||||||
|
pc: "normal",
|
||||||
|
mobile: "heading",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content:
|
||||||
|
type === "success"
|
||||||
|
? "✅ Database Backup Successful"
|
||||||
|
: "❌ Database Backup Failed",
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
template: type === "success" ? "green" : "red",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Project:**\n${projectName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Database Type:**\n${databaseType}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Status:**\n${type === "success" ? "Successful" : "Failed"}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Application:**\n${applicationName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Database Name:**\n${databaseName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...(type === "error" && truncatedErrorMessage
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Error Message:**\n\`\`\`\n${truncatedErrorMessage}\n\`\`\``,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
@@ -31,11 +32,13 @@ export const sendDockerCleanupNotifications = async (
|
|||||||
slack: true,
|
slack: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy } = notification;
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
|
notification;
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
const template = await renderAsync(
|
const template = await renderAsync(
|
||||||
@@ -135,5 +138,83 @@ export const sendDockerCleanupNotifications = async (
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lark) {
|
||||||
|
await sendLarkNotification(lark, {
|
||||||
|
msg_type: "interactive",
|
||||||
|
card: {
|
||||||
|
schema: "2.0",
|
||||||
|
config: {
|
||||||
|
update_multi: true,
|
||||||
|
style: {
|
||||||
|
text_size: {
|
||||||
|
normal_v2: {
|
||||||
|
default: "normal",
|
||||||
|
pc: "normal",
|
||||||
|
mobile: "heading",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "✅ Docker Cleanup",
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
template: "green",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Status:**\nSuccessful`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Cleanup Details:**\n${message}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Date:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendGotifyNotification,
|
sendGotifyNotification,
|
||||||
sendNtfyNotification,
|
sendNtfyNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
@@ -25,11 +26,13 @@ export const sendDokployRestartNotifications = async () => {
|
|||||||
slack: true,
|
slack: true,
|
||||||
gotify: true,
|
gotify: true,
|
||||||
ntfy: true,
|
ntfy: true,
|
||||||
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { email, discord, telegram, slack, gotify, ntfy } = notification;
|
const { email, discord, telegram, slack, gotify, ntfy, lark } =
|
||||||
|
notification;
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
const template = await renderAsync(
|
const template = await renderAsync(
|
||||||
@@ -135,5 +138,81 @@ export const sendDokployRestartNotifications = async () => {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lark) {
|
||||||
|
try {
|
||||||
|
await sendLarkNotification(lark, {
|
||||||
|
msg_type: "interactive",
|
||||||
|
card: {
|
||||||
|
schema: "2.0",
|
||||||
|
config: {
|
||||||
|
update_multi: true,
|
||||||
|
style: {
|
||||||
|
text_size: {
|
||||||
|
normal_v2: {
|
||||||
|
default: "normal",
|
||||||
|
pc: "normal",
|
||||||
|
mobile: "heading",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "✅ Dokploy Server Restarted",
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
template: "green",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Status:**\nSuccessful`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Restart Time:**\n${format(date, "PP pp")}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { db } from "../../db";
|
|||||||
import { notifications } from "../../db/schema";
|
import { notifications } from "../../db/schema";
|
||||||
import {
|
import {
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
|
sendLarkNotification,
|
||||||
sendSlackNotification,
|
sendSlackNotification,
|
||||||
sendTelegramNotification,
|
sendTelegramNotification,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
@@ -34,6 +35,7 @@ export const sendServerThresholdNotifications = async (
|
|||||||
discord: true,
|
discord: true,
|
||||||
telegram: true,
|
telegram: true,
|
||||||
slack: true,
|
slack: true,
|
||||||
|
lark: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ export const sendServerThresholdNotifications = async (
|
|||||||
const typeColor = 0xff0000; // Rojo para indicar alerta
|
const typeColor = 0xff0000; // Rojo para indicar alerta
|
||||||
|
|
||||||
for (const notification of notificationList) {
|
for (const notification of notificationList) {
|
||||||
const { discord, telegram, slack } = notification;
|
const { discord, telegram, slack, lark } = notification;
|
||||||
|
|
||||||
if (discord) {
|
if (discord) {
|
||||||
const decorate = (decoration: string, text: string) =>
|
const decorate = (decoration: string, text: string) =>
|
||||||
@@ -151,5 +153,101 @@ export const sendServerThresholdNotifications = async (
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lark) {
|
||||||
|
await sendLarkNotification(lark, {
|
||||||
|
msg_type: "interactive",
|
||||||
|
card: {
|
||||||
|
schema: "2.0",
|
||||||
|
config: {
|
||||||
|
update_multi: true,
|
||||||
|
style: {
|
||||||
|
text_size: {
|
||||||
|
normal_v2: {
|
||||||
|
default: "normal",
|
||||||
|
pc: "normal",
|
||||||
|
mobile: "heading",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
title: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: `⚠️ Server ${payload.Type} Alert`,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
tag: "plain_text",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
template: "red",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
direction: "vertical",
|
||||||
|
padding: "12px 12px 12px 12px",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "column_set",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Server Name:**\n${payload.ServerName}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Current Value:**\n${payload.Value.toFixed(2)}%`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Alert Message:**\n${payload.Message}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "column",
|
||||||
|
width: "weighted",
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Type:**\n${payload.Type === "CPU" ? "🔲" : "💾"} ${payload.Type}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Threshold:**\n${payload.Threshold.toFixed(2)}%`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "markdown",
|
||||||
|
content: `**Alert Time:**\n${date.toLocaleString()}`,
|
||||||
|
text_align: "left",
|
||||||
|
text_size: "normal_v2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vertical_align: "top",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
discord,
|
discord,
|
||||||
email,
|
email,
|
||||||
|
lark,
|
||||||
gotify,
|
gotify,
|
||||||
ntfy,
|
ntfy,
|
||||||
slack,
|
slack,
|
||||||
@@ -152,3 +153,18 @@ export const sendNtfyNotification = async (
|
|||||||
throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
|
throw new Error(`Failed to send ntfy notification: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendLarkNotification = async (
|
||||||
|
connection: typeof lark.$inferInsert,
|
||||||
|
message: any,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await fetch(connection.webhookUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(message),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
125
packages/server/src/utils/tracking/hubspot.ts
Normal file
125
packages/server/src/utils/tracking/hubspot.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
interface HubSpotFormField {
|
||||||
|
objectTypeId: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HubSpotFormData {
|
||||||
|
fields: HubSpotFormField[];
|
||||||
|
context: {
|
||||||
|
pageUri: string;
|
||||||
|
pageName: string;
|
||||||
|
hutk?: string; // HubSpot UTK from cookies
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignUpFormData {
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract HubSpot UTK (User Token) from cookies
|
||||||
|
* This is used for tracking and attribution in HubSpot
|
||||||
|
*/
|
||||||
|
export function getHubSpotUTK(cookieHeader?: string): string | null {
|
||||||
|
if (!cookieHeader) return null;
|
||||||
|
|
||||||
|
const name = "hubspotutk=";
|
||||||
|
const decodedCookie = decodeURIComponent(cookieHeader);
|
||||||
|
const cookieArray = decodedCookie.split(";");
|
||||||
|
|
||||||
|
for (let i = 0; i < cookieArray.length; i++) {
|
||||||
|
const cookie = cookieArray[i]?.trim();
|
||||||
|
if (!cookie) continue;
|
||||||
|
if (cookie.indexOf(name) === 0) {
|
||||||
|
return cookie.substring(name.length, cookie.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert contact form data to HubSpot form format
|
||||||
|
*/
|
||||||
|
export function formatContactDataForHubSpot(
|
||||||
|
contactData: SignUpFormData,
|
||||||
|
hutk?: string | null,
|
||||||
|
): HubSpotFormData {
|
||||||
|
const formData: HubSpotFormData = {
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
objectTypeId: "0-1", // Contact object type
|
||||||
|
name: "firstname",
|
||||||
|
value: contactData.firstName || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objectTypeId: "0-1",
|
||||||
|
name: "lastname",
|
||||||
|
value: contactData.lastName || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objectTypeId: "0-1",
|
||||||
|
name: "email",
|
||||||
|
value: contactData.email || "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
context: {
|
||||||
|
pageUri: "https://app.dokploy.com/register",
|
||||||
|
pageName: "Sign Up",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add HubSpot UTK if available
|
||||||
|
if (hutk) {
|
||||||
|
formData.context.hutk = hutk;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submit form data to HubSpot Forms API
|
||||||
|
*/
|
||||||
|
export async function submitToHubSpot(
|
||||||
|
contactData: SignUpFormData,
|
||||||
|
hutk?: string | null,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const portalId = process.env.HUBSPOT_PORTAL_ID;
|
||||||
|
const formGuid = process.env.HUBSPOT_FORM_GUID;
|
||||||
|
|
||||||
|
if (!portalId || !formGuid) {
|
||||||
|
console.error(
|
||||||
|
"HubSpot configuration missing: HUBSPOT_PORTAL_ID or HUBSPOT_FORM_GUID not set",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = formatContactDataForHubSpot(contactData, hutk);
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formGuid}`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error("HubSpot API error:", response.status, errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log("HubSpot submission successful:", result);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting to HubSpot:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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