refactor: update webhooks and added validation to prevent deploy when the server is inactive

This commit is contained in:
Mauricio Siu 2024-10-21 00:34:16 -06:00
parent 1907e7e59c
commit fbda00f059
18 changed files with 440 additions and 202 deletions

View File

@ -8,7 +8,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
server: z.boolean().optional(), server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]), type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application"), applicationType: z.literal("application"),
serverId: z.string(), serverId: z.string().min(1),
}), }),
z.object({ z.object({
composeId: z.string(), composeId: z.string(),
@ -17,7 +17,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
server: z.boolean().optional(), server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]), type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("compose"), applicationType: z.literal("compose"),
serverId: z.string(), serverId: z.string().min(1),
}), }),
]); ]);

View File

@ -96,7 +96,6 @@ export const ShowBilling = () => {
)} )}
</div> </div>
)} )}
{products?.map((product) => { {products?.map((product) => {
const featured = true; const featured = true;
return ( return (

View File

@ -118,6 +118,7 @@ export const ShowServers = () => {
<TableBody> <TableBody>
{data?.map((server) => { {data?.map((server) => {
const canDelete = server.totalSum === 0; const canDelete = server.totalSum === 0;
const isActive = server.serverStatus === "active";
return ( return (
<TableRow key={server.serverId}> <TableRow key={server.serverId}>
<TableCell className="w-[100px]">{server.name}</TableCell> <TableCell className="w-[100px]">{server.name}</TableCell>
@ -164,18 +165,25 @@ export const ShowServers = () => {
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuLabel>Actions</DropdownMenuLabel>
{server.sshKeyId && (
<TerminalModal serverId={server.serverId}> {isActive && (
<span>Enter the terminal</span> <>
</TerminalModal> {server.sshKeyId && (
<TerminalModal serverId={server.serverId}>
<span>Enter the terminal</span>
</TerminalModal>
)}
<SetupServer serverId={server.serverId} />
<UpdateServer serverId={server.serverId} />
{server.sshKeyId && (
<ShowServerActions
serverId={server.serverId}
/>
)}
</>
)} )}
<SetupServer serverId={server.serverId} />
<UpdateServer serverId={server.serverId} />
{server.sshKeyId && (
<ShowServerActions serverId={server.serverId} />
)}
<DialogAction <DialogAction
disabled={!canDelete} disabled={!canDelete}
title={ title={
@ -220,17 +228,21 @@ export const ShowServers = () => {
</DropdownMenuItem> </DropdownMenuItem>
</DialogAction> </DialogAction>
{server.sshKeyId && ( {isActive && (
<> <>
<DropdownMenuSeparator /> {server.sshKeyId && (
<DropdownMenuLabel>Extra</DropdownMenuLabel> <>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
<ShowTraefikFileSystemModal <ShowTraefikFileSystemModal
serverId={server.serverId} serverId={server.serverId}
/> />
<ShowDockerContainersModal <ShowDockerContainersModal
serverId={server.serverId} serverId={server.serverId}
/> />
</>
)}
</> </>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@ -75,21 +75,7 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found"); return res.status(400).send("Webhook Error: Admin not found");
} }
const newServersQuantity = admin.serversQuantity; const newServersQuantity = admin.serversQuantity;
const servers = await findServersByAdminIdSorted(admin.adminId); await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
if (servers.length > newServersQuantity) {
for (const [index, server] of servers.entries()) {
if (index < newServersQuantity) {
await activateServer(server.serverId);
} else {
await deactivateServer(server.serverId);
}
}
} else {
for (const server of servers) {
await activateServer(server.serverId);
}
}
break; break;
} }
case "customer.subscription.created": { case "customer.subscription.created": {
@ -101,20 +87,11 @@ export default async function handler(
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0, serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
stripeCustomerId: newSubscription.customer as string, stripeCustomerId: newSubscription.customer as string,
}) })
.where( .where(eq(admins.stripeCustomerId, newSubscription.customer as string))
eq(
admins.stripeCustomerId,
typeof newSubscription.customer === "string"
? newSubscription.customer
: "",
),
)
.returning(); .returning();
const admin = await findAdminByStripeCustomerId( const admin = await findAdminByStripeCustomerId(
typeof newSubscription.customer === "string" newSubscription.customer as string,
? newSubscription.customer
: "",
); );
if (!admin) { if (!admin) {
@ -122,26 +99,7 @@ export default async function handler(
} }
const newServersQuantity = admin.serversQuantity; const newServersQuantity = admin.serversQuantity;
const servers = await findServersByAdminIdSorted(admin.adminId); await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
// 4 > 3
if (servers.length > newServersQuantity) {
for (const [index, server] of servers.entries()) {
// 0 < 3 = true
// 1 < 3 = true
// 2 < 3 = true
// 3 < 3 = false
if (index < newServersQuantity) {
await activateServer(server.serverId);
} else {
await deactivateServer(server.serverId);
}
}
} else {
for (const server of servers) {
await activateServer(server.serverId);
}
}
break; break;
} }
@ -155,19 +113,10 @@ export default async function handler(
stripeSubscriptionId: null, stripeSubscriptionId: null,
serversQuantity: 0, serversQuantity: 0,
}) })
.where( .where(eq(admins.stripeCustomerId, newSubscription.customer as string));
eq(
admins.stripeCustomerId,
typeof newSubscription.customer === "string"
? newSubscription.customer
: "",
),
);
const admin = await findAdminByStripeCustomerId( const admin = await findAdminByStripeCustomerId(
typeof newSubscription.customer === "string" newSubscription.customer as string,
? newSubscription.customer
: "",
); );
if (!admin) { if (!admin) {
@ -179,25 +128,15 @@ export default async function handler(
} }
case "customer.subscription.updated": { case "customer.subscription.updated": {
const newSubscription = event.data.object as Stripe.Subscription; const newSubscription = event.data.object as Stripe.Subscription;
await db await db
.update(admins) .update(admins)
.set({ .set({
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0, serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
}) })
.where( .where(eq(admins.stripeCustomerId, newSubscription.customer as string));
eq(
admins.stripeCustomerId,
typeof newSubscription.customer === "string"
? newSubscription.customer
: "",
),
);
const admin = await findAdminByStripeCustomerId( const admin = await findAdminByStripeCustomerId(
typeof newSubscription.customer === "string" newSubscription.customer as string,
? newSubscription.customer
: "",
); );
if (!admin) { if (!admin) {
@ -205,21 +144,7 @@ export default async function handler(
} }
const newServersQuantity = admin.serversQuantity; const newServersQuantity = admin.serversQuantity;
const servers = await findServersByAdminIdSorted(admin.adminId); await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
if (servers.length > newServersQuantity) {
for (const [index, server] of servers.entries()) {
if (index < newServersQuantity) {
await activateServer(server.serverId);
} else {
await deactivateServer(server.serverId);
}
}
} else {
for (const server of servers) {
await activateServer(server.serverId);
}
}
break; break;
} }
@ -245,21 +170,7 @@ export default async function handler(
return res.status(400).send("Webhook Error: Admin not found"); return res.status(400).send("Webhook Error: Admin not found");
} }
const newServersQuantity = admin.serversQuantity; const newServersQuantity = admin.serversQuantity;
const servers = await findServersByAdminIdSorted(admin.adminId); await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
if (servers.length > newServersQuantity) {
for (const [index, server] of servers.entries()) {
if (index < newServersQuantity) {
await activateServer(server.serverId);
} else {
await deactivateServer(server.serverId);
}
}
} else {
for (const server of servers) {
await activateServer(server.serverId);
}
}
break; break;
} }
case "invoice.payment_failed": { case "invoice.payment_failed": {
@ -269,15 +180,10 @@ export default async function handler(
.set({ .set({
serversQuantity: 0, serversQuantity: 0,
}) })
.where( .where(eq(admins.stripeCustomerId, newInvoice.customer as string));
eq(
admins.stripeCustomerId,
typeof newInvoice.customer === "string" ? newInvoice.customer : "",
),
);
const admin = await findAdminByStripeCustomerId( const admin = await findAdminByStripeCustomerId(
typeof newInvoice.customer === "string" ? newInvoice.customer : "", newInvoice.customer as string,
); );
if (!admin) { if (!admin) {
@ -308,7 +214,6 @@ export default async function handler(
break; break;
} }
default: default:
console.log(`Unhandled event type: ${event.type}`); console.log(`Unhandled event type: ${event.type}`);
} }
@ -354,3 +259,23 @@ export const findServersByAdminIdSorted = async (adminId: string) => {
return servers; return servers;
}; };
export const updateServersBasedOnQuantity = async (
adminId: string,
newServersQuantity: number,
) => {
const servers = await findServersByAdminIdSorted(adminId);
if (servers.length > newServersQuantity) {
for (const [index, server] of servers.entries()) {
if (index < newServersQuantity) {
await activateServer(server.serverId);
} else {
await deactivateServer(server.serverId);
}
}
} else {
for (const server of servers) {
await activateServer(server.serverId);
}
}
};

View File

@ -22,13 +22,20 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { GlobeIcon } from "lucide-react"; import { GlobeIcon, HelpCircle } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
InferGetServerSidePropsType, InferGetServerSidePropsType,
@ -100,8 +107,38 @@ const Service = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div> <div className="flex flex-row h-fit w-fit gap-2">
<Badge>{data?.server?.name || "Dokploy Server"}</Badge> <Badge
variant={
data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the server
is inactive, please upgrade your plan to add more
servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div> </div>
{data?.description && ( {data?.description && (

View File

@ -16,13 +16,21 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { CircuitBoard } from "lucide-react"; import { CircuitBoard } from "lucide-react";
import { HelpCircle } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
InferGetServerSidePropsType, InferGetServerSidePropsType,
@ -94,8 +102,38 @@ const Service = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div> <div className="flex flex-row h-fit w-fit gap-2">
<Badge>{data?.server?.name || "Dokploy Server"}</Badge> <Badge
variant={
data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the server
is inactive, please upgrade your plan to add more
servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div> </div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">

View File

@ -17,12 +17,20 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
InferGetServerSidePropsType, InferGetServerSidePropsType,
@ -82,8 +90,38 @@ const Mariadb = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div> <div className="flex flex-row h-fit w-fit gap-2">
<Badge>{data?.server?.name || "Dokploy Server"}</Badge> <Badge
variant={
data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the server
is inactive, please upgrade your plan to add more
servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div> </div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">

View File

@ -17,12 +17,20 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
InferGetServerSidePropsType, InferGetServerSidePropsType,
@ -83,8 +91,38 @@ const Mongo = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div> <div className="flex flex-row h-fit w-fit gap-2">
<Badge>{data?.server?.name || "Dokploy Server"}</Badge> <Badge
variant={
data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the server
is inactive, please upgrade your plan to add more
servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div> </div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">

View File

@ -17,12 +17,20 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
InferGetServerSidePropsType, InferGetServerSidePropsType,
@ -81,8 +89,38 @@ const MySql = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div> <div className="flex flex-row h-fit w-fit gap-2">
<Badge>{data?.server?.name || "Dokploy Server"}</Badge> <Badge
variant={
data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the server
is inactive, please upgrade your plan to add more
servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div> </div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">

View File

@ -17,12 +17,20 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
InferGetServerSidePropsType, InferGetServerSidePropsType,
@ -82,8 +90,38 @@ const Postgresql = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div> <div className="flex flex-row h-fit w-fit gap-2">
<Badge>{data?.server?.name || "Dokploy Server"}</Badge> <Badge
variant={
data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the server
is inactive, please upgrade your plan to add more
servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div> </div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">

View File

@ -16,12 +16,20 @@ import {
BreadcrumbItem, BreadcrumbItem,
BreadcrumbLink, BreadcrumbLink,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle } from "lucide-react";
import type { import type {
GetServerSidePropsContext, GetServerSidePropsContext,
InferGetServerSidePropsType, InferGetServerSidePropsType,
@ -81,8 +89,38 @@ const Redis = (
</h1> </h1>
<span className="text-sm">{data?.appName}</span> <span className="text-sm">{data?.appName}</span>
</div> </div>
<div> <div className="flex flex-row h-fit w-fit gap-2">
<Badge>{data?.server?.name || "Dokploy Server"}</Badge> <Badge
variant={
data?.server?.serverStatus === "active"
? "default"
: "destructive"
}
>
{data?.server?.name || "Dokploy Server"}
</Badge>
{data?.server?.serverStatus === "inactive" && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent
className="z-[999] w-[300px]"
align="start"
side="top"
>
<span>
You cannot, deploy this application because the server
is inactive, please upgrade your plan to add more
servers.
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div> </div>
{data?.description && ( {data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl"> <p className="text-sm text-muted-foreground max-w-6xl">

View File

@ -14,6 +14,7 @@ import {
findMongoByBackupId, findMongoByBackupId,
findMySqlByBackupId, findMySqlByBackupId,
findPostgresByBackupId, findPostgresByBackupId,
findServerById,
removeBackupById, removeBackupById,
removeScheduleBackup, removeScheduleBackup,
runMariadbBackup, runMariadbBackup,
@ -36,6 +37,25 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(newBackup.backupId); const backup = await findBackupById(newBackup.backupId);
if (IS_CLOUD && backup.enabled) { if (IS_CLOUD && backup.enabled) {
const databaseType = backup.databaseType;
let serverId = "";
if (databaseType === "postgres" && backup.postgres?.serverId) {
serverId = backup.postgres.serverId;
} else if (databaseType === "mysql" && backup.mysql?.serverId) {
serverId = backup.mysql.serverId;
} else if (databaseType === "mongo" && backup.mongo?.serverId) {
serverId = backup.mongo.serverId;
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
serverId = backup.mariadb.serverId;
}
const server = await findServerById(serverId);
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
await schedule({ await schedule({
cronSchedule: backup.schedule, cronSchedule: backup.schedule,
backupId: backup.backupId, backupId: backup.backupId,

View File

@ -18,6 +18,7 @@ import {
deployMariadb, deployMariadb,
findMariadbById, findMariadbById,
findProjectById, findProjectById,
findServerById,
removeMariadbById, removeMariadbById,
removeService, removeService,
startService, startService,
@ -151,6 +152,7 @@ export const mariadbRouter = createTRPCRouter({
message: "You are not authorized to deploy this mariadb", message: "You are not authorized to deploy this mariadb",
}); });
} }
return deployMariadb(input.mariadbId); return deployMariadb(input.mariadbId);
}), }),
changeStatus: protectedProcedure changeStatus: protectedProcedure

View File

@ -1,3 +1,4 @@
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
@ -15,15 +16,17 @@ import {
server, server,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { import {
IS_CLOUD,
createServer, createServer,
deleteServer, deleteServer,
findAdminById,
findServerById, findServerById,
findServersByAdminId,
haveActiveServices, haveActiveServices,
removeDeploymentsByServerId, removeDeploymentsByServerId,
serverSetup, serverSetup,
updateServerById, updateServerById,
} from "@dokploy/server"; } from "@dokploy/server";
// import { serverSetup } from "@/server/setup/server-setup";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm"; import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
@ -32,6 +35,14 @@ export const serverRouter = createTRPCRouter({
.input(apiCreateServer) .input(apiCreateServer)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
try { try {
const admin = await findAdminById(ctx.user.adminId);
const servers = await findServersByAdminId(admin.adminId);
if (IS_CLOUD && servers.length >= admin.serversQuantity) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You cannot create more servers",
});
}
const project = await createServer(input, ctx.user.adminId); const project = await createServer(input, ctx.user.adminId);
return project; return project;
} catch (error) { } catch (error) {
@ -77,13 +88,17 @@ export const serverRouter = createTRPCRouter({
return result; return result;
}), }),
withSSHKey: protectedProcedure.query(async ({ ctx }) => { withSSHKey: protectedProcedure.query(async ({ ctx }) => {
return await db.query.server.findMany({ const result = await db.query.server.findMany({
orderBy: desc(server.createdAt), orderBy: desc(server.createdAt),
where: and( where: IS_CLOUD
isNotNull(server.sshKeyId), ? and(
eq(server.adminId, ctx.user.adminId), isNotNull(server.sshKeyId),
), eq(server.adminId, ctx.user.adminId),
eq(server.serverStatus, "active"),
)
: and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)),
}); });
return result;
}), }),
setup: protectedProcedure setup: protectedProcedure
.input(apiFindOneServer) .input(apiFindOneServer)
@ -124,7 +139,12 @@ export const serverRouter = createTRPCRouter({
const currentServer = await findServerById(input.serverId); const currentServer = await findServerById(input.serverId);
await removeDeploymentsByServerId(currentServer); await removeDeploymentsByServerId(currentServer);
await deleteServer(input.serverId); await deleteServer(input.serverId);
const admin = await findAdminById(ctx.user.adminId);
await updateServersBasedOnQuantity(
admin.adminId,
admin.serversQuantity,
);
return currentServer; return currentServer;
} catch (error) { } catch (error) {
throw error; throw error;
@ -141,6 +161,13 @@ export const serverRouter = createTRPCRouter({
message: "You are not authorized to update this server", message: "You are not authorized to update this server",
}); });
} }
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
const currentServer = await updateServerById(input.serverId, { const currentServer = await updateServerById(input.serverId, {
...input, ...input,
}); });

View File

@ -221,6 +221,13 @@ export const settingsRouter = createTRPCRouter({
} }
if (server.enableDockerCleanup) { if (server.enableDockerCleanup) {
const server = await findServerById(input.serverId);
if (server.serverStatus === "inactive") {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server is inactive",
});
}
if (IS_CLOUD) { if (IS_CLOUD) {
await schedule({ await schedule({
cronSchedule: "0 0 * * *", cronSchedule: "0 0 * * *",

View File

@ -1,4 +1,3 @@
import { admins } from "@/server/db/schema";
import { getStripeItems } from "@/server/utils/stripe"; import { getStripeItems } from "@/server/utils/stripe";
import { import {
IS_CLOUD, IS_CLOUD,
@ -7,7 +6,6 @@ import {
updateAdmin, updateAdmin,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import Stripe from "stripe"; import Stripe from "stripe";
import { z } from "zod"; import { z } from "zod";
import { adminProcedure, createTRPCRouter } from "../trpc"; import { adminProcedure, createTRPCRouter } from "../trpc";
@ -56,7 +54,7 @@ export const stripeRouter = createTRPCRouter({
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia", apiVersion: "2024-09-30.acacia",
}); });
// await updateAdmin(ctx.user.a, { // await updateAdmin(ctx.user.authId, {
// stripeCustomerId: null, // stripeCustomerId: null,
// stripeSubscriptionId: null, // stripeSubscriptionId: null,
// serversQuantity: 0, // serversQuantity: 0,
@ -110,55 +108,7 @@ export const stripeRouter = createTRPCRouter({
} }
}, },
), ),
success: adminProcedure.query(async ({ ctx }) => {
const sessionId = ctx.req.query.sessionId as string;
if (!sessionId) {
throw new Error("No session_id provided");
}
// const session = await stripe.checkout.sessions.retrieve(sessionId);
// if (session.payment_status === "paid") {
// const admin = await findAdminById(ctx.user.adminId);
// // if (admin.stripeSubscriptionId) {
// // const subscription = await stripe.subscriptions.retrieve(
// // admin.stripeSubscriptionId,
// // );
// // if (subscription.status === "active") {
// // await stripe.subscriptions.update(admin.stripeSubscriptionId, {
// // cancel_at_period_end: true,
// // });
// // }
// // }
// console.log("Payment successful!");
// const stripeCustomerId = session.customer as string;
// console.log("Stripe Customer ID:", stripeCustomerId);
// const stripeSubscriptionId = session.subscription as string;
// const suscription =
// await stripe.subscriptions.retrieve(stripeSubscriptionId);
// console.log("Stripe Subscription ID:", stripeSubscriptionId);
// await db
// ?.update(admins)
// .set({
// stripeCustomerId,
// stripeSubscriptionId,
// serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
// })
// .where(eq(admins.adminId, ctx.user.adminId))
// .returning();
// } else {
// console.log("Payment not completed or failed.");
// }
ctx.res.redirect("/dashboard/settings/billing");
return true;
}),
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => { canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
const admin = await findAdminById(ctx.user.adminId); const admin = await findAdminById(ctx.user.adminId);
const servers = await findServersByAdminId(admin.adminId); const servers = await findServersByAdminId(admin.adminId);

View File

@ -1,7 +1,12 @@
import { findServerById } from "@dokploy/server";
import type { DeploymentJob } from "../queues/deployments-queue"; import type { DeploymentJob } from "../queues/deployments-queue";
export const deploy = async (jobData: DeploymentJob) => { export const deploy = async (jobData: DeploymentJob) => {
try { try {
const server = await findServerById(jobData.serverId as string);
if (server.serverStatus === "inactive") {
throw new Error("Server is inactive");
}
const result = await fetch(`${process.env.SERVER_URL}/deploy`, { const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
method: "POST", method: "POST",
headers: { headers: {

View File

@ -3,6 +3,7 @@ import {
cleanUpSystemPrune, cleanUpSystemPrune,
cleanUpUnusedImages, cleanUpUnusedImages,
findBackupById, findBackupById,
findServerById,
runMariadbBackup, runMariadbBackup,
runMongoBackup, runMongoBackup,
runMySqlBackup, runMySqlBackup,
@ -21,22 +22,47 @@ export const runJobs = async (job: QueueJob) => {
const { backupId } = job; const { backupId } = job;
const backup = await findBackupById(backupId); const backup = await findBackupById(backupId);
const { databaseType, postgres, mysql, mongo, mariadb } = backup; const { databaseType, postgres, mysql, mongo, mariadb } = backup;
if (databaseType === "postgres" && postgres) { if (databaseType === "postgres" && postgres) {
const server = await findServerById(postgres.serverId as string);
if (server.serverStatus === "inactive") {
logger.info("Server is inactive");
return;
}
await runPostgresBackup(postgres, backup); await runPostgresBackup(postgres, backup);
} else if (databaseType === "mysql" && mysql) { } else if (databaseType === "mysql" && mysql) {
const server = await findServerById(mysql.serverId as string);
if (server.serverStatus === "inactive") {
logger.info("Server is inactive");
return;
}
await runMySqlBackup(mysql, backup); await runMySqlBackup(mysql, backup);
} else if (databaseType === "mongo" && mongo) { } else if (databaseType === "mongo" && mongo) {
const server = await findServerById(mongo.serverId as string);
if (server.serverStatus === "inactive") {
logger.info("Server is inactive");
return;
}
await runMongoBackup(mongo, backup); await runMongoBackup(mongo, backup);
} else if (databaseType === "mariadb" && mariadb) { } else if (databaseType === "mariadb" && mariadb) {
const server = await findServerById(mariadb.serverId as string);
if (server.serverStatus === "inactive") {
logger.info("Server is inactive");
return;
}
await runMariadbBackup(mariadb, backup); await runMariadbBackup(mariadb, backup);
} }
} }
if (job.type === "server") { if (job.type === "server") {
const { serverId } = job; const { serverId } = job;
const server = await findServerById(serverId);
if (server.serverStatus === "inactive") {
logger.info("Server is inactive");
return;
}
await cleanUpUnusedImages(serverId); await cleanUpUnusedImages(serverId);
await cleanUpDockerBuilder(serverId); await cleanUpDockerBuilder(serverId);
await cleanUpSystemPrune(serverId); await cleanUpSystemPrune(serverId);
// await sendDockerCleanupNotifications();
} }
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);