mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
109 Commits
feat/intro
...
711-custom
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80e6f21840 | ||
|
|
5b519151e8 | ||
|
|
aa475e6123 | ||
|
|
66756c34fe | ||
|
|
946a5739dc | ||
|
|
6c817a9e5d | ||
|
|
6aea937e86 | ||
|
|
19612d4b66 | ||
|
|
47dd003461 | ||
|
|
def99225fc | ||
|
|
32405fc61a | ||
|
|
25e1a9af57 | ||
|
|
1fcb1f2c5e | ||
|
|
fdaba7e752 | ||
|
|
c1640cba29 | ||
|
|
3bd54ff61e | ||
|
|
5853d18bc1 | ||
|
|
f575317906 | ||
|
|
e6028e73ac | ||
|
|
bcbed151e8 | ||
|
|
c708f7ba62 | ||
|
|
95a538f261 | ||
|
|
f854457d69 | ||
|
|
cd998c37f1 | ||
|
|
d46a61098b | ||
|
|
8f14d854a0 | ||
|
|
388399b370 | ||
|
|
a8b4bb9c41 | ||
|
|
ebc8c2f73d | ||
|
|
1227d2b5fc | ||
|
|
314438b84c | ||
|
|
cc5574e08a | ||
|
|
11a8fcc476 | ||
|
|
c50229a33c | ||
|
|
0609d74d2b | ||
|
|
fce8eca894 | ||
|
|
3de0d674ed | ||
|
|
7faab54a65 | ||
|
|
40d9db7ccf | ||
|
|
c7c01f57d4 | ||
|
|
45cf295be0 | ||
|
|
79372527e6 | ||
|
|
edcfc7d670 | ||
|
|
6277ebaaec | ||
|
|
2b081166f9 | ||
|
|
d8f12f1780 | ||
|
|
95d949f112 | ||
|
|
1ec0c8e8b3 | ||
|
|
b9ac25ef42 | ||
|
|
9fe2460b88 | ||
|
|
44af0ec975 | ||
|
|
b84b4549a0 | ||
|
|
c110fae965 | ||
|
|
86b56e2597 | ||
|
|
7e365e1947 | ||
|
|
d458536803 | ||
|
|
9cb5b9a7d0 | ||
|
|
7e9fccfcb0 | ||
|
|
1c73dab719 | ||
|
|
3ec339fc89 | ||
|
|
c13a68dab4 | ||
|
|
eb5ba2f219 | ||
|
|
1eda6513df | ||
|
|
4bb60b9f7e | ||
|
|
9948dd7f19 | ||
|
|
e3ec8f1589 | ||
|
|
9aa56870b0 | ||
|
|
06c9e43143 | ||
|
|
e09447d4b4 | ||
|
|
2fa0c7dfd2 | ||
|
|
27521decbd | ||
|
|
4bec311ad0 | ||
|
|
432f616896 | ||
|
|
4575f16be4 | ||
|
|
d6f5f6e6cb | ||
|
|
96b1df2199 | ||
|
|
614b9d25a8 | ||
|
|
66dd890448 | ||
|
|
557c89ac6d | ||
|
|
b3e2af3b40 | ||
|
|
a8159e5f99 | ||
|
|
89306a7619 | ||
|
|
0690f07262 | ||
|
|
e437903ef8 | ||
|
|
50aeeb2fb8 | ||
|
|
d85fc2e513 | ||
|
|
cfba9a7d79 | ||
|
|
4a8cadc6ee | ||
|
|
b2d938d2fc | ||
|
|
9277427153 | ||
|
|
ccb141339b | ||
|
|
c87af312ca | ||
|
|
a5fb5532fd | ||
|
|
43ab1aa7b8 | ||
|
|
3072795232 | ||
|
|
98d0f1d5bf | ||
|
|
49e55961db | ||
|
|
4ee220c1d8 | ||
|
|
c69992c4f0 | ||
|
|
1f6ba45c12 | ||
|
|
ef02ba22b5 | ||
|
|
d7daa6d8e0 | ||
|
|
fafa14c10a | ||
|
|
2c90103823 | ||
|
|
f2bb01c800 | ||
|
|
442f051457 | ||
|
|
e84ce38994 | ||
|
|
0ea264ea42 | ||
|
|
d4064805eb |
BIN
.github/sponsors/agentdock.png
vendored
Normal file
BIN
.github/sponsors/agentdock.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
.github/sponsors/american-cloud.png
vendored
Normal file
BIN
.github/sponsors/american-cloud.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -29,7 +29,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy only the necessary files
|
# Copy only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
@@ -56,7 +56,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
# Install Railpack
|
# Install Railpack
|
||||||
ARG RAILPACK_VERSION=0.0.37
|
ARG RAILPACK_VERSION=0.0.64
|
||||||
RUN curl -sSL https://railpack.com/install.sh | bash
|
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||||
|
|
||||||
# Install buildpacks
|
# Install buildpacks
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -80,12 +80,30 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
|
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;">
|
||||||
|
<img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;">
|
||||||
|
<img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### Elite Contributors 🥈
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center; gap: 20px;">
|
||||||
|
|
||||||
|
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
|
||||||
|
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
<!-- Elite Contributors 🥈 -->
|
<!-- Elite Contributors 🥈 -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Add Elite Contributors here -->
|
<!-- Add Elite Contributors here -->
|
||||||
|
|
||||||
### Supporting Members 🥉
|
### Supporting Members 🥉
|
||||||
@@ -97,6 +115,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
||||||
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
||||||
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { beforeEach, expect, test, vi } from "vitest";
|
|||||||
const baseAdmin: User = {
|
const baseAdmin: User = {
|
||||||
https: false,
|
https: false,
|
||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
|
allowImpersonation: false,
|
||||||
|
role: "user",
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
refreshRate: 20,
|
refreshRate: 20,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
@@ -200,6 +201,22 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
<AlertBlock>
|
||||||
|
Builders can consume significant memory and CPU resources
|
||||||
|
(recommended: 4+ GB RAM and 2+ CPU cores). For production
|
||||||
|
environments, please review our{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.dokploy.com/docs/core/applications/going-production"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="font-medium underline underline-offset-4"
|
||||||
|
>
|
||||||
|
Production Guide
|
||||||
|
</a>{" "}
|
||||||
|
for best practices and optimization recommendations. Builders are
|
||||||
|
suitable for development and prototyping purposes when you have
|
||||||
|
sufficient resources available.
|
||||||
|
</AlertBlock>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 p-2"
|
className="grid w-full gap-4 p-2"
|
||||||
|
|||||||
@@ -15,11 +15,15 @@ import { Paintbrush } from "lucide-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CancelQueues = ({ applicationId }: Props) => {
|
export const CancelQueues = ({ id, type }: Props) => {
|
||||||
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
const { mutateAsync, isLoading } =
|
||||||
|
type === "application"
|
||||||
|
? api.application.cleanQueues.useMutation()
|
||||||
|
: api.compose.cleanQueues.useMutation();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
@@ -48,7 +52,8 @@ export const CancelQueues = ({ applicationId }: Props) => {
|
|||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId: id || "",
|
||||||
|
composeId: id || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Queues are being cleaned");
|
toast.success("Queues are being cleaned");
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ import { RefreshCcw } from "lucide-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
}
|
}
|
||||||
export const RefreshToken = ({ applicationId }: Props) => {
|
export const RefreshToken = ({ id, type }: Props) => {
|
||||||
const { mutateAsync } = api.application.refreshToken.useMutation();
|
const { mutateAsync } =
|
||||||
|
type === "application"
|
||||||
|
? api.application.refreshToken.useMutation()
|
||||||
|
: api.compose.refreshToken.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
@@ -37,12 +41,19 @@ export const RefreshToken = ({ applicationId }: Props) => {
|
|||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId,
|
applicationId: id || "",
|
||||||
|
composeId: id || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
utils.application.one.invalidate({
|
if (type === "application") {
|
||||||
applicationId,
|
utils.application.one.invalidate({
|
||||||
});
|
applicationId: id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
utils.compose.one.invalidate({
|
||||||
|
composeId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
toast.success("Refresh updated");
|
toast.success("Refresh updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowDeployment } from "../deployments/show-deployment";
|
||||||
|
import { ShowDeployments } from "./show-deployments";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
type:
|
||||||
|
| "application"
|
||||||
|
| "compose"
|
||||||
|
| "schedule"
|
||||||
|
| "server"
|
||||||
|
| "backup"
|
||||||
|
| "previewDeployment";
|
||||||
|
serverId?: string;
|
||||||
|
refreshToken?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDuration = (seconds: number) => {
|
||||||
|
if (seconds < 60) return `${seconds}s`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}m ${remainingSeconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShowDeploymentsModal = ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
serverId,
|
||||||
|
refreshToken,
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
const [activeLog, setActiveLog] = useState<
|
||||||
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
|
>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{children ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
||||||
|
View Logs
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl p-0">
|
||||||
|
<ShowDeployments
|
||||||
|
id={id}
|
||||||
|
type={type}
|
||||||
|
serverId={serverId}
|
||||||
|
refreshToken={refreshToken}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<ShowDeployment
|
||||||
|
serverId={serverId || ""}
|
||||||
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
|
onClose={() => setActiveLog(null)}
|
||||||
|
logPath={activeLog?.logPath || ""}
|
||||||
|
errorMessage={activeLog?.errorMessage || ""}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,28 +9,53 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { RocketIcon } from "lucide-react";
|
import { RocketIcon, Clock, Loader2 } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { CancelQueues } from "./cancel-queues";
|
import { CancelQueues } from "./cancel-queues";
|
||||||
import { RefreshToken } from "./refresh-token";
|
import { RefreshToken } from "./refresh-token";
|
||||||
import { ShowDeployment } from "./show-deployment";
|
import { ShowDeployment } from "./show-deployment";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type:
|
||||||
|
| "application"
|
||||||
|
| "compose"
|
||||||
|
| "schedule"
|
||||||
|
| "server"
|
||||||
|
| "backup"
|
||||||
|
| "previewDeployment";
|
||||||
|
refreshToken?: string;
|
||||||
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDeployments = ({ applicationId }: Props) => {
|
export const formatDuration = (seconds: number) => {
|
||||||
|
if (seconds < 60) return `${seconds}s`;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = seconds % 60;
|
||||||
|
return `${minutes}m ${remainingSeconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShowDeployments = ({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
refreshToken,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<
|
const [activeLog, setActiveLog] = useState<
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
>(null);
|
>(null);
|
||||||
const { data } = api.application.one.useQuery({ applicationId });
|
const { data: deployments, isLoading: isLoadingDeployments } =
|
||||||
const { data: deployments } = api.deployment.all.useQuery(
|
api.deployment.allByType.useQuery(
|
||||||
{ applicationId },
|
{
|
||||||
{
|
id,
|
||||||
enabled: !!applicationId,
|
type,
|
||||||
refetchInterval: 1000,
|
},
|
||||||
},
|
{
|
||||||
);
|
enabled: !!id,
|
||||||
|
refetchInterval: 1000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,34 +63,48 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background border-none">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
See all the 10 last deployments for this application
|
See all the 10 last deployments for this {type}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<CancelQueues applicationId={applicationId} />
|
{(type === "application" || type === "compose") && (
|
||||||
|
<CancelQueues id={id} type={type} />
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2 text-sm">
|
{refreshToken && (
|
||||||
<span>
|
<div className="flex flex-col gap-2 text-sm">
|
||||||
If you want to re-deploy this application use this URL in the config
|
<span>
|
||||||
of your git provider or docker
|
If you want to re-deploy this application use this URL in the
|
||||||
</span>
|
config of your git provider or docker
|
||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
</span>
|
||||||
<span>Webhook URL: </span>
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<span>Webhook URL: </span>
|
||||||
<span className="break-all text-muted-foreground">
|
<div className="flex flex-row items-center gap-2">
|
||||||
{`${url}/api/deploy/${data?.refreshToken}`}
|
<span className="break-all text-muted-foreground">
|
||||||
</span>
|
{`${url}/api/deploy/${refreshToken}`}
|
||||||
<RefreshToken applicationId={applicationId} />
|
</span>
|
||||||
|
{(type === "application" || type === "compose") && (
|
||||||
|
<RefreshToken id={id} type={type} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
{data?.deployments?.length === 0 ? (
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
{isLoadingDeployments ? (
|
||||||
|
<div className="flex w-full flex-row items-center justify-center gap-3 pt-10 min-h-[25vh]">
|
||||||
|
<Loader2 className="size-6 text-muted-foreground animate-spin" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Loading deployments...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : deployments?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10 min-h-[25vh]">
|
||||||
<RocketIcon className="size-8 text-muted-foreground" />
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No deployments found
|
No deployments found
|
||||||
@@ -96,8 +135,23 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<div className="text-sm capitalize text-muted-foreground">
|
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
|
||||||
<DateTooltip date={deployment.createdAt} />
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
{deployment.startedAt && deployment.finishedAt && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="text-[10px] gap-1 flex items-center"
|
||||||
|
>
|
||||||
|
<Clock className="size-3" />
|
||||||
|
{formatDuration(
|
||||||
|
Math.floor(
|
||||||
|
(new Date(deployment.finishedAt).getTime() -
|
||||||
|
new Date(deployment.startedAt).getTime()) /
|
||||||
|
1000,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -113,7 +167,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
serverId={data?.serverId || ""}
|
serverId={serverId}
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog?.logPath || ""}
|
logPath={activeLog?.logPath || ""}
|
||||||
|
|||||||
@@ -38,26 +38,67 @@ import { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { domain } from "@/server/db/validations/domain";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Dices } from "lucide-react";
|
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
export type CacheType = "fetch" | "cache";
|
||||||
|
|
||||||
|
export const domain = z
|
||||||
|
.object({
|
||||||
|
host: z.string().min(1, { message: "Add a hostname" }),
|
||||||
|
path: z.string().min(1).optional(),
|
||||||
|
port: z
|
||||||
|
.number()
|
||||||
|
.min(1, { message: "Port must be at least 1" })
|
||||||
|
.max(65535, { message: "Port must be 65535 or below" })
|
||||||
|
.optional(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
|
customCertResolver: z.string().optional(),
|
||||||
|
serviceName: z.string().optional(),
|
||||||
|
domainType: z.enum(["application", "compose", "preview"]).optional(),
|
||||||
|
})
|
||||||
|
.superRefine((input, ctx) => {
|
||||||
|
if (input.https && !input.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.certificateType === "custom" && !input.customCertResolver) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["customCertResolver"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.domainType === "compose" && !input.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["serviceName"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
domainId?: string;
|
domainId?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDomain = ({
|
export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||||
applicationId,
|
|
||||||
domainId = "",
|
|
||||||
children,
|
|
||||||
}: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, refetch } = api.domain.one.useQuery(
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -68,14 +109,24 @@ export const AddDomain = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: application } = api.application.one.useQuery(
|
const { data: application } =
|
||||||
{
|
type === "application"
|
||||||
applicationId,
|
? api.application.one.useQuery(
|
||||||
},
|
{
|
||||||
{
|
applicationId: id,
|
||||||
enabled: !!applicationId,
|
},
|
||||||
},
|
{
|
||||||
);
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } = domainId
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
? api.domain.update.useMutation()
|
? api.domain.update.useMutation()
|
||||||
@@ -89,6 +140,23 @@ export const AddDomain = ({
|
|||||||
serverId: application?.serverId || "",
|
serverId: application?.serverId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: type === "compose" && !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const form = useForm<Domain>({
|
const form = useForm<Domain>({
|
||||||
resolver: zodResolver(domain),
|
resolver: zodResolver(domain),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -98,12 +166,15 @@ export const AddDomain = ({
|
|||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
customCertResolver: undefined,
|
customCertResolver: undefined,
|
||||||
|
serviceName: undefined,
|
||||||
|
domainType: type,
|
||||||
},
|
},
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateType = form.watch("certificateType");
|
const certificateType = form.watch("certificateType");
|
||||||
const https = form.watch("https");
|
const https = form.watch("https");
|
||||||
|
const domainType = form.watch("domainType");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -114,6 +185,8 @@ export const AddDomain = ({
|
|||||||
port: data?.port || undefined,
|
port: data?.port || undefined,
|
||||||
certificateType: data?.certificateType || undefined,
|
certificateType: data?.certificateType || undefined,
|
||||||
customCertResolver: data?.customCertResolver || undefined,
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
|
serviceName: data?.serviceName || undefined,
|
||||||
|
domainType: data?.domainType || type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +198,7 @@ export const AddDomain = ({
|
|||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
customCertResolver: undefined,
|
customCertResolver: undefined,
|
||||||
|
domainType: type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, data, isLoading, domainId]);
|
}, [form, data, isLoading, domainId]);
|
||||||
@@ -148,22 +222,37 @@ export const AddDomain = ({
|
|||||||
const onSubmit = async (data: Domain) => {
|
const onSubmit = async (data: Domain) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
domainId,
|
domainId,
|
||||||
applicationId,
|
...(data.domainType === "application" && {
|
||||||
|
applicationId: id,
|
||||||
|
}),
|
||||||
|
...(data.domainType === "compose" && {
|
||||||
|
composeId: id,
|
||||||
|
}),
|
||||||
...data,
|
...data,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success(dictionary.success);
|
toast.success(dictionary.success);
|
||||||
await utils.domain.byApplicationId.invalidate({
|
|
||||||
applicationId,
|
if (data.domainType === "application") {
|
||||||
});
|
await utils.domain.byApplicationId.invalidate({
|
||||||
await utils.application.readTraefikConfig.invalidate({ applicationId });
|
applicationId: id,
|
||||||
|
});
|
||||||
|
await utils.application.readTraefikConfig.invalidate({
|
||||||
|
applicationId: id,
|
||||||
|
});
|
||||||
|
} else if (data.domainType === "compose") {
|
||||||
|
await utils.domain.byComposeId.invalidate({
|
||||||
|
composeId: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (domainId) {
|
if (domainId) {
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
|
console.log(e);
|
||||||
toast.error(dictionary.error);
|
toast.error(dictionary.error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -187,6 +276,119 @@ export const AddDomain = ({
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex flex-row items-end w-full gap-4">
|
||||||
|
{domainType === "compose" && (
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock
|
||||||
|
type="warning"
|
||||||
|
className="[overflow-wrap:anywhere]"
|
||||||
|
>
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load
|
||||||
|
the services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this
|
||||||
|
compose, it will read the services from
|
||||||
|
the last deployment/fetch from the
|
||||||
|
repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="host"
|
name="host"
|
||||||
@@ -16,12 +16,13 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
PenBoxIcon,
|
PenBoxIcon,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
Server,
|
||||||
Trash2,
|
Trash2,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AddDomain } from "./add-domain";
|
import { AddDomain } from "./handle-domain";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -29,36 +30,70 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import type { ValidationStates } from "../../compose/domains/show-domains";
|
import { DnsHelperModal } from "./dns-helper-modal";
|
||||||
import { DnsHelperModal } from "../../compose/domains/dns-helper-modal";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
export type ValidationState = {
|
||||||
|
isLoading: boolean;
|
||||||
|
isValid?: boolean;
|
||||||
|
error?: string;
|
||||||
|
resolvedIp?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValidationStates = Record<string, ValidationState>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
id: string;
|
||||||
|
type: "application" | "compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ applicationId }: Props) => {
|
export const ShowDomains = ({ id, type }: Props) => {
|
||||||
const { data: application } = api.application.one.useQuery(
|
const { data: application } =
|
||||||
{
|
type === "application"
|
||||||
applicationId,
|
? api.application.one.useQuery(
|
||||||
},
|
{
|
||||||
{
|
applicationId: id,
|
||||||
enabled: !!applicationId,
|
},
|
||||||
},
|
{
|
||||||
);
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
|
|
||||||
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
const {
|
||||||
{
|
data,
|
||||||
applicationId,
|
refetch,
|
||||||
},
|
isLoading: isLoadingDomains,
|
||||||
{
|
} = type === "application"
|
||||||
enabled: !!applicationId,
|
? api.domain.byApplicationId.useQuery(
|
||||||
},
|
{
|
||||||
);
|
applicationId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: api.domain.byComposeId.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { mutateAsync: validateDomain } =
|
const { mutateAsync: validateDomain } =
|
||||||
api.domain.validateDomain.useMutation();
|
api.domain.validateDomain.useMutation();
|
||||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||||
@@ -113,7 +148,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<AddDomain applicationId={applicationId}>
|
<AddDomain id={id} type={type}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
@@ -122,15 +157,22 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
{data?.length === 0 ? (
|
{isLoadingDomains ? (
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
<div className="flex w-full flex-row gap-4 min-h-[40vh] justify-center items-center">
|
||||||
|
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Loading domains...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : data?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 min-h-[40vh]">
|
||||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To access the application it is required to set at least 1
|
To access the application it is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<AddDomain applicationId={applicationId}>
|
<AddDomain id={id} type={type}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
@@ -138,19 +180,26 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
||||||
{data?.map((item) => {
|
{data?.map((item) => {
|
||||||
const validationState = validationStates[item.host];
|
const validationState = validationStates[item.host];
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={item.domainId}
|
key={item.domainId}
|
||||||
className="relative overflow-hidden w-full border bg-card transition-all hover:shadow-md bg-transparent"
|
className="relative overflow-hidden w-full border bg-card transition-all hover:shadow-md bg-transparent h-fit"
|
||||||
>
|
>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Service & Domain Info */}
|
{/* Service & Domain Info */}
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
{item.serviceName && (
|
||||||
|
<Badge variant="outline" className="w-fit">
|
||||||
|
<Server className="size-3 mr-1" />
|
||||||
|
{item.serviceName}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className="flex items-center gap-2 text-base font-medium hover:underline"
|
className="flex items-center gap-2 text-base font-medium hover:underline"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -175,7 +224,8 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddDomain
|
<AddDomain
|
||||||
applicationId={applicationId}
|
id={id}
|
||||||
|
type={type}
|
||||||
domainId={item.domainId}
|
domainId={item.domainId}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -381,6 +381,9 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandEmpty>No branch found.</CommandEmpty>
|
<CommandEmpty>No branch found.</CommandEmpty>
|
||||||
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
|
{branches && branches.length === 0 && (
|
||||||
|
<CommandItem>No branches found.</CommandItem>
|
||||||
|
)}
|
||||||
{branches?.map((branch: GiteaBranch) => (
|
{branches?.map((branch: GiteaBranch) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={branch.name}
|
value={branch.name}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { GitBranch, UploadCloud } from "lucide-react";
|
import { GitBranch, Loader2, UploadCloud } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||||
@@ -34,14 +34,49 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowProviderForm = ({ applicationId }: Props) => {
|
export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
const { data: githubProviders, isLoading: isLoadingGithub } =
|
||||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
api.github.githubProviders.useQuery();
|
||||||
const { data: bitbucketProviders } =
|
const { data: gitlabProviders, isLoading: isLoadingGitlab } =
|
||||||
|
api.gitlab.gitlabProviders.useQuery();
|
||||||
|
const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
||||||
|
api.gitea.giteaProviders.useQuery();
|
||||||
|
|
||||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className="group relative w-full bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-start justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||||
|
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||||
|
Select the source of your code
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||||
|
<GitBranch className="size-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex min-h-[25vh] items-center justify-center">
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
<span>Loading providers...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group relative w-full bg-transparent">
|
<Card className="group relative w-full bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -123,7 +158,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{githubProviders && githubProviders?.length > 0 ? (
|
{githubProviders && githubProviders?.length > 0 ? (
|
||||||
<SaveGithubProvider applicationId={applicationId} />
|
<SaveGithubProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GithubIcon className="size-8 text-muted-foreground" />
|
<GithubIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitHub, you need to configure your account
|
To deploy using GitHub, you need to configure your account
|
||||||
@@ -143,7 +178,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
||||||
<SaveGitlabProvider applicationId={applicationId} />
|
<SaveGitlabProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GitlabIcon className="size-8 text-muted-foreground" />
|
<GitlabIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitLab, you need to configure your account
|
To deploy using GitLab, you need to configure your account
|
||||||
@@ -163,7 +198,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
||||||
<SaveBitbucketProvider applicationId={applicationId} />
|
<SaveBitbucketProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<BitbucketIcon className="size-8 text-muted-foreground" />
|
<BitbucketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Bitbucket, you need to configure your account
|
To deploy using Bitbucket, you need to configure your account
|
||||||
@@ -183,7 +218,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||||
<SaveGiteaProvider applicationId={applicationId} />
|
<SaveGiteaProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Gitea, you need to configure your account
|
To deploy using Gitea, you need to configure your account
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@/utils/api";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { ShowDeployment } from "../deployments/show-deployment";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
deployments: RouterOutputs["deployment"]["all"];
|
|
||||||
serverId?: string;
|
|
||||||
trigger?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowPreviewBuilds = ({
|
|
||||||
deployments,
|
|
||||||
serverId,
|
|
||||||
trigger,
|
|
||||||
}: Props) => {
|
|
||||||
const [activeLog, setActiveLog] = useState<
|
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
|
||||||
>(null);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
{trigger ? (
|
|
||||||
trigger
|
|
||||||
) : (
|
|
||||||
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
|
||||||
View Builds
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Preview Builds</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
See all the preview builds for this application on this Pull Request
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
{deployments?.map((deployment) => (
|
|
||||||
<div
|
|
||||||
key={deployment.deploymentId}
|
|
||||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
|
||||||
{deployment.status}
|
|
||||||
|
|
||||||
<StatusTooltip
|
|
||||||
status={deployment?.status}
|
|
||||||
className="size-2.5"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{deployment.title}
|
|
||||||
</span>
|
|
||||||
{deployment.description && (
|
|
||||||
<span className="break-all text-sm text-muted-foreground">
|
|
||||||
{deployment.description}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-end gap-2">
|
|
||||||
<div className="text-sm capitalize text-muted-foreground">
|
|
||||||
<DateTooltip date={deployment.createdAt} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setActiveLog(deployment);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
<ShowDeployment
|
|
||||||
serverId={serverId || ""}
|
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
|
||||||
onClose={() => setActiveLog(null)}
|
|
||||||
logPath={activeLog?.logPath || ""}
|
|
||||||
errorMessage={activeLog?.errorMessage || ""}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
FileText,
|
FileText,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
Layers,
|
Loader2,
|
||||||
PenSquare,
|
PenSquare,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -25,8 +25,8 @@ import {
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
import { AddPreviewDomain } from "./add-preview-domain";
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
import { ShowPreviewBuilds } from "./show-preview-builds";
|
|
||||||
import { ShowPreviewSettings } from "./show-preview-settings";
|
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||||
|
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
@@ -38,13 +38,16 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
api.previewDeployment.delete.useMutation();
|
api.previewDeployment.delete.useMutation();
|
||||||
|
|
||||||
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
const {
|
||||||
api.previewDeployment.all.useQuery(
|
data: previewDeployments,
|
||||||
{ applicationId },
|
refetch: refetchPreviewDeployments,
|
||||||
{
|
isLoading: isLoadingPreviewDeployments,
|
||||||
enabled: !!applicationId,
|
} = api.previewDeployment.all.useQuery(
|
||||||
},
|
{ applicationId },
|
||||||
);
|
{
|
||||||
|
enabled: !!applicationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
||||||
deletePreviewDeployment({
|
deletePreviewDeployment({
|
||||||
@@ -80,8 +83,15 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
each pull request you create.
|
each pull request you create.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{!previewDeployments?.length ? (
|
{isLoadingPreviewDeployments ? (
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
<div className="flex w-full flex-row items-center justify-center gap-3 min-h-[35vh]">
|
||||||
|
<Loader2 className="size-5 text-muted-foreground animate-spin" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
Loading preview deployments...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : !previewDeployments?.length ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 min-h-[35vh]">
|
||||||
<RocketIcon className="size-8 text-muted-foreground" />
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No preview deployments found
|
No preview deployments found
|
||||||
@@ -168,19 +178,10 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ShowModalLogs>
|
</ShowModalLogs>
|
||||||
|
|
||||||
<ShowPreviewBuilds
|
<ShowDeploymentsModal
|
||||||
deployments={deployment.deployments || []}
|
id={deployment.previewDeploymentId}
|
||||||
|
type="previewDeployment"
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
trigger={
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<Layers className="size-4" />
|
|
||||||
Builds
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddPreviewDomain
|
<AddPreviewDomain
|
||||||
|
|||||||
@@ -0,0 +1,538 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
FormDescription,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
Info,
|
||||||
|
PlusCircle,
|
||||||
|
PenBoxIcon,
|
||||||
|
RefreshCw,
|
||||||
|
DatabaseZap,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { CacheType } from "../domains/handle-domain";
|
||||||
|
|
||||||
|
export const commonCronExpressions = [
|
||||||
|
{ label: "Every minute", value: "* * * * *" },
|
||||||
|
{ label: "Every hour", value: "0 * * * *" },
|
||||||
|
{ label: "Every day at midnight", value: "0 0 * * *" },
|
||||||
|
{ label: "Every Sunday at midnight", value: "0 0 * * 0" },
|
||||||
|
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
|
||||||
|
{ label: "Every 15 minutes", value: "*/15 * * * *" },
|
||||||
|
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const formSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
cronExpression: z.string().min(1, "Cron expression is required"),
|
||||||
|
shellType: z.enum(["bash", "sh"]).default("bash"),
|
||||||
|
command: z.string(),
|
||||||
|
enabled: z.boolean().default(true),
|
||||||
|
serviceName: z.string(),
|
||||||
|
scheduleType: z.enum([
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"server",
|
||||||
|
"dokploy-server",
|
||||||
|
]),
|
||||||
|
script: z.string(),
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.scheduleType === "compose" && !data.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Service name is required",
|
||||||
|
path: ["serviceName"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(data.scheduleType === "dokploy-server" ||
|
||||||
|
data.scheduleType === "server") &&
|
||||||
|
!data.script
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Script is required",
|
||||||
|
path: ["script"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(data.scheduleType === "application" ||
|
||||||
|
data.scheduleType === "compose") &&
|
||||||
|
!data.command
|
||||||
|
) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Command is required",
|
||||||
|
path: ["command"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
scheduleId?: string;
|
||||||
|
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
cronExpression: "",
|
||||||
|
shellType: "bash",
|
||||||
|
command: "",
|
||||||
|
enabled: true,
|
||||||
|
serviceName: "",
|
||||||
|
scheduleType: scheduleType || "application",
|
||||||
|
script: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const scheduleTypeForm = form.watch("scheduleType");
|
||||||
|
|
||||||
|
const { data: schedule } = api.schedule.one.useQuery(
|
||||||
|
{ scheduleId: scheduleId || "" },
|
||||||
|
{ enabled: !!scheduleId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId: id || "",
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
enabled: !!id && scheduleType === "compose",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scheduleId && schedule) {
|
||||||
|
form.reset({
|
||||||
|
name: schedule.name,
|
||||||
|
cronExpression: schedule.cronExpression,
|
||||||
|
shellType: schedule.shellType,
|
||||||
|
command: schedule.command,
|
||||||
|
enabled: schedule.enabled,
|
||||||
|
serviceName: schedule.serviceName || "",
|
||||||
|
scheduleType: schedule.scheduleType,
|
||||||
|
script: schedule.script || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, schedule, scheduleId]);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = scheduleId
|
||||||
|
? api.schedule.update.useMutation()
|
||||||
|
: api.schedule.create.useMutation();
|
||||||
|
|
||||||
|
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||||
|
if (!id && !scheduleId) return;
|
||||||
|
|
||||||
|
await mutateAsync({
|
||||||
|
...values,
|
||||||
|
scheduleId: scheduleId || "",
|
||||||
|
...(scheduleType === "application" && {
|
||||||
|
applicationId: id || "",
|
||||||
|
}),
|
||||||
|
...(scheduleType === "compose" && {
|
||||||
|
composeId: id || "",
|
||||||
|
}),
|
||||||
|
...(scheduleType === "server" && {
|
||||||
|
serverId: id || "",
|
||||||
|
}),
|
||||||
|
...(scheduleType === "dokploy-server" && {
|
||||||
|
userId: id || "",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
`Schedule ${scheduleId ? "updated" : "created"} successfully`,
|
||||||
|
);
|
||||||
|
utils.schedule.list.invalidate({
|
||||||
|
id,
|
||||||
|
scheduleType,
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : "An unknown error occurred",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{scheduleId ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10"
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button>
|
||||||
|
<PlusCircle className="w-4 h-4 mr-2" />
|
||||||
|
Add Schedule
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
className={cn(
|
||||||
|
"max-h-screen overflow-y-auto",
|
||||||
|
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
|
||||||
|
? "max-h-[95vh] sm:max-w-2xl"
|
||||||
|
: " sm:max-w-lg",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
{scheduleTypeForm === "compose" && (
|
||||||
|
<div className="flex flex-col w-full gap-4">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock
|
||||||
|
type="warning"
|
||||||
|
className="[overflow-wrap:anywhere]"
|
||||||
|
>
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load the
|
||||||
|
services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this compose,
|
||||||
|
it will read the services from the last
|
||||||
|
deployment/fetch from the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Task Name
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Daily Database Backup" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
A descriptive name for your scheduled task
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cronExpression"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Schedule
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Cron expression format: minute hour day month
|
||||||
|
weekday
|
||||||
|
</p>
|
||||||
|
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</FormLabel>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a predefined schedule" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{commonCronExpressions.map((expr) => (
|
||||||
|
<SelectItem key={expr.value} value={expr.value}>
|
||||||
|
{expr.label} ({expr.value})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="relative">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
Choose a predefined schedule or enter a custom cron
|
||||||
|
expression
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(scheduleTypeForm === "application" ||
|
||||||
|
scheduleTypeForm === "compose") && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="shellType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Shell Type
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select shell type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="bash">Bash</SelectItem>
|
||||||
|
<SelectItem value="sh">Sh</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
Choose the shell to execute your command
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="command"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Command
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="npm run backup" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The command to execute in your container
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(scheduleTypeForm === "dokploy-server" ||
|
||||||
|
scheduleTypeForm === "server") && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="script"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Script</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<CodeEditor
|
||||||
|
language="shell"
|
||||||
|
placeholder={`# This is a comment
|
||||||
|
echo "Hello, world!"
|
||||||
|
`}
|
||||||
|
className="h-96 font-mono"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
Enabled
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={isLoading} className="w-full">
|
||||||
|
{scheduleId ? "Update" : "Create"} Schedule
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { HandleSchedules } from "./handle-schedules";
|
||||||
|
import {
|
||||||
|
Clock,
|
||||||
|
Play,
|
||||||
|
Terminal,
|
||||||
|
Trash2,
|
||||||
|
ClipboardList,
|
||||||
|
Loader2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||||
|
const {
|
||||||
|
data: schedules,
|
||||||
|
isLoading: isLoadingSchedules,
|
||||||
|
refetch: refetchSchedules,
|
||||||
|
} = api.schedule.list.useQuery(
|
||||||
|
{
|
||||||
|
id: id || "",
|
||||||
|
scheduleType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
||||||
|
api.schedule.delete.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: runManually, isLoading } =
|
||||||
|
api.schedule.runManually.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||||
|
<CardHeader className="px-0">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||||
|
Scheduled Tasks
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Schedule tasks to run automatically at specified intervals.
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{schedules && schedules.length > 0 && (
|
||||||
|
<HandleSchedules id={id} scheduleType={scheduleType} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="px-0">
|
||||||
|
{isLoadingSchedules ? (
|
||||||
|
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||||
|
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||||
|
<span className="text-sm text-muted-foreground/70">
|
||||||
|
Loading scheduled tasks...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : schedules && schedules.length > 0 ? (
|
||||||
|
<div className="grid xl:grid-cols-2 gap-4 grid-cols-1 h-full">
|
||||||
|
{schedules.map((schedule) => {
|
||||||
|
const serverId =
|
||||||
|
schedule.serverId ||
|
||||||
|
schedule.application?.serverId ||
|
||||||
|
schedule.compose?.serverId;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={schedule.scheduleId}
|
||||||
|
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||||
|
<Clock className="size-4 text-primary/70" />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-sm font-medium leading-none">
|
||||||
|
{schedule.name}
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
variant={schedule.enabled ? "default" : "secondary"}
|
||||||
|
className="text-[10px] px-1 py-0"
|
||||||
|
>
|
||||||
|
{schedule.enabled ? "Enabled" : "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono text-[10px] bg-transparent"
|
||||||
|
>
|
||||||
|
Cron: {schedule.cronExpression}
|
||||||
|
</Badge>
|
||||||
|
{schedule.scheduleType !== "server" &&
|
||||||
|
schedule.scheduleType !== "dokploy-server" && (
|
||||||
|
<>
|
||||||
|
<span className="text-xs text-muted-foreground/50">
|
||||||
|
•
|
||||||
|
</span>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono text-[10px] bg-transparent"
|
||||||
|
>
|
||||||
|
{schedule.shellType}
|
||||||
|
</Badge>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{schedule.command && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Terminal className="size-3.5 text-muted-foreground/70" />
|
||||||
|
<code className="font-mono text-[10px] text-muted-foreground/70">
|
||||||
|
{schedule.command}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ShowDeploymentsModal
|
||||||
|
id={schedule.scheduleId}
|
||||||
|
type="schedule"
|
||||||
|
serverId={serverId || undefined}
|
||||||
|
>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<ClipboardList className="size-4 transition-colors " />
|
||||||
|
</Button>
|
||||||
|
</ShowDeploymentsModal>
|
||||||
|
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
toast.success("Schedule run successfully");
|
||||||
|
|
||||||
|
await runManually({
|
||||||
|
scheduleId: schedule.scheduleId,
|
||||||
|
}).then(async () => {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 1500),
|
||||||
|
);
|
||||||
|
refetchSchedules();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play className="size-4 transition-colors" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Run Manual Schedule</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<HandleSchedules
|
||||||
|
scheduleId={schedule.scheduleId}
|
||||||
|
id={id}
|
||||||
|
scheduleType={scheduleType}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Schedule"
|
||||||
|
description="Are you sure you want to delete this schedule?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteSchedule({
|
||||||
|
scheduleId: schedule.scheduleId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
utils.schedule.list.invalidate({
|
||||||
|
id,
|
||||||
|
scheduleType,
|
||||||
|
});
|
||||||
|
toast.success("Schedule deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting schedule");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isDeleting}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||||
|
<Clock className="size-8 mb-4 text-muted-foreground" />
|
||||||
|
<p className="text-lg font-medium text-muted-foreground">
|
||||||
|
No scheduled tasks
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Create your first scheduled task to automate your workflows
|
||||||
|
</p>
|
||||||
|
<HandleSchedules id={id} scheduleType={scheduleType} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { Paintbrush } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
|
||||||
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
|
|
||||||
if (isCloud) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
|
|
||||||
Cancel Queues
|
|
||||||
<Paintbrush className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
Are you sure to cancel the incoming deployments?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This will cancel all the incoming deployments
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Queues are being cleaned");
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(err.message);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { RefreshCcw } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
export const RefreshTokenCompose = ({ composeId }: Props) => {
|
|
||||||
const { mutateAsync } = api.compose.refreshToken.useMutation();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
return (
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger>
|
|
||||||
<RefreshCcw className="h-4 w-4 cursor-pointer text-muted-foreground" />
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This action cannot be undone. This will permanently change the token
|
|
||||||
and all the previous tokens will be invalidated
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
onClick={async () => {
|
|
||||||
await mutateAsync({
|
|
||||||
composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
utils.compose.one.invalidate({
|
|
||||||
composeId,
|
|
||||||
});
|
|
||||||
toast.success("Refresh Token updated");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the refresh token");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
logPath: string | null;
|
|
||||||
serverId?: string;
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
errorMessage?: string;
|
|
||||||
}
|
|
||||||
export const ShowDeploymentCompose = ({
|
|
||||||
logPath,
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
serverId,
|
|
||||||
errorMessage,
|
|
||||||
}: Props) => {
|
|
||||||
const [data, setData] = useState("");
|
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
|
||||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
|
||||||
if (autoScroll && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (!scrollRef.current) return;
|
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
||||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
|
||||||
setAutoScroll(isAtBottom);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open || !logPath) return;
|
|
||||||
|
|
||||||
setData("");
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
|
||||||
const ws = new WebSocket(wsUrl);
|
|
||||||
|
|
||||||
wsRef.current = ws; // Store WebSocket instance in ref
|
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
|
||||||
setData((currentData) => currentData + e.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
|
||||||
console.error("WebSocket error: ", error);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
wsRef.current = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
||||||
ws.close();
|
|
||||||
wsRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [logPath, open]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const logs = parseLogs(data);
|
|
||||||
let filteredLogsResult = logs;
|
|
||||||
if (serverId) {
|
|
||||||
let hideSubsequentLogs = false;
|
|
||||||
filteredLogsResult = logs.filter((log) => {
|
|
||||||
if (
|
|
||||||
log.message.includes(
|
|
||||||
"===================================EXTRA LOGS============================================",
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
hideSubsequentLogs = true;
|
|
||||||
return showExtraLogs;
|
|
||||||
}
|
|
||||||
return showExtraLogs ? true : !hideSubsequentLogs;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilteredLogs(filteredLogsResult);
|
|
||||||
}, [data, showExtraLogs]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
scrollToBottom();
|
|
||||||
|
|
||||||
if (autoScroll && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [filteredLogs, autoScroll]);
|
|
||||||
|
|
||||||
const optionalErrors = parseLogs(errorMessage || "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(e) => {
|
|
||||||
onClose();
|
|
||||||
if (!e) {
|
|
||||||
setData("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wsRef.current) {
|
|
||||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
|
||||||
wsRef.current.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent className={"sm:max-w-5xl max-h-screen"}>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
|
||||||
<DialogDescription className="flex items-center gap-2">
|
|
||||||
<span>
|
|
||||||
See all the details of this deployment |{" "}
|
|
||||||
<Badge variant="blank" className="text-xs">
|
|
||||||
{filteredLogs.length} lines
|
|
||||||
</Badge>
|
|
||||||
</span>
|
|
||||||
{serverId && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="show-extra-logs"
|
|
||||||
checked={showExtraLogs}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setShowExtraLogs(checked as boolean)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="show-extra-logs"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
Show Extra Logs
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={scrollRef}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
|
||||||
>
|
|
||||||
{filteredLogs.length > 0 ? (
|
|
||||||
filteredLogs.map((log: LogLine, index: number) => (
|
|
||||||
<TerminalLine key={index} log={log} noTimestamp />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{optionalErrors.length > 0 ? (
|
|
||||||
optionalErrors.map((log: LogLine, index: number) => (
|
|
||||||
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
|
||||||
import { RocketIcon } from "lucide-react";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
|
||||||
import { RefreshTokenCompose } from "./refresh-token-compose";
|
|
||||||
import { ShowDeploymentCompose } from "./show-deployment-compose";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
|
||||||
const [activeLog, setActiveLog] = useState<
|
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
|
||||||
>(null);
|
|
||||||
const { data } = api.compose.one.useQuery({ composeId });
|
|
||||||
const { data: deployments } = api.deployment.allByCompose.useQuery(
|
|
||||||
{ composeId },
|
|
||||||
{
|
|
||||||
enabled: !!composeId,
|
|
||||||
refetchInterval: 5000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const [url, setUrl] = React.useState("");
|
|
||||||
useEffect(() => {
|
|
||||||
setUrl(document.location.origin);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
See all the 10 last deployments for this compose
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<CancelQueuesCompose composeId={composeId} />
|
|
||||||
{/* <CancelQueues applicationId={applicationId} /> */}
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-2 text-sm">
|
|
||||||
<span>
|
|
||||||
If you want to re-deploy this application use this URL in the config
|
|
||||||
of your git provider or docker
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
|
||||||
<span>Webhook URL: </span>
|
|
||||||
<div className="flex flex-row items-center gap-2">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{`${url}/api/deploy/compose/${data?.refreshToken}`}
|
|
||||||
</span>
|
|
||||||
<RefreshTokenCompose composeId={composeId} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{data?.deployments?.length === 0 ? (
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
|
||||||
<RocketIcon className="size-8 text-muted-foreground" />
|
|
||||||
<span className="text-base text-muted-foreground">
|
|
||||||
No deployments found
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{deployments?.map((deployment) => (
|
|
||||||
<div
|
|
||||||
key={deployment.deploymentId}
|
|
||||||
className="flex items-center justify-between rounded-lg border p-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
|
||||||
{deployment.status}
|
|
||||||
|
|
||||||
<StatusTooltip
|
|
||||||
status={deployment?.status}
|
|
||||||
className="size-2.5"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{deployment.title}
|
|
||||||
</span>
|
|
||||||
{deployment.description && (
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{deployment.description}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col items-end gap-2">
|
|
||||||
<div className="text-sm capitalize text-muted-foreground">
|
|
||||||
<DateTooltip date={deployment.createdAt} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setActiveLog(deployment);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<ShowDeploymentCompose
|
|
||||||
serverId={data?.serverId || ""}
|
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
|
||||||
onClose={() => setActiveLog(null)}
|
|
||||||
logPath={activeLog?.logPath || ""}
|
|
||||||
errorMessage={activeLog?.errorMessage || ""}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,508 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input, NumberInput } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { domainCompose } from "@/server/db/validations/domain";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import type z from "zod";
|
|
||||||
|
|
||||||
type Domain = z.infer<typeof domainCompose>;
|
|
||||||
|
|
||||||
export type CacheType = "fetch" | "cache";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
domainId?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AddDomainCompose = ({
|
|
||||||
composeId,
|
|
||||||
domainId = "",
|
|
||||||
children,
|
|
||||||
}: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { data, refetch } = api.domain.one.useQuery(
|
|
||||||
{
|
|
||||||
domainId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!domainId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: compose } = api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!composeId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: services,
|
|
||||||
isFetching: isLoadingServices,
|
|
||||||
error: errorServices,
|
|
||||||
refetch: refetchServices,
|
|
||||||
} = api.compose.loadServices.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
type: cacheType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
retry: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
|
||||||
api.domain.generateDomain.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } = domainId
|
|
||||||
? api.domain.update.useMutation()
|
|
||||||
: api.domain.create.useMutation();
|
|
||||||
|
|
||||||
const { data: canGenerateTraefikMeDomains } =
|
|
||||||
api.domain.canGenerateTraefikMeDomains.useQuery({
|
|
||||||
serverId: compose?.serverId || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<Domain>({
|
|
||||||
resolver: zodResolver(domainCompose),
|
|
||||||
defaultValues: {
|
|
||||||
host: "",
|
|
||||||
path: undefined,
|
|
||||||
port: undefined,
|
|
||||||
https: false,
|
|
||||||
certificateType: undefined,
|
|
||||||
customCertResolver: undefined,
|
|
||||||
serviceName: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const https = form.watch("https");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
...data,
|
|
||||||
/* Convert null to undefined */
|
|
||||||
path: data?.path || undefined,
|
|
||||||
port: data?.port || undefined,
|
|
||||||
serviceName: data?.serviceName || undefined,
|
|
||||||
certificateType: data?.certificateType || undefined,
|
|
||||||
customCertResolver: data?.customCertResolver || undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!domainId) {
|
|
||||||
form.reset({
|
|
||||||
host: "",
|
|
||||||
path: undefined,
|
|
||||||
port: undefined,
|
|
||||||
https: false,
|
|
||||||
certificateType: undefined,
|
|
||||||
customCertResolver: undefined,
|
|
||||||
serviceName: "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, form.reset, data, isLoading]);
|
|
||||||
|
|
||||||
const dictionary = {
|
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
|
||||||
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
|
||||||
submit: domainId ? "Update" : "Create",
|
|
||||||
dialogDescription: domainId
|
|
||||||
? "In this section you can edit a domain"
|
|
||||||
: "In this section you can add domains",
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data: Domain) => {
|
|
||||||
await mutateAsync({
|
|
||||||
domainId,
|
|
||||||
composeId,
|
|
||||||
domainType: "compose",
|
|
||||||
...data,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await utils.domain.byComposeId.invalidate({
|
|
||||||
composeId,
|
|
||||||
});
|
|
||||||
toast.success(dictionary.success);
|
|
||||||
if (domainId) {
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(dictionary.error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger className="" asChild>
|
|
||||||
{children}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Domain</DialogTitle>
|
|
||||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<AlertBlock type="info">
|
|
||||||
Deploy is required to apply changes after creating or updating a
|
|
||||||
domain.
|
|
||||||
</AlertBlock>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-8 "
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{errorServices && (
|
|
||||||
<AlertBlock
|
|
||||||
type="warning"
|
|
||||||
className="[overflow-wrap:anywhere]"
|
|
||||||
>
|
|
||||||
{errorServices?.message}
|
|
||||||
</AlertBlock>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-row items-end w-full gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="serviceName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full">
|
|
||||||
<FormLabel>Service Name</FormLabel>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value || ""}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a service name" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<SelectContent>
|
|
||||||
{services?.map((service, index) => (
|
|
||||||
<SelectItem
|
|
||||||
value={service}
|
|
||||||
key={`${service}-${index}`}
|
|
||||||
>
|
|
||||||
{service}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectItem value="none" disabled>
|
|
||||||
Empty
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingServices}
|
|
||||||
onClick={() => {
|
|
||||||
if (cacheType === "fetch") {
|
|
||||||
refetchServices();
|
|
||||||
} else {
|
|
||||||
setCacheType("fetch");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RefreshCw className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Fetch: Will clone the repository and load the
|
|
||||||
services
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingServices}
|
|
||||||
onClick={() => {
|
|
||||||
if (cacheType === "cache") {
|
|
||||||
refetchServices();
|
|
||||||
} else {
|
|
||||||
setCacheType("cache");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DatabaseZap className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Cache: If you previously deployed this
|
|
||||||
compose, it will read the services from the
|
|
||||||
last deployment/fetch from the repository
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="host"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
{!canGenerateTraefikMeDomains &&
|
|
||||||
field.value.includes("traefik.me") && (
|
|
||||||
<AlertBlock type="warning">
|
|
||||||
You need to set an IP address in your{" "}
|
|
||||||
<Link
|
|
||||||
href="/dashboard/settings/server"
|
|
||||||
className="text-primary"
|
|
||||||
>
|
|
||||||
{compose?.serverId
|
|
||||||
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
|
||||||
: "Web Server -> Server -> Update Server IP"}
|
|
||||||
</Link>{" "}
|
|
||||||
to make your traefik.me domain work.
|
|
||||||
</AlertBlock>
|
|
||||||
)}
|
|
||||||
<FormLabel>Host</FormLabel>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="api.dokploy.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingGenerate}
|
|
||||||
onClick={() => {
|
|
||||||
generateDomain({
|
|
||||||
serverId: compose?.serverId || "",
|
|
||||||
appName: compose?.appName || "",
|
|
||||||
})
|
|
||||||
.then((domain) => {
|
|
||||||
field.onChange(domain);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(err.message);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dices className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>Generate traefik.me domain</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="path"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Path</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder={"/"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="port"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Container Port</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
The port where your application is running inside the
|
|
||||||
container (e.g., 3000 for Node.js, 80 for Nginx, 8080
|
|
||||||
for Java)
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<NumberInput placeholder={"3000"} {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="https"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>HTTPS</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Automatically provision SSL Certificate.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{https && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="certificateType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="col-span-2">
|
|
||||||
<FormLabel>Certificate Provider</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value || ""}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a certificate provider" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="none">None</SelectItem>
|
|
||||||
<SelectItem value={"letsencrypt"}>
|
|
||||||
Let's Encrypt
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={"custom"}>Custom</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.getValues().certificateType === "custom" && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="customCertResolver"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="col-span-2">
|
|
||||||
<FormLabel>Custom Certificate Resolver</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter your custom certificate resolver"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
form="hook-form"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{dictionary.submit}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import {
|
|
||||||
ExternalLink,
|
|
||||||
GlobeIcon,
|
|
||||||
PenBoxIcon,
|
|
||||||
Trash2,
|
|
||||||
InfoIcon,
|
|
||||||
Server,
|
|
||||||
CheckCircle2,
|
|
||||||
XCircle,
|
|
||||||
Loader2,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { AddDomainCompose } from "./add-domain";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { DnsHelperModal } from "./dns-helper-modal";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
composeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ValidationState = {
|
|
||||||
isLoading: boolean;
|
|
||||||
isValid?: boolean;
|
|
||||||
error?: string;
|
|
||||||
resolvedIp?: string;
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ValidationStates = {
|
|
||||||
[key: string]: ValidationState;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
|
||||||
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
|
||||||
|
|
||||||
const { data, refetch } = api.domain.byComposeId.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!composeId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: compose } = api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!composeId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: validateDomain } =
|
|
||||||
api.domain.validateDomain.useMutation();
|
|
||||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
|
||||||
api.domain.delete.useMutation();
|
|
||||||
|
|
||||||
const handleValidateDomain = async (host: string) => {
|
|
||||||
setValidationStates((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[host]: { isLoading: true },
|
|
||||||
}));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await validateDomain({
|
|
||||||
domain: host,
|
|
||||||
serverIp:
|
|
||||||
compose?.server?.ipAddress?.toString() || ip?.toString() || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
setValidationStates((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[host]: {
|
|
||||||
isLoading: false,
|
|
||||||
isValid: result.isValid,
|
|
||||||
error: result.error,
|
|
||||||
resolvedIp: result.resolvedIp,
|
|
||||||
message: result.error && result.isValid ? result.error : undefined,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
|
||||||
const error = err as Error;
|
|
||||||
setValidationStates((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[host]: {
|
|
||||||
isLoading: false,
|
|
||||||
isValid: false,
|
|
||||||
error: error.message || "Failed to validate domain",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full flex-col gap-5">
|
|
||||||
<Card className="bg-background">
|
|
||||||
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<CardTitle className="text-xl">Domains</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Domains are used to access to the application
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
|
||||||
{data && data?.length > 0 && (
|
|
||||||
<AddDomainCompose composeId={composeId}>
|
|
||||||
<Button>
|
|
||||||
<GlobeIcon className="size-4 mr-2" /> Add Domain
|
|
||||||
</Button>
|
|
||||||
</AddDomainCompose>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{data?.length === 0 ? (
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 py-8">
|
|
||||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
|
||||||
<span className="text-base text-muted-foreground text-center">
|
|
||||||
To access to the application it is required to set at least 1
|
|
||||||
domain
|
|
||||||
</span>
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
|
||||||
<AddDomainCompose composeId={composeId}>
|
|
||||||
<Button>
|
|
||||||
<GlobeIcon className="size-4 mr-2" /> Add Domain
|
|
||||||
</Button>
|
|
||||||
</AddDomainCompose>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
|
||||||
{data?.map((item) => {
|
|
||||||
const validationState = validationStates[item.host];
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={item.domainId}
|
|
||||||
className="relative overflow-hidden border bg-card transition-all hover:shadow-md bg-transparent"
|
|
||||||
>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{/* Service & Domain Info */}
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Badge variant="outline" className="w-fit">
|
|
||||||
<Server className="size-3 mr-1" />
|
|
||||||
{item.serviceName}
|
|
||||||
</Badge>
|
|
||||||
<Link
|
|
||||||
className="flex items-center gap-2 text-base font-medium hover:underline"
|
|
||||||
target="_blank"
|
|
||||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
|
||||||
>
|
|
||||||
{item.host}
|
|
||||||
<ExternalLink className="size-4" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{!item.host.includes("traefik.me") && (
|
|
||||||
<DnsHelperModal
|
|
||||||
domain={{
|
|
||||||
host: item.host,
|
|
||||||
https: item.https,
|
|
||||||
path: item.path || undefined,
|
|
||||||
}}
|
|
||||||
serverIp={
|
|
||||||
compose?.server?.ipAddress?.toString() ||
|
|
||||||
ip?.toString()
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<AddDomainCompose
|
|
||||||
composeId={composeId}
|
|
||||||
domainId={item.domainId}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10"
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
|
||||||
</AddDomainCompose>
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Domain"
|
|
||||||
description="Are you sure you want to delete this domain?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteDomain({
|
|
||||||
domainId: item.domainId,
|
|
||||||
})
|
|
||||||
.then((_data) => {
|
|
||||||
refetch();
|
|
||||||
toast.success(
|
|
||||||
"Domain deleted successfully",
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting domain");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Domain Details */}
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge variant="secondary">
|
|
||||||
<InfoIcon className="size-3 mr-1" />
|
|
||||||
Path: {item.path || "/"}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>URL path for this service</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge variant="secondary">
|
|
||||||
<InfoIcon className="size-3 mr-1" />
|
|
||||||
Port: {item.port}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Container port exposed</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge
|
|
||||||
variant={item.https ? "outline" : "secondary"}
|
|
||||||
>
|
|
||||||
{item.https ? "HTTPS" : "HTTP"}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
{item.https
|
|
||||||
? "Secure HTTPS connection"
|
|
||||||
: "Standard HTTP connection"}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
{item.certificateType && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge variant="outline">
|
|
||||||
Cert: {item.certificateType}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>SSL Certificate Provider</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={
|
|
||||||
validationState?.isValid
|
|
||||||
? "bg-green-500/10 text-green-500 cursor-pointer"
|
|
||||||
: validationState?.error
|
|
||||||
? "bg-red-500/10 text-red-500 cursor-pointer"
|
|
||||||
: "bg-yellow-500/10 text-yellow-500 cursor-pointer"
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
handleValidateDomain(item.host)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{validationState?.isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="size-3 mr-1 animate-spin" />
|
|
||||||
Checking DNS...
|
|
||||||
</>
|
|
||||||
) : validationState?.isValid ? (
|
|
||||||
<>
|
|
||||||
<CheckCircle2 className="size-3 mr-1" />
|
|
||||||
{validationState.message
|
|
||||||
? "Behind Cloudflare"
|
|
||||||
: "DNS Valid"}
|
|
||||||
</>
|
|
||||||
) : validationState?.error ? (
|
|
||||||
<>
|
|
||||||
<XCircle className="size-3 mr-1" />
|
|
||||||
{validationState.error}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<RefreshCw className="size-3 mr-1" />
|
|
||||||
Validate DNS
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-xs">
|
|
||||||
{validationState?.error &&
|
|
||||||
!validationState.isValid ? (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p className="font-medium text-red-500">
|
|
||||||
Error:
|
|
||||||
</p>
|
|
||||||
<p>{validationState.error}</p>
|
|
||||||
</div>
|
|
||||||
) : validationState?.isValid ? (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p className="font-medium text-green-500">
|
|
||||||
{validationState.message
|
|
||||||
? "Info:"
|
|
||||||
: "Valid Configuration:"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{validationState.message ||
|
|
||||||
`Domain points to ${validationState.resolvedIp}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
"Click to validate DNS configuration"
|
|
||||||
)}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { CodeIcon, GitBranch } from "lucide-react";
|
import { CodeIcon, GitBranch, Loader2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ComposeFileEditor } from "../compose-file-editor";
|
import { ComposeFileEditor } from "../compose-file-editor";
|
||||||
@@ -25,15 +25,49 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||||
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
const { data: githubProviders, isLoading: isLoadingGithub } =
|
||||||
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
api.github.githubProviders.useQuery();
|
||||||
const { data: bitbucketProviders } =
|
const { data: gitlabProviders, isLoading: isLoadingGitlab } =
|
||||||
|
api.gitlab.gitlabProviders.useQuery();
|
||||||
|
const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
||||||
|
api.gitea.giteaProviders.useQuery();
|
||||||
|
|
||||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className="group relative w-full bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-start justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="flex flex-col space-y-0.5">Provider</span>
|
||||||
|
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
||||||
|
Select the source of your code
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="hidden space-y-1 text-sm font-normal md:block">
|
||||||
|
<GitBranch className="size-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex min-h-[25vh] items-center justify-center">
|
||||||
|
<div className="flex items-center gap-2 text-muted-foreground">
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
<span>Loading providers...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group relative w-full bg-transparent">
|
<Card className="group relative w-full bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -108,7 +142,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
{githubProviders && githubProviders?.length > 0 ? (
|
{githubProviders && githubProviders?.length > 0 ? (
|
||||||
<SaveGithubProviderCompose composeId={composeId} />
|
<SaveGithubProviderCompose composeId={composeId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GithubIcon className="size-8 text-muted-foreground" />
|
<GithubIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitHub, you need to configure your account
|
To deploy using GitHub, you need to configure your account
|
||||||
@@ -128,7 +162,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
||||||
<SaveGitlabProviderCompose composeId={composeId} />
|
<SaveGitlabProviderCompose composeId={composeId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GitlabIcon className="size-8 text-muted-foreground" />
|
<GitlabIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitLab, you need to configure your account
|
To deploy using GitLab, you need to configure your account
|
||||||
@@ -148,7 +182,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
||||||
<SaveBitbucketProviderCompose composeId={composeId} />
|
<SaveBitbucketProviderCompose composeId={composeId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<BitbucketIcon className="size-8 text-muted-foreground" />
|
<BitbucketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Bitbucket, you need to configure your account
|
To deploy using Bitbucket, you need to configure your account
|
||||||
@@ -168,7 +202,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||||
<SaveGiteaProviderCompose composeId={composeId} />
|
<SaveGiteaProviderCompose composeId={composeId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Gitea, you need to configure your account
|
To deploy using Gitea, you need to configure your account
|
||||||
|
|||||||
@@ -49,12 +49,19 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { DatabaseZap, PenBoxIcon, PlusIcon, RefreshCw } from "lucide-react";
|
import {
|
||||||
|
DatabaseZap,
|
||||||
|
Info,
|
||||||
|
PenBoxIcon,
|
||||||
|
PlusIcon,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { commonCronExpressions } from "../../application/schedules/handle-schedules";
|
||||||
|
|
||||||
type CacheType = "cache" | "fetch";
|
type CacheType = "cache" | "fetch";
|
||||||
|
|
||||||
@@ -108,6 +115,15 @@ const Schema = z
|
|||||||
path: ["databaseType"],
|
path: ["databaseType"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.backupType === "compose" && !data.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Service name is required for compose backups",
|
||||||
|
path: ["serviceName"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.backupType === "compose" && data.databaseType) {
|
if (data.backupType === "compose" && data.databaseType) {
|
||||||
if (data.databaseType === "postgres") {
|
if (data.databaseType === "postgres") {
|
||||||
if (!data.metadata?.postgres?.databaseUser) {
|
if (!data.metadata?.postgres?.databaseUser) {
|
||||||
@@ -568,10 +584,55 @@ export const HandleBackup = ({
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Schedule (Cron)</FormLabel>
|
<FormLabel className="flex items-center gap-2">
|
||||||
<FormControl>
|
Schedule
|
||||||
<Input placeholder={"0 0 * * *"} {...field} />
|
<TooltipProvider>
|
||||||
</FormControl>
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Cron expression format: minute hour day month
|
||||||
|
weekday
|
||||||
|
</p>
|
||||||
|
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</FormLabel>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a predefined schedule" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{commonCronExpressions.map((expr) => (
|
||||||
|
<SelectItem key={expr.value} value={expr.value}>
|
||||||
|
{expr.label} ({expr.value})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="relative">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
Choose a predefined schedule or enter a custom cron
|
||||||
|
expression
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ const RestoreBackupSchema = z
|
|||||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||||
.optional(),
|
.optional(),
|
||||||
backupType: z.enum(["database", "compose"]).default("database"),
|
backupType: z.enum(["database", "compose"]).default("database"),
|
||||||
serviceName: z.string().nullable().optional(),
|
|
||||||
metadata: z
|
metadata: z
|
||||||
.object({
|
.object({
|
||||||
postgres: z
|
postgres: z
|
||||||
@@ -128,6 +127,7 @@ const RestoreBackupSchema = z
|
|||||||
databaseRootPassword: z.string(),
|
databaseRootPassword: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
serviceName: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
@@ -139,6 +139,15 @@ const RestoreBackupSchema = z
|
|||||||
path: ["databaseType"],
|
path: ["databaseType"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.backupType === "compose" && !data.metadata?.serviceName) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Service name is required for compose backups",
|
||||||
|
path: ["metadata", "serviceName"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (data.backupType === "compose" && data.databaseType) {
|
if (data.backupType === "compose" && data.databaseType) {
|
||||||
if (data.databaseType === "postgres") {
|
if (data.databaseType === "postgres") {
|
||||||
if (!data.metadata?.postgres?.databaseUser) {
|
if (!data.metadata?.postgres?.databaseUser) {
|
||||||
@@ -217,6 +226,7 @@ export const RestoreBackup = ({
|
|||||||
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||||
databaseType:
|
databaseType:
|
||||||
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
|
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
|
||||||
|
backupType: backupType,
|
||||||
metadata: {},
|
metadata: {},
|
||||||
},
|
},
|
||||||
resolver: zodResolver(RestoreBackupSchema),
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
@@ -520,7 +530,11 @@ export const RestoreBackup = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Database Name</FormLabel>
|
<FormLabel>Database Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Enter database name" {...field} />
|
<Input
|
||||||
|
placeholder="Enter database name"
|
||||||
|
{...field}
|
||||||
|
disabled={databaseType === "web-server"}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -559,7 +573,7 @@ export const RestoreBackup = ({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="serviceName"
|
name="metadata.serviceName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormLabel>Service Name</FormLabel>
|
<FormLabel>Service Name</FormLabel>
|
||||||
@@ -777,10 +791,10 @@ export const RestoreBackup = ({
|
|||||||
isLoading={isDeploying}
|
isLoading={isDeploying}
|
||||||
form="hook-form-restore-backup"
|
form="hook-form-restore-backup"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={
|
// disabled={
|
||||||
!form.watch("backupFile") ||
|
// !form.watch("backupFile") ||
|
||||||
(backupType === "compose" && !form.watch("databaseType"))
|
// (backupType === "compose" && !form.watch("databaseType"))
|
||||||
}
|
// }
|
||||||
>
|
>
|
||||||
Restore
|
Restore
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
|
import {
|
||||||
|
ClipboardList,
|
||||||
|
Database,
|
||||||
|
DatabaseBackup,
|
||||||
|
Play,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -29,6 +35,7 @@ import {
|
|||||||
PostgresqlIcon,
|
PostgresqlIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -124,7 +131,7 @@ export const ShowBackups = ({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3 min-h-[35vh] justify-center">
|
||||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
To create a backup it is required to set at least 1 provider.
|
To create a backup it is required to set at least 1 provider.
|
||||||
@@ -156,6 +163,7 @@ export const ShowBackups = ({
|
|||||||
<RestoreBackup
|
<RestoreBackup
|
||||||
id={id}
|
id={id}
|
||||||
databaseType={databaseType}
|
databaseType={databaseType}
|
||||||
|
backupType={backupType}
|
||||||
serverId={
|
serverId={
|
||||||
"serverId" in postgres ? postgres.serverId : undefined
|
"serverId" in postgres ? postgres.serverId : undefined
|
||||||
}
|
}
|
||||||
@@ -170,175 +178,198 @@ export const ShowBackups = ({
|
|||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{postgres?.backups.map((backup) => (
|
{postgres?.backups.map((backup) => {
|
||||||
<div key={backup.backupId}>
|
const serverId =
|
||||||
<div className="flex w-full flex-col md:flex-row md:items-start justify-between gap-4 border rounded-lg p-4 hover:bg-muted/50 transition-colors">
|
"serverId" in postgres ? postgres.serverId : undefined;
|
||||||
<div className="flex flex-col w-full gap-4">
|
|
||||||
<div className="flex items-center gap-3">
|
return (
|
||||||
{backup.backupType === "compose" && (
|
<div key={backup.backupId}>
|
||||||
<div className="flex items-center justify-center size-10 rounded-lg">
|
<div className="flex w-full flex-col md:flex-row md:items-start justify-between gap-4 border rounded-lg p-4 hover:bg-muted/50 transition-colors">
|
||||||
{backup.databaseType === "postgres" && (
|
<div className="flex flex-col w-full gap-4">
|
||||||
<PostgresqlIcon className="size-7" />
|
<div className="flex items-center gap-3">
|
||||||
)}
|
|
||||||
{backup.databaseType === "mysql" && (
|
|
||||||
<MysqlIcon className="size-7" />
|
|
||||||
)}
|
|
||||||
{backup.databaseType === "mariadb" && (
|
|
||||||
<MariadbIcon className="size-7" />
|
|
||||||
)}
|
|
||||||
{backup.databaseType === "mongo" && (
|
|
||||||
<MongodbIcon className="size-7" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{backup.backupType === "compose" && (
|
{backup.backupType === "compose" && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-center size-10 rounded-lg">
|
||||||
<h3 className="font-medium">
|
{backup.databaseType === "postgres" && (
|
||||||
{backup.serviceName}
|
<PostgresqlIcon className="size-7" />
|
||||||
</h3>
|
)}
|
||||||
<span className="px-1.5 py-0.5 rounded-full bg-muted text-xs font-medium capitalize">
|
{backup.databaseType === "mysql" && (
|
||||||
{backup.databaseType}
|
<MysqlIcon className="size-7" />
|
||||||
</span>
|
)}
|
||||||
|
{backup.databaseType === "mariadb" && (
|
||||||
|
<MariadbIcon className="size-7" />
|
||||||
|
)}
|
||||||
|
{backup.databaseType === "mongo" && (
|
||||||
|
<MongodbIcon className="size-7" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col gap-1">
|
||||||
<div
|
{backup.backupType === "compose" && (
|
||||||
className={cn(
|
<div className="flex items-center gap-2">
|
||||||
"size-1.5 rounded-full",
|
<h3 className="font-medium">
|
||||||
backup.enabled
|
{backup.serviceName}
|
||||||
? "bg-green-500"
|
</h3>
|
||||||
: "bg-red-500",
|
<span className="px-1.5 py-0.5 rounded-full bg-muted text-xs font-medium capitalize">
|
||||||
)}
|
{backup.databaseType}
|
||||||
/>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
</div>
|
||||||
{backup.enabled ? "Active" : "Inactive"}
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"size-1.5 rounded-full",
|
||||||
|
backup.enabled
|
||||||
|
? "bg-green-500"
|
||||||
|
: "bg-red-500",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{backup.enabled ? "Active" : "Inactive"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||||
|
<div className="min-w-[200px]">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Destination
|
||||||
</span>
|
</span>
|
||||||
|
<p className="font-medium text-sm mt-0.5">
|
||||||
|
{backup.destination.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-[150px]">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Database
|
||||||
|
</span>
|
||||||
|
<p className="font-medium text-sm mt-0.5">
|
||||||
|
{backup.database}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-[120px]">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Schedule
|
||||||
|
</span>
|
||||||
|
<p className="font-medium text-sm mt-0.5">
|
||||||
|
{backup.schedule}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-[150px]">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Prefix Storage
|
||||||
|
</span>
|
||||||
|
<p className="font-medium text-sm mt-0.5">
|
||||||
|
{backup.prefix}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-[100px]">
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
Keep Latest
|
||||||
|
</span>
|
||||||
|
<p className="font-medium text-sm mt-0.5">
|
||||||
|
{backup.keepLatestCount || "All"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
<div className="flex flex-row md:flex-col gap-1.5">
|
||||||
<div className="min-w-[200px]">
|
<ShowDeploymentsModal
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
id={backup.backupId}
|
||||||
Destination
|
type="backup"
|
||||||
</span>
|
serverId={serverId || undefined}
|
||||||
<p className="font-medium text-sm mt-0.5">
|
>
|
||||||
{backup.destination.name}
|
<Button
|
||||||
</p>
|
variant="ghost"
|
||||||
</div>
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
>
|
||||||
|
<ClipboardList className="size-4 transition-colors " />
|
||||||
|
</Button>
|
||||||
|
</ShowDeploymentsModal>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
isLoading={
|
||||||
|
isManualBackup &&
|
||||||
|
activeManualBackup === backup.backupId
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
setActiveManualBackup(backup.backupId);
|
||||||
|
await manualBackup({
|
||||||
|
backupId: backup.backupId as string,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success(
|
||||||
|
"Manual Backup Successful",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error creating the manual backup",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setActiveManualBackup(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play className="size-4 " />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Run Manual Backup
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
<div className="min-w-[150px]">
|
<HandleBackup
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
backupType={backup.backupType}
|
||||||
Database
|
backupId={backup.backupId}
|
||||||
</span>
|
databaseType={backup.databaseType}
|
||||||
<p className="font-medium text-sm mt-0.5">
|
refetch={refetch}
|
||||||
{backup.database}
|
/>
|
||||||
</p>
|
<DialogAction
|
||||||
</div>
|
title="Delete Backup"
|
||||||
|
description="Are you sure you want to delete this backup?"
|
||||||
<div className="min-w-[120px]">
|
type="destructive"
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
onClick={async () => {
|
||||||
Schedule
|
await deleteBackup({
|
||||||
</span>
|
backupId: backup.backupId,
|
||||||
<p className="font-medium text-sm mt-0.5">
|
})
|
||||||
{backup.schedule}
|
.then(() => {
|
||||||
</p>
|
refetch();
|
||||||
</div>
|
toast.success(
|
||||||
|
"Backup deleted successfully",
|
||||||
<div className="min-w-[150px]">
|
);
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
})
|
||||||
Prefix Storage
|
.catch(() => {
|
||||||
</span>
|
toast.error("Error deleting backup");
|
||||||
<p className="font-medium text-sm mt-0.5">
|
});
|
||||||
{backup.prefix}
|
}}
|
||||||
</p>
|
>
|
||||||
</div>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
<div className="min-w-[100px]">
|
size="icon"
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
className="group hover:bg-red-500/10 size-8"
|
||||||
Keep Latest
|
isLoading={isRemoving}
|
||||||
</span>
|
>
|
||||||
<p className="font-medium text-sm mt-0.5">
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
{backup.keepLatestCount || "All"}
|
</Button>
|
||||||
</p>
|
</DialogAction>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row md:flex-col gap-1.5">
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-8"
|
|
||||||
isLoading={
|
|
||||||
isManualBackup &&
|
|
||||||
activeManualBackup === backup.backupId
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setActiveManualBackup(backup.backupId);
|
|
||||||
await manualBackup({
|
|
||||||
backupId: backup.backupId as string,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success(
|
|
||||||
"Manual Backup Successful",
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
|
||||||
"Error creating the manual backup",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
setActiveManualBackup(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Play className="size-4 " />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Run Manual Backup</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<HandleBackup
|
|
||||||
backupType={backup.backupType}
|
|
||||||
backupId={backup.backupId}
|
|
||||||
refetch={refetch}
|
|
||||||
/>
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Backup"
|
|
||||||
description="Are you sure you want to delete this backup?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteBackup({
|
|
||||||
backupId: backup.backupId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
toast.success("Backup deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting backup");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10 size-8"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -140,7 +140,14 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
if (!isCurrentConnection) return;
|
if (!isCurrentConnection) return;
|
||||||
setRawLogs((prev) => prev + e.data);
|
setRawLogs((prev) => {
|
||||||
|
const updated = prev + e.data;
|
||||||
|
const splitLines = updated.split("\n");
|
||||||
|
if (splitLines.length > lines) {
|
||||||
|
return splitLines.slice(-lines).join("\n");
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,454 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
CheckIcon,
|
||||||
|
ChevronsUpDown,
|
||||||
|
Settings2,
|
||||||
|
UserIcon,
|
||||||
|
XIcon,
|
||||||
|
Shield,
|
||||||
|
Calendar,
|
||||||
|
Key,
|
||||||
|
Copy,
|
||||||
|
Fingerprint,
|
||||||
|
Building2,
|
||||||
|
CreditCard,
|
||||||
|
Server,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Logo } from "@/components/shared/logo";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipProvider,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
type User = typeof authClient.$Infer.Session.user;
|
||||||
|
|
||||||
|
export const ImpersonationBar = () => {
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
|
const [isImpersonating, setIsImpersonating] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showBar, setShowBar] = useState(false);
|
||||||
|
const { data } = api.user.get.useQuery();
|
||||||
|
|
||||||
|
const fetchUsers = async (search?: string) => {
|
||||||
|
try {
|
||||||
|
const session = await authClient.getSession();
|
||||||
|
if (session?.data?.session?.impersonatedBy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await authClient.admin.listUsers({
|
||||||
|
query: {
|
||||||
|
limit: 30,
|
||||||
|
...(search && {
|
||||||
|
searchField: "email",
|
||||||
|
searchOperator: "contains",
|
||||||
|
searchValue: search,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredUsers = response.data?.users.filter(
|
||||||
|
// @ts-ignore
|
||||||
|
(user) => user.allowImpersonation && data?.user?.email !== user.email,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.error) {
|
||||||
|
// @ts-ignore
|
||||||
|
setUsers(filteredUsers || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching users:", error);
|
||||||
|
toast.error("Error loading users");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImpersonate = async () => {
|
||||||
|
if (!selectedUser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authClient.admin.impersonateUser({
|
||||||
|
userId: selectedUser.id,
|
||||||
|
});
|
||||||
|
setIsImpersonating(true);
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
toast.success("Successfully impersonating user", {
|
||||||
|
description: `You are now viewing as ${selectedUser.name || selectedUser.email}`,
|
||||||
|
});
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error impersonating user:", error);
|
||||||
|
toast.error("Error impersonating user");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStopImpersonating = async () => {
|
||||||
|
try {
|
||||||
|
await authClient.admin.stopImpersonating();
|
||||||
|
setIsImpersonating(false);
|
||||||
|
setSelectedUser(null);
|
||||||
|
setShowBar(false);
|
||||||
|
toast.success("Stopped impersonating user");
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error stopping impersonation:", error);
|
||||||
|
toast.error("Error stopping impersonation");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkImpersonation = async () => {
|
||||||
|
try {
|
||||||
|
const session = await authClient.getSession();
|
||||||
|
if (session?.data?.session?.impersonatedBy) {
|
||||||
|
setIsImpersonating(true);
|
||||||
|
setShowBar(true);
|
||||||
|
// setSelectedUser(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking impersonation status:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkImpersonation();
|
||||||
|
fetchUsers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className={cn(
|
||||||
|
"fixed bottom-4 right-4 z-50 rounded-full shadow-lg",
|
||||||
|
isImpersonating &&
|
||||||
|
!showBar &&
|
||||||
|
"bg-red-100 hover:bg-red-200 border-red-200",
|
||||||
|
)}
|
||||||
|
onClick={() => setShowBar(!showBar)}
|
||||||
|
>
|
||||||
|
<Settings2
|
||||||
|
className={cn(
|
||||||
|
"h-4 w-4",
|
||||||
|
isImpersonating && !showBar && "text-red-500",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{isImpersonating ? "Impersonation Controls" : "User Impersonation"}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"fixed bottom-0 left-0 right-0 bg-background border-t border-border p-4 flex items-center justify-center gap-4 z-40 transition-all duration-200 ease-in-out",
|
||||||
|
showBar ? "translate-y-0" : "translate-y-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
||||||
|
<Logo className="w-10 h-10" />
|
||||||
|
{!isImpersonating ? (
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="w-[300px] justify-between"
|
||||||
|
>
|
||||||
|
{selectedUser ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="truncate flex flex-col items-start">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{selectedUser.name || ""}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{selectedUser.email}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<UserIcon className="mr-2 h-4 w-4" />
|
||||||
|
<span>Select user to impersonate</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[300px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search users by email or name..."
|
||||||
|
onValueChange={(search) => {
|
||||||
|
fetchUsers(search);
|
||||||
|
}}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="py-6 text-center text-sm">
|
||||||
|
Loading users...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CommandEmpty>No users found.</CommandEmpty>
|
||||||
|
<CommandList>
|
||||||
|
<CommandGroup heading="All Users">
|
||||||
|
{users.map((user) => (
|
||||||
|
<CommandItem
|
||||||
|
key={user.id}
|
||||||
|
value={user.email}
|
||||||
|
onSelect={() => {
|
||||||
|
setSelectedUser(user);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2 flex-1">
|
||||||
|
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
|
<span className="flex flex-col items-start">
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{user.name || ""}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{user.email} • {user.role}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"ml-auto h-4 w-4",
|
||||||
|
selectedUser?.id === user.id
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Button
|
||||||
|
onClick={handleImpersonate}
|
||||||
|
disabled={!selectedUser}
|
||||||
|
variant="default"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Shield className="h-4 w-4" />
|
||||||
|
Impersonate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-4 w-full flex-wrap">
|
||||||
|
<div className="flex items-center gap-4 flex-1 flex-wrap">
|
||||||
|
<Avatar className="h-10 w-10">
|
||||||
|
<AvatarImage
|
||||||
|
src={data?.user?.image || ""}
|
||||||
|
alt={data?.user?.name || ""}
|
||||||
|
/>
|
||||||
|
<AvatarFallback>
|
||||||
|
{data?.user?.name?.slice(0, 2).toUpperCase() || "U"}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="gap-1 py-1 text-yellow-500 bg-yellow-50/20"
|
||||||
|
>
|
||||||
|
<Shield className="h-3 w-3" />
|
||||||
|
Impersonating
|
||||||
|
</Badge>
|
||||||
|
<span className="font-medium">
|
||||||
|
{data?.user?.name || ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<UserIcon className="h-3 w-3" />
|
||||||
|
{data?.user?.email} • {data?.role}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Key className="h-3 w-3" />
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
ID: {data?.user?.id?.slice(0, 8)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-4 w-4 hover:bg-muted/50"
|
||||||
|
onClick={() => {
|
||||||
|
if (data?.id) {
|
||||||
|
copy(data.id);
|
||||||
|
toast.success("ID copied to clipboard");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Building2 className="h-3 w-3" />
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
Org: {data?.organizationId?.slice(0, 8)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-4 w-4 hover:bg-muted/50"
|
||||||
|
onClick={() => {
|
||||||
|
if (data?.organizationId) {
|
||||||
|
copy(data.organizationId);
|
||||||
|
toast.success(
|
||||||
|
"Organization ID copied to clipboard",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{data?.user?.stripeCustomerId && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<CreditCard className="h-3 w-3" />
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
Customer:
|
||||||
|
{data?.user?.stripeCustomerId?.slice(0, 8)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-4 w-4 hover:bg-muted/50"
|
||||||
|
onClick={() => {
|
||||||
|
copy(data?.user?.stripeCustomerId || "");
|
||||||
|
toast.success(
|
||||||
|
"Stripe Customer ID copied to clipboard",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{data?.user?.stripeSubscriptionId && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<CreditCard className="h-3 w-3" />
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
Sub: {data?.user?.stripeSubscriptionId?.slice(0, 8)}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-4 w-4 hover:bg-muted/50"
|
||||||
|
onClick={() => {
|
||||||
|
copy(data.user.stripeSubscriptionId || "");
|
||||||
|
toast.success(
|
||||||
|
"Stripe Subscription ID copied to clipboard",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{data?.user?.serversQuantity !== undefined && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Server className="h-3 w-3" />
|
||||||
|
<span>Servers: {data.user.serversQuantity}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{data?.createdAt && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-3 w-3" />
|
||||||
|
Created:{" "}
|
||||||
|
{format(new Date(data.createdAt), "MMM d, yyyy")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="flex items-center gap-1 cursor-default">
|
||||||
|
<Fingerprint
|
||||||
|
className={cn(
|
||||||
|
"h-3 w-3",
|
||||||
|
data?.user?.twoFactorEnabled
|
||||||
|
? "text-green-500"
|
||||||
|
: "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
data?.user?.twoFactorEnabled
|
||||||
|
? "green"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
className="text-[10px] px-1 py-0"
|
||||||
|
>
|
||||||
|
2FA{" "}
|
||||||
|
{data?.user?.twoFactorEnabled
|
||||||
|
? "Enabled"
|
||||||
|
: "Disabled"}
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Two-Factor Authentication Status
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleStopImpersonating}
|
||||||
|
variant="secondary"
|
||||||
|
className="gap-2"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<XIcon className="w-4 h-4" />
|
||||||
|
Stop Impersonating
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -145,10 +145,8 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
form.setValue(
|
const serviceName = slugify(val);
|
||||||
"appName",
|
form.setValue("appName", `${slug}-${serviceName}`);
|
||||||
`${slug}-${val.toLowerCase().replaceAll(" ", "-")}`,
|
|
||||||
);
|
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -152,10 +152,8 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
form.setValue(
|
const serviceName = slugify(val);
|
||||||
"appName",
|
form.setValue("appName", `${slug}-${serviceName}`);
|
||||||
`${slug}-${val.toLowerCase()}`,
|
|
||||||
);
|
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -363,10 +363,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
form.setValue(
|
const serviceName = slugify(val);
|
||||||
"appName",
|
form.setValue("appName", `${slug}-${serviceName}`);
|
||||||
`${slug}-${val.toLowerCase()}`,
|
|
||||||
);
|
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -33,12 +33,23 @@ import { z } from "zod";
|
|||||||
const AddProjectSchema = z.object({
|
const AddProjectSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, {
|
.min(1, "Project name is required")
|
||||||
message: "Name is required",
|
.refine(
|
||||||
})
|
(name) => {
|
||||||
.regex(/^[a-zA-Z]/, {
|
const trimmedName = name.trim();
|
||||||
|
const validNameRegex =
|
||||||
|
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_-]*[\p{L}\p{N}_-]$/u;
|
||||||
|
return validNameRegex.test(trimmedName);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
"Project name must start and end with a letter, number, hyphen or underscore. Spaces are allowed in between.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine((name) => !/^\d/.test(name.trim()), {
|
||||||
message: "Project name cannot start with a number",
|
message: "Project name cannot start with a number",
|
||||||
}),
|
})
|
||||||
|
.transform((name) => name.trim()),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,191 +0,0 @@
|
|||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { SparklesIcon } from "lucide-react";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { SetupMonitoring } from "./servers/setup-monitoring";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
|
|
||||||
export const EnablePaidFeatures = () => {
|
|
||||||
const { data, refetch } = api.user.get.useQuery();
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { mutateAsync: saveLicense } = api.user.saveLicense.useMutation();
|
|
||||||
const { mutateAsync: deactivateLicense } =
|
|
||||||
api.user.deactivateLicense.useMutation();
|
|
||||||
const { mutateAsync: validateLicense } =
|
|
||||||
api.user.validateLicense.useMutation();
|
|
||||||
const { mutateAsync: update } = api.user.update.useMutation();
|
|
||||||
const [licenseKey, setLicenseKey] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data?.user?.enablePaidFeatures) {
|
|
||||||
setLicenseKey(data.user.licenseKey || "");
|
|
||||||
}
|
|
||||||
}, [data?.user?.enablePaidFeatures]);
|
|
||||||
|
|
||||||
const handleSaveLicense = async () => {
|
|
||||||
if (!licenseKey) {
|
|
||||||
toast.error("Please enter a license key");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLoading(true);
|
|
||||||
await saveLicense({
|
|
||||||
licenseKey,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("License validated successfully");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error("Error validating license", {
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
refetch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleValidateLicense = async () => {
|
|
||||||
if (!licenseKey) {
|
|
||||||
toast.error("Please enter a license key");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLoading(true);
|
|
||||||
await validateLicense({
|
|
||||||
licenseKey,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("License validated successfully");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error("Error validating license", {
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
|
||||||
<div className="rounded-xl bg-background shadow-md">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-xl flex flex-row items-center gap-3">
|
|
||||||
<div className="p-2 rounded-lg bg-primary/10">
|
|
||||||
<SparklesIcon className="size-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
Paid Features
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="mt-2">
|
|
||||||
Unlock advanced capabilities like monitoring and enhanced
|
|
||||||
performance tracking
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-6">
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<div className="flex flex-row items-center justify-between p-4 border rounded-lg bg-card/50 hover:bg-card/80 transition-colors">
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h3 className="font-medium">Enable Premium Features</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Access advanced monitoring tools and premium capabilities
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
className="ml-4"
|
|
||||||
checked={data?.user?.enablePaidFeatures}
|
|
||||||
onCheckedChange={() => {
|
|
||||||
update({
|
|
||||||
enablePaidFeatures: !data?.user?.enablePaidFeatures,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
`Premium features ${
|
|
||||||
data?.user?.enablePaidFeatures
|
|
||||||
? "disabled"
|
|
||||||
: "enabled"
|
|
||||||
} successfully`,
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating premium features");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data?.user?.enablePaidFeatures && (
|
|
||||||
<div className="flex flex-row items-center gap-4 p-4 border rounded-lg bg-card/50">
|
|
||||||
<div className="flex-grow">
|
|
||||||
<Input
|
|
||||||
placeholder="Enter your license key"
|
|
||||||
value={licenseKey}
|
|
||||||
disabled={data?.user?.licenseKey !== null}
|
|
||||||
onChange={(e) => setLicenseKey(e.target.value)}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{!data?.user?.licenseKey ? (
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveLicense}
|
|
||||||
variant="secondary"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? "Saving..." : "Save License"}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
onClick={handleValidateLicense}
|
|
||||||
variant="secondary"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? "Validating..." : "Validate"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setIsLoading(true);
|
|
||||||
deactivateLicense({
|
|
||||||
licenseKey,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
setLicenseKey("");
|
|
||||||
toast.success("License removed successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
toast.error("Error removing license", {
|
|
||||||
description: e.message,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
variant="destructive"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
{data?.user?.enablePaidFeatures && <SetupMonitoring />}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -28,12 +29,14 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Disable2FA } from "./disable-2fa";
|
import { Disable2FA } from "./disable-2fa";
|
||||||
import { Enable2FA } from "./enable-2fa";
|
import { Enable2FA } from "./enable-2fa";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
password: z.string().nullable(),
|
password: z.string().nullable(),
|
||||||
currentPassword: z.string().nullable(),
|
currentPassword: z.string().nullable(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
|
allowImpersonation: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Profile = z.infer<typeof profileSchema>;
|
type Profile = z.infer<typeof profileSchema>;
|
||||||
@@ -56,6 +59,7 @@ const randomImages = [
|
|||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const _utils = api.useUtils();
|
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 {
|
const {
|
||||||
mutateAsync,
|
mutateAsync,
|
||||||
@@ -79,6 +83,7 @@ export const ProfileForm = () => {
|
|||||||
password: "",
|
password: "",
|
||||||
image: data?.user?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
|
allowImpersonation: data?.user?.allowImpersonation || false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(profileSchema),
|
resolver: zodResolver(profileSchema),
|
||||||
});
|
});
|
||||||
@@ -91,11 +96,13 @@ export const ProfileForm = () => {
|
|||||||
password: form.getValues("password") || "",
|
password: form.getValues("password") || "",
|
||||||
image: data?.user?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: form.getValues("currentPassword") || "",
|
currentPassword: form.getValues("currentPassword") || "",
|
||||||
|
allowImpersonation: data?.user?.allowImpersonation,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keepValues: true,
|
keepValues: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
form.setValue("allowImpersonation", data?.user?.allowImpersonation);
|
||||||
|
|
||||||
if (data.user.email) {
|
if (data.user.email) {
|
||||||
generateSHA256Hash(data.user.email).then((hash) => {
|
generateSHA256Hash(data.user.email).then((hash) => {
|
||||||
@@ -111,6 +118,7 @@ export const ProfileForm = () => {
|
|||||||
password: values.password || undefined,
|
password: values.password || undefined,
|
||||||
image: values.image,
|
image: values.image,
|
||||||
currentPassword: values.currentPassword || undefined,
|
currentPassword: values.currentPassword || undefined,
|
||||||
|
allowImpersonation: values.allowImpersonation,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
@@ -256,7 +264,34 @@ export const ProfileForm = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{isCloud && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="allowImpersonation"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Allow Impersonation</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Enable this option to allow Dokploy Cloud
|
||||||
|
administrators to temporarily access your
|
||||||
|
account for troubleshooting and support
|
||||||
|
purposes. This helps them quickly identify and
|
||||||
|
resolve any issues you may encounter.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Button type="submit" isLoading={isUpdating}>
|
<Button type="submit" isLoading={isUpdating}>
|
||||||
{t("settings.common.save")}
|
{t("settings.common.save")}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
const { mutateAsync } = serverId
|
const { mutateAsync } = serverId
|
||||||
? api.server.setupMonitoring.useMutation()
|
? api.server.setupMonitoring.useMutation()
|
||||||
: api.user.setupMonitoring.useMutation();
|
: api.admin.setupMonitoring.useMutation();
|
||||||
|
|
||||||
const generateToken = () => {
|
const generateToken = () => {
|
||||||
const array = new Uint8Array(64);
|
const array = new Uint8Array(64);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowSchedulesModal = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Show Schedules
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
||||||
|
<ShowSchedules id={serverId} scheduleType="server" />
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -43,6 +43,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
|
|||||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||||
|
import { ShowSchedulesModal } from "./show-schedules-modal";
|
||||||
|
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
@@ -332,6 +333,10 @@ export const ShowServers = () => {
|
|||||||
<ShowNodesModal
|
<ShowNodesModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ShowSchedulesModal
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -185,24 +185,21 @@ export const ShowInvitations = () => {
|
|||||||
Cancel Invitation
|
Cancel Invitation
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onSelect={async (_e) => {
|
|
||||||
await removeInvitation({
|
|
||||||
invitationId: invitation.id,
|
|
||||||
}).then(() => {
|
|
||||||
refetch();
|
|
||||||
toast.success(
|
|
||||||
"Invitation removed",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove Invitation
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={async (_e) => {
|
||||||
|
await removeInvitation({
|
||||||
|
invitationId: invitation.id,
|
||||||
|
}).then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Invitation removed");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove Invitation
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import Page from "./side";
|
import Page from "./side";
|
||||||
|
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
metaName?: string;
|
metaName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardLayout = ({ children }: Props) => {
|
export const DashboardLayout = ({ children }: Props) => {
|
||||||
return <Page>{children}</Page>;
|
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Page>{children}</Page>
|
||||||
|
{haveRootAccess === true && <ImpersonationBar />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
Clock,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -158,6 +159,14 @@ const MENU: Menu = {
|
|||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ isCloud }) => !isCloud,
|
isEnabled: ({ isCloud }) => !isCloud,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
isSingle: true,
|
||||||
|
title: "Schedules",
|
||||||
|
url: "/dashboard/schedules",
|
||||||
|
icon: Clock,
|
||||||
|
// Only enabled in non-cloud environments
|
||||||
|
isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Traefik File System",
|
title: "Traefik File System",
|
||||||
|
|||||||
28
apps/dokploy/drizzle/0088_illegal_ma_gnuci.sql
Normal file
28
apps/dokploy/drizzle/0088_illegal_ma_gnuci.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TYPE "public"."scheduleType" AS ENUM('application', 'compose', 'server', 'dokploy-server');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."shellType" AS ENUM('bash', 'sh');--> statement-breakpoint
|
||||||
|
CREATE TABLE "schedule" (
|
||||||
|
"scheduleId" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"cronExpression" text NOT NULL,
|
||||||
|
"appName" text NOT NULL,
|
||||||
|
"serviceName" text,
|
||||||
|
"shellType" "shellType" DEFAULT 'bash' NOT NULL,
|
||||||
|
"scheduleType" "scheduleType" DEFAULT 'application' NOT NULL,
|
||||||
|
"command" text NOT NULL,
|
||||||
|
"script" text,
|
||||||
|
"applicationId" text,
|
||||||
|
"composeId" text,
|
||||||
|
"serverId" text,
|
||||||
|
"userId" text,
|
||||||
|
"enabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"createdAt" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD COLUMN "startedAt" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD COLUMN "finishedAt" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD COLUMN "scheduleId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_scheduleId_schedule_scheduleId_fk" FOREIGN KEY ("scheduleId") REFERENCES "public"."schedule"("scheduleId") ON DELETE cascade ON UPDATE no action;
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
CREATE TYPE "public"."backupType" AS ENUM('database', 'compose');--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD COLUMN "serviceName" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD COLUMN "backupType" "backupType" DEFAULT 'database' NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD COLUMN "composeId" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD CONSTRAINT "backup_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "backup" ADD COLUMN "metadata" jsonb;
|
|
||||||
29
apps/dokploy/drizzle/0089_noisy_sandman.sql
Normal file
29
apps/dokploy/drizzle/0089_noisy_sandman.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
CREATE TYPE "public"."backupType" AS ENUM('database', 'compose');--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "appName" text;
|
||||||
|
|
||||||
|
UPDATE "backup"
|
||||||
|
SET "appName" = 'backup-' ||
|
||||||
|
(
|
||||||
|
ARRAY['optimize', 'parse', 'quantify', 'bypass', 'override', 'generate',
|
||||||
|
'secure', 'hack', 'backup', 'connect', 'index', 'compress']::text[]
|
||||||
|
)[floor(random() * 12) + 1] || '-' ||
|
||||||
|
(
|
||||||
|
ARRAY['digital', 'virtual', 'mobile', 'neural', 'optical', 'auxiliary',
|
||||||
|
'primary', 'backup', 'wireless', 'haptic', 'solid-state']::text[]
|
||||||
|
)[floor(random() * 11) + 1] || '-' ||
|
||||||
|
(
|
||||||
|
ARRAY['driver', 'protocol', 'array', 'matrix', 'system', 'bandwidth',
|
||||||
|
'monitor', 'firewall', 'card', 'sensor', 'bus']::text[]
|
||||||
|
)[floor(random() * 11) + 1] || '-' ||
|
||||||
|
substr(md5(random()::text), 1, 6);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE "backup" ALTER COLUMN "appName" SET NOT NULL;
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "serviceName" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "backupType" "backupType" DEFAULT 'database' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "composeId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "metadata" jsonb;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD COLUMN "backupId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD CONSTRAINT "backup_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_backupId_backup_backupId_fk" FOREIGN KEY ("backupId") REFERENCES "public"."backup"("backupId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD CONSTRAINT "backup_appName_unique" UNIQUE("appName");
|
||||||
1
apps/dokploy/drizzle/0090_clean_wolf_cub.sql
Normal file
1
apps/dokploy/drizzle/0090_clean_wolf_cub.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "allowImpersonation" boolean DEFAULT false NOT NULL;
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "user_temp" ADD COLUMN "licenseKey" text;
|
|
||||||
1
apps/dokploy/drizzle/0091_spotty_kulan_gath.sql
Normal file
1
apps/dokploy/drizzle/0091_spotty_kulan_gath.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "7fea81ef-e2a7-4a8b-b755-e98903a08b57",
|
"id": "c7eae4ce-5acc-439b-962f-bb2ef8922187",
|
||||||
"prevId": "7fb3716c-3cc6-4b18-b8d1-762844da26be",
|
"prevId": "7fb3716c-3cc6-4b18-b8d1-762844da26be",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -1770,12 +1770,6 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"serviceName": {
|
|
||||||
"name": "serviceName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"destinationId": {
|
"destinationId": {
|
||||||
"name": "destinationId",
|
"name": "destinationId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1788,14 +1782,6 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"backupType": {
|
|
||||||
"name": "backupType",
|
|
||||||
"type": "backupType",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'database'"
|
|
||||||
},
|
|
||||||
"databaseType": {
|
"databaseType": {
|
||||||
"name": "databaseType",
|
"name": "databaseType",
|
||||||
"type": "databaseType",
|
"type": "databaseType",
|
||||||
@@ -1803,12 +1789,6 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"composeId": {
|
|
||||||
"name": "composeId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"postgresId": {
|
"postgresId": {
|
||||||
"name": "postgresId",
|
"name": "postgresId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1855,19 +1835,6 @@
|
|||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
"backup_composeId_compose_composeId_fk": {
|
|
||||||
"name": "backup_composeId_compose_composeId_fk",
|
|
||||||
"tableFrom": "backup",
|
|
||||||
"tableTo": "compose",
|
|
||||||
"columnsFrom": [
|
|
||||||
"composeId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"composeId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"backup_postgresId_postgres_postgresId_fk": {
|
"backup_postgresId_postgres_postgresId_fk": {
|
||||||
"name": "backup_postgresId_postgres_postgresId_fk",
|
"name": "backup_postgresId_postgres_postgresId_fk",
|
||||||
"tableFrom": "backup",
|
"tableFrom": "backup",
|
||||||
@@ -2101,11 +2068,29 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"startedAt": {
|
||||||
|
"name": "startedAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"finishedAt": {
|
||||||
|
"name": "finishedAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"errorMessage": {
|
"errorMessage": {
|
||||||
"name": "errorMessage",
|
"name": "errorMessage",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scheduleId": {
|
||||||
|
"name": "scheduleId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -2161,6 +2146,19 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"deployment_scheduleId_schedule_scheduleId_fk": {
|
||||||
|
"name": "deployment_scheduleId_schedule_scheduleId_fk",
|
||||||
|
"tableFrom": "deployment",
|
||||||
|
"tableTo": "schedule",
|
||||||
|
"columnsFrom": [
|
||||||
|
"scheduleId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"scheduleId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -5255,6 +5253,167 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.schedule": {
|
||||||
|
"name": "schedule",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"scheduleId": {
|
||||||
|
"name": "scheduleId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"cronExpression": {
|
||||||
|
"name": "cronExpression",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"appName": {
|
||||||
|
"name": "appName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"serviceName": {
|
||||||
|
"name": "serviceName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"shellType": {
|
||||||
|
"name": "shellType",
|
||||||
|
"type": "shellType",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'bash'"
|
||||||
|
},
|
||||||
|
"scheduleType": {
|
||||||
|
"name": "scheduleType",
|
||||||
|
"type": "scheduleType",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'application'"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"name": "command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"script": {
|
||||||
|
"name": "script",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"applicationId": {
|
||||||
|
"name": "applicationId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"composeId": {
|
||||||
|
"name": "composeId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"serverId": {
|
||||||
|
"name": "serverId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"schedule_applicationId_application_applicationId_fk": {
|
||||||
|
"name": "schedule_applicationId_application_applicationId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "application",
|
||||||
|
"columnsFrom": [
|
||||||
|
"applicationId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"applicationId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_composeId_compose_composeId_fk": {
|
||||||
|
"name": "schedule_composeId_compose_composeId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "compose",
|
||||||
|
"columnsFrom": [
|
||||||
|
"composeId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"composeId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_serverId_server_serverId_fk": {
|
||||||
|
"name": "schedule_serverId_server_serverId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "server",
|
||||||
|
"columnsFrom": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_userId_user_temp_id_fk": {
|
||||||
|
"name": "schedule_userId_user_temp_id_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "user_temp",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
@@ -5292,14 +5451,6 @@
|
|||||||
"preview"
|
"preview"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"public.backupType": {
|
|
||||||
"name": "backupType",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"database",
|
|
||||||
"compose"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"public.databaseType": {
|
"public.databaseType": {
|
||||||
"name": "databaseType",
|
"name": "databaseType",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
@@ -5433,6 +5584,24 @@
|
|||||||
"active",
|
"active",
|
||||||
"inactive"
|
"inactive"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"public.scheduleType": {
|
||||||
|
"name": "scheduleType",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"server",
|
||||||
|
"dokploy-server"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.shellType": {
|
||||||
|
"name": "shellType",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"bash",
|
||||||
|
"sh"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "83185289-c593-420c-b8da-a90143aebf8b",
|
"id": "3ec09926-2da4-41c9-8eae-8ef6b023035e",
|
||||||
"prevId": "7fea81ef-e2a7-4a8b-b755-e98903a08b57",
|
"prevId": "c7eae4ce-5acc-439b-962f-bb2ef8922187",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -1746,6 +1746,12 @@
|
|||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"appName": {
|
||||||
|
"name": "appName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"name": "schedule",
|
"name": "schedule",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1941,7 +1947,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {
|
||||||
|
"backup_appName_unique": {
|
||||||
|
"name": "backup_appName_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"appName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
@@ -2107,11 +2121,35 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"startedAt": {
|
||||||
|
"name": "startedAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"finishedAt": {
|
||||||
|
"name": "finishedAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"errorMessage": {
|
"errorMessage": {
|
||||||
"name": "errorMessage",
|
"name": "errorMessage",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scheduleId": {
|
||||||
|
"name": "scheduleId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"backupId": {
|
||||||
|
"name": "backupId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -2167,6 +2205,32 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"deployment_scheduleId_schedule_scheduleId_fk": {
|
||||||
|
"name": "deployment_scheduleId_schedule_scheduleId_fk",
|
||||||
|
"tableFrom": "deployment",
|
||||||
|
"tableTo": "schedule",
|
||||||
|
"columnsFrom": [
|
||||||
|
"scheduleId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"scheduleId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"deployment_backupId_backup_backupId_fk": {
|
||||||
|
"name": "deployment_backupId_backup_backupId_fk",
|
||||||
|
"tableFrom": "deployment",
|
||||||
|
"tableTo": "backup",
|
||||||
|
"columnsFrom": [
|
||||||
|
"backupId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"backupId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -5261,6 +5325,167 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.schedule": {
|
||||||
|
"name": "schedule",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"scheduleId": {
|
||||||
|
"name": "scheduleId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"cronExpression": {
|
||||||
|
"name": "cronExpression",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"appName": {
|
||||||
|
"name": "appName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"serviceName": {
|
||||||
|
"name": "serviceName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"shellType": {
|
||||||
|
"name": "shellType",
|
||||||
|
"type": "shellType",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'bash'"
|
||||||
|
},
|
||||||
|
"scheduleType": {
|
||||||
|
"name": "scheduleType",
|
||||||
|
"type": "scheduleType",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'application'"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"name": "command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"script": {
|
||||||
|
"name": "script",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"applicationId": {
|
||||||
|
"name": "applicationId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"composeId": {
|
||||||
|
"name": "composeId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"serverId": {
|
||||||
|
"name": "serverId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"schedule_applicationId_application_applicationId_fk": {
|
||||||
|
"name": "schedule_applicationId_application_applicationId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "application",
|
||||||
|
"columnsFrom": [
|
||||||
|
"applicationId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"applicationId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_composeId_compose_composeId_fk": {
|
||||||
|
"name": "schedule_composeId_compose_composeId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "compose",
|
||||||
|
"columnsFrom": [
|
||||||
|
"composeId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"composeId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_serverId_server_serverId_fk": {
|
||||||
|
"name": "schedule_serverId_server_serverId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "server",
|
||||||
|
"columnsFrom": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_userId_user_temp_id_fk": {
|
||||||
|
"name": "schedule_userId_user_temp_id_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "user_temp",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
@@ -5439,6 +5664,24 @@
|
|||||||
"active",
|
"active",
|
||||||
"inactive"
|
"inactive"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"public.scheduleType": {
|
||||||
|
"name": "scheduleType",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"server",
|
||||||
|
"dokploy-server"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.shellType": {
|
||||||
|
"name": "shellType",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"bash",
|
||||||
|
"sh"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "d5010e21-a520-4d05-aef3-c549b55456e5",
|
"id": "f8b568f0-83c1-4cfb-95c9-ab6b5b5bce18",
|
||||||
"prevId": "83185289-c593-420c-b8da-a90143aebf8b",
|
"prevId": "3ec09926-2da4-41c9-8eae-8ef6b023035e",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -959,11 +959,12 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"licenseKey": {
|
"allowImpersonation": {
|
||||||
"name": "licenseKey",
|
"name": "allowImpersonation",
|
||||||
"type": "text",
|
"type": "boolean",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
},
|
},
|
||||||
"metricsConfig": {
|
"metricsConfig": {
|
||||||
"name": "metricsConfig",
|
"name": "metricsConfig",
|
||||||
@@ -1752,6 +1753,12 @@
|
|||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"appName": {
|
||||||
|
"name": "appName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"name": "schedule",
|
"name": "schedule",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1947,7 +1954,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {
|
||||||
|
"backup_appName_unique": {
|
||||||
|
"name": "backup_appName_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"appName"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
@@ -2113,11 +2128,35 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"startedAt": {
|
||||||
|
"name": "startedAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"finishedAt": {
|
||||||
|
"name": "finishedAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"errorMessage": {
|
"errorMessage": {
|
||||||
"name": "errorMessage",
|
"name": "errorMessage",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scheduleId": {
|
||||||
|
"name": "scheduleId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"backupId": {
|
||||||
|
"name": "backupId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -2173,6 +2212,32 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"deployment_scheduleId_schedule_scheduleId_fk": {
|
||||||
|
"name": "deployment_scheduleId_schedule_scheduleId_fk",
|
||||||
|
"tableFrom": "deployment",
|
||||||
|
"tableTo": "schedule",
|
||||||
|
"columnsFrom": [
|
||||||
|
"scheduleId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"scheduleId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"deployment_backupId_backup_backupId_fk": {
|
||||||
|
"name": "deployment_backupId_backup_backupId_fk",
|
||||||
|
"tableFrom": "deployment",
|
||||||
|
"tableTo": "backup",
|
||||||
|
"columnsFrom": [
|
||||||
|
"backupId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"backupId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -5267,6 +5332,167 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.schedule": {
|
||||||
|
"name": "schedule",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"scheduleId": {
|
||||||
|
"name": "scheduleId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"cronExpression": {
|
||||||
|
"name": "cronExpression",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"appName": {
|
||||||
|
"name": "appName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"serviceName": {
|
||||||
|
"name": "serviceName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"shellType": {
|
||||||
|
"name": "shellType",
|
||||||
|
"type": "shellType",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'bash'"
|
||||||
|
},
|
||||||
|
"scheduleType": {
|
||||||
|
"name": "scheduleType",
|
||||||
|
"type": "scheduleType",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'application'"
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"name": "command",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"script": {
|
||||||
|
"name": "script",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"applicationId": {
|
||||||
|
"name": "applicationId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"composeId": {
|
||||||
|
"name": "composeId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"serverId": {
|
||||||
|
"name": "serverId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"schedule_applicationId_application_applicationId_fk": {
|
||||||
|
"name": "schedule_applicationId_application_applicationId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "application",
|
||||||
|
"columnsFrom": [
|
||||||
|
"applicationId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"applicationId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_composeId_compose_composeId_fk": {
|
||||||
|
"name": "schedule_composeId_compose_composeId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "compose",
|
||||||
|
"columnsFrom": [
|
||||||
|
"composeId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"composeId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_serverId_server_serverId_fk": {
|
||||||
|
"name": "schedule_serverId_server_serverId_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "server",
|
||||||
|
"columnsFrom": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"schedule_userId_user_temp_id_fk": {
|
||||||
|
"name": "schedule_userId_user_temp_id_fk",
|
||||||
|
"tableFrom": "schedule",
|
||||||
|
"tableTo": "user_temp",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
@@ -5445,6 +5671,24 @@
|
|||||||
"active",
|
"active",
|
||||||
"inactive"
|
"inactive"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"public.scheduleType": {
|
||||||
|
"name": "scheduleType",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"server",
|
||||||
|
"dokploy-server"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public.shellType": {
|
||||||
|
"name": "shellType",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"bash",
|
||||||
|
"sh"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
|||||||
5711
apps/dokploy/drizzle/meta/0091_snapshot.json
Normal file
5711
apps/dokploy/drizzle/meta/0091_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -621,22 +621,29 @@
|
|||||||
{
|
{
|
||||||
"idx": 88,
|
"idx": 88,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1745801614194,
|
"when": 1746256928101,
|
||||||
"tag": "0088_same_ezekiel",
|
"tag": "0088_illegal_ma_gnuci",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 89,
|
"idx": 89,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1745812150155,
|
"when": 1746392564463,
|
||||||
"tag": "0089_dazzling_marrow",
|
"tag": "0089_noisy_sandman",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 90,
|
"idx": 90,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1746156076450,
|
"when": 1746509318678,
|
||||||
"tag": "0090_tiny_phil_sheldon",
|
"tag": "0090_clean_wolf_cub",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 91,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1746518402168,
|
||||||
|
"tag": "0091_spotty_kulan_gath",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { organizationClient } from "better-auth/client/plugins";
|
import { organizationClient } from "better-auth/client/plugins";
|
||||||
import { twoFactorClient } from "better-auth/client/plugins";
|
import { twoFactorClient } from "better-auth/client/plugins";
|
||||||
import { apiKeyClient } from "better-auth/client/plugins";
|
import { apiKeyClient } from "better-auth/client/plugins";
|
||||||
|
import { adminClient } from "better-auth/client/plugins";
|
||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
// baseURL: "http://localhost:3000", // the base url of your auth server
|
// baseURL: "http://localhost:3000", // the base url of your auth server
|
||||||
plugins: [organizationClient(), twoFactorClient(), apiKeyClient()],
|
plugins: [
|
||||||
|
organizationClient(),
|
||||||
|
twoFactorClient(),
|
||||||
|
apiKeyClient(),
|
||||||
|
adminClient(),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const slugify = (text: string | undefined) => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "");
|
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "") || "service";
|
||||||
|
|
||||||
return slug(cleanedText, {
|
return slug(cleanedText, {
|
||||||
lower: true,
|
lower: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.21.8",
|
"version": "v0.22.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
"adm-zip": "^0.5.14",
|
"adm-zip": "^0.5.14",
|
||||||
"ai": "^4.0.23",
|
"ai": "^4.0.23",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "1.2.6",
|
"better-auth": "v1.2.8-beta.7",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
"bullmq": "5.4.2",
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
import { ShowPaidMonitoring } from "@/components/dashboard/monitoring/paid/servers/show-paid-monitoring";
|
import { ShowPaidMonitoring } from "@/components/dashboard/monitoring/paid/servers/show-paid-monitoring";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
@@ -26,7 +23,7 @@ const Dashboard = () => {
|
|||||||
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 pb-10">
|
<div className="space-y-4 pb-10">
|
||||||
<AlertBlock>
|
{/* <AlertBlock>
|
||||||
You are watching the <strong>Free</strong> plan.{" "}
|
You are watching the <strong>Free</strong> plan.{" "}
|
||||||
<a
|
<a
|
||||||
href="https://dokploy.com#pricing"
|
href="https://dokploy.com#pricing"
|
||||||
@@ -37,7 +34,7 @@ const Dashboard = () => {
|
|||||||
Upgrade
|
Upgrade
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
to get more features.
|
to get more features.
|
||||||
</AlertBlock>
|
</AlertBlock> */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto items-center">
|
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto items-center">
|
||||||
<div className="rounded-xl bg-background flex shadow-md px-4 min-h-[50vh] justify-center items-center text-muted-foreground">
|
<div className="rounded-xl bg-background flex shadow-md px-4 min-h-[50vh] justify-center items-center text-muted-foreground">
|
||||||
@@ -47,15 +44,15 @@ const Dashboard = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{monitoring?.enabledFeatures && (
|
{/* {monitoring?.enabledFeatures && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
<Label className="text-muted-foreground">Change Monitoring</Label>
|
<Label className="text-muted-foreground">Change Monitoring</Label>
|
||||||
<Switch
|
<Switch
|
||||||
checked={toggleMonitoring}
|
checked={toggleMonitoring}
|
||||||
onCheckedChange={_setToggleMonitoring}
|
onCheckedChange={setToggleMonitoring}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
{toggleMonitoring ? (
|
{toggleMonitoring ? (
|
||||||
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto">
|
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto">
|
||||||
<div className="rounded-xl bg-background shadow-md">
|
<div className="rounded-xl bg-background shadow-md">
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
|
ServerIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -968,6 +969,11 @@ const Project = (
|
|||||||
}}
|
}}
|
||||||
className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border"
|
className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border"
|
||||||
>
|
>
|
||||||
|
{service.serverId && (
|
||||||
|
<div className="absolute -left-1 -top-2">
|
||||||
|
<ServerIcon className="size-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="absolute -right-1 -top-2">
|
<div className="absolute -right-1 -top-2">
|
||||||
<StatusTooltip status={service.status} />
|
<StatusTooltip status={service.status} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ShowEnvironment } from "@/components/dashboard/application/environment/
|
|||||||
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
|
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
|
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
|
||||||
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
import { UpdateApplication } from "@/components/dashboard/application/update-application";
|
import { UpdateApplication } from "@/components/dashboard/application/update-application";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
@@ -28,7 +29,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -66,7 +66,7 @@ type TabState =
|
|||||||
const Service = (
|
const Service = (
|
||||||
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
||||||
) => {
|
) => {
|
||||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(true);
|
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
||||||
const { applicationId, activeTab } = props;
|
const { applicationId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
@@ -87,7 +87,6 @@ const Service = (
|
|||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
@@ -234,6 +233,7 @@ const Service = (
|
|||||||
<TabsTrigger value="preview-deployments">
|
<TabsTrigger value="preview-deployments">
|
||||||
Preview Deployments
|
Preview Deployments
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
@@ -267,19 +267,21 @@ const Service = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{monitoring?.enabledFeatures && (
|
{/* {monitoring?.enabledFeatures &&
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
isCloud &&
|
||||||
<Label className="text-muted-foreground">
|
data?.serverId && (
|
||||||
Change Monitoring
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
</Label>
|
<Label className="text-muted-foreground">
|
||||||
<Switch
|
Change Monitoring
|
||||||
checked={_toggleMonitoring}
|
</Label>
|
||||||
onCheckedChange={_setToggleMonitoring}
|
<Switch
|
||||||
/>
|
checked={toggleMonitoring}
|
||||||
</div>
|
onCheckedChange={setToggleMonitoring}
|
||||||
)}
|
/>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
|
||||||
{_toggleMonitoring ? (
|
{/* {toggleMonitoring ? (
|
||||||
<ContainerPaidMonitoring
|
<ContainerPaidMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
||||||
@@ -287,13 +289,13 @@ const Service = (
|
|||||||
monitoring?.metricsConfig?.server?.token || ""
|
monitoring?.metricsConfig?.server?.token || ""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : ( */}
|
||||||
<div>
|
<div>
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -308,9 +310,22 @@ const Service = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="deployments" className="w-full">
|
<TabsContent value="schedules">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDeployments applicationId={applicationId} />
|
<ShowSchedules
|
||||||
|
id={applicationId}
|
||||||
|
scheduleType="application"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||||
|
<div className="flex flex-col gap-4 border rounded-lg">
|
||||||
|
<ShowDeployments
|
||||||
|
id={applicationId}
|
||||||
|
type="application"
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
refreshToken={data?.refreshToken || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="preview-deployments" className="w-full">
|
<TabsContent value="preview-deployments" className="w-full">
|
||||||
@@ -320,7 +335,7 @@ const Service = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="domains" className="w-full">
|
<TabsContent value="domains" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDomains applicationId={applicationId} />
|
<ShowDomains id={applicationId} type="application" />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||||
|
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
|
||||||
|
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
|
|
||||||
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
|
|
||||||
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
||||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||||
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
|
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
|
||||||
@@ -218,10 +219,10 @@ const Service = (
|
|||||||
className={cn(
|
className={cn(
|
||||||
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
isCloud && data?.serverId
|
isCloud && data?.serverId
|
||||||
? "lg:grid-cols-8"
|
? "lg:grid-cols-9"
|
||||||
: data?.serverId
|
: data?.serverId
|
||||||
? "lg:grid-cols-7"
|
? "lg:grid-cols-8"
|
||||||
: "lg:grid-cols-8",
|
: "lg:grid-cols-9",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
@@ -229,6 +230,7 @@ const Service = (
|
|||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
@@ -253,6 +255,12 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="schedules">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowSchedules id={composeId} scheduleType="compose" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="monitoring">
|
<TabsContent value="monitoring">
|
||||||
<div className="pt-2.5">
|
<div className="pt-2.5">
|
||||||
<div className="flex flex-col border rounded-lg ">
|
<div className="flex flex-col border rounded-lg ">
|
||||||
@@ -323,15 +331,20 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="deployments">
|
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 border rounded-lg">
|
||||||
<ShowDeploymentsCompose composeId={composeId} />
|
<ShowDeployments
|
||||||
|
id={composeId}
|
||||||
|
type="compose"
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
refreshToken={data?.refreshToken || ""}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="domains">
|
<TabsContent value="domains">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDomainsCompose composeId={composeId} />
|
<ShowDomains id={composeId} type="compose" />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
54
apps/dokploy/pages/dashboard/schedules.tsx
Normal file
54
apps/dokploy/pages/dashboard/schedules.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
function SchedulesPage() {
|
||||||
|
const { data: user } = api.user.get.useQuery();
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
|
||||||
|
<div className="rounded-xl bg-background shadow-md h-full">
|
||||||
|
<ShowSchedules
|
||||||
|
scheduleType="dokploy-server"
|
||||||
|
id={user?.user.id || ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default SchedulesPage;
|
||||||
|
|
||||||
|
SchedulesPage.getLayout = (page: ReactElement) => {
|
||||||
|
return <DashboardLayout>{page}</DashboardLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(
|
||||||
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
|
) {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/dashboard/projects",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { user } = await validateRequest(ctx.req);
|
||||||
|
if (!user || user.role !== "owner") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
||||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||||
import { EnablePaidFeatures } from "@/components/dashboard/settings/enable-paid-features";
|
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
@@ -13,7 +12,6 @@ import { api } from "@/utils/api";
|
|||||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
const { data: user } = api.user.get.useQuery();
|
const { data: user } = api.user.get.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -29,7 +27,6 @@ const Page = () => {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
{!isCloud && <EnablePaidFeatures />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createTRPCRouter } from "../api/trpc";
|
import { createTRPCRouter } from "../api/trpc";
|
||||||
|
import { adminRouter } from "./routers/admin";
|
||||||
import { aiRouter } from "./routers/ai";
|
import { aiRouter } from "./routers/ai";
|
||||||
import { applicationRouter } from "./routers/application";
|
import { applicationRouter } from "./routers/application";
|
||||||
import { backupRouter } from "./routers/backup";
|
import { backupRouter } from "./routers/backup";
|
||||||
@@ -34,6 +35,7 @@ import { sshRouter } from "./routers/ssh-key";
|
|||||||
import { stripeRouter } from "./routers/stripe";
|
import { stripeRouter } from "./routers/stripe";
|
||||||
import { swarmRouter } from "./routers/swarm";
|
import { swarmRouter } from "./routers/swarm";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
|
import { scheduleRouter } from "./routers/schedule";
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
*
|
*
|
||||||
@@ -41,6 +43,7 @@ import { userRouter } from "./routers/user";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
|
admin: adminRouter,
|
||||||
docker: dockerRouter,
|
docker: dockerRouter,
|
||||||
project: projectRouter,
|
project: projectRouter,
|
||||||
application: applicationRouter,
|
application: applicationRouter,
|
||||||
@@ -76,6 +79,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
swarm: swarmRouter,
|
swarm: swarmRouter,
|
||||||
ai: aiRouter,
|
ai: aiRouter,
|
||||||
organization: organizationRouter,
|
organization: organizationRouter,
|
||||||
|
schedule: scheduleRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
61
apps/dokploy/server/api/routers/admin.ts
Normal file
61
apps/dokploy/server/api/routers/admin.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
|
||||||
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
|
findUserById,
|
||||||
|
setupWebMonitoring,
|
||||||
|
updateUser,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||||
|
|
||||||
|
export const adminRouter = createTRPCRouter({
|
||||||
|
setupMonitoring: adminProcedure
|
||||||
|
.input(apiUpdateWebServerMonitoring)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
try {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "Feature disabled on cloud",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const user = await findUserById(ctx.user.ownerId);
|
||||||
|
if (user.id !== ctx.user.ownerId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You are not authorized to setup the monitoring",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUser(user.id, {
|
||||||
|
metricsConfig: {
|
||||||
|
server: {
|
||||||
|
type: "Dokploy",
|
||||||
|
refreshRate: input.metricsConfig.server.refreshRate,
|
||||||
|
port: input.metricsConfig.server.port,
|
||||||
|
token: input.metricsConfig.server.token,
|
||||||
|
cronJob: input.metricsConfig.server.cronJob,
|
||||||
|
urlCallback: input.metricsConfig.server.urlCallback,
|
||||||
|
retentionDays: input.metricsConfig.server.retentionDays,
|
||||||
|
thresholds: {
|
||||||
|
cpu: input.metricsConfig.server.thresholds.cpu,
|
||||||
|
memory: input.metricsConfig.server.thresholds.memory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
containers: {
|
||||||
|
refreshRate: input.metricsConfig.containers.refreshRate,
|
||||||
|
services: {
|
||||||
|
include: input.metricsConfig.containers.services.include || [],
|
||||||
|
exclude: input.metricsConfig.containers.services.exclude || [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentServer = await setupWebMonitoring(user.id);
|
||||||
|
return currentServer;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -163,7 +163,7 @@ export const aiRouter = createTRPCRouter({
|
|||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(deploySuggestionSchema)
|
.input(deploySuggestionSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -216,7 +216,7 @@ export const aiRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.user.ownerId,
|
ctx.user.ownerId,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiCreateApplication)
|
.input(apiCreateApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -87,7 +87,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const newApplication = await createApplication(input);
|
const newApplication = await createApplication(input);
|
||||||
|
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newApplication.applicationId,
|
newApplication.applicationId,
|
||||||
@@ -109,7 +109,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.applicationId,
|
input.applicationId,
|
||||||
@@ -168,7 +168,7 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiFindOneApplication)
|
.input(apiFindOneApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.applicationId,
|
input.applicationId,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
apiCreateBackup,
|
apiCreateBackup,
|
||||||
apiFindOneBackup,
|
apiFindOneBackup,
|
||||||
apiRemoveBackup,
|
apiRemoveBackup,
|
||||||
|
apiRestoreBackup,
|
||||||
apiUpdateBackup,
|
apiUpdateBackup,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
|
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
|
||||||
@@ -366,23 +367,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
override: true,
|
override: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(
|
.input(apiRestoreBackup)
|
||||||
z.object({
|
|
||||||
databaseId: z.string(),
|
|
||||||
databaseType: z.enum([
|
|
||||||
"postgres",
|
|
||||||
"mysql",
|
|
||||||
"mariadb",
|
|
||||||
"mongo",
|
|
||||||
"web-server",
|
|
||||||
]),
|
|
||||||
backupType: z.enum(["database", "compose"]),
|
|
||||||
databaseName: z.string().min(1),
|
|
||||||
backupFile: z.string().min(1),
|
|
||||||
destinationId: z.string().min(1),
|
|
||||||
metadata: z.any(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.subscription(async ({ input }) => {
|
.subscription(async ({ input }) => {
|
||||||
const destination = await findDestinationById(input.destinationId);
|
const destination = await findDestinationById(input.destinationId);
|
||||||
if (input.backupType === "database") {
|
if (input.backupType === "database") {
|
||||||
@@ -390,57 +375,33 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const postgres = await findPostgresById(input.databaseId);
|
const postgres = await findPostgresById(input.databaseId);
|
||||||
|
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restorePostgresBackup(
|
restorePostgresBackup(postgres, destination, input, (log) => {
|
||||||
postgres,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "mysql") {
|
if (input.databaseType === "mysql") {
|
||||||
const mysql = await findMySqlById(input.databaseId);
|
const mysql = await findMySqlById(input.databaseId);
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restoreMySqlBackup(
|
restoreMySqlBackup(mysql, destination, input, (log) => {
|
||||||
mysql,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "mariadb") {
|
if (input.databaseType === "mariadb") {
|
||||||
const mariadb = await findMariadbById(input.databaseId);
|
const mariadb = await findMariadbById(input.databaseId);
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restoreMariadbBackup(
|
restoreMariadbBackup(mariadb, destination, input, (log) => {
|
||||||
mariadb,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "mongo") {
|
if (input.databaseType === "mongo") {
|
||||||
const mongo = await findMongoById(input.databaseId);
|
const mongo = await findMongoById(input.databaseId);
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restoreMongoBackup(
|
restoreMongoBackup(mongo, destination, input, (log) => {
|
||||||
mongo,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "web-server") {
|
if (input.databaseType === "web-server") {
|
||||||
@@ -454,16 +415,9 @@ export const backupRouter = createTRPCRouter({
|
|||||||
if (input.backupType === "compose") {
|
if (input.backupType === "compose") {
|
||||||
const compose = await findComposeById(input.databaseId);
|
const compose = await findComposeById(input.databaseId);
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
restoreComposeBackup(
|
restoreComposeBackup(compose, destination, input, (log) => {
|
||||||
compose,
|
emit.next(log);
|
||||||
destination,
|
});
|
||||||
input.databaseName,
|
|
||||||
input.backupFile,
|
|
||||||
input.metadata,
|
|
||||||
(log) => {
|
|
||||||
emit.next(log);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.input(apiCreateCompose)
|
.input(apiCreateCompose)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -86,7 +86,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
const newService = await createCompose(input);
|
const newService = await createCompose(input);
|
||||||
|
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newService.composeId,
|
newService.composeId,
|
||||||
@@ -103,7 +103,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiFindCompose)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.composeId,
|
input.composeId,
|
||||||
@@ -137,7 +137,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiDeleteCompose)
|
.input(apiDeleteCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.composeId,
|
input.composeId,
|
||||||
@@ -408,7 +408,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -455,7 +455,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
isolatedDeployment: true,
|
isolatedDeployment: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
compose.composeId,
|
compose.composeId,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import {
|
|||||||
apiFindAllByApplication,
|
apiFindAllByApplication,
|
||||||
apiFindAllByCompose,
|
apiFindAllByCompose,
|
||||||
apiFindAllByServer,
|
apiFindAllByServer,
|
||||||
|
apiFindAllByType,
|
||||||
|
deployments,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
findAllDeploymentsByApplicationId,
|
findAllDeploymentsByApplicationId,
|
||||||
@@ -12,7 +14,9 @@ import {
|
|||||||
findServerById,
|
findServerById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { desc, eq } from "drizzle-orm";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
|
||||||
export const deploymentRouter = createTRPCRouter({
|
export const deploymentRouter = createTRPCRouter({
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
@@ -54,4 +58,14 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return await findAllDeploymentsByServerId(input.serverId);
|
return await findAllDeploymentsByServerId(input.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
allByType: protectedProcedure
|
||||||
|
.input(apiFindAllByType)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const deploymentsList = await db.query.deployments.findMany({
|
||||||
|
where: eq(deployments[`${input.type}Id`], input.id),
|
||||||
|
orderBy: desc(deployments.createdAt),
|
||||||
|
});
|
||||||
|
return deploymentsList;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ export const domainRouter = createTRPCRouter({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error creating the domain",
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Error creating the domain",
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMariaDB)
|
.input(apiCreateMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -65,7 +65,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newMariadb = await createMariadb(input);
|
const newMariadb = await createMariadb(input);
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newMariadb.mariadbId,
|
newMariadb.mariadbId,
|
||||||
@@ -92,7 +92,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mariadbId,
|
input.mariadbId,
|
||||||
@@ -219,7 +219,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMariaDB)
|
.input(apiFindOneMariaDB)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mariadbId,
|
input.mariadbId,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMongo)
|
.input(apiCreateMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -65,7 +65,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newMongo = await createMongo(input);
|
const newMongo = await createMongo(input);
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newMongo.mongoId,
|
newMongo.mongoId,
|
||||||
@@ -96,7 +96,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneMongo)
|
.input(apiFindOneMongo)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mongoId,
|
input.mongoId,
|
||||||
@@ -261,7 +261,7 @@ export const mongoRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMongo)
|
.input(apiFindOneMongo)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mongoId,
|
input.mongoId,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
.input(apiCreateMySql)
|
.input(apiCreateMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -69,7 +69,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newMysql = await createMysql(input);
|
const newMysql = await createMysql(input);
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newMysql.mysqlId,
|
newMysql.mysqlId,
|
||||||
@@ -100,7 +100,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mysqlId,
|
input.mysqlId,
|
||||||
@@ -260,7 +260,7 @@ export const mysqlRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneMySql)
|
.input(apiFindOneMySql)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.mysqlId,
|
input.mysqlId,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol !== "owner" && !IS_CLOUD) {
|
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Only the organization owner can create an organization",
|
message: "Only the organization owner can create an organization",
|
||||||
@@ -86,7 +86,7 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol !== "owner" && !IS_CLOUD) {
|
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Only the organization owner can update it",
|
message: "Only the organization owner can update it",
|
||||||
@@ -109,7 +109,7 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol !== "owner" && !IS_CLOUD) {
|
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: "Only the organization owner can delete it",
|
message: "Only the organization owner can delete it",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
.input(apiCreatePostgres)
|
.input(apiCreatePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -65,7 +65,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newPostgres = await createPostgres(input);
|
const newPostgres = await createPostgres(input);
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newPostgres.postgresId,
|
newPostgres.postgresId,
|
||||||
@@ -96,7 +96,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOnePostgres)
|
.input(apiFindOnePostgres)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.postgresId,
|
input.postgresId,
|
||||||
@@ -244,7 +244,7 @@ export const postgresRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOnePostgres)
|
.input(apiFindOnePostgres)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.postgresId,
|
input.postgresId,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
.input(apiCreateProject)
|
.input(apiCreateProject)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkProjectAccess(
|
await checkProjectAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
"create",
|
"create",
|
||||||
@@ -78,7 +78,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
);
|
);
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
project.projectId,
|
project.projectId,
|
||||||
@@ -99,7 +99,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneProject)
|
.input(apiFindOneProject)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
const { accessedServices } = await findMemberById(
|
const { accessedServices } = await findMemberById(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
@@ -118,15 +118,15 @@ export const projectRouter = createTRPCRouter({
|
|||||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||||
),
|
),
|
||||||
with: {
|
with: {
|
||||||
compose: {
|
|
||||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
|
||||||
},
|
|
||||||
applications: {
|
applications: {
|
||||||
where: buildServiceFilter(
|
where: buildServiceFilter(
|
||||||
applications.applicationId,
|
applications.applicationId,
|
||||||
accessedServices,
|
accessedServices,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
compose: {
|
||||||
|
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||||
|
},
|
||||||
mariadb: {
|
mariadb: {
|
||||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||||
},
|
},
|
||||||
@@ -164,8 +164,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
return project;
|
return project;
|
||||||
}),
|
}),
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
// console.log(ctx.user);
|
if (ctx.user.role === "member") {
|
||||||
if (ctx.user.rol === "member") {
|
|
||||||
const { accessedProjects, accessedServices } = await findMemberById(
|
const { accessedProjects, accessedServices } = await findMemberById(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
@@ -175,7 +174,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = await db.query.projects.findMany({
|
return await db.query.projects.findMany({
|
||||||
where: and(
|
where: and(
|
||||||
sql`${projects.projectId} IN (${sql.join(
|
sql`${projects.projectId} IN (${sql.join(
|
||||||
accessedProjects.map((projectId) => sql`${projectId}`),
|
accessedProjects.map((projectId) => sql`${projectId}`),
|
||||||
@@ -213,8 +212,6 @@ export const projectRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
orderBy: desc(projects.createdAt),
|
orderBy: desc(projects.createdAt),
|
||||||
});
|
});
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await db.query.projects.findMany({
|
return await db.query.projects.findMany({
|
||||||
@@ -244,7 +241,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
.input(apiRemoveProject)
|
.input(apiRemoveProject)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkProjectAccess(
|
await checkProjectAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
"delete",
|
"delete",
|
||||||
@@ -316,7 +313,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkProjectAccess(
|
await checkProjectAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
"create",
|
"create",
|
||||||
@@ -581,7 +578,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newProject.projectId,
|
newProject.projectId,
|
||||||
@@ -604,10 +601,10 @@ function buildServiceFilter(
|
|||||||
fieldName: AnyPgColumn,
|
fieldName: AnyPgColumn,
|
||||||
accessedServices: string[],
|
accessedServices: string[],
|
||||||
) {
|
) {
|
||||||
return accessedServices.length > 0
|
return accessedServices.length === 0
|
||||||
? sql`${fieldName} IN (${sql.join(
|
? sql`false`
|
||||||
|
: sql`${fieldName} IN (${sql.join(
|
||||||
accessedServices.map((serviceId) => sql`${serviceId}`),
|
accessedServices.map((serviceId) => sql`${serviceId}`),
|
||||||
sql`, `,
|
sql`, `,
|
||||||
)})`
|
)})`;
|
||||||
: sql`1 = 0`;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
.input(apiCreateRedis)
|
.input(apiCreateRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.projectId,
|
input.projectId,
|
||||||
@@ -65,7 +65,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newRedis = await createRedis(input);
|
const newRedis = await createRedis(input);
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newRedis.redisId,
|
newRedis.redisId,
|
||||||
@@ -89,7 +89,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneRedis)
|
.input(apiFindOneRedis)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.redisId,
|
input.redisId,
|
||||||
@@ -251,7 +251,7 @@ export const redisRouter = createTRPCRouter({
|
|||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiFindOneRedis)
|
.input(apiFindOneRedis)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.redisId,
|
input.redisId,
|
||||||
|
|||||||
142
apps/dokploy/server/api/routers/schedule.ts
Normal file
142
apps/dokploy/server/api/routers/schedule.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
createScheduleSchema,
|
||||||
|
schedules,
|
||||||
|
updateScheduleSchema,
|
||||||
|
} from "@dokploy/server/db/schema/schedule";
|
||||||
|
import { desc, eq } from "drizzle-orm";
|
||||||
|
import { db } from "@dokploy/server/db";
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import { runCommand } from "@dokploy/server/index";
|
||||||
|
import { deployments } from "@dokploy/server/db/schema/deployment";
|
||||||
|
import {
|
||||||
|
deleteSchedule,
|
||||||
|
findScheduleById,
|
||||||
|
createSchedule,
|
||||||
|
updateSchedule,
|
||||||
|
} from "@dokploy/server/services/schedule";
|
||||||
|
import { IS_CLOUD, scheduleJob } from "@dokploy/server";
|
||||||
|
import { removeJob, schedule } from "@/server/utils/backup";
|
||||||
|
import { removeScheduleJob } from "@dokploy/server";
|
||||||
|
export const scheduleRouter = createTRPCRouter({
|
||||||
|
create: protectedProcedure
|
||||||
|
.input(createScheduleSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const newSchedule = await createSchedule(input);
|
||||||
|
|
||||||
|
if (newSchedule?.enabled) {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
schedule({
|
||||||
|
scheduleId: newSchedule.scheduleId,
|
||||||
|
type: "schedule",
|
||||||
|
cronSchedule: newSchedule.cronExpression,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
scheduleJob(newSchedule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSchedule;
|
||||||
|
}),
|
||||||
|
|
||||||
|
update: protectedProcedure
|
||||||
|
.input(updateScheduleSchema)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const updatedSchedule = await updateSchedule(input);
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
if (updatedSchedule?.enabled) {
|
||||||
|
schedule({
|
||||||
|
scheduleId: updatedSchedule.scheduleId,
|
||||||
|
type: "schedule",
|
||||||
|
cronSchedule: updatedSchedule.cronExpression,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await removeJob({
|
||||||
|
cronSchedule: updatedSchedule.cronExpression,
|
||||||
|
scheduleId: updatedSchedule.scheduleId,
|
||||||
|
type: "schedule",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (updatedSchedule?.enabled) {
|
||||||
|
removeScheduleJob(updatedSchedule.scheduleId);
|
||||||
|
scheduleJob(updatedSchedule);
|
||||||
|
} else {
|
||||||
|
removeScheduleJob(updatedSchedule.scheduleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatedSchedule;
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: protectedProcedure
|
||||||
|
.input(z.object({ scheduleId: z.string() }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const schedule = await findScheduleById(input.scheduleId);
|
||||||
|
await deleteSchedule(input.scheduleId);
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await removeJob({
|
||||||
|
cronSchedule: schedule.cronExpression,
|
||||||
|
scheduleId: schedule.scheduleId,
|
||||||
|
type: "schedule",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
removeScheduleJob(schedule.scheduleId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
list: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
id: z.string(),
|
||||||
|
scheduleType: z.enum([
|
||||||
|
"application",
|
||||||
|
"compose",
|
||||||
|
"server",
|
||||||
|
"dokploy-server",
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const where = {
|
||||||
|
application: eq(schedules.applicationId, input.id),
|
||||||
|
compose: eq(schedules.composeId, input.id),
|
||||||
|
server: eq(schedules.serverId, input.id),
|
||||||
|
"dokploy-server": eq(schedules.userId, input.id),
|
||||||
|
};
|
||||||
|
return db.query.schedules.findMany({
|
||||||
|
where: where[input.scheduleType],
|
||||||
|
with: {
|
||||||
|
application: true,
|
||||||
|
server: true,
|
||||||
|
compose: true,
|
||||||
|
deployments: {
|
||||||
|
orderBy: [desc(deployments.createdAt)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
|
one: protectedProcedure
|
||||||
|
.input(z.object({ scheduleId: z.string() }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await findScheduleById(input.scheduleId);
|
||||||
|
}),
|
||||||
|
|
||||||
|
runManually: protectedProcedure
|
||||||
|
.input(z.object({ scheduleId: z.string().min(1) }))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
try {
|
||||||
|
await runCommand(input.scheduleId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message:
|
||||||
|
error instanceof Error ? error.message : "Error running schedule",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -407,7 +407,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
.input(apiServerSchema)
|
.input(apiServerSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
const canAccess = await canAccessToTraefikFiles(
|
const canAccess = await canAccessToTraefikFiles(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
@@ -428,7 +428,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
updateTraefikFile: protectedProcedure
|
updateTraefikFile: protectedProcedure
|
||||||
.input(apiModifyTraefikConfig)
|
.input(apiModifyTraefikConfig)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
const canAccess = await canAccessToTraefikFiles(
|
const canAccess = await canAccessToTraefikFiles(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
@@ -449,7 +449,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
readTraefikFile: protectedProcedure
|
readTraefikFile: protectedProcedure
|
||||||
.input(apiReadTraefikConfig)
|
.input(apiReadTraefikConfig)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "member") {
|
if (ctx.user.role === "member") {
|
||||||
const canAccess = await canAccessToTraefikFiles(
|
const canAccess = await canAccessToTraefikFiles(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
findUserById,
|
findUserById,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
removeUserById,
|
removeUserById,
|
||||||
setupWebMonitoring,
|
|
||||||
updateUser,
|
updateUser,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
@@ -14,7 +13,6 @@ import {
|
|||||||
apiAssignPermissions,
|
apiAssignPermissions,
|
||||||
apiFindOneToken,
|
apiFindOneToken,
|
||||||
apiUpdateUser,
|
apiUpdateUser,
|
||||||
apiUpdateWebServerMonitoring,
|
|
||||||
apikey,
|
apikey,
|
||||||
invitation,
|
invitation,
|
||||||
member,
|
member,
|
||||||
@@ -29,11 +27,7 @@ import {
|
|||||||
protectedProcedure,
|
protectedProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
import {
|
|
||||||
validateLicense,
|
|
||||||
activateLicense,
|
|
||||||
deactivateLicense,
|
|
||||||
} from "@/server/utils/validate-license";
|
|
||||||
const apiCreateApiKey = z.object({
|
const apiCreateApiKey = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
prefix: z.string().optional(),
|
prefix: z.string().optional(),
|
||||||
@@ -97,6 +91,18 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return memberResult;
|
return memberResult;
|
||||||
}),
|
}),
|
||||||
|
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
process.env.USER_ADMIN_ID === ctx.user.id ||
|
||||||
|
ctx.session?.impersonatedBy === process.env.USER_ADMIN_ID
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
getBackups: adminProcedure.query(async ({ ctx }) => {
|
getBackups: adminProcedure.query(async ({ ctx }) => {
|
||||||
const memberResult = await db.query.member.findFirst({
|
const memberResult = await db.query.member.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
@@ -109,6 +115,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
backups: {
|
backups: {
|
||||||
with: {
|
with: {
|
||||||
destination: true,
|
destination: true,
|
||||||
|
deployments: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@@ -166,73 +173,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return await updateUser(ctx.user.id, input);
|
return await updateUser(ctx.user.id, input);
|
||||||
}),
|
}),
|
||||||
saveLicense: adminProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
licenseKey: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const owner = await findUserById(ctx.user.ownerId);
|
|
||||||
|
|
||||||
const result = await activateLicense(
|
|
||||||
input.licenseKey,
|
|
||||||
owner?.serverIp || "",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: result.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUser(ctx.user.id, {
|
|
||||||
licenseKey: input.licenseKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}),
|
|
||||||
deactivateLicense: adminProcedure
|
|
||||||
.input(z.object({ licenseKey: z.string().min(1) }))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const owner = await findUserById(ctx.user.ownerId);
|
|
||||||
const result = await deactivateLicense(
|
|
||||||
input.licenseKey,
|
|
||||||
owner?.serverIp || "",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: result.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUser(ctx.user.id, {
|
|
||||||
licenseKey: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}),
|
|
||||||
validateLicense: adminProcedure
|
|
||||||
.input(z.object({ licenseKey: z.string().min(1) }))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const owner = await findUserById(ctx.user.ownerId);
|
|
||||||
const result = await validateLicense(
|
|
||||||
input.licenseKey,
|
|
||||||
owner?.serverIp || "",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: result.error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}),
|
|
||||||
getUserByToken: publicProcedure
|
getUserByToken: publicProcedure
|
||||||
.input(apiFindOneToken)
|
.input(apiFindOneToken)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
@@ -422,62 +362,4 @@ export const userRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return organizations.length;
|
return organizations.length;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setupMonitoring: adminProcedure
|
|
||||||
.input(apiUpdateWebServerMonitoring)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
try {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Feature disabled on cloud",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await findUserById(ctx.user.id);
|
|
||||||
|
|
||||||
if (!validateLicense(user?.licenseKey || "", user?.serverIp || "")) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Invalid license key",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (user.id !== ctx.user.ownerId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to setup the monitoring",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUser(user.id, {
|
|
||||||
metricsConfig: {
|
|
||||||
server: {
|
|
||||||
type: "Dokploy",
|
|
||||||
refreshRate: input.metricsConfig.server.refreshRate,
|
|
||||||
port: input.metricsConfig.server.port,
|
|
||||||
token: input.metricsConfig.server.token,
|
|
||||||
cronJob: input.metricsConfig.server.cronJob,
|
|
||||||
urlCallback: input.metricsConfig.server.urlCallback,
|
|
||||||
retentionDays: input.metricsConfig.server.retentionDays,
|
|
||||||
thresholds: {
|
|
||||||
cpu: input.metricsConfig.server.thresholds.cpu,
|
|
||||||
memory: input.metricsConfig.server.thresholds.memory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
containers: {
|
|
||||||
refreshRate: input.metricsConfig.containers.refreshRate,
|
|
||||||
services: {
|
|
||||||
include: input.metricsConfig.containers.services.include || [],
|
|
||||||
exclude: input.metricsConfig.containers.services.exclude || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentServer = await setupWebMonitoring(user.id);
|
|
||||||
return currentServer;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ import { ZodError } from "zod";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
interface CreateContextOptions {
|
interface CreateContextOptions {
|
||||||
user: (User & { rol: "member" | "admin" | "owner"; ownerId: string }) | null;
|
user: (User & { role: "member" | "admin" | "owner"; ownerId: string }) | null;
|
||||||
session: (Session & { activeOrganizationId: string }) | null;
|
session:
|
||||||
|
| (Session & { activeOrganizationId: string; impersonatedBy?: string })
|
||||||
|
| null;
|
||||||
req: CreateNextContextOptions["req"];
|
req: CreateNextContextOptions["req"];
|
||||||
res: CreateNextContextOptions["res"];
|
res: CreateNextContextOptions["res"];
|
||||||
}
|
}
|
||||||
@@ -83,7 +85,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||||||
? {
|
? {
|
||||||
...user,
|
...user,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
rol: user.role as "owner" | "member" | "admin",
|
role: user.role as "owner" | "member" | "admin",
|
||||||
id: user.id,
|
id: user.id,
|
||||||
ownerId: user.ownerId,
|
ownerId: user.ownerId,
|
||||||
}
|
}
|
||||||
@@ -180,7 +182,7 @@ export const uploadProcedure = async (opts: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
if (!ctx.session || !ctx.user || ctx.user.rol !== "owner") {
|
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
return next({
|
return next({
|
||||||
@@ -194,7 +196,7 @@ export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||||
if (!ctx.session || !ctx.user || ctx.user.rol !== "owner") {
|
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
return next({
|
return next({
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
createDefaultTraefikConfig,
|
createDefaultTraefikConfig,
|
||||||
initCronJobs,
|
initCronJobs,
|
||||||
|
initSchedules,
|
||||||
initializeNetwork,
|
initializeNetwork,
|
||||||
sendDokployRestartNotifications,
|
sendDokployRestartNotifications,
|
||||||
setupDirectories,
|
setupDirectories,
|
||||||
@@ -49,6 +50,7 @@ void app.prepare().then(async () => {
|
|||||||
createDefaultServerTraefikConfig();
|
createDefaultServerTraefikConfig();
|
||||||
await migration();
|
await migration();
|
||||||
await initCronJobs();
|
await initCronJobs();
|
||||||
|
await initSchedules();
|
||||||
await sendDokployRestartNotifications();
|
await sendDokployRestartNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ type QueueJob =
|
|||||||
type: "server";
|
type: "server";
|
||||||
cronSchedule: string;
|
cronSchedule: string;
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "schedule";
|
||||||
|
cronSchedule: string;
|
||||||
|
scheduleId: string;
|
||||||
};
|
};
|
||||||
export const schedule = async (job: QueueJob) => {
|
export const schedule = async (job: QueueJob) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
const licensesUrl = process.env.LICENSES_URL || "http://localhost:4002";
|
|
||||||
|
|
||||||
export const validateLicense = async (licenseKey: string, serverIp: string) => {
|
|
||||||
const response = await fetch(`${licensesUrl}/api/license/validate`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ licenseKey, serverIp }),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok && data.error?.issues) {
|
|
||||||
console.log("Validation errors:", data.error.issues);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const activateLicense = async (licenseKey: string, serverIp: string) => {
|
|
||||||
const response = await fetch(`${licensesUrl}/api/license/activate`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ licenseKey, serverIp }),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deactivateLicense = async (
|
|
||||||
licenseKey: string,
|
|
||||||
serverIp: string,
|
|
||||||
) => {
|
|
||||||
const response = await fetch(`${licensesUrl}/api/license/deactivate`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ licenseKey, serverIp }),
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
15
apps/dokploy/utils/hooks/use-debounce.ts
Normal file
15
apps/dokploy/utils/hooks/use-debounce.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export function useDebounce<T>(value: T, delay?: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
LEMON_SQUEEZY_API_KEY=""
|
|
||||||
LEMON_SQUEEZY_STORE_ID=""
|
|
||||||
28
apps/licenses/.gitignore
vendored
28
apps/licenses/.gitignore
vendored
@@ -1,28 +0,0 @@
|
|||||||
# dev
|
|
||||||
.yarn/
|
|
||||||
!.yarn/releases
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/*.code-snippets
|
|
||||||
.idea/workspace.xml
|
|
||||||
.idea/usage.statistics.xml
|
|
||||||
.idea/shelf
|
|
||||||
|
|
||||||
# deps
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# env
|
|
||||||
.env
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# logs
|
|
||||||
logs/
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
```
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
open http://localhost:4002/api/health
|
|
||||||
```
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { Config } from "drizzle-kit";
|
|
||||||
import "dotenv/config";
|
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL!;
|
|
||||||
|
|
||||||
export default {
|
|
||||||
schema: "./src/schema.ts",
|
|
||||||
out: "./drizzle",
|
|
||||||
dialect: "postgresql",
|
|
||||||
dbCredentials: {
|
|
||||||
url: connectionString,
|
|
||||||
},
|
|
||||||
verbose: true,
|
|
||||||
strict: true,
|
|
||||||
} satisfies Config;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
CREATE TABLE "licenses" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"product_id" text NOT NULL,
|
|
||||||
"license_key" text NOT NULL,
|
|
||||||
"server_ips" text[],
|
|
||||||
"activated_at" timestamp,
|
|
||||||
"last_verified_at" timestamp,
|
|
||||||
"stripeCustomerId" text NOT NULL,
|
|
||||||
"stripeSubscriptionId" text NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"metadata" text,
|
|
||||||
"user_id" uuid,
|
|
||||||
CONSTRAINT "licenses_license_key_unique" UNIQUE("license_key")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "user" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"email" text NOT NULL,
|
|
||||||
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"otp_code" text,
|
|
||||||
"otp_code_expires_at" timestamp,
|
|
||||||
"temporal_id" uuid DEFAULT gen_random_uuid(),
|
|
||||||
CONSTRAINT "user_email_unique" UNIQUE("email")
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "licenses" ADD CONSTRAINT "licenses_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "553c7c08-f9c6-4090-8372-8d27a389eaa7",
|
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"tables": {
|
|
||||||
"public.licenses": {
|
|
||||||
"name": "licenses",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"product_id": {
|
|
||||||
"name": "product_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"license_key": {
|
|
||||||
"name": "license_key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"server_ips": {
|
|
||||||
"name": "server_ips",
|
|
||||||
"type": "text[]",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"activated_at": {
|
|
||||||
"name": "activated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"last_verified_at": {
|
|
||||||
"name": "last_verified_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"stripeCustomerId": {
|
|
||||||
"name": "stripeCustomerId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"stripeSubscriptionId": {
|
|
||||||
"name": "stripeSubscriptionId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"name": "metadata",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"licenses_user_id_user_id_fk": {
|
|
||||||
"name": "licenses_user_id_user_id_fk",
|
|
||||||
"tableFrom": "licenses",
|
|
||||||
"tableTo": "user",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"licenses_license_key_unique": {
|
|
||||||
"name": "licenses_license_key_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"license_key"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.user": {
|
|
||||||
"name": "user",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"created_at": {
|
|
||||||
"name": "created_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
},
|
|
||||||
"updated_at": {
|
|
||||||
"name": "updated_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
},
|
|
||||||
"otp_code": {
|
|
||||||
"name": "otp_code",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"otp_code_expires_at": {
|
|
||||||
"name": "otp_code_expires_at",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"temporal_id": {
|
|
||||||
"name": "temporal_id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"user_email_unique": {
|
|
||||||
"name": "user_email_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"sequences": {},
|
|
||||||
"roles": {},
|
|
||||||
"policies": {},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "postgresql",
|
|
||||||
"entries": [
|
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1742773711509,
|
|
||||||
"tag": "0000_famous_vermin",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
|
||||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
||||||
import postgres from "postgres";
|
|
||||||
import "dotenv/config";
|
|
||||||
import * as schema from "./src/schema";
|
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL!;
|
|
||||||
const sql = postgres(connectionString, { max: 1 });
|
|
||||||
const db = drizzle(sql, { schema });
|
|
||||||
|
|
||||||
await migrate(db, { migrationsFolder: "drizzle" })
|
|
||||||
.then(() => {
|
|
||||||
console.log("Migration complete");
|
|
||||||
sql.end();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Migration failed", error);
|
|
||||||
process.exit(1);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
sql.end();
|
|
||||||
});
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@dokploy/licenses",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "PORT=4002 tsx watch -r dotenv/config src/index.ts",
|
|
||||||
"build": "tsc --project tsconfig.json",
|
|
||||||
"start": "node dist/index.js",
|
|
||||||
"typecheck": "tsc --noEmit",
|
|
||||||
"generate": "drizzle-kit generate",
|
|
||||||
"drop": "drizzle-kit drop",
|
|
||||||
"push": "drizzle-kit push",
|
|
||||||
"migrate": "tsx ./migrate.ts",
|
|
||||||
"truncate": "tsx ./truncate.ts",
|
|
||||||
"reset:all": "tsx ./truncate.ts && tsx ./migrate.ts",
|
|
||||||
"studio": "drizzle-kit studio"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"nanoid": "5.1.5",
|
|
||||||
"@react-email/components": "^0.0.21",
|
|
||||||
"@hono/node-server": "^1.12.1",
|
|
||||||
"@hono/zod-validator": "0.3.0",
|
|
||||||
"@react-email/render": "^1.0.5",
|
|
||||||
"@types/pg": "^8.11.11",
|
|
||||||
"dotenv": "^16.3.1",
|
|
||||||
"drizzle-orm": "^0.39.1",
|
|
||||||
"hono": "^4.5.8",
|
|
||||||
"nodemailer": "6.9.14",
|
|
||||||
"pg": "^8.14.1",
|
|
||||||
"pino": "9.4.0",
|
|
||||||
"pino-pretty": "11.2.2",
|
|
||||||
"postgres": "3.4.4",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"stripe": "17.2.0",
|
|
||||||
"zod": "^3.23.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.11.17",
|
|
||||||
"@types/nodemailer": "^6.4.16",
|
|
||||||
"@types/react": "^18.2.37",
|
|
||||||
"@types/react-dom": "^18.2.15",
|
|
||||||
"drizzle-kit": "^0.30.4",
|
|
||||||
"tsx": "^4.7.1",
|
|
||||||
"typescript": "^5.4.2"
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@9.5.0"
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
import { zValidator } from "@hono/zod-validator";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
activateLicense,
|
|
||||||
deactivateLicense,
|
|
||||||
validateLicense,
|
|
||||||
cleanLicense,
|
|
||||||
} from "../utils/license";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
import { eq, desc } from "drizzle-orm";
|
|
||||||
import { users, licenses } from "../schema";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { transporter } from "../email";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { stripe } from "../stripe";
|
|
||||||
import type Stripe from "stripe";
|
|
||||||
import { getLicenseTypeFromPriceId } from "../utils";
|
|
||||||
const validateSchema = z.object({
|
|
||||||
licenseKey: z.string(),
|
|
||||||
serverIp: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const licenseRouter = new Hono();
|
|
||||||
|
|
||||||
licenseRouter.post(
|
|
||||||
"/validate",
|
|
||||||
zValidator("json", validateSchema),
|
|
||||||
async (c) => {
|
|
||||||
const { licenseKey, serverIp } = c.req.valid("json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await validateLicense(licenseKey, serverIp);
|
|
||||||
return c.json(result);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error validating license:", { error });
|
|
||||||
return c.json({ success: false, error: "Error validating license" }, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
licenseRouter.post(
|
|
||||||
"/activate",
|
|
||||||
zValidator("json", validateSchema),
|
|
||||||
async (c) => {
|
|
||||||
const { licenseKey, serverIp } = c.req.valid("json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const license = await activateLicense(licenseKey, serverIp);
|
|
||||||
return c.json({ success: true, license });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error activating license:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return c.json({ success: false, error: error.message }, 400);
|
|
||||||
}
|
|
||||||
return c.json({ success: false, error: "Unknown error occurred" }, 400);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
licenseRouter.post(
|
|
||||||
"/deactivate",
|
|
||||||
zValidator("json", validateSchema),
|
|
||||||
async (c) => {
|
|
||||||
const { licenseKey, serverIp } = c.req.valid("json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const license = await deactivateLicense(licenseKey, serverIp);
|
|
||||||
return c.json({ success: true, license });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error deactivating license:", error);
|
|
||||||
return c.json(
|
|
||||||
{ success: false, error: "Error deactivating license" },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
licenseRouter.post(
|
|
||||||
"/remove-server",
|
|
||||||
zValidator(
|
|
||||||
"json",
|
|
||||||
z.object({ licenseKey: z.string().min(1), serverIp: z.string().min(1) }),
|
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const { licenseKey, serverIp } = c.req.valid("json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const license = await cleanLicense(licenseKey, serverIp);
|
|
||||||
return c.json({ success: true, license });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error cleaning license:", error);
|
|
||||||
return c.json({ success: false, error: "Error cleaning license" }, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// router.post("/resend-license", zValidator("json", resendSchema), async (c) => {
|
|
||||||
// const { licenseKey } = c.req.valid("json");
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// const license = await db.query.licenses.findFirst({
|
|
||||||
// where: eq(licenses.licenseKey, licenseKey),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!license) {
|
|
||||||
// return c.json({ success: false, error: "License not found" }, 404);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const suscription = await stripe.subscriptions.retrieve(
|
|
||||||
// license.stripeSubscriptionId,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const priceId = suscription.items.data[0].price.id;
|
|
||||||
// const { type } = getLicenseTypeFromPriceId(priceId);
|
|
||||||
|
|
||||||
// const emailHtml = await render(
|
|
||||||
// ResendLicenseEmail({
|
|
||||||
// licenseKey: license.licenseKey,
|
|
||||||
// productName: `Dokploy Self Hosted ${type}`,
|
|
||||||
// requestDate: new Date(),
|
|
||||||
// customerName: license.email,
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// await transporter.sendMail({
|
|
||||||
// from: process.env.SMTP_FROM_ADDRESS,
|
|
||||||
// to: license.email,
|
|
||||||
// subject: "Your Dokploy License Key",
|
|
||||||
// html: emailHtml,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// return c.json({ success: true });
|
|
||||||
// } catch (error) {
|
|
||||||
// logger.error("Error resending license:", error);
|
|
||||||
// return c.json({ success: false, error: "Error resending license" }, 500);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
licenseRouter.post(
|
|
||||||
"/send-otp",
|
|
||||||
zValidator("json", z.object({ email: z.string().email() })),
|
|
||||||
async (c) => {
|
|
||||||
const { email } = c.req.valid("json");
|
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.email, email.toLowerCase()),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return c.json({ success: false, error: "User not found" }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateOtpCode = Math.floor(100000 + Math.random() * 900000);
|
|
||||||
const otpCodeExpiresAt = new Date(Date.now() + 10 * 60 * 1000);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(users)
|
|
||||||
.set({ otpCode: generateOtpCode.toString(), otpCodeExpiresAt })
|
|
||||||
.where(eq(users.id, user.id));
|
|
||||||
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: process.env.SMTP_FROM_ADDRESS,
|
|
||||||
to: user.email,
|
|
||||||
subject: "Your Dokploy License Key ",
|
|
||||||
html: `Your OTP code is ${generateOtpCode}, it will expire in 10 minutes`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ success: true });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
licenseRouter.post(
|
|
||||||
"/verify-otp",
|
|
||||||
zValidator(
|
|
||||||
"json",
|
|
||||||
z.object({ email: z.string().email(), otpCode: z.string().length(6) }),
|
|
||||||
),
|
|
||||||
async (c) => {
|
|
||||||
const { email, otpCode } = c.req.valid("json");
|
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.email, email.toLowerCase()),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return c.json({ success: false, error: "User not found" }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.otpCode !== otpCode) {
|
|
||||||
return c.json({ success: false, error: "Invalid code" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.otpCodeExpiresAt && user.otpCodeExpiresAt < new Date()) {
|
|
||||||
return c.json({ success: false, error: "Code expired" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await db
|
|
||||||
.update(users)
|
|
||||||
.set({
|
|
||||||
otpCode: null,
|
|
||||||
otpCodeExpiresAt: null,
|
|
||||||
temporalId: nanoid(),
|
|
||||||
temporalIdExpiresAt: new Date(Date.now() + 20 * 60 * 1000),
|
|
||||||
})
|
|
||||||
.where(eq(users.id, user.id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return c.json({ success: true, temporalId: result[0].temporalId });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
licenseRouter.get(
|
|
||||||
"/all",
|
|
||||||
zValidator("query", z.object({ temporalId: z.string() })),
|
|
||||||
async (c) => {
|
|
||||||
const { temporalId } = c.req.valid("query");
|
|
||||||
|
|
||||||
const user = await db.query.users.findFirst({
|
|
||||||
where: eq(users.temporalId, temporalId),
|
|
||||||
with: {
|
|
||||||
licenses: true,
|
|
||||||
},
|
|
||||||
orderBy: desc(licenses.createdAt),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return c.json({ success: false, error: "User not found" }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.temporalIdExpiresAt && user.temporalIdExpiresAt < new Date()) {
|
|
||||||
return c.json({ success: false, error: "Session expired" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const suscriptions: Stripe.Subscription[] = [];
|
|
||||||
for (const license of user.licenses) {
|
|
||||||
const suscription = await stripe.subscriptions.retrieve(
|
|
||||||
license.stripeSubscriptionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
suscriptions.push(suscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
const formated = user.licenses.map((license) => {
|
|
||||||
const suscription = suscriptions.find(
|
|
||||||
(suscription) => suscription.id === license.stripeSubscriptionId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { type } = getLicenseTypeFromPriceId(
|
|
||||||
suscription?.items.data[0].price.id || "",
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
license: license,
|
|
||||||
stripeSuscription: {
|
|
||||||
quantity: suscription?.items.data[0].quantity,
|
|
||||||
billingType: suscription?.items.data[0].price.recurring?.interval,
|
|
||||||
type: type,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ success: true, licenses: formated });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
import { zValidator } from "@hono/zod-validator";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import { createCheckoutSessionSchema } from "../validators/stripe";
|
|
||||||
import { getStripeItems } from "../utils/license";
|
|
||||||
import { stripe } from "../stripe";
|
|
||||||
import { WEBSITE_URL } from "../constants";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { licenses } from "../schema";
|
|
||||||
import { db } from "../db";
|
|
||||||
import { getLicenseFeatures, getLicenseTypeFromPriceId } from "../utils";
|
|
||||||
import { z } from "zod";
|
|
||||||
import type Stripe from "stripe";
|
|
||||||
import { createLicense } from "../utils/license";
|
|
||||||
import { render } from "@react-email/render";
|
|
||||||
import { LicenseEmail } from "../../templates/emails/license-email";
|
|
||||||
import { transporter } from "../email";
|
|
||||||
|
|
||||||
export const stripeRouter = new Hono();
|
|
||||||
|
|
||||||
stripeRouter.post(
|
|
||||||
"/create-checkout-session",
|
|
||||||
zValidator("json", createCheckoutSessionSchema),
|
|
||||||
async (c) => {
|
|
||||||
const { type, serverQuantity, isAnnual } = c.req.valid("json");
|
|
||||||
|
|
||||||
const items = getStripeItems(type, serverQuantity, isAnnual);
|
|
||||||
const session = await stripe.checkout.sessions.create({
|
|
||||||
mode: "subscription",
|
|
||||||
line_items: items,
|
|
||||||
allow_promotion_codes: true,
|
|
||||||
success_url: `${WEBSITE_URL}/license/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
||||||
cancel_url: `${WEBSITE_URL}#pricing`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ sessionId: session.id });
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
stripeRouter.get(
|
|
||||||
"/get-license-from-session",
|
|
||||||
zValidator("query", z.object({ sessionId: z.string().min(1) })),
|
|
||||||
async (c) => {
|
|
||||||
const { sessionId } = c.req.valid("query");
|
|
||||||
console.log("Session ID", sessionId);
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
return c.json({ error: "Session ID is required" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
|
||||||
if (session.status !== "complete") {
|
|
||||||
return c.json({ error: "Session is not complete" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscription = await stripe.subscriptions.retrieve(
|
|
||||||
session.subscription as string,
|
|
||||||
);
|
|
||||||
|
|
||||||
const license = await db.query.licenses.findFirst({
|
|
||||||
where: eq(licenses.stripeSubscriptionId, subscription.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
const priceId = subscription.items.data[0].price.id;
|
|
||||||
const { type, billingType } = getLicenseTypeFromPriceId(priceId);
|
|
||||||
|
|
||||||
return c.json({ type, billingType, key: license?.licenseKey });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error retrieving session:", error);
|
|
||||||
return c.json({ error: "Error retrieving session" }, 400);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
stripeRouter.post("/webhook", async (c) => {
|
|
||||||
const rawBody = await c.req.raw.text();
|
|
||||||
const sig = c.req.header("stripe-signature");
|
|
||||||
|
|
||||||
let event: Stripe.Event;
|
|
||||||
|
|
||||||
try {
|
|
||||||
event = stripe.webhooks.constructEvent(
|
|
||||||
rawBody,
|
|
||||||
sig!,
|
|
||||||
process.env.STRIPE_WEBHOOK_SECRET!,
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Webhook signature verification failed:", err);
|
|
||||||
return c.json({ error: "Webhook signature verification failed" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowedEvents = ["invoice.paid"];
|
|
||||||
|
|
||||||
if (!allowedEvents.includes(event.type)) {
|
|
||||||
return c.json({ error: "Event not allowed" }, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (event.type) {
|
|
||||||
case "invoice.paid": {
|
|
||||||
const invoice = event.data.object as Stripe.Invoice;
|
|
||||||
|
|
||||||
if (!invoice.subscription) break;
|
|
||||||
|
|
||||||
if (invoice.billing_reason === "subscription_create") {
|
|
||||||
const customerResponse = await stripe.customers.retrieve(
|
|
||||||
invoice.customer as string,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (customerResponse.deleted) {
|
|
||||||
throw new Error("Customer was deleted");
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscriptionId = invoice.subscription as string;
|
|
||||||
const subscription =
|
|
||||||
await stripe.subscriptions.retrieve(subscriptionId);
|
|
||||||
const priceId = subscription.items.data[0].price.id;
|
|
||||||
const { type } = getLicenseTypeFromPriceId(priceId);
|
|
||||||
|
|
||||||
const { license, user } = await createLicense({
|
|
||||||
productId: subscriptionId,
|
|
||||||
email: customerResponse.email!.toLowerCase(),
|
|
||||||
stripeCustomerId: customerResponse.id,
|
|
||||||
stripeSubscriptionId: subscriptionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const features = getLicenseFeatures(type);
|
|
||||||
const emailHtml = await render(
|
|
||||||
LicenseEmail({
|
|
||||||
customerName: customerResponse.name || "Customer",
|
|
||||||
licenseKey: license.licenseKey,
|
|
||||||
productName: `Dokploy Self Hosted ${type}`,
|
|
||||||
features: features,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: process.env.SMTP_FROM_ADDRESS,
|
|
||||||
to: user.email,
|
|
||||||
subject: "Your Dokploy License Key ",
|
|
||||||
html: emailHtml,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json({ received: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing webhook:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return c.json({ error: error.message }, 500);
|
|
||||||
}
|
|
||||||
return c.json({ error: "Unknown error occurred" }, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
stripeRouter.post(
|
|
||||||
"/create-customer-portal-session",
|
|
||||||
zValidator("json", z.object({ customerId: z.string().min(1) })),
|
|
||||||
async (c) => {
|
|
||||||
try {
|
|
||||||
const { customerId } = c.req.valid("json");
|
|
||||||
|
|
||||||
const session = await stripe.billingPortal.sessions.create({
|
|
||||||
customer: customerId,
|
|
||||||
configuration: process.env.STRIPE_PORTAL_CONFIGURATION_ID,
|
|
||||||
return_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json({ url: session.url });
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error creating customer portal session:", error);
|
|
||||||
return c.json({ error: "Error creating customer portal session" }, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// Execute 1 time to create the configuration
|
|
||||||
// const configuration = await stripe.billingPortal.configurations.create({
|
|
||||||
// business_profile: {
|
|
||||||
// headline: "Manage your Dokploy subscription",
|
|
||||||
// },
|
|
||||||
// features: {
|
|
||||||
// subscription_update: {
|
|
||||||
// enabled: true,
|
|
||||||
// products: [
|
|
||||||
// {
|
|
||||||
// product: "prod_RxSOzNrwlfVWmv",
|
|
||||||
// prices: [
|
|
||||||
// "price_1R3XTqF3cxQuHeOzZjgaG262",
|
|
||||||
// "price_1R3XmmF3cxQuHeOzCEjz0HFz",
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// product: "prod_RxSVDolGpUy97g",
|
|
||||||
// prices: [
|
|
||||||
// "price_1R3XanF3cxQuHeOzh7VdbbUs",
|
|
||||||
// "price_1R3Xp4F3cxQuHeOzUTNYDC9I",
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// product: "prod_RxSaEQpbsiPFgv",
|
|
||||||
// prices: [
|
|
||||||
// "price_1R3XfOF3cxQuHeOzYFUa0eNy",
|
|
||||||
// "price_1R3XptF3cxQuHeOzZEBGMsEm",
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// default_allowed_updates: ["price", "quantity"],
|
|
||||||
// },
|
|
||||||
// subscription_cancel: {
|
|
||||||
// enabled: true,
|
|
||||||
// },
|
|
||||||
// payment_method_update: {
|
|
||||||
// enabled: true,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export const WEBSITE_URL =
|
|
||||||
process.env.NODE_ENV === "development"
|
|
||||||
? "http://localhost:3001"
|
|
||||||
: process.env.SITE_URL;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user