mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
25 Commits
1852-defau
...
feat/intro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73d9373c70 | ||
|
|
bbc2cbc78c | ||
|
|
e2e6513ee1 | ||
|
|
4de599745e | ||
|
|
60e40f4ad0 | ||
|
|
439d2fe116 | ||
|
|
09d0435971 | ||
|
|
5810c94f4b | ||
|
|
1acd330462 | ||
|
|
4074942dbf | ||
|
|
5fd8fcfa1e | ||
|
|
39e6a98179 | ||
|
|
5180c785b4 | ||
|
|
c8e6df4c29 | ||
|
|
9e30525569 | ||
|
|
b7874f053f | ||
|
|
fb0272a64d | ||
|
|
2f4a3c964c | ||
|
|
feb6970b09 | ||
|
|
78682fa359 | ||
|
|
473d729416 | ||
|
|
523720606d | ||
|
|
4b6db35f16 | ||
|
|
9d047164ee | ||
|
|
a61436b8f0 |
BIN
.github/sponsors/agentdock.png
vendored
BIN
.github/sponsors/agentdock.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
BIN
.github/sponsors/american-cloud.png
vendored
BIN
.github/sponsors/american-cloud.png
vendored
Binary file not shown.
|
Before 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 rsync && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && 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.64
|
ARG RAILPACK_VERSION=0.0.37
|
||||||
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, Schedules, 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, 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, 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.
|
- **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.
|
||||||
- **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.
|
- **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.
|
||||||
|
|
||||||
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,30 +80,12 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 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>
|
||||||
<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 🥉
|
||||||
@@ -115,7 +97,6 @@ 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,8 +16,6 @@ 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,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -201,22 +200,6 @@ 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,15 +15,11 @@ import { Paintbrush } from "lucide-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: "application" | "compose";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CancelQueues = ({ id, type }: Props) => {
|
export const CancelQueues = ({ applicationId }: Props) => {
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
||||||
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) {
|
||||||
@@ -52,8 +48,7 @@ export const CancelQueues = ({ id, type }: Props) => {
|
|||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId: id || "",
|
applicationId,
|
||||||
composeId: id || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Queues are being cleaned");
|
toast.success("Queues are being cleaned");
|
||||||
|
|||||||
@@ -14,14 +14,10 @@ import { RefreshCcw } from "lucide-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: "application" | "compose";
|
|
||||||
}
|
}
|
||||||
export const RefreshToken = ({ id, type }: Props) => {
|
export const RefreshToken = ({ applicationId }: Props) => {
|
||||||
const { mutateAsync } =
|
const { mutateAsync } = api.application.refreshToken.useMutation();
|
||||||
type === "application"
|
|
||||||
? api.application.refreshToken.useMutation()
|
|
||||||
: api.compose.refreshToken.useMutation();
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
@@ -41,19 +37,12 @@ export const RefreshToken = ({ id, type }: Props) => {
|
|||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId: id || "",
|
applicationId,
|
||||||
composeId: id || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (type === "application") {
|
utils.application.one.invalidate({
|
||||||
utils.application.one.invalidate({
|
applicationId,
|
||||||
applicationId: id,
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
utils.compose.one.invalidate({
|
|
||||||
composeId: id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
toast.success("Refresh updated");
|
toast.success("Refresh updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
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,53 +9,28 @@ 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, Clock, Loader2 } from "lucide-react";
|
import { RocketIcon } 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 {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type:
|
|
||||||
| "application"
|
|
||||||
| "compose"
|
|
||||||
| "schedule"
|
|
||||||
| "server"
|
|
||||||
| "backup"
|
|
||||||
| "previewDeployment";
|
|
||||||
refreshToken?: string;
|
|
||||||
serverId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDuration = (seconds: number) => {
|
export const ShowDeployments = ({ applicationId }: Props) => {
|
||||||
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: deployments, isLoading: isLoadingDeployments } =
|
const { data } = api.application.one.useQuery({ applicationId });
|
||||||
api.deployment.allByType.useQuery(
|
const { data: deployments } = api.deployment.all.useQuery(
|
||||||
{
|
{ applicationId },
|
||||||
id,
|
{
|
||||||
type,
|
enabled: !!applicationId,
|
||||||
},
|
refetchInterval: 1000,
|
||||||
{
|
},
|
||||||
enabled: !!id,
|
);
|
||||||
refetchInterval: 1000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,48 +38,34 @@ export const ShowDeployments = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background border-none">
|
<Card className="bg-background">
|
||||||
<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 {type}
|
See all the 10 last deployments for this application
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{(type === "application" || type === "compose") && (
|
<CancelQueues applicationId={applicationId} />
|
||||||
<CancelQueues id={id} type={type} />
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
{refreshToken && (
|
<div className="flex flex-col gap-2 text-sm">
|
||||||
<div className="flex flex-col gap-2 text-sm">
|
<span>
|
||||||
<span>
|
If you want to re-deploy this application use this URL in the config
|
||||||
If you want to re-deploy this application use this URL in the
|
of your git provider or docker
|
||||||
config of your git provider or docker
|
</span>
|
||||||
</span>
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
<span>Webhook URL: </span>
|
||||||
<span>Webhook URL: </span>
|
<div className="flex flex-row items-center gap-2">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<span className="break-all text-muted-foreground">
|
||||||
<span className="break-all text-muted-foreground">
|
{`${url}/api/deploy/${data?.refreshToken}`}
|
||||||
{`${url}/api/deploy/${refreshToken}`}
|
</span>
|
||||||
</span>
|
<RefreshToken applicationId={applicationId} />
|
||||||
{(type === "application" || type === "compose") && (
|
|
||||||
<RefreshToken id={id} type={type} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
{data?.deployments?.length === 0 ? (
|
||||||
{isLoadingDeployments ? (
|
<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 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
|
||||||
@@ -135,23 +96,8 @@ export const ShowDeployments = ({
|
|||||||
)}
|
)}
|
||||||
</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 flex items-center gap-2">
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
<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
|
||||||
@@ -167,7 +113,7 @@ export const ShowDeployments = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
serverId={serverId}
|
serverId={data?.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,67 +38,26 @@ 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 { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
import { Dices } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import z from "zod";
|
import type 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 {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: "application" | "compose";
|
|
||||||
domainId?: string;
|
domainId?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
export const AddDomain = ({
|
||||||
|
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(
|
||||||
{
|
{
|
||||||
@@ -109,24 +68,14 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: application } =
|
const { data: application } = api.application.one.useQuery(
|
||||||
type === "application"
|
{
|
||||||
? api.application.one.useQuery(
|
applicationId,
|
||||||
{
|
},
|
||||||
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()
|
||||||
@@ -140,23 +89,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
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: {
|
||||||
@@ -166,15 +98,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
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) {
|
||||||
@@ -185,8 +114,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +125,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
customCertResolver: undefined,
|
customCertResolver: undefined,
|
||||||
domainType: type,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, data, isLoading, domainId]);
|
}, [form, data, isLoading, domainId]);
|
||||||
@@ -222,37 +148,22 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
const onSubmit = async (data: Domain) => {
|
const onSubmit = async (data: Domain) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
domainId,
|
domainId,
|
||||||
...(data.domainType === "application" && {
|
applicationId,
|
||||||
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({
|
||||||
if (data.domainType === "application") {
|
applicationId,
|
||||||
await utils.domain.byApplicationId.invalidate({
|
});
|
||||||
applicationId: id,
|
await utils.application.readTraefikConfig.invalidate({ applicationId });
|
||||||
});
|
|
||||||
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((e) => {
|
.catch(() => {
|
||||||
console.log(e);
|
|
||||||
toast.error(dictionary.error);
|
toast.error(dictionary.error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -276,119 +187,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
>
|
>
|
||||||
<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,13 +16,12 @@ 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 "./handle-domain";
|
import { AddDomain } from "./add-domain";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -30,70 +29,36 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { DnsHelperModal } from "./dns-helper-modal";
|
import type { ValidationStates } from "../../compose/domains/show-domains";
|
||||||
|
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 {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: "application" | "compose";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ id, type }: Props) => {
|
export const ShowDomains = ({ applicationId }: Props) => {
|
||||||
const { data: application } =
|
const { data: application } = api.application.one.useQuery(
|
||||||
type === "application"
|
{
|
||||||
? api.application.one.useQuery(
|
applicationId,
|
||||||
{
|
},
|
||||||
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 {
|
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
||||||
data,
|
{
|
||||||
refetch,
|
applicationId,
|
||||||
isLoading: isLoadingDomains,
|
},
|
||||||
} = type === "application"
|
{
|
||||||
? api.domain.byApplicationId.useQuery(
|
enabled: !!applicationId,
|
||||||
{
|
},
|
||||||
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 } =
|
||||||
@@ -148,7 +113,7 @@ export const ShowDomains = ({ id, type }: 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 id={id} type={type}>
|
<AddDomain applicationId={applicationId}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
@@ -157,22 +122,15 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
{isLoadingDomains ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex w-full flex-row gap-4 min-h-[40vh] justify-center items-center">
|
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||||
<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 id={id} type={type}>
|
<AddDomain applicationId={applicationId}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
@@ -180,26 +138,19 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full">
|
||||||
{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 h-fit"
|
className="relative overflow-hidden w-full border bg-card transition-all hover:shadow-md bg-transparent"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
@@ -224,8 +175,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddDomain
|
<AddDomain
|
||||||
id={id}
|
applicationId={applicationId}
|
||||||
type={type}
|
|
||||||
domainId={item.domainId}
|
domainId={item.domainId}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -381,9 +381,6 @@ 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, Loader2, UploadCloud } from "lucide-react";
|
import { GitBranch, 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,49 +34,14 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowProviderForm = ({ applicationId }: Props) => {
|
export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||||
const { data: githubProviders, isLoading: isLoadingGithub } =
|
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||||
api.github.githubProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: gitlabProviders, isLoading: isLoadingGitlab } =
|
const { data: bitbucketProviders } =
|
||||||
api.gitlab.gitlabProviders.useQuery();
|
|
||||||
const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
|
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
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>
|
||||||
@@ -158,7 +123,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-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] 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
|
||||||
@@ -178,7 +143,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-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] 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
|
||||||
@@ -198,7 +163,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-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] 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
|
||||||
@@ -218,7 +183,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-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] 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
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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,
|
||||||
Loader2,
|
Layers,
|
||||||
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,16 +38,13 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
api.previewDeployment.delete.useMutation();
|
api.previewDeployment.delete.useMutation();
|
||||||
|
|
||||||
const {
|
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
||||||
data: previewDeployments,
|
api.previewDeployment.all.useQuery(
|
||||||
refetch: refetchPreviewDeployments,
|
{ applicationId },
|
||||||
isLoading: isLoadingPreviewDeployments,
|
{
|
||||||
} = api.previewDeployment.all.useQuery(
|
enabled: !!applicationId,
|
||||||
{ applicationId },
|
},
|
||||||
{
|
);
|
||||||
enabled: !!applicationId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
||||||
deletePreviewDeployment({
|
deletePreviewDeployment({
|
||||||
@@ -83,15 +80,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
each pull request you create.
|
each pull request you create.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isLoadingPreviewDeployments ? (
|
{!previewDeployments?.length ? (
|
||||||
<div className="flex w-full flex-row items-center justify-center gap-3 min-h-[35vh]">
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<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
|
||||||
@@ -178,10 +168,19 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ShowModalLogs>
|
</ShowModalLogs>
|
||||||
|
|
||||||
<ShowDeploymentsModal
|
<ShowPreviewBuilds
|
||||||
id={deployment.previewDeploymentId}
|
deployments={deployment.deployments || []}
|
||||||
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
|
||||||
|
|||||||
@@ -1,538 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
508
apps/dokploy/components/dashboard/compose/domains/add-domain.tsx
Normal file
508
apps/dokploy/components/dashboard/compose/domains/add-domain.tsx
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
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, Loader2 } from "lucide-react";
|
import { CodeIcon, GitBranch } 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,49 +25,15 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||||
const { data: githubProviders, isLoading: isLoadingGithub } =
|
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||||
api.github.githubProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: gitlabProviders, isLoading: isLoadingGitlab } =
|
const { data: bitbucketProviders } =
|
||||||
api.gitlab.gitlabProviders.useQuery();
|
|
||||||
const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
|
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
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>
|
||||||
@@ -142,7 +108,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-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] 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
|
||||||
@@ -162,7 +128,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-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] 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
|
||||||
@@ -182,7 +148,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-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] 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
|
||||||
@@ -202,7 +168,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-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] 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,19 +49,12 @@ 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 {
|
import { DatabaseZap, PenBoxIcon, PlusIcon, RefreshCw } from "lucide-react";
|
||||||
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";
|
||||||
|
|
||||||
@@ -115,15 +108,6 @@ 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) {
|
||||||
@@ -584,55 +568,10 @@ export const HandleBackup = ({
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex items-center gap-2">
|
<FormLabel>Schedule (Cron)</FormLabel>
|
||||||
Schedule
|
<FormControl>
|
||||||
<TooltipProvider>
|
<Input placeholder={"0 0 * * *"} {...field} />
|
||||||
<Tooltip>
|
</FormControl>
|
||||||
<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,6 +103,7 @@ 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
|
||||||
@@ -127,7 +128,6 @@ const RestoreBackupSchema = z
|
|||||||
databaseRootPassword: z.string(),
|
databaseRootPassword: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
serviceName: z.string().optional(),
|
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
@@ -139,15 +139,6 @@ 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) {
|
||||||
@@ -226,7 +217,6 @@ 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),
|
||||||
@@ -530,11 +520,7 @@ export const RestoreBackup = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Database Name</FormLabel>
|
<FormLabel>Database Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="Enter database name" {...field} />
|
||||||
placeholder="Enter database name"
|
|
||||||
{...field}
|
|
||||||
disabled={databaseType === "web-server"}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -573,7 +559,7 @@ export const RestoreBackup = ({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="metadata.serviceName"
|
name="serviceName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormLabel>Service Name</FormLabel>
|
<FormLabel>Service Name</FormLabel>
|
||||||
@@ -791,10 +777,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,13 +14,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||||
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";
|
||||||
@@ -35,7 +29,6 @@ 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;
|
||||||
@@ -131,7 +124,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 min-h-[35vh] justify-center">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<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.
|
||||||
@@ -163,7 +156,6 @@ 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
|
||||||
}
|
}
|
||||||
@@ -178,198 +170,175 @@ 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) => (
|
||||||
const serverId =
|
<div key={backup.backupId}>
|
||||||
"serverId" in postgres ? postgres.serverId : undefined;
|
<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">
|
||||||
|
<div className="flex flex-col w-full gap-4">
|
||||||
return (
|
<div className="flex items-center gap-3">
|
||||||
<div key={backup.backupId}>
|
{backup.backupType === "compose" && (
|
||||||
<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">
|
<div className="flex items-center justify-center size-10 rounded-lg">
|
||||||
<div className="flex flex-col w-full gap-4">
|
{backup.databaseType === "postgres" && (
|
||||||
<div className="flex items-center gap-3">
|
<PostgresqlIcon className="size-7" />
|
||||||
{backup.backupType === "compose" && (
|
|
||||||
<div className="flex items-center justify-center size-10 rounded-lg">
|
|
||||||
{backup.databaseType === "postgres" && (
|
|
||||||
<PostgresqlIcon className="size-7" />
|
|
||||||
)}
|
|
||||||
{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" && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-medium">
|
|
||||||
{backup.serviceName}
|
|
||||||
</h3>
|
|
||||||
<span className="px-1.5 py-0.5 rounded-full bg-muted text-xs font-medium capitalize">
|
|
||||||
{backup.databaseType}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
{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" && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<h3 className="font-medium">
|
||||||
className={cn(
|
{backup.serviceName}
|
||||||
"size-1.5 rounded-full",
|
</h3>
|
||||||
backup.enabled
|
<span className="px-1.5 py-0.5 rounded-full bg-muted text-xs font-medium capitalize">
|
||||||
? "bg-green-500"
|
{backup.databaseType}
|
||||||
: "bg-red-500",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{backup.enabled ? "Active" : "Inactive"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
className={cn(
|
||||||
<div className="min-w-[200px]">
|
"size-1.5 rounded-full",
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
backup.enabled
|
||||||
Destination
|
? "bg-green-500"
|
||||||
|
: "bg-red-500",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{backup.enabled ? "Active" : "Inactive"}
|
||||||
</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-row md:flex-col gap-1.5">
|
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||||
<ShowDeploymentsModal
|
<div className="min-w-[200px]">
|
||||||
id={backup.backupId}
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
type="backup"
|
Destination
|
||||||
serverId={serverId || undefined}
|
</span>
|
||||||
>
|
<p className="font-medium text-sm mt-0.5">
|
||||||
<Button
|
{backup.destination.name}
|
||||||
variant="ghost"
|
</p>
|
||||||
size="icon"
|
</div>
|
||||||
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>
|
|
||||||
|
|
||||||
<HandleBackup
|
<div className="min-w-[150px]">
|
||||||
backupType={backup.backupType}
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
backupId={backup.backupId}
|
Database
|
||||||
databaseType={backup.databaseType}
|
</span>
|
||||||
refetch={refetch}
|
<p className="font-medium text-sm mt-0.5">
|
||||||
/>
|
{backup.database}
|
||||||
<DialogAction
|
</p>
|
||||||
title="Delete Backup"
|
</div>
|
||||||
description="Are you sure you want to delete this backup?"
|
|
||||||
type="destructive"
|
<div className="min-w-[120px]">
|
||||||
onClick={async () => {
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
await deleteBackup({
|
Schedule
|
||||||
backupId: backup.backupId,
|
</span>
|
||||||
})
|
<p className="font-medium text-sm mt-0.5">
|
||||||
.then(() => {
|
{backup.schedule}
|
||||||
refetch();
|
</p>
|
||||||
toast.success(
|
</div>
|
||||||
"Backup deleted successfully",
|
|
||||||
);
|
<div className="min-w-[150px]">
|
||||||
})
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
.catch(() => {
|
Prefix Storage
|
||||||
toast.error("Error deleting backup");
|
</span>
|
||||||
});
|
<p className="font-medium text-sm mt-0.5">
|
||||||
}}
|
{backup.prefix}
|
||||||
>
|
</p>
|
||||||
<Button
|
</div>
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
<div className="min-w-[100px]">
|
||||||
className="group hover:bg-red-500/10 size-8"
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
isLoading={isRemoving}
|
Keep Latest
|
||||||
>
|
</span>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<p className="font-medium text-sm mt-0.5">
|
||||||
</Button>
|
{backup.keepLatestCount || "All"}
|
||||||
</DialogAction>
|
</p>
|
||||||
|
</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,14 +140,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
if (!isCurrentConnection) return;
|
if (!isCurrentConnection) return;
|
||||||
setRawLogs((prev) => {
|
setRawLogs((prev) => prev + e.data);
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,454 +0,0 @@
|
|||||||
"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,8 +145,10 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
const serviceName = slugify(val);
|
form.setValue(
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
"appName",
|
||||||
|
`${slug}-${val.toLowerCase().replaceAll(" ", "-")}`,
|
||||||
|
);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -152,8 +152,10 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
const serviceName = slugify(val);
|
form.setValue(
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
"appName",
|
||||||
|
`${slug}-${val.toLowerCase()}`,
|
||||||
|
);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -363,8 +363,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
const serviceName = slugify(val);
|
form.setValue(
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
"appName",
|
||||||
|
`${slug}-${val.toLowerCase()}`,
|
||||||
|
);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -33,23 +33,12 @@ import { z } from "zod";
|
|||||||
const AddProjectSchema = z.object({
|
const AddProjectSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Project name is required")
|
.min(1, {
|
||||||
.refine(
|
message: "Name is required",
|
||||||
(name) => {
|
|
||||||
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",
|
|
||||||
})
|
})
|
||||||
.transform((name) => name.trim()),
|
.regex(/^[a-zA-Z]/, {
|
||||||
|
message: "Project name cannot start with a number",
|
||||||
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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,7 +10,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -29,14 +28,12 @@ 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>;
|
||||||
@@ -59,7 +56,6 @@ 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,
|
||||||
@@ -83,7 +79,6 @@ 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),
|
||||||
});
|
});
|
||||||
@@ -96,13 +91,11 @@ 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) => {
|
||||||
@@ -118,7 +111,6 @@ 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();
|
||||||
@@ -264,34 +256,7 @@ 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.admin.setupMonitoring.useMutation();
|
: api.user.setupMonitoring.useMutation();
|
||||||
|
|
||||||
const generateToken = () => {
|
const generateToken = () => {
|
||||||
const array = new Uint8Array(64);
|
const array = new Uint8Array(64);
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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,7 +43,6 @@ 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");
|
||||||
@@ -333,10 +332,6 @@ export const ShowServers = () => {
|
|||||||
<ShowNodesModal
|
<ShowNodesModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ShowSchedulesModal
|
|
||||||
serverId={server.serverId}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -185,21 +185,24 @@ 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,19 +1,9 @@
|
|||||||
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) => {
|
||||||
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
return <Page>{children}</Page>;
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Page>{children}</Page>
|
|
||||||
{haveRootAccess === true && <ImpersonationBar />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Clock,
|
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -159,14 +158,6 @@ 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",
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
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;
|
|
||||||
5
apps/dokploy/drizzle/0088_same_ezekiel.sql
Normal file
5
apps/dokploy/drizzle/0088_same_ezekiel.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
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
apps/dokploy/drizzle/0089_dazzling_marrow.sql
Normal file
1
apps/dokploy/drizzle/0089_dazzling_marrow.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "backup" ADD COLUMN "metadata" jsonb;
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
ALTER TABLE "user_temp" ADD COLUMN "allowImpersonation" boolean DEFAULT false NOT NULL;
|
|
||||||
1
apps/dokploy/drizzle/0090_tiny_phil_sheldon.sql
Normal file
1
apps/dokploy/drizzle/0090_tiny_phil_sheldon.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "licenseKey" text;
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "user_temp" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "c7eae4ce-5acc-439b-962f-bb2ef8922187",
|
"id": "7fea81ef-e2a7-4a8b-b755-e98903a08b57",
|
||||||
"prevId": "7fb3716c-3cc6-4b18-b8d1-762844da26be",
|
"prevId": "7fb3716c-3cc6-4b18-b8d1-762844da26be",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -1770,6 +1770,12 @@
|
|||||||
"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",
|
||||||
@@ -1782,6 +1788,14 @@
|
|||||||
"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",
|
||||||
@@ -1789,6 +1803,12 @@
|
|||||||
"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",
|
||||||
@@ -1835,6 +1855,19 @@
|
|||||||
"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",
|
||||||
@@ -2068,29 +2101,11 @@
|
|||||||
"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": {},
|
||||||
@@ -2146,19 +2161,6 @@
|
|||||||
],
|
],
|
||||||
"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": {},
|
||||||
@@ -5253,167 +5255,6 @@
|
|||||||
"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": {
|
||||||
@@ -5451,6 +5292,14 @@
|
|||||||
"preview"
|
"preview"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"public.backupType": {
|
||||||
|
"name": "backupType",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"database",
|
||||||
|
"compose"
|
||||||
|
]
|
||||||
|
},
|
||||||
"public.databaseType": {
|
"public.databaseType": {
|
||||||
"name": "databaseType",
|
"name": "databaseType",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
@@ -5584,24 +5433,6 @@
|
|||||||
"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": "3ec09926-2da4-41c9-8eae-8ef6b023035e",
|
"id": "83185289-c593-420c-b8da-a90143aebf8b",
|
||||||
"prevId": "c7eae4ce-5acc-439b-962f-bb2ef8922187",
|
"prevId": "7fea81ef-e2a7-4a8b-b755-e98903a08b57",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -1746,12 +1746,6 @@
|
|||||||
"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,15 +1941,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {},
|
||||||
"backup_appName_unique": {
|
|
||||||
"name": "backup_appName_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"appName"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
@@ -2121,35 +2107,11 @@
|
|||||||
"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": {},
|
||||||
@@ -2205,32 +2167,6 @@
|
|||||||
],
|
],
|
||||||
"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": {},
|
||||||
@@ -5325,167 +5261,6 @@
|
|||||||
"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": {
|
||||||
@@ -5664,24 +5439,6 @@
|
|||||||
"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": "f8b568f0-83c1-4cfb-95c9-ab6b5b5bce18",
|
"id": "d5010e21-a520-4d05-aef3-c549b55456e5",
|
||||||
"prevId": "3ec09926-2da4-41c9-8eae-8ef6b023035e",
|
"prevId": "83185289-c593-420c-b8da-a90143aebf8b",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -959,12 +959,11 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"allowImpersonation": {
|
"licenseKey": {
|
||||||
"name": "allowImpersonation",
|
"name": "licenseKey",
|
||||||
"type": "boolean",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": false
|
||||||
"default": false
|
|
||||||
},
|
},
|
||||||
"metricsConfig": {
|
"metricsConfig": {
|
||||||
"name": "metricsConfig",
|
"name": "metricsConfig",
|
||||||
@@ -1753,12 +1752,6 @@
|
|||||||
"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",
|
||||||
@@ -1954,15 +1947,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {},
|
||||||
"backup_appName_unique": {
|
|
||||||
"name": "backup_appName_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"appName"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
@@ -2128,35 +2113,11 @@
|
|||||||
"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": {},
|
||||||
@@ -2212,32 +2173,6 @@
|
|||||||
],
|
],
|
||||||
"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": {},
|
||||||
@@ -5332,167 +5267,6 @@
|
|||||||
"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": {
|
||||||
@@ -5671,24 +5445,6 @@
|
|||||||
"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": {},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -621,29 +621,22 @@
|
|||||||
{
|
{
|
||||||
"idx": 88,
|
"idx": 88,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1746256928101,
|
"when": 1745801614194,
|
||||||
"tag": "0088_illegal_ma_gnuci",
|
"tag": "0088_same_ezekiel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 89,
|
"idx": 89,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1746392564463,
|
"when": 1745812150155,
|
||||||
"tag": "0089_noisy_sandman",
|
"tag": "0089_dazzling_marrow",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 90,
|
"idx": 90,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1746509318678,
|
"when": 1746156076450,
|
||||||
"tag": "0090_clean_wolf_cub",
|
"tag": "0090_tiny_phil_sheldon",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 91,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1746518402168,
|
|
||||||
"tag": "0091_spotty_kulan_gath",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
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: [
|
plugins: [organizationClient(), twoFactorClient(), apiKeyClient()],
|
||||||
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, "") || "service";
|
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "");
|
||||||
|
|
||||||
return slug(cleanedText, {
|
return slug(cleanedText, {
|
||||||
lower: true,
|
lower: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.22.3",
|
"version": "v0.21.8",
|
||||||
"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": "v1.2.8-beta.7",
|
"better-auth": "1.2.6",
|
||||||
"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,7 +1,10 @@
|
|||||||
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";
|
||||||
@@ -23,7 +26,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"
|
||||||
@@ -34,7 +37,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">
|
||||||
@@ -44,15 +47,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,7 +80,6 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
ServerIcon,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -969,11 +968,6 @@ 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,7 +12,6 @@ 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";
|
||||||
@@ -29,6 +28,7 @@ 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(false);
|
const [_toggleMonitoring, _setToggleMonitoring] = useState(true);
|
||||||
const { applicationId, activeTab } = props;
|
const { applicationId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
@@ -87,6 +87,7 @@ 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">
|
||||||
@@ -233,7 +234,6 @@ 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,21 +267,19 @@ const Service = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* {monitoring?.enabledFeatures &&
|
{monitoring?.enabledFeatures && (
|
||||||
isCloud &&
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
data?.serverId && (
|
<Label className="text-muted-foreground">
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
Change Monitoring
|
||||||
<Label className="text-muted-foreground">
|
</Label>
|
||||||
Change Monitoring
|
<Switch
|
||||||
</Label>
|
checked={_toggleMonitoring}
|
||||||
<Switch
|
onCheckedChange={_setToggleMonitoring}
|
||||||
checked={toggleMonitoring}
|
/>
|
||||||
onCheckedChange={setToggleMonitoring}
|
</div>
|
||||||
/>
|
)}
|
||||||
</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}`}
|
||||||
@@ -289,13 +287,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>
|
||||||
@@ -310,22 +308,9 @@ const Service = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="schedules">
|
<TabsContent value="deployments" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowSchedules
|
<ShowDeployments applicationId={applicationId} />
|
||||||
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">
|
||||||
@@ -335,7 +320,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 id={applicationId} type="application" />
|
<ShowDomains applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
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";
|
||||||
@@ -219,10 +218,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-9"
|
? "lg:grid-cols-8"
|
||||||
: data?.serverId
|
: data?.serverId
|
||||||
? "lg:grid-cols-8"
|
? "lg:grid-cols-7"
|
||||||
: "lg:grid-cols-9",
|
: "lg:grid-cols-8",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
@@ -230,7 +229,6 @@ 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>
|
||||||
@@ -255,12 +253,6 @@ 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 ">
|
||||||
@@ -331,20 +323,15 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
<TabsContent value="deployments">
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDeployments
|
<ShowDeploymentsCompose composeId={composeId} />
|
||||||
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">
|
||||||
<ShowDomains id={composeId} type="compose" />
|
<ShowDomainsCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
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,5 +1,6 @@
|
|||||||
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";
|
||||||
@@ -12,6 +13,7 @@ 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">
|
||||||
@@ -27,6 +29,7 @@ const Page = () => {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
{!isCloud && <EnablePaidFeatures />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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";
|
||||||
@@ -35,7 +34,6 @@ 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.
|
||||||
*
|
*
|
||||||
@@ -43,7 +41,6 @@ import { scheduleRouter } from "./routers/schedule";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
admin: adminRouter,
|
|
||||||
docker: dockerRouter,
|
docker: dockerRouter,
|
||||||
project: projectRouter,
|
project: projectRouter,
|
||||||
application: applicationRouter,
|
application: applicationRouter,
|
||||||
@@ -79,7 +76,6 @@ 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
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.applicationId,
|
input.applicationId,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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";
|
||||||
@@ -367,7 +366,23 @@ export const backupRouter = createTRPCRouter({
|
|||||||
override: true,
|
override: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(apiRestoreBackup)
|
.input(
|
||||||
|
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") {
|
||||||
@@ -375,33 +390,57 @@ 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(postgres, destination, input, (log) => {
|
restorePostgresBackup(
|
||||||
emit.next(log);
|
postgres,
|
||||||
});
|
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(mysql, destination, input, (log) => {
|
restoreMySqlBackup(
|
||||||
emit.next(log);
|
mysql,
|
||||||
});
|
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(mariadb, destination, input, (log) => {
|
restoreMariadbBackup(
|
||||||
emit.next(log);
|
mariadb,
|
||||||
});
|
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(mongo, destination, input, (log) => {
|
restoreMongoBackup(
|
||||||
emit.next(log);
|
mongo,
|
||||||
});
|
destination,
|
||||||
|
input.databaseName,
|
||||||
|
input.backupFile,
|
||||||
|
(log) => {
|
||||||
|
emit.next(log);
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (input.databaseType === "web-server") {
|
if (input.databaseType === "web-server") {
|
||||||
@@ -415,9 +454,16 @@ 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(compose, destination, input, (log) => {
|
restoreComposeBackup(
|
||||||
emit.next(log);
|
compose,
|
||||||
});
|
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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "member") {
|
||||||
await addNewService(
|
await addNewService(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
compose.composeId,
|
compose.composeId,
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import {
|
|||||||
apiFindAllByApplication,
|
apiFindAllByApplication,
|
||||||
apiFindAllByCompose,
|
apiFindAllByCompose,
|
||||||
apiFindAllByServer,
|
apiFindAllByServer,
|
||||||
apiFindAllByType,
|
|
||||||
deployments,
|
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
findAllDeploymentsByApplicationId,
|
findAllDeploymentsByApplicationId,
|
||||||
@@ -14,9 +12,7 @@ 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
|
||||||
@@ -58,14 +54,4 @@ 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,10 +57,7 @@ export const domainRouter = createTRPCRouter({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message:
|
message: "Error creating the domain",
|
||||||
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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role !== "owner" && !IS_CLOUD) {
|
if (ctx.user.rol !== "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.role !== "owner" && !IS_CLOUD) {
|
if (ctx.user.rol !== "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.role !== "owner" && !IS_CLOUD) {
|
if (ctx.user.rol !== "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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,7 +164,8 @@ export const projectRouter = createTRPCRouter({
|
|||||||
return project;
|
return project;
|
||||||
}),
|
}),
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
if (ctx.user.role === "member") {
|
// console.log(ctx.user);
|
||||||
|
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,
|
||||||
@@ -174,7 +175,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return await db.query.projects.findMany({
|
const query = 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}`),
|
||||||
@@ -212,6 +213,8 @@ 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({
|
||||||
@@ -241,7 +244,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
.input(apiRemoveProject)
|
.input(apiRemoveProject)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkProjectAccess(
|
await checkProjectAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
"delete",
|
"delete",
|
||||||
@@ -313,7 +316,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkProjectAccess(
|
await checkProjectAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
"create",
|
"create",
|
||||||
@@ -578,7 +581,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
if (ctx.user.rol === "member") {
|
||||||
await addNewProject(
|
await addNewProject(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
newProject.projectId,
|
newProject.projectId,
|
||||||
@@ -601,10 +604,10 @@ function buildServiceFilter(
|
|||||||
fieldName: AnyPgColumn,
|
fieldName: AnyPgColumn,
|
||||||
accessedServices: string[],
|
accessedServices: string[],
|
||||||
) {
|
) {
|
||||||
return accessedServices.length === 0
|
return accessedServices.length > 0
|
||||||
? sql`false`
|
? sql`${fieldName} IN (${sql.join(
|
||||||
: 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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "member") {
|
||||||
await checkServiceAccess(
|
await checkServiceAccess(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
input.redisId,
|
input.redisId,
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "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.role === "member") {
|
if (ctx.user.rol === "member") {
|
||||||
const canAccess = await canAccessToTraefikFiles(
|
const canAccess = await canAccessToTraefikFiles(
|
||||||
ctx.user.id,
|
ctx.user.id,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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";
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
apiAssignPermissions,
|
apiAssignPermissions,
|
||||||
apiFindOneToken,
|
apiFindOneToken,
|
||||||
apiUpdateUser,
|
apiUpdateUser,
|
||||||
|
apiUpdateWebServerMonitoring,
|
||||||
apikey,
|
apikey,
|
||||||
invitation,
|
invitation,
|
||||||
member,
|
member,
|
||||||
@@ -27,7 +29,11 @@ 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(),
|
||||||
@@ -91,18 +97,6 @@ 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(
|
||||||
@@ -115,7 +109,6 @@ export const userRouter = createTRPCRouter({
|
|||||||
backups: {
|
backups: {
|
||||||
with: {
|
with: {
|
||||||
destination: true,
|
destination: true,
|
||||||
deployments: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
apiKeys: true,
|
apiKeys: true,
|
||||||
@@ -173,6 +166,73 @@ 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 }) => {
|
||||||
@@ -362,4 +422,62 @@ 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,10 +30,8 @@ import { ZodError } from "zod";
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
interface CreateContextOptions {
|
interface CreateContextOptions {
|
||||||
user: (User & { role: "member" | "admin" | "owner"; ownerId: string }) | null;
|
user: (User & { rol: "member" | "admin" | "owner"; ownerId: string }) | null;
|
||||||
session:
|
session: (Session & { activeOrganizationId: string }) | null;
|
||||||
| (Session & { activeOrganizationId: string; impersonatedBy?: string })
|
|
||||||
| null;
|
|
||||||
req: CreateNextContextOptions["req"];
|
req: CreateNextContextOptions["req"];
|
||||||
res: CreateNextContextOptions["res"];
|
res: CreateNextContextOptions["res"];
|
||||||
}
|
}
|
||||||
@@ -85,7 +83,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
|||||||
? {
|
? {
|
||||||
...user,
|
...user,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role as "owner" | "member" | "admin",
|
rol: user.role as "owner" | "member" | "admin",
|
||||||
id: user.id,
|
id: user.id,
|
||||||
ownerId: user.ownerId,
|
ownerId: user.ownerId,
|
||||||
}
|
}
|
||||||
@@ -182,7 +180,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.role !== "owner") {
|
if (!ctx.session || !ctx.user || ctx.user.rol !== "owner") {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
return next({
|
return next({
|
||||||
@@ -196,7 +194,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.role !== "owner") {
|
if (!ctx.session || !ctx.user || ctx.user.rol !== "owner") {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
return next({
|
return next({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
createDefaultTraefikConfig,
|
createDefaultTraefikConfig,
|
||||||
initCronJobs,
|
initCronJobs,
|
||||||
initSchedules,
|
|
||||||
initializeNetwork,
|
initializeNetwork,
|
||||||
sendDokployRestartNotifications,
|
sendDokployRestartNotifications,
|
||||||
setupDirectories,
|
setupDirectories,
|
||||||
@@ -50,7 +49,6 @@ void app.prepare().then(async () => {
|
|||||||
createDefaultServerTraefikConfig();
|
createDefaultServerTraefikConfig();
|
||||||
await migration();
|
await migration();
|
||||||
await initCronJobs();
|
await initCronJobs();
|
||||||
await initSchedules();
|
|
||||||
await sendDokployRestartNotifications();
|
await sendDokployRestartNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ 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 {
|
||||||
|
|||||||
47
apps/dokploy/server/utils/validate-license.ts
Normal file
47
apps/dokploy/server/utils/validate-license.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
2
apps/licenses/.env.example
Normal file
2
apps/licenses/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
LEMON_SQUEEZY_API_KEY=""
|
||||||
|
LEMON_SQUEEZY_STORE_ID=""
|
||||||
28
apps/licenses/.gitignore
vendored
Normal file
28
apps/licenses/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 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
|
||||||
8
apps/licenses/README.md
Normal file
8
apps/licenses/README.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
open http://localhost:4002/api/health
|
||||||
|
```
|
||||||
15
apps/licenses/drizzle.config.ts
Normal file
15
apps/licenses/drizzle.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
28
apps/licenses/drizzle/0000_famous_vermin.sql
Normal file
28
apps/licenses/drizzle/0000_famous_vermin.sql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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;
|
||||||
196
apps/licenses/drizzle/meta/0000_snapshot.json
Normal file
196
apps/licenses/drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/licenses/drizzle/meta/_journal.json
Normal file
13
apps/licenses/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1742773711509,
|
||||||
|
"tag": "0000_famous_vermin",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
22
apps/licenses/migrate.ts
Normal file
22
apps/licenses/migrate.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
48
apps/licenses/package.json
Normal file
48
apps/licenses/package.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
265
apps/licenses/src/api/license.ts
Normal file
265
apps/licenses/src/api/license.ts
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
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 });
|
||||||
|
},
|
||||||
|
);
|
||||||
220
apps/licenses/src/api/stripe.ts
Normal file
220
apps/licenses/src/api/stripe.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
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,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
4
apps/licenses/src/constants.ts
Normal file
4
apps/licenses/src/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
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