mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into feat/introduce-monitoring-self-hosted-pay
This commit is contained in:
@@ -40,7 +40,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AddRedirectchema = z.object({
|
const AddRedirectchema = z.object({
|
||||||
replicas: z.number(),
|
replicas: z.number().min(1, "Replicas must be at least 1"),
|
||||||
registryId: z.string(),
|
registryId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,9 +130,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
placeholder="1"
|
placeholder="1"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
field.onChange(Number(e.target.value));
|
const value = e.target.value;
|
||||||
|
field.onChange(value === "" ? 0 : Number(value));
|
||||||
}}
|
}}
|
||||||
type="number"
|
type="number"
|
||||||
|
value={field.value || ""}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { domain } from "@/server/db/validations/domain";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Dices } from "lucide-react";
|
import { Dices } from "lucide-react";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
|
|
||||||
@@ -83,6 +84,13 @@ export const AddDomain = ({
|
|||||||
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||||
api.domain.generateDomain.useMutation();
|
api.domain.generateDomain.useMutation();
|
||||||
|
|
||||||
|
const { data: canGenerateTraefikMeDomains } =
|
||||||
|
api.domain.canGenerateTraefikMeDomains.useQuery({
|
||||||
|
serverId: application?.serverId || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("canGenerateTraefikMeDomains", canGenerateTraefikMeDomains);
|
||||||
|
|
||||||
const form = useForm<Domain>({
|
const form = useForm<Domain>({
|
||||||
resolver: zodResolver(domain),
|
resolver: zodResolver(domain),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -186,6 +194,21 @@ export const AddDomain = ({
|
|||||||
name="host"
|
name="host"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<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"
|
||||||
|
>
|
||||||
|
{application?.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>
|
<FormLabel>Host</FormLabel>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { domainCompose } 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 { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
type Domain = z.infer<typeof domainCompose>;
|
type Domain = z.infer<typeof domainCompose>;
|
||||||
|
|
||||||
@@ -102,6 +103,11 @@ export const AddDomainCompose = ({
|
|||||||
? api.domain.update.useMutation()
|
? api.domain.update.useMutation()
|
||||||
: api.domain.create.useMutation();
|
: api.domain.create.useMutation();
|
||||||
|
|
||||||
|
const { data: canGenerateTraefikMeDomains } =
|
||||||
|
api.domain.canGenerateTraefikMeDomains.useQuery({
|
||||||
|
serverId: compose?.serverId || "",
|
||||||
|
});
|
||||||
|
|
||||||
const form = useForm<Domain>({
|
const form = useForm<Domain>({
|
||||||
resolver: zodResolver(domainCompose),
|
resolver: zodResolver(domainCompose),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -313,6 +319,21 @@ export const AddDomainCompose = ({
|
|||||||
name="host"
|
name="host"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<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>
|
<FormLabel>Host</FormLabel>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { toast } from "sonner";
|
|||||||
interface Props {
|
interface Props {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
databaseType: Exclude<ServiceType, "application" | "redis">;
|
databaseType: Exclude<ServiceType, "application" | "redis">;
|
||||||
|
serverId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RestoreBackupSchema = z.object({
|
const RestoreBackupSchema = z.object({
|
||||||
@@ -76,7 +77,11 @@ const RestoreBackupSchema = z.object({
|
|||||||
|
|
||||||
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
|
type RestoreBackup = z.infer<typeof RestoreBackupSchema>;
|
||||||
|
|
||||||
export const RestoreBackup = ({ databaseId, databaseType }: Props) => {
|
export const RestoreBackup = ({
|
||||||
|
databaseId,
|
||||||
|
databaseType,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
@@ -101,6 +106,7 @@ export const RestoreBackup = ({ databaseId, databaseType }: Props) => {
|
|||||||
{
|
{
|
||||||
destinationId: destionationId,
|
destinationId: destionationId,
|
||||||
search,
|
search,
|
||||||
|
serverId: serverId ?? "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isOpen && !!destionationId,
|
enabled: isOpen && !!destionationId,
|
||||||
@@ -304,7 +310,9 @@ export const RestoreBackup = ({ databaseId, databaseType }: Props) => {
|
|||||||
form.setValue("backupFile", file);
|
form.setValue("backupFile", file);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{file}
|
<div className="flex w-full justify-between">
|
||||||
|
<span>{file}</span>
|
||||||
|
</div>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-4 w-4",
|
"ml-auto h-4 w-4",
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
{postgres && postgres?.backups?.length > 0 && (
|
{postgres && postgres?.backups?.length > 0 && (
|
||||||
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
<div className="flex flex-col lg:flex-row gap-4 w-full lg:w-auto">
|
||||||
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
<AddBackup databaseId={id} databaseType={type} refetch={refetch} />
|
||||||
<RestoreBackup databaseId={id} databaseType={type} />
|
<RestoreBackup
|
||||||
|
databaseId={id}
|
||||||
|
databaseType={type}
|
||||||
|
serverId={postgres.serverId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -108,7 +112,11 @@ export const ShowBackups = ({ id, type }: Props) => {
|
|||||||
databaseType={type}
|
databaseType={type}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
/>
|
/>
|
||||||
<RestoreBackup databaseId={id} databaseType={type} />
|
<RestoreBackup
|
||||||
|
databaseId={id}
|
||||||
|
databaseType={type}
|
||||||
|
serverId={postgres.serverId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { DrawerLogs } from "@/components/shared/drawer-logs";
|
|||||||
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";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||||
@@ -16,236 +16,246 @@ import { toast } from "sonner";
|
|||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
const { data, refetch } = api.mongo.one.useQuery(
|
||||||
{
|
{
|
||||||
mongoId,
|
mongoId,
|
||||||
},
|
},
|
||||||
{ enabled: !!mongoId }
|
{ enabled: !!mongoId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.mongo.reload.useMutation();
|
api.mongo.reload.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: start, isLoading: isStarting } =
|
const { mutateAsync: start, isLoading: isStarting } =
|
||||||
api.mongo.start.useMutation();
|
api.mongo.start.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.mongo.stop.useMutation();
|
api.mongo.stop.useMutation();
|
||||||
|
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const [isDeploying, setIsDeploying] = useState(false);
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
api.mongo.deployWithLogs.useSubscription(
|
api.mongo.deployWithLogs.useSubscription(
|
||||||
{
|
{
|
||||||
mongoId: mongoId,
|
mongoId: mongoId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isDeploying,
|
enabled: isDeploying,
|
||||||
onData(log) {
|
onData(log) {
|
||||||
if (!isDrawerOpen) {
|
if (!isDrawerOpen) {
|
||||||
setIsDrawerOpen(true);
|
setIsDrawerOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log === "Deployment completed successfully!") {
|
if (log === "Deployment completed successfully!") {
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedLogs = parseLogs(log);
|
const parsedLogs = parseLogs(log);
|
||||||
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
setFilteredLogs((prev) => [...prev, ...parsedLogs]);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.error("Deployment logs error:", error);
|
console.error("Deployment logs error:", error);
|
||||||
setIsDeploying(false);
|
setIsDeploying(false);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Deploy Mongo"
|
title="Deploy Mongo"
|
||||||
description="Are you sure you want to deploy this mongo?"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setIsDeploying(true);
|
setIsDeploying(true);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
refetch();
|
refetch();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="default"
|
||||||
<Button
|
isLoading={data?.applicationStatus === "running"}
|
||||||
variant="default"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
isLoading={data?.applicationStatus === "running"}
|
>
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
<Tooltip>
|
||||||
>
|
<TooltipTrigger asChild>
|
||||||
<Rocket className="size-4 mr-1" />
|
<div className="flex items-center">
|
||||||
Deploy
|
<Rocket className="size-4 mr-1" />
|
||||||
</Button>
|
Deploy
|
||||||
</TooltipTrigger>
|
</div>
|
||||||
<TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipPrimitive.Portal>
|
||||||
<p>Downloads and sets up the MongoDB database</p>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipContent>
|
<p>Downloads and sets up the MongoDB database</p>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</TooltipPrimitive.Portal>
|
||||||
</DialogAction>
|
</Tooltip>
|
||||||
<DialogAction
|
</Button>
|
||||||
title="Reload Mongo"
|
</DialogAction>
|
||||||
description="Are you sure you want to reload this mongo?"
|
<DialogAction
|
||||||
type="default"
|
title="Reload Mongo"
|
||||||
onClick={async () => {
|
description="Are you sure you want to reload this mongo?"
|
||||||
await reload({
|
type="default"
|
||||||
mongoId: mongoId,
|
onClick={async () => {
|
||||||
appName: data?.appName || "",
|
await reload({
|
||||||
})
|
mongoId: mongoId,
|
||||||
.then(() => {
|
appName: data?.appName || "",
|
||||||
toast.success("Mongo reloaded successfully");
|
})
|
||||||
refetch();
|
.then(() => {
|
||||||
})
|
toast.success("Mongo reloaded successfully");
|
||||||
.catch(() => {
|
refetch();
|
||||||
toast.error("Error reloading Mongo");
|
})
|
||||||
});
|
.catch(() => {
|
||||||
}}
|
toast.error("Error reloading Mongo");
|
||||||
>
|
});
|
||||||
<Tooltip>
|
}}
|
||||||
<TooltipTrigger asChild>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
isLoading={isReloading}
|
isLoading={isReloading}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
<Tooltip>
|
||||||
Reload
|
<TooltipTrigger asChild>
|
||||||
</Button>
|
<div className="flex items-center">
|
||||||
</TooltipTrigger>
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<TooltipPrimitive.Portal>
|
Reload
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
</div>
|
||||||
<p>Restart the MongoDB service without rebuilding</p>
|
</TooltipTrigger>
|
||||||
</TooltipContent>
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</Tooltip>
|
<p>Restart the MongoDB service without rebuilding</p>
|
||||||
</DialogAction>
|
</TooltipContent>
|
||||||
{data?.applicationStatus === "idle" ? (
|
</TooltipPrimitive.Portal>
|
||||||
<DialogAction
|
</Tooltip>
|
||||||
title="Start Mongo"
|
</Button>
|
||||||
description="Are you sure you want to start this mongo?"
|
</DialogAction>
|
||||||
type="default"
|
{data?.applicationStatus === "idle" ? (
|
||||||
onClick={async () => {
|
<DialogAction
|
||||||
await start({
|
title="Start Mongo"
|
||||||
mongoId: mongoId,
|
description="Are you sure you want to start this mongo?"
|
||||||
})
|
type="default"
|
||||||
.then(() => {
|
onClick={async () => {
|
||||||
toast.success("Mongo started successfully");
|
await start({
|
||||||
refetch();
|
mongoId: mongoId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error starting Mongo");
|
toast.success("Mongo started successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Tooltip>
|
toast.error("Error starting Mongo");
|
||||||
<TooltipTrigger asChild>
|
});
|
||||||
<Button
|
}}
|
||||||
variant="secondary"
|
>
|
||||||
isLoading={isStarting}
|
<Button
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
variant="secondary"
|
||||||
>
|
isLoading={isStarting}
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Start
|
>
|
||||||
</Button>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
<p>
|
Start
|
||||||
Start the MongoDB database (requires a previous
|
</div>
|
||||||
successful setup)
|
</TooltipTrigger>
|
||||||
</p>
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipPrimitive.Portal>
|
<p>
|
||||||
</Tooltip>
|
Start the MongoDB database (requires a previous
|
||||||
</DialogAction>
|
successful setup)
|
||||||
) : (
|
</p>
|
||||||
<DialogAction
|
</TooltipContent>
|
||||||
title="Stop Mongo"
|
</TooltipPrimitive.Portal>
|
||||||
description="Are you sure you want to stop this mongo?"
|
</Tooltip>
|
||||||
onClick={async () => {
|
</Button>
|
||||||
await stop({
|
</DialogAction>
|
||||||
mongoId: mongoId,
|
) : (
|
||||||
})
|
<DialogAction
|
||||||
.then(() => {
|
title="Stop Mongo"
|
||||||
toast.success("Mongo stopped successfully");
|
description="Are you sure you want to stop this mongo?"
|
||||||
refetch();
|
onClick={async () => {
|
||||||
})
|
await stop({
|
||||||
.catch(() => {
|
mongoId: mongoId,
|
||||||
toast.error("Error stopping Mongo");
|
})
|
||||||
});
|
.then(() => {
|
||||||
}}
|
toast.success("Mongo stopped successfully");
|
||||||
>
|
refetch();
|
||||||
<Tooltip>
|
})
|
||||||
<TooltipTrigger asChild>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error stopping Mongo");
|
||||||
variant="destructive"
|
});
|
||||||
isLoading={isStopping}
|
}}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
>
|
||||||
>
|
<Button
|
||||||
<Ban className="size-4 mr-1" />
|
variant="destructive"
|
||||||
Stop
|
isLoading={isStopping}
|
||||||
</Button>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
</TooltipTrigger>
|
>
|
||||||
<TooltipPrimitive.Portal>
|
<Tooltip>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipTrigger asChild>
|
||||||
<p>Stop the currently running MongoDB database</p>
|
<div className="flex items-center">
|
||||||
</TooltipContent>
|
<Ban className="size-4 mr-1" />
|
||||||
</TooltipPrimitive.Portal>
|
Stop
|
||||||
</Tooltip>
|
</div>
|
||||||
</DialogAction>
|
</TooltipTrigger>
|
||||||
)}
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipProvider>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<DockerTerminalModal
|
<p>Stop the currently running MongoDB database</p>
|
||||||
appName={data?.appName || ""}
|
</TooltipContent>
|
||||||
serverId={data?.serverId || ""}
|
</TooltipPrimitive.Portal>
|
||||||
>
|
</Tooltip>
|
||||||
<Tooltip>
|
</Button>
|
||||||
<TooltipTrigger asChild>
|
</DialogAction>
|
||||||
<Button
|
)}
|
||||||
variant="outline"
|
</TooltipProvider>
|
||||||
className="flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2"
|
<DockerTerminalModal
|
||||||
>
|
appName={data?.appName || ""}
|
||||||
<Terminal className="size-4" />
|
serverId={data?.serverId || ""}
|
||||||
Open Terminal
|
>
|
||||||
</Button>
|
<Button
|
||||||
</TooltipTrigger>
|
variant="outline"
|
||||||
<TooltipPrimitive.Portal>
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
>
|
||||||
<p>Open a terminal to the MongoDB container</p>
|
<Tooltip>
|
||||||
</TooltipContent>
|
<TooltipTrigger asChild>
|
||||||
</TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
</Tooltip>
|
<Terminal className="size-4 mr-1" />
|
||||||
</DockerTerminalModal>
|
Open Terminal
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</TooltipTrigger>
|
||||||
<DrawerLogs
|
<TooltipPrimitive.Portal>
|
||||||
isOpen={isDrawerOpen}
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
onClose={() => {
|
<p>Open a terminal to the MongoDB container</p>
|
||||||
setIsDrawerOpen(false);
|
</TooltipContent>
|
||||||
setFilteredLogs([]);
|
</TooltipPrimitive.Portal>
|
||||||
setIsDeploying(false);
|
</Tooltip>
|
||||||
refetch();
|
</Button>
|
||||||
}}
|
</DockerTerminalModal>
|
||||||
filteredLogs={filteredLogs}
|
</CardContent>
|
||||||
/>
|
</Card>
|
||||||
</div>
|
<DrawerLogs
|
||||||
</>
|
isOpen={isDrawerOpen}
|
||||||
);
|
onClose={() => {
|
||||||
|
setIsDrawerOpen(false);
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setIsDeploying(false);
|
||||||
|
refetch();
|
||||||
|
}}
|
||||||
|
filteredLogs={filteredLogs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ export const AddNode = ({ serverId }: Props) => {
|
|||||||
<TabsTrigger value="worker">Worker</TabsTrigger>
|
<TabsTrigger value="worker">Worker</TabsTrigger>
|
||||||
<TabsTrigger value="manager">Manager</TabsTrigger>
|
<TabsTrigger value="manager">Manager</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="worker" className="pt-4">
|
<TabsContent value="worker" className="pt-4 overflow-hidden">
|
||||||
<AddWorker serverId={serverId} />
|
<AddWorker serverId={serverId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="manager" className="pt-4">
|
<TabsContent value="manager" className="pt-4 overflow-hidden">
|
||||||
<AddManager serverId={serverId} />
|
<AddManager serverId={serverId} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
@@ -6,7 +7,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { CopyIcon } from "lucide-react";
|
import { CopyIcon, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,56 +15,66 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddManager = ({ serverId }: Props) => {
|
export const AddManager = ({ serverId }: Props) => {
|
||||||
const { data } = api.cluster.addManager.useQuery({ serverId });
|
const { data, isLoading, error, isError } = api.cluster.addManager.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
|
||||||
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
|
<DialogHeader>
|
||||||
<DialogHeader>
|
<DialogTitle>Add a new manager</DialogTitle>
|
||||||
<DialogTitle>Add a new manager</DialogTitle>
|
<DialogDescription>Add a new manager</DialogDescription>
|
||||||
<DialogDescription>Add a new manager</DialogDescription>
|
</DialogHeader>
|
||||||
</DialogHeader>
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-col gap-2.5 text-sm">
|
{isLoading ? (
|
||||||
<span>1. Go to your new server and run the following command</span>
|
<Loader2 className="w-full animate-spin text-muted-foreground" />
|
||||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
) : (
|
||||||
curl https://get.docker.com | sh -s -- --version {data?.version}
|
<>
|
||||||
<button
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
type="button"
|
<span>
|
||||||
className="self-center"
|
1. Go to your new server and run the following command
|
||||||
onClick={() => {
|
</span>
|
||||||
copy(
|
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||||
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||||
);
|
<button
|
||||||
toast.success("Copied to clipboard");
|
type="button"
|
||||||
}}
|
className="self-center"
|
||||||
>
|
onClick={() => {
|
||||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
copy(
|
||||||
</button>
|
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
||||||
</span>
|
);
|
||||||
</div>
|
toast.success("Copied to clipboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2.5 text-sm">
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
<span>
|
<span>
|
||||||
2. Run the following command to add the node(manager) to your
|
2. Run the following command to add the node(manager) to your
|
||||||
cluster
|
cluster
|
||||||
</span>
|
</span>
|
||||||
<span className="bg-muted rounded-lg p-2 flex">
|
|
||||||
{data?.command}
|
<span className="bg-muted rounded-lg p-2 flex">
|
||||||
<button
|
{data?.command}
|
||||||
type="button"
|
<button
|
||||||
className="self-start"
|
type="button"
|
||||||
onClick={() => {
|
className="self-start"
|
||||||
copy(data?.command || "");
|
onClick={() => {
|
||||||
toast.success("Copied to clipboard");
|
copy(data?.command || "");
|
||||||
}}
|
toast.success("Copied to clipboard");
|
||||||
>
|
}}
|
||||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
>
|
||||||
</button>
|
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||||
</span>
|
</button>
|
||||||
</div>
|
</span>
|
||||||
</CardContent>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const ShowNodesModal = ({ serverId }: Props) => {
|
|||||||
className="w-full cursor-pointer "
|
className="w-full cursor-pointer "
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
Show Nodes
|
Show Swarm Nodes
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CardContent } from "@/components/ui/card";
|
import { CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
@@ -6,7 +7,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
import { CopyIcon } from "lucide-react";
|
import { CopyIcon, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -14,54 +15,62 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddWorker = ({ serverId }: Props) => {
|
export const AddWorker = ({ serverId }: Props) => {
|
||||||
const { data } = api.cluster.addWorker.useQuery({ serverId });
|
const { data, isLoading, error, isError } = api.cluster.addWorker.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<CardContent className="sm:max-w-4xl flex flex-col gap-4 px-0">
|
||||||
<CardContent className="sm:max-w-4xl max-h-screen overflow-y-auto flex flex-col gap-4 px-0">
|
<DialogHeader>
|
||||||
<DialogHeader>
|
<DialogTitle>Add a new worker</DialogTitle>
|
||||||
<DialogTitle>Add a new worker</DialogTitle>
|
<DialogDescription>Add a new worker</DialogDescription>
|
||||||
<DialogDescription>Add a new worker</DialogDescription>
|
</DialogHeader>
|
||||||
</DialogHeader>
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<div className="flex flex-col gap-2.5 text-sm">
|
{isLoading ? (
|
||||||
<span>1. Go to your new server and run the following command</span>
|
<Loader2 className="w-full animate-spin text-muted-foreground" />
|
||||||
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
) : (
|
||||||
curl https://get.docker.com | sh -s -- --version {data?.version}
|
<>
|
||||||
<button
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
type="button"
|
<span>1. Go to your new server and run the following command</span>
|
||||||
className="self-center"
|
<span className="bg-muted rounded-lg p-2 flex justify-between">
|
||||||
onClick={() => {
|
curl https://get.docker.com | sh -s -- --version {data?.version}
|
||||||
copy(
|
<button
|
||||||
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
type="button"
|
||||||
);
|
className="self-center"
|
||||||
toast.success("Copied to clipboard");
|
onClick={() => {
|
||||||
}}
|
copy(
|
||||||
>
|
`curl https://get.docker.com | sh -s -- --version ${data?.version}`,
|
||||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
);
|
||||||
</button>
|
toast.success("Copied to clipboard");
|
||||||
</span>
|
}}
|
||||||
</div>
|
>
|
||||||
|
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2.5 text-sm">
|
<div className="flex flex-col gap-2.5 text-sm">
|
||||||
<span>
|
<span>
|
||||||
2. Run the following command to add the node(worker) to your cluster
|
2. Run the following command to add the node(worker) to your
|
||||||
</span>
|
cluster
|
||||||
|
</span>
|
||||||
|
|
||||||
<span className="bg-muted rounded-lg p-2 flex">
|
<span className="bg-muted rounded-lg p-2 flex">
|
||||||
{data?.command}
|
{data?.command}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="self-start"
|
className="self-start"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copy(data?.command || "");
|
copy(data?.command || "");
|
||||||
toast.success("Copied to clipboard");
|
toast.success("Copied to clipboard");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
<CopyIcon className="h-4 w-4 cursor-pointer" />
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</>
|
||||||
</div>
|
)}
|
||||||
|
</CardContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -663,13 +663,16 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value) {
|
if (value === "") {
|
||||||
|
field.onChange(undefined);
|
||||||
|
} else {
|
||||||
const port = Number.parseInt(value);
|
const port = Number.parseInt(value);
|
||||||
if (port > 0 && port < 65536) {
|
if (port > 0 && port < 65536) {
|
||||||
field.onChange(port);
|
field.onChange(port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
value={field.value || ""}
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -159,9 +159,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
field.onChange(Number(e.target.value))
|
const value = e.target.value;
|
||||||
}
|
field.onChange(value === "" ? undefined : Number(value));
|
||||||
|
}}
|
||||||
|
value={field.value || ""}
|
||||||
className="w-full dark:bg-black"
|
className="w-full dark:bg-black"
|
||||||
placeholder="e.g. 8080"
|
placeholder="e.g. 8080"
|
||||||
/>
|
/>
|
||||||
@@ -185,9 +187,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
field.onChange(Number(e.target.value))
|
const value = e.target.value;
|
||||||
}
|
field.onChange(value === "" ? undefined : Number(value));
|
||||||
|
}}
|
||||||
|
value={field.value || ""}
|
||||||
className="w-full dark:bg-black"
|
className="w-full dark:bg-black"
|
||||||
placeholder="e.g. 80"
|
placeholder="e.g. 80"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
className={cn("text-left", className)}
|
className={cn("text-left", className)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
value={props.value === undefined || props.value === "" ? "" : String(props.value)}
|
value={props.value === undefined ? undefined : String(props.value)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
@@ -60,21 +60,6 @@ const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={(e) => {
|
|
||||||
// If input is empty, make 0 when focus is lost
|
|
||||||
if (e.target.value === "") {
|
|
||||||
const syntheticEvent = {
|
|
||||||
...e,
|
|
||||||
target: {
|
|
||||||
...e.target,
|
|
||||||
value: "0",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
props.onChange?.(
|
|
||||||
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.20.5",
|
"version": "v0.20.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ import {
|
|||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { execAsync } from "@dokploy/server/utils/process/execAsync";
|
import {
|
||||||
|
execAsync,
|
||||||
|
execAsyncRemote,
|
||||||
|
} from "@dokploy/server/utils/process/execAsync";
|
||||||
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
|
import { getS3Credentials } from "@dokploy/server/utils/backups/utils";
|
||||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||||
import {
|
import {
|
||||||
@@ -229,6 +232,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
z.object({
|
z.object({
|
||||||
destinationId: z.string(),
|
destinationId: z.string(),
|
||||||
search: z.string(),
|
search: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
@@ -250,7 +254,16 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
|
const searchPath = baseDir ? `${bucketPath}/${baseDir}` : bucketPath;
|
||||||
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
|
const listCommand = `rclone lsf ${rcloneFlags.join(" ")} "${searchPath}" | head -n 100`;
|
||||||
|
|
||||||
const { stdout } = await execAsync(listCommand);
|
let stdout = "";
|
||||||
|
|
||||||
|
if (input.serverId) {
|
||||||
|
const result = await execAsyncRemote(listCommand, input.serverId);
|
||||||
|
stdout = result.stdout;
|
||||||
|
} else {
|
||||||
|
const result = await execAsync(listCommand);
|
||||||
|
stdout = result.stdout;
|
||||||
|
}
|
||||||
|
|
||||||
const files = stdout.split("\n").filter(Boolean);
|
const files = stdout.split("\n").filter(Boolean);
|
||||||
|
|
||||||
const results = baseDir
|
const results = baseDir
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import {
|
|||||||
findDomainById,
|
findDomainById,
|
||||||
findDomainsByApplicationId,
|
findDomainsByApplicationId,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
|
findOrganizationById,
|
||||||
findPreviewDeploymentById,
|
findPreviewDeploymentById,
|
||||||
|
findServerById,
|
||||||
generateTraefikMeDomain,
|
generateTraefikMeDomain,
|
||||||
manageDomain,
|
manageDomain,
|
||||||
removeDomain,
|
removeDomain,
|
||||||
@@ -94,6 +96,19 @@ export const domainRouter = createTRPCRouter({
|
|||||||
input.serverId,
|
input.serverId,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
canGenerateTraefikMeDomains: protectedProcedure
|
||||||
|
.input(z.object({ serverId: z.string() }))
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const organization = await findOrganizationById(
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (input.serverId) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
return server.ipAddress;
|
||||||
|
}
|
||||||
|
return organization?.owner.serverIp;
|
||||||
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateDomain)
|
.input(apiUpdateDomain)
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import { db } from "../db";
|
|||||||
import * as schema from "../db/schema";
|
import * as schema from "../db/schema";
|
||||||
import { sendEmail } from "../verification/send-verification-email";
|
import { sendEmail } from "../verification/send-verification-email";
|
||||||
import { IS_CLOUD } from "../constants";
|
import { IS_CLOUD } from "../constants";
|
||||||
|
import { getPublicIpWithFallback } from "../wss/utils";
|
||||||
|
import { updateUser } from "../services/user";
|
||||||
|
import { getUserByToken } from "../services/admin";
|
||||||
|
import { APIError } from "better-auth/api";
|
||||||
|
|
||||||
const { handler, api } = betterAuth({
|
const { handler, api } = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
@@ -88,11 +92,40 @@ const { handler, api } = betterAuth({
|
|||||||
databaseHooks: {
|
databaseHooks: {
|
||||||
user: {
|
user: {
|
||||||
create: {
|
create: {
|
||||||
|
before: async (_user, context) => {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
const xDokployToken =
|
||||||
|
context?.request?.headers?.get("x-dokploy-token");
|
||||||
|
if (xDokployToken) {
|
||||||
|
const user = await getUserByToken(xDokployToken);
|
||||||
|
if (!user) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const isAdminPresent = await db.query.member.findFirst({
|
||||||
|
where: eq(schema.member.role, "owner"),
|
||||||
|
});
|
||||||
|
if (isAdminPresent) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "Admin is already created",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
after: async (user) => {
|
after: async (user) => {
|
||||||
const isAdminPresent = await db.query.member.findFirst({
|
const isAdminPresent = await db.query.member.findFirst({
|
||||||
where: eq(schema.member.role, "owner"),
|
where: eq(schema.member.role, "owner"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
await updateUser(user.id, {
|
||||||
|
serverIp: await getPublicIpWithFallback(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (IS_CLOUD || !isAdminPresent) {
|
if (IS_CLOUD || !isAdminPresent) {
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
const organization = await tx
|
const organization = await tx
|
||||||
|
|||||||
@@ -361,7 +361,7 @@ const installUtilities = () => `
|
|||||||
alpine)
|
alpine)
|
||||||
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
|
||||||
apk update >/dev/null
|
apk update >/dev/null
|
||||||
apk add curl wget git jq openssl >/dev/null
|
apk add curl wget git jq openssl sudo unzip tar >/dev/null
|
||||||
;;
|
;;
|
||||||
ubuntu | debian | raspbian)
|
ubuntu | debian | raspbian)
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
|
||||||
|
|||||||
Reference in New Issue
Block a user