Merge pull request #591 from Dokploy/feat/remove-build-on-server

Feat/remove build on server
This commit is contained in:
Mauricio Siu
2024-10-25 00:23:56 -06:00
committed by GitHub
168 changed files with 8246 additions and 1491 deletions

View File

@@ -1,12 +1,23 @@
import fs from "node:fs/promises";
import path from "node:path";
import { paths } from "@dokploy/server/dist/constants";
import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server";
import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
// @ts-ignore
...actual,
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
};
});
if (typeof window === "undefined") {
const undici = require("undici");
globalThis.File = undici.File as any;
@@ -82,16 +93,6 @@ const baseApp: ApplicationNested = {
dockerContextPath: null,
};
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
const actual = await importOriginal();
return {
// @ts-ignore
...actual,
paths: () => ({
APPLICATIONS_PATH: "./__test__/drop/zips/output",
}),
};
});
describe("unzipDrop using real zip files", () => {
// const { APPLICATIONS_PATH } = paths();
beforeAll(async () => {

View File

@@ -24,6 +24,9 @@ const baseAdmin: Admin = {
sshPrivateKey: null,
enableDockerCleanup: false,
enableLogRotation: false,
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
};
beforeEach(() => {

View File

@@ -1,13 +1,8 @@
import path from "node:path";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [
tsconfigPaths({
root: "./",
projects: ["tsconfig.json"],
}),
],
test: {
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
@@ -18,4 +13,12 @@ export default defineConfig({
NODE: "test",
},
},
resolve: {
alias: {
"@dokploy/server": path.resolve(
__dirname,
"../../../packages/server/src",
),
},
},
});

View File

@@ -0,0 +1,241 @@
import { Button } from "@/components/ui/button";
import { NumberInput } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { loadStripe } from "@stripe/stripe-js";
import clsx from "clsx";
import { AlertTriangle, CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
import React, { useState } from "react";
const stripePromise = loadStripe(
"pk_test_51QAm7bF3cxQuHeOz0xg04o9teeyTbbNHQPJ5Tr98MlTEan9MzewT3gwh0jSWBNvrRWZ5vASoBgxUSF4gPWsJwATk00Ir2JZ0S1",
);
export const calculatePrice = (count: number, isAnnual = false) => {
if (isAnnual) {
if (count <= 1) return 45.9;
return 35.7 * count;
}
if (count <= 1) return 4.5;
return count * 3.5;
};
// 178.156.147.118
export const ShowBilling = () => {
const { data: servers } = api.server.all.useQuery(undefined);
const { data: admin } = api.admin.one.useQuery();
const { data } = api.stripe.getProducts.useQuery();
const { mutateAsync: createCheckoutSession } =
api.stripe.createCheckoutSession.useMutation();
const { mutateAsync: createCustomerPortalSession } =
api.stripe.createCustomerPortalSession.useMutation();
const [serverQuantity, setServerQuantity] = useState(3);
const [isAnnual, setIsAnnual] = useState(false);
const handleCheckout = async (productId: string) => {
const stripe = await stripePromise;
if (data && data.subscriptions.length === 0) {
createCheckoutSession({
productId,
serverQuantity: serverQuantity,
isAnnual,
}).then(async (session) => {
await stripe?.redirectToCheckout({
sessionId: session.sessionId,
});
});
}
};
const products = data?.products.filter((product) => {
// @ts-ignore
const interval = product?.default_price?.recurring?.interval;
return isAnnual ? interval === "year" : interval === "month";
});
const maxServers = admin?.serversQuantity ?? 1;
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
const safePercentage = Math.min(percentage, 100);
return (
<div className="flex flex-col gap-4 w-full">
<Tabs
defaultValue="monthly"
value={isAnnual ? "annual" : "monthly"}
className="w-full"
onValueChange={(e) => setIsAnnual(e === "annual")}
>
<TabsList>
<TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="annual">Annual</TabsTrigger>
</TabsList>
</Tabs>
{admin?.stripeSubscriptionId && (
<div className="space-y-2">
<h3 className="text-lg font-medium">Servers Plan</h3>
<p className="text-sm text-muted-foreground">
You have {servers?.length} server on your plan of{" "}
{admin?.serversQuantity} servers
</p>
<div className="pb-5">
<Progress value={safePercentage} className="max-w-lg" />
</div>
{admin && (
<>
{admin.serversQuantity! <= servers?.length! && (
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">
You have reached the maximum number of servers you can
create, please upgrade your plan to add more servers.
</span>
</div>
)}
</>
)}
</div>
)}
{products?.map((product) => {
const featured = true;
return (
<div key={product.id}>
<section
className={clsx(
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
featured
? "order-first bg-black border py-8 lg:order-none"
: "lg:py-8",
)}
>
{isAnnual ? (
<div className="flex flex-row gap-2 items-center">
<p className=" text-2xl font-semibold tracking-tight text-primary ">
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
</p>
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
${" "}
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(2)}{" "}
/ Month USD
</p>
</div>
) : (
<p className=" text-2xl font-semibold tracking-tight text-primary ">
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
</p>
)}
<h3 className="mt-5 font-medium text-lg text-white">
{product.name}
</h3>
<p
className={clsx(
"text-sm",
featured ? "text-white" : "text-slate-400",
)}
>
{product.description}
</p>
<ul
role="list"
className={clsx(
" mt-4 flex flex-col gap-y-2 text-sm",
featured ? "text-white" : "text-slate-200",
)}
>
{[
"All the features of Dokploy",
"Unlimited deployments",
"Self-hosted on your own infrastructure",
"Full access to all deployment features",
"Dokploy integration",
"Backups",
"All Incoming features",
].map((feature) => (
<li key={feature} className="flex text-muted-foreground">
<CheckIcon />
<span className="ml-4">{feature}</span>
</li>
))}
</ul>
<div className="flex flex-col gap-2 mt-4">
<div className="flex items-center gap-2 justify-center">
<span className="text-sm text-muted-foreground">
{serverQuantity} Servers
</span>
</div>
<div className="flex items-center space-x-2">
<Button
disabled={serverQuantity <= 1}
variant="outline"
onClick={() => {
if (serverQuantity <= 1) return;
setServerQuantity(serverQuantity - 1);
}}
>
<MinusIcon className="h-4 w-4" />
</Button>
<NumberInput
value={serverQuantity}
onChange={(e) => {
setServerQuantity(e.target.value as unknown as number);
}}
/>
<Button
variant="outline"
onClick={() => {
setServerQuantity(serverQuantity + 1);
}}
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
<div
className={cn(
data?.subscriptions && data?.subscriptions?.length > 0
? "justify-between"
: "justify-end",
"flex flex-row items-center gap-2 mt-4",
)}
>
{admin?.stripeCustomerId && (
<Button
variant="secondary"
className="w-full"
onClick={async () => {
const session = await createCustomerPortalSession();
window.open(session.url);
}}
>
Manage Subscription
</Button>
)}
{data?.subscriptions?.length === 0 && (
<div className="justify-end w-full">
<Button
className="w-full"
onClick={async () => {
handleCheckout(product.id);
}}
disabled={serverQuantity < 1}
>
Subscribe
</Button>
</div>
)}
</div>
</div>
</section>
</div>
);
})}
</div>
);
};

View File

@@ -1,181 +0,0 @@
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 } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Container } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddRegistrySchema = z.object({
username: z
.string()
.min(1, {
message: "Username is required",
})
.regex(/^[a-zA-Z0-9]+$/, {
message: "Username can only contain letters and numbers",
}),
password: z.string().min(1, {
message: "Password is required",
}),
registryUrl: z.string().min(1, {
message: "Registry URL is required",
}),
});
type AddRegistry = z.infer<typeof AddRegistrySchema>;
export const AddSelfHostedRegistry = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, error, isError, isLoading } =
api.registry.enableSelfHostedRegistry.useMutation();
const router = useRouter();
const form = useForm<AddRegistry>({
defaultValues: {
username: "",
password: "",
registryUrl: "",
},
resolver: zodResolver(AddRegistrySchema),
});
useEffect(() => {
form.reset({
registryUrl: "",
username: "",
password: "",
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (data: AddRegistry) => {
await mutateAsync({
registryUrl: data.registryUrl,
username: data.username,
password: data.password,
})
.then(async (data) => {
await utils.registry.all.invalidate();
toast.success("Self Hosted Registry Created");
setIsOpen(false);
})
.catch(() => {
toast.error("Error to create a self hosted registry");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="max-sm:w-full">
<Container className="h-4 w-4" />
Enable Self Hosted Registry
</Button>
</DialogTrigger>
<DialogContent className="sm:m:max-w-lg ">
<DialogHeader>
<DialogTitle>Add a self hosted registry</DialogTitle>
<DialogDescription>
Fill the next fields to add a self hosted registry.
</DialogDescription>
</DialogHeader>
{isError && (
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4"
>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
placeholder="Password"
{...field}
type="password"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="registryUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Registry URL</FormLabel>
<FormControl>
<Input placeholder="registry.dokploy.com" {...field} />
</FormControl>
<FormDescription>
Point a DNS record to the VPS IP address.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter>
<Button isLoading={isLoading} type="submit">
Create
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -8,7 +8,6 @@ import {
import { api } from "@/utils/api";
import { Server } from "lucide-react";
import { AddRegistry } from "./add-docker-registry";
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
import { DeleteRegistry } from "./delete-registry";
import { UpdateDockerRegistry } from "./update-docker-registry";
@@ -31,8 +30,6 @@ export const ShowRegistry = () => {
<div className="flex flex-row gap-2">
{data && data?.length > 0 && (
<>
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
<AddRegistry />
</>
)}
@@ -47,7 +44,6 @@ export const ShowRegistry = () => {
</span>
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
<AddSelfHostedRegistry />
<AddRegistry />
</div>
</div>

View File

@@ -18,6 +18,16 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
@@ -32,12 +42,15 @@ const addDestination = z.object({
bucket: z.string(),
region: z.string(),
endpoint: z.string(),
serverId: z.string().optional(),
});
type AddDestination = z.infer<typeof addDestination>;
export const AddDestination = () => {
const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync, isError, error, isLoading } =
api.destination.create.useMutation();
@@ -189,30 +202,106 @@ export const AddDestination = () => {
/>
</form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
<Button
isLoading={isLoadingConnection}
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
})
.then(async () => {
toast.success("Connection Success");
<DialogFooter
className={cn(
isCloud ? "!flex-col" : "flex-row",
"flex w-full !justify-between pt-3 gap-4",
)}
>
{isCloud ? (
<div className="flex flex-col gap-4 border p-2 rounded-lg">
<span className="text-sm text-muted-foreground">
Select a server to test the destination. If you don't have a
server choose the default one.
</span>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Server (Optional)</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant={"secondary"}
isLoading={isLoading}
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
serverId: form.getValues("serverId"),
})
.then(async () => {
toast.success("Connection Success");
})
.catch((e) => {
toast.error("Error to connect the provider", {
description: e.message,
});
});
}}
>
Test Connection
</Button>
</div>
) : (
<Button
isLoading={isLoadingConnection}
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
)}
<Button
isLoading={isLoading}
form="hook-form-destination-add"

View File

@@ -18,6 +18,16 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
@@ -33,6 +43,7 @@ const updateDestination = z.object({
bucket: z.string(),
region: z.string(),
endpoint: z.string(),
serverId: z.string().optional(),
});
type UpdateDestination = z.infer<typeof updateDestination>;
@@ -43,6 +54,8 @@ interface Props {
export const UpdateDestination = ({ destinationId }: Props) => {
const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const [isOpen, setIsOpen] = useState(false);
const { data, refetch } = api.destination.one.useQuery(
{
@@ -220,34 +233,107 @@ export const UpdateDestination = ({ destinationId }: Props) => {
</div>
</form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
<Button
isLoading={isLoadingConnection}
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
})
.then(async () => {
toast.success("Connection Success");
<DialogFooter
className={cn(
isCloud ? "!flex-col" : "flex-row",
"flex w-full !justify-between pt-3 gap-4",
)}
>
{isCloud ? (
<div className="flex flex-col gap-4 border p-2 rounded-lg">
<span className="text-sm text-muted-foreground">
Select a server to test the destination. If you don't have a
server choose the default one.
</span>
<FormField
control={form.control}
name="serverId"
render={({ field }) => (
<FormItem>
<FormLabel>Server (Optional)</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a server" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Servers</SelectLabel>
{servers?.map((server) => (
<SelectItem
key={server.serverId}
value={server.serverId}
>
{server.name}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
variant={"secondary"}
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
serverId: form.getValues("serverId"),
})
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test Connection
</Button>
</div>
) : (
<Button
isLoading={isLoadingConnection}
type="button"
variant="secondary"
onClick={async () => {
await testConnection({
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
name: "Test",
region: form.getValues("region"),
secretAccessKey: form.getValues("secretAccessKey"),
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
)}
<Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
isLoading={form.formState.isSubmitting}
>
Update
</Button>

View File

@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
export const AddNotification = () => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
api.notification.testSlackConnection.useMutation();
@@ -660,26 +660,28 @@ export const AddNotification = () => {
)}
/>
<FormField
control={form.control}
name="dokployRestart"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{!isCloud && (
<FormField
control={form.control}
name="dokployRestart"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
</div>
</form>

View File

@@ -63,7 +63,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
const telegramMutation = api.notification.updateTelegram.useMutation();
const discordMutation = api.notification.updateDiscord.useMutation();
const emailMutation = api.notification.updateEmail.useMutation();
const { data: isCloud } = api.settings.isCloud.useQuery();
const form = useForm<NotificationSchema>({
defaultValues: {
type: "slack",
@@ -618,27 +618,29 @@ export const UpdateNotification = ({ notificationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
defaultValue={form.control._defaultValues.dokployRestart}
name="dokployRestart"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{!isCloud && (
<FormField
control={form.control}
defaultValue={form.control._defaultValues.dokployRestart}
name="dokployRestart"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
</div>
</form>

View File

@@ -31,6 +31,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -57,6 +58,9 @@ type Schema = z.infer<typeof Schema>;
export const AddServer = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data: canCreateMoreServers, refetch } =
api.stripe.canCreateMoreServers.useQuery();
const { data: sshKeys } = api.sshKey.all.useQuery();
const { mutateAsync, error, isError } = api.server.create.useMutation();
const form = useForm<Schema>({
@@ -82,6 +86,10 @@ export const AddServer = () => {
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
useEffect(() => {
refetch();
}, [isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
name: data.name,
@@ -116,6 +124,14 @@ export const AddServer = () => {
Add a server to deploy your applications remotely.
</DialogDescription>
</DialogHeader>
{!canCreateMoreServers && (
<AlertBlock type="warning">
You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan
</Link>
</AlertBlock>
)}
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
@@ -254,6 +270,7 @@ export const AddServer = () => {
<DialogFooter>
<Button
isLoading={form.formState.isSubmitting}
disabled={!canCreateMoreServers}
form="hook-form-add-server"
type="submit"
>

View File

@@ -36,6 +36,9 @@ export const ShowServers = () => {
const { data, refetch } = api.server.all.useQuery();
const { mutateAsync } = api.server.remove.useMutation();
const { data: sshKeys } = api.sshKey.all.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: canCreateMoreServers } =
api.stripe.canCreateMoreServers.useQuery();
return (
<div className="p-6 space-y-6">
@@ -74,8 +77,22 @@ export const ShowServers = () => {
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
<ServerIcon className="size-8" />
<span className="text-base text-muted-foreground">
No Servers found. Add a server to deploy your applications
remotely.
{!canCreateMoreServers ? (
<div>
You cannot create more servers,{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Please upgrade your plan
</Link>
</div>
) : (
<span>
No Servers found. Add a server to deploy your applications
remotely.
</span>
)}
</span>
</div>
)
@@ -87,6 +104,9 @@ export const ShowServers = () => {
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Name</TableHead>
{isCloud && (
<TableHead className="text-center">Status</TableHead>
)}
<TableHead className="text-center">IP Address</TableHead>
<TableHead className="text-center">Port</TableHead>
<TableHead className="text-center">Username</TableHead>
@@ -98,9 +118,23 @@ export const ShowServers = () => {
<TableBody>
{data?.map((server) => {
const canDelete = server.totalSum === 0;
const isActive = server.serverStatus === "active";
return (
<TableRow key={server.serverId}>
<TableCell className="w-[100px]">{server.name}</TableCell>
{isCloud && (
<TableHead className="text-center">
<Badge
variant={
server.serverStatus === "active"
? "default"
: "destructive"
}
>
{server.serverStatus}
</Badge>
</TableHead>
)}
<TableCell className="text-center">
<Badge>{server.ipAddress}</Badge>
</TableCell>
@@ -131,18 +165,25 @@ export const ShowServers = () => {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
{server.sshKeyId && (
<TerminalModal serverId={server.serverId}>
<span>Enter the terminal</span>
</TerminalModal>
{isActive && (
<>
{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
disabled={!canDelete}
title={
@@ -187,17 +228,21 @@ export const ShowServers = () => {
</DropdownMenuItem>
</DialogAction>
{server.sshKeyId && (
{isActive && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
{server.sshKeyId && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
</>
)}
</>
)}
</DropdownMenuContent>

View File

@@ -80,6 +80,7 @@ export const SettingsLayout = ({ children }: Props) => {
icon: ListMusic,
href: "/dashboard/settings/registry",
},
...(!isCloud
? [
{
@@ -102,6 +103,16 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Server,
href: "/dashboard/settings/servers",
},
...(isCloud
? [
{
title: "Billing",
label: "",
icon: CreditCardIcon,
href: "/dashboard/settings/billing",
},
]
: []),
]
: []),
...(user?.canAccessToSSHKeys
@@ -137,6 +148,7 @@ import {
Activity,
Bell,
BoxesIcon,
CreditCardIcon,
Database,
GitBranch,
KeyIcon,

View File

@@ -0,0 +1,12 @@
DO $$ BEGIN
CREATE TYPE "public"."serverStatus" AS ENUM('active', 'inactive');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "stripeCustomerId" text;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "stripeSubscriptionId" text;--> statement-breakpoint
ALTER TABLE "admin" ADD COLUMN "serversQuantity" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "auth" ADD COLUMN "resetPasswordToken" text;--> statement-breakpoint
ALTER TABLE "auth" ADD COLUMN "resetPasswordExpiresAt" text;--> statement-breakpoint
ALTER TABLE "server" ADD COLUMN "serverStatus" "serverStatus" DEFAULT 'active' NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -288,6 +288,13 @@
"when": 1728780577084,
"tag": "0040_graceful_wolfsbane",
"breakpoints": true
},
{
"idx": 41,
"version": "6",
"when": 1729667438853,
"tag": "0041_huge_bruce_banner",
"breakpoints": true
}
]
}

View File

@@ -11,7 +11,7 @@
"build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
"migration:run": "tsx -r dotenv/config migration.ts",
@@ -34,12 +34,12 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"@dokploy/server": "workspace:*",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
"@codemirror/legacy-modes": "6.4.0",
"@codemirror/view": "6.29.0",
"@dokploy/server": "workspace:*",
"@dokploy/trpc-openapi": "0.0.4",
"@hookform/resolvers": "^3.3.4",
"@octokit/webhooks": "^13.2.7",
@@ -61,6 +61,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@stripe/stripe-js": "4.8.0",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.16.0",
"@trpc/client": "^10.43.6",
@@ -89,7 +90,7 @@
"lucia": "^3.0.1",
"lucide-react": "^0.312.0",
"nanoid": "3",
"next": "^14.1.3",
"next": "^15.0.1",
"next-themes": "^0.2.1",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
@@ -102,6 +103,8 @@
"recharts": "^2.12.7",
"slugify": "^1.6.6",
"sonner": "^1.4.0",
"ssh2": "1.15.0",
"stripe": "17.2.0",
"superjson": "^2.2.1",
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0",
@@ -111,11 +114,9 @@
"ws": "8.16.0",
"xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4",
"zod-form-data": "^2.0.2",
"ssh2": "1.15.0"
"zod-form-data": "^2.0.2"
},
"devDependencies": {
"autoprefixer": "10.4.12",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/js-yaml": "4.0.9",
@@ -124,8 +125,10 @@
"@types/node-schedule": "2.1.6",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/ssh2": "1.15.1",
"@types/swagger-ui-react": "^4.18.3",
"@types/ws": "8.5.10",
"autoprefixer": "10.4.12",
"drizzle-kit": "^0.21.1",
"esbuild": "0.20.2",
"lint-staged": "^15.2.7",
@@ -134,8 +137,7 @@
"tsx": "^4.7.0",
"typescript": "^5.4.2",
"vite-tsconfig-paths": "4.3.2",
"vitest": "^1.6.0",
"@types/ssh2": "1.15.1"
"vitest": "^1.6.0"
},
"ct3aMetadata": {
"initVersion": "7.25.2"

View File

@@ -0,0 +1,274 @@
import { buffer } from "node:stream/consumers";
import { db } from "@/server/db";
import { admins, server } from "@/server/db/schema";
import { findAdminById } from "@dokploy/server";
import { asc, eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET || "";
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (!endpointSecret) {
return res.status(400).send("Webhook Error: Missing Stripe Secret Key");
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
maxNetworkRetries: 3,
});
const buf = await buffer(req);
const sig = req.headers["stripe-signature"] as string;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(buf, sig, endpointSecret);
} catch (err) {
console.error(
"Webhook signature verification failed.",
err instanceof Error ? err.message : err,
);
return res.status(400).send("Webhook Error: ");
}
const webhooksAllowed = [
"customer.subscription.created",
"customer.subscription.deleted",
"customer.subscription.updated",
"invoice.payment_succeeded",
"invoice.payment_failed",
"customer.deleted",
"checkout.session.completed",
];
if (!webhooksAllowed.includes(event.type)) {
return res.status(400).send("Webhook Error: Invalid Event Type");
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
const adminId = session?.metadata?.adminId as string;
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string,
);
await db
.update(admins)
.set({
stripeCustomerId: session.customer as string,
stripeSubscriptionId: session.subscription as string,
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
})
.where(eq(admins.adminId, adminId))
.returning();
const admin = await findAdminById(adminId);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
break;
}
case "customer.subscription.created": {
const newSubscription = event.data.object as Stripe.Subscription;
await db
.update(admins)
.set({
stripeSubscriptionId: newSubscription.id,
serversQuantity: 0,
stripeCustomerId: newSubscription.customer as string,
})
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
.returning();
break;
}
case "customer.subscription.deleted": {
const newSubscription = event.data.object as Stripe.Subscription;
await db
.update(admins)
.set({
stripeSubscriptionId: null,
serversQuantity: 0,
})
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
const admin = await findAdminByStripeCustomerId(
newSubscription.customer as string,
);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
await disableServers(admin.adminId);
break;
}
case "customer.subscription.updated": {
const newSubscription = event.data.object as Stripe.Subscription;
await db
.update(admins)
.set({
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
})
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
const admin = await findAdminByStripeCustomerId(
newSubscription.customer as string,
);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
break;
}
case "invoice.payment_succeeded": {
const newInvoice = event.data.object as Stripe.Invoice;
const suscription = await stripe.subscriptions.retrieve(
newInvoice.subscription as string,
);
await db
.update(admins)
.set({
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
})
.where(eq(admins.stripeCustomerId, suscription.customer as string));
const admin = await findAdminByStripeCustomerId(
suscription.customer as string,
);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
const newServersQuantity = admin.serversQuantity;
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
break;
}
case "invoice.payment_failed": {
const newInvoice = event.data.object as Stripe.Invoice;
await db
.update(admins)
.set({
serversQuantity: 0,
})
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
const admin = await findAdminByStripeCustomerId(
newInvoice.customer as string,
);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
await disableServers(admin.adminId);
break;
}
case "customer.deleted": {
const customer = event.data.object as Stripe.Customer;
const admin = await findAdminByStripeCustomerId(customer.id);
if (!admin) {
return res.status(400).send("Webhook Error: Admin not found");
}
await disableServers(admin.adminId);
await db
.update(admins)
.set({
stripeCustomerId: null,
stripeSubscriptionId: null,
serversQuantity: 0,
})
.where(eq(admins.stripeCustomerId, customer.id));
break;
}
default:
console.log(`Unhandled event type: ${event.type}`);
}
return res.status(200).json({ received: true });
}
const disableServers = async (adminId: string) => {
await db
.update(server)
.set({
serverStatus: "inactive",
})
.where(eq(server.adminId, adminId));
};
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
const admin = db.query.admins.findFirst({
where: eq(admins.stripeCustomerId, stripeCustomerId),
});
return admin;
};
const activateServer = async (serverId: string) => {
await db
.update(server)
.set({ serverStatus: "active" })
.where(eq(server.serverId, serverId));
};
const deactivateServer = async (serverId: string) => {
await db
.update(server)
.set({ serverStatus: "inactive" })
.where(eq(server.serverId, serverId));
};
export const findServersByAdminIdSorted = async (adminId: string) => {
const servers = await db.query.server.findMany({
where: eq(server.adminId, adminId),
orderBy: asc(server.createdAt),
});
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,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { GlobeIcon } from "lucide-react";
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -100,8 +107,38 @@ const Service = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
@@ -119,90 +156,111 @@ const Service = (
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateApplication applicationId={applicationId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteApplication applicationId={applicationId} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateApplication applicationId={applicationId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteApplication applicationId={applicationId} />
)}
</div>
</div>
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralApplication applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment applicationId={applicationId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralApplication applicationId={applicationId} />
</div>
</TabsContent>
)}
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironment applicationId={applicationId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
appName={data?.appName || ""}
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
<TabsContent value="deployments" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeployments applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="domains" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomains applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommand applicationId={applicationId} />
<ShowClusterSettings applicationId={applicationId} />
<ShowApplicationResources applicationId={applicationId} />
<ShowVolumes applicationId={applicationId} />
<ShowRedirects applicationId={applicationId} />
<ShowSecurity applicationId={applicationId} />
<ShowPorts applicationId={applicationId} />
<ShowTraefikConfig applicationId={applicationId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
appName={data?.appName || ""}
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
<TabsContent value="deployments" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeployments applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="domains" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomains applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommand applicationId={applicationId} />
<ShowClusterSettings applicationId={applicationId} />
<ShowApplicationResources applicationId={applicationId} />
<ShowVolumes applicationId={applicationId} />
<ShowRedirects applicationId={applicationId} />
<ShowSecurity applicationId={applicationId} />
<ShowPorts applicationId={applicationId} />
<ShowTraefikConfig applicationId={applicationId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -16,13 +16,21 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { CircuitBoard } from "lucide-react";
import { CircuitBoard, ServerOff } from "lucide-react";
import { HelpCircle } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -94,8 +102,38 @@ const Service = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -113,98 +151,118 @@ const Service = (
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
data?.serverId && data?.composeType === "stack"
? "md:grid-cols-5"
: "",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
{data?.composeType === "docker-compose" && (
<TabsTrigger value="environment">Environment</TabsTrigger>
)}
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
data?.serverId && data?.composeType === "stack"
? "md:grid-cols-5"
: "",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
{data?.composeType === "docker-compose" && (
<TabsTrigger value="environment">Environment</TabsTrigger>
)}
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateCompose composeId={composeId} />
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
)}
</div>
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironmentCompose composeId={composeId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
<ShowGeneralCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowEnvironmentCompose composeId={composeId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMonitoringCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogsCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogsCompose
serverId={data?.serverId || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
<TabsContent value="deployments">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeploymentsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="deployments">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeploymentsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="domains">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomainsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />
<ShowVolumesCompose composeId={composeId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="domains">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomainsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />
<ShowVolumesCompose composeId={composeId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -17,12 +17,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -82,8 +90,38 @@ const Mariadb = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -99,79 +137,99 @@ const Mariadb = (
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateMariadb mariadbId={mariadbId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMariadb mariadbId={mariadbId} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMariadb mariadbId={mariadbId} />
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateMariadb mariadbId={mariadbId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMariadb mariadbId={mariadbId} />
)}
</div>
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMariadbEnvironment mariadbId={mariadbId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralMariadb mariadbId={mariadbId} />
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackupMariadb mariadbId={mariadbId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedMariadb mariadbId={mariadbId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMariadbEnvironment mariadbId={mariadbId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackupMariadb mariadbId={mariadbId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedMariadb mariadbId={mariadbId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -17,12 +17,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -83,8 +91,38 @@ const Mongo = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -100,80 +138,100 @@ const Mongo = (
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateMongo mongoId={mongoId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMongo mongoId={mongoId} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMongo mongoId={mongoId} />
<ShowInternalMongoCredentials mongoId={mongoId} />
<ShowExternalMongoCredentials mongoId={mongoId} />
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateMongo mongoId={mongoId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMongo mongoId={mongoId} />
)}
</div>
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMongoEnvironment mongoId={mongoId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralMongo mongoId={mongoId} />
<ShowInternalMongoCredentials mongoId={mongoId} />
<ShowExternalMongoCredentials mongoId={mongoId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackupMongo mongoId={mongoId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedMongo mongoId={mongoId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMongoEnvironment mongoId={mongoId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackupMongo mongoId={mongoId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedMongo mongoId={mongoId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -17,12 +17,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -81,8 +89,38 @@ const MySql = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -99,80 +137,100 @@ const MySql = (
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateMysql mysqlId={mysqlId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMysql mysqlId={mysqlId} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMysql mysqlId={mysqlId} />
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateMysql mysqlId={mysqlId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteMysql mysqlId={mysqlId} />
)}
</div>
</div>
</TabsContent>
<TabsContent value="environment" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMysqlEnvironment mysqlId={mysqlId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralMysql mysqlId={mysqlId} />
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackupMySql mysqlId={mysqlId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedMysql mysqlId={mysqlId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="environment" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMysqlEnvironment mysqlId={mysqlId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackupMySql mysqlId={mysqlId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedMysql mysqlId={mysqlId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -17,12 +17,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -82,8 +90,38 @@ const Postgresql = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -100,80 +138,100 @@ const Postgresql = (
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdatePostgres postgresId={postgresId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeletePostgres postgresId={postgresId} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralPostgres postgresId={postgresId} />
<ShowInternalPostgresCredentials postgresId={postgresId} />
<ShowExternalPostgresCredentials postgresId={postgresId} />
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="backups">Backups</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdatePostgres postgresId={postgresId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeletePostgres postgresId={postgresId} />
)}
</div>
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowPostgresEnvironment postgresId={postgresId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralPostgres postgresId={postgresId} />
<ShowInternalPostgresCredentials postgresId={postgresId} />
<ShowExternalPostgresCredentials postgresId={postgresId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackupPostgres postgresId={postgresId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedPostgres postgresId={postgresId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowPostgresEnvironment postgresId={postgresId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="backups">
<div className="flex flex-col gap-4 pt-2.5">
<ShowBackupPostgres postgresId={postgresId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedPostgres postgresId={postgresId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -16,12 +16,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -81,8 +89,38 @@ const Redis = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -99,74 +137,94 @@ const Redis = (
</div>
</header>
</div>
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateRedis redisId={redisId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteRedis redisId={redisId} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</div>
</div>
) : (
<Tabs
value={tab}
defaultValue="general"
className="w-full"
onValueChange={(e) => {
setSab(e as TabState);
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralRedis redisId={redisId} />
<ShowInternalRedisCredentials redisId={redisId} />
<ShowExternalRedisCredentials redisId={redisId} />
router.push(newPath, undefined, { shallow: true });
}}
>
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger>
{!data?.serverId && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<UpdateRedis redisId={redisId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteRedis redisId={redisId} />
)}
</div>
</div>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowRedisEnvironment redisId={redisId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralRedis redisId={redisId} />
<ShowInternalRedisCredentials redisId={redisId} />
<ShowExternalRedisCredentials redisId={redisId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedRedis redisId={redisId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowRedisEnvironment redisId={redisId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<ShowAdvancedRedis redisId={redisId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -1,8 +1,11 @@
import { ShowProjects } from "@/components/dashboard/projects/show";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Dashboard = () => {
return <ShowProjects />;
@@ -16,7 +19,22 @@ Dashboard.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
if (!user) {
return {
@@ -27,6 +45,8 @@ export async function getServerSideProps(
};
}
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,9 +1,12 @@
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -25,7 +28,23 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
if (!user) {
return {
redirect: {
@@ -34,8 +53,9 @@ export async function getServerSideProps(
},
};
}
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -0,0 +1,65 @@
import { ShowBilling } from "@/components/dashboard/settings/billing/show-billing";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return <ShowBilling />;
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return (
<DashboardLayout tab={"settings"}>
<SettingsLayout>{page}</SettingsLayout>
</DashboardLayout>
);
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,10 +1,12 @@
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
<div className="flex flex-col gap-4 w-full">
@@ -25,7 +27,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
return {
redirect: {
@@ -35,7 +38,23 @@ export async function getServerSideProps(
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,5 +1,4 @@
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { IS_CLOUD, validateRequest } from "@dokploy/server";

View File

@@ -1,9 +1,12 @@
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
return {
redirect: {
@@ -35,7 +39,23 @@ export async function getServerSideProps(
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -52,6 +52,7 @@ export async function getServerSideProps(
try {
await helpers.project.all.prefetch();
await helpers.settings.isCloud.prefetch();
const auth = await helpers.auth.get.fetch();
if (auth.rol === "user") {

View File

@@ -1,10 +1,12 @@
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -26,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
return {
redirect: {
@@ -36,7 +39,23 @@ export async function getServerSideProps(
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -2,10 +2,13 @@ import { GenerateToken } from "@/components/dashboard/settings/profile/generate-
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
const { data } = api.auth.get.useQuery();
@@ -37,7 +40,22 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
if (!user) {
return {
redirect: {
@@ -48,6 +66,8 @@ export async function getServerSideProps(
}
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,9 +1,12 @@
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user, session } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
return {
redirect: {
@@ -34,8 +38,23 @@ export async function getServerSideProps(
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,9 +1,12 @@
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user) {
return {
redirect: {
@@ -43,7 +47,23 @@ export async function getServerSideProps(
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -52,7 +52,9 @@ export async function getServerSideProps(
try {
await helpers.project.all.prefetch();
const auth = await helpers.auth.get.fetch();
await helpers.settings.isCloud.prefetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({

View File

@@ -1,9 +1,12 @@
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import React, { type ReactElement } from "react";
import superjson from "superjson";
const Page = () => {
return (
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { user } = await validateRequest(ctx.req, ctx.res);
const { req, res } = ctx;
const { user, session } = await validateRequest(req, res);
if (!user || user.rol === "user") {
return {
redirect: {
@@ -35,7 +39,23 @@ export async function getServerSideProps(
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session,
user: user,
},
transformer: superjson,
});
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -55,7 +55,10 @@ type AuthResponse = {
authId: string;
};
export default function Home() {
interface Props {
IS_CLOUD: boolean;
}
export default function Home({ IS_CLOUD }: Props) {
const [temp, setTemp] = useState<AuthResponse>({
is2FAEnabled: false,
authId: "",
@@ -176,13 +179,22 @@ export default function Home() {
</div>
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
<Link
className="hover:underline text-muted-foreground"
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
target="_blank"
>
Lost your password?
</Link>
{IS_CLOUD ? (
<Link
className="hover:underline text-muted-foreground"
href="/send-reset-password"
>
Lost your password?
</Link>
) : (
<Link
className="hover:underline text-muted-foreground"
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
target="_blank"
>
Lost your password?
</Link>
)}
</div>
</div>
<div className="p-2" />
@@ -212,7 +224,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
} catch (error) {}
return {
props: {},
props: {
IS_CLOUD: IS_CLOUD,
},
};
}
const hasAdmin = await isAdminPresent();

View File

@@ -0,0 +1,228 @@
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { auth } from "@/server/db/schema";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const loginSchema = z
.object({
password: z
.string()
.min(1, {
message: "Password is required",
})
.min(8, {
message: "Password must be at least 8 characters",
}),
confirmPassword: z
.string()
.min(1, {
message: "Password is required",
})
.min(8, {
message: "Password must be at least 8 characters",
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
type Login = z.infer<typeof loginSchema>;
interface Props {
token: string;
}
export default function Home({ token }: Props) {
const { mutateAsync, isLoading, isError, error } =
api.auth.resetPassword.useMutation();
const router = useRouter();
const form = useForm<Login>({
defaultValues: {
password: "",
confirmPassword: "",
},
resolver: zodResolver(loginSchema),
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => {
await mutateAsync({
resetPasswordToken: token,
password: values.password,
})
.then((data) => {
toast.success("Password reset succesfully", {
duration: 2000,
});
router.push("/");
})
.catch(() => {
toast.error("Error to reset password", {
duration: 2000,
});
});
};
return (
<div className="flex h-screen w-full items-center justify-center ">
<div className="flex flex-col items-center gap-4 w-full">
<Link href="/" className="flex flex-row items-center gap-2">
<Logo />
<span className="font-medium text-sm">Dokploy</span>
</Link>
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
<CardDescription>
Enter your email to reset your password
</CardDescription>
<Card className="mx-auto w-full max-w-lg bg-transparent ">
<div className="p-3.5" />
<CardContent>
{isError && (
<AlertBlock type="error" className="my-2">
{error?.message}
</AlertBlock>
)}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
isLoading={isLoading}
className="w-full"
>
Confirm
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</div>
);
}
Home.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>;
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const { token } = context.query;
if (typeof token !== "string") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const authR = await db?.query.auth.findFirst({
where: eq(auth.resetPasswordToken, token),
});
if (!authR || authR?.resetPasswordExpiresAt === null) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const isExpired = isBefore(
new Date(authR.resetPasswordExpiresAt),
new Date(),
);
if (isExpired) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
return {
props: {
token: authR.resetPasswordToken,
},
};
}

View File

@@ -0,0 +1,172 @@
import { Login2FA } from "@/components/auth/login-2fa";
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const loginSchema = z.object({
email: z
.string()
.min(1, {
message: "Email is required",
})
.email({
message: "Email must be a valid email",
}),
});
type Login = z.infer<typeof loginSchema>;
type AuthResponse = {
is2FAEnabled: boolean;
authId: string;
};
export default function Home() {
const [temp, setTemp] = useState<AuthResponse>({
is2FAEnabled: false,
authId: "",
});
const { mutateAsync, isLoading, isError, error } =
api.auth.sendResetPasswordEmail.useMutation();
const router = useRouter();
const form = useForm<Login>({
defaultValues: {
email: "",
},
resolver: zodResolver(loginSchema),
});
useEffect(() => {
form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => {
await mutateAsync({
email: values.email,
})
.then((data) => {
toast.success("Email sent", {
duration: 2000,
});
})
.catch(() => {
toast.error("Error to send email", {
duration: 2000,
});
});
};
return (
<div className="flex h-screen w-full items-center justify-center ">
<div className="flex flex-col items-center gap-4 w-full">
<Link href="/" className="flex flex-row items-center gap-2">
<Logo />
<span className="font-medium text-sm">Dokploy</span>
</Link>
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
<CardDescription>
Enter your email to reset your password
</CardDescription>
<Card className="mx-auto w-full max-w-lg bg-transparent ">
<div className="p-3.5" />
<CardContent>
{isError && (
<AlertBlock type="error" className="my-2">
{error?.message}
</AlertBlock>
)}
{!temp.is2FAEnabled ? (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
isLoading={isLoading}
className="w-full"
>
Send Reset Link
</Button>
</div>
</form>
</Form>
) : (
<Login2FA authId={temp.authId} />
)}
<div className="flex flex-row justify-between flex-wrap">
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
<Link
className="hover:underline text-muted-foreground"
href="/"
>
Login
</Link>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
);
}
Home.getLayout = (page: ReactElement) => {
return <OnboardingLayout>{page}</OnboardingLayout>;
};
export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
return {
props: {},
};
}

View File

@@ -29,6 +29,7 @@ import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings";
import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { userRouter } from "./routers/user";
/**
@@ -69,6 +70,7 @@ export const appRouter = createTRPCRouter({
gitlab: gitlabRouter,
github: githubRouter,
server: serverRouter,
stripe: stripeRouter,
});
// export type definition of API

View File

@@ -7,6 +7,7 @@ import {
apiUpdateAuthByAdmin,
apiVerify2FA,
apiVerifyLogin2FA,
auth,
} from "@/server/db/schema";
import {
IS_CLOUD,
@@ -18,12 +19,17 @@ import {
getUserByToken,
lucia,
luciaToken,
sendEmailNotification,
updateAuthById,
validateRequest,
verify2FA,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import * as bcrypt from "bcrypt";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import { db } from "../../db";
import {
adminProcedure,
@@ -233,4 +239,101 @@ export const authRouter = createTRPCRouter({
verifyToken: protectedProcedure.mutation(async () => {
return true;
}),
sendResetPasswordEmail: publicProcedure
.input(
z.object({
email: z.string().min(1).email(),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "This feature is only available in the cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.email, input.email),
});
if (!authR) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const token = nanoid();
await updateAuthById(authR.id, {
resetPasswordToken: token,
// Make resetPassword in 24 hours
resetPasswordExpiresAt: new Date(
new Date().getTime() + 24 * 60 * 60 * 1000,
).toISOString(),
});
const email = await sendEmailNotification(
{
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
toAddresses: [authR.email],
smtpServer: process.env.SMTP_SERVER || "",
smtpPort: Number(process.env.SMTP_PORT),
username: process.env.SMTP_USERNAME || "",
password: process.env.SMTP_PASSWORD || "",
},
"Reset Password",
`
Reset your password by clicking the link below:
The link will expire in 24 hours.
<a href="http://localhost:3000/reset-password?token=${token}">
Reset Password
</a>
`,
);
}),
resetPassword: publicProcedure
.input(
z.object({
resetPasswordToken: z.string().min(1),
password: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "This feature is only available in the cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.resetPasswordToken, input.resetPasswordToken),
});
if (!authR || authR.resetPasswordExpiresAt === null) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token not found",
});
}
const isExpired = isBefore(
new Date(authR.resetPasswordExpiresAt),
new Date(),
);
if (isExpired) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token expired",
});
}
await updateAuthById(authR.id, {
resetPasswordExpiresAt: null,
resetPasswordToken: null,
password: bcrypt.hashSync(input.password, 10),
});
return true;
}),
});

View File

@@ -14,6 +14,7 @@ import {
findMongoByBackupId,
findMySqlByBackupId,
findPostgresByBackupId,
findServerById,
removeBackupById,
removeScheduleBackup,
runMariadbBackup,
@@ -36,6 +37,25 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(newBackup.backupId);
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({
cronSchedule: backup.schedule,
backupId: backup.backupId,

View File

@@ -252,7 +252,6 @@ export const composeRouter = createTRPCRouter({
descriptionLog: "",
server: !!compose.serverId,
};
console.log(jobData);
if (IS_CLOUD && compose.serverId) {
jobData.serverId = compose.serverId;

View File

@@ -12,9 +12,10 @@ import {
destinations,
} from "@/server/db/schema";
import {
IS_CLOUD,
createDestintation,
execAsync,
findAdmin,
execAsyncRemote,
findDestinationById,
removeDestinationById,
updateDestinationById,
@@ -53,11 +54,26 @@ export const destinationRouter = createTRPCRouter({
];
const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
await execAsync(rcloneCommand);
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Server not found",
});
}
if (IS_CLOUD) {
await execAsyncRemote(input.serverId || "", rcloneCommand);
} else {
await execAsync(rcloneCommand);
}
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to connect to bucket",
message:
error instanceof Error
? error?.message
: "Error to connect to bucket",
cause: error,
});
}

View File

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

View File

@@ -148,12 +148,6 @@ export const notificationRouter = createTRPCRouter({
.input(apiCreateDiscord)
.mutation(async ({ input, ctx }) => {
try {
// go to your discord server
// go to settings
// go to integrations
// add a new integration
// select webhook
// copy the webhook url
return await createDiscordNotification(input, ctx.user.adminId);
} catch (error) {
throw new TRPCError({

View File

@@ -1,6 +1,5 @@
import {
apiCreateRegistry,
apiEnableSelfHostedRegistry,
apiFindOneRegistry,
apiRemoveRegistry,
apiTestRegistry,
@@ -13,8 +12,6 @@ import {
execAsyncRemote,
findAllRegistryByAdminId,
findRegistryById,
initializeRegistry,
manageRegistry,
removeRegistry,
updateRegistry,
} from "@dokploy/server";
@@ -84,6 +81,13 @@ export const registryRouter = createTRPCRouter({
try {
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
if (IS_CLOUD && !input.serverId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Select a server to test the registry",
});
}
if (input.serverId && input.serverId !== "none") {
await execAsyncRemote(input.serverId, loginCommand);
} else {
@@ -96,34 +100,4 @@ export const registryRouter = createTRPCRouter({
return false;
}
}),
enableSelfHostedRegistry: adminProcedure
.input(apiEnableSelfHostedRegistry)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Self Hosted Registry is not available in the cloud version",
});
}
const selfHostedRegistry = await createRegistry(
{
...input,
registryName: "Self Hosted Registry",
registryType: "selfHosted",
registryUrl:
process.env.NODE_ENV === "production"
? input.registryUrl
: "dokploy-registry.docker.localhost",
imagePrefix: null,
serverId: undefined,
},
ctx.user.adminId,
);
await manageRegistry(selfHostedRegistry);
await initializeRegistry(input.username, input.password);
return selfHostedRegistry;
}),
});

View File

@@ -1,3 +1,4 @@
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
@@ -15,15 +16,17 @@ import {
server,
} from "@/server/db/schema";
import {
IS_CLOUD,
createServer,
deleteServer,
findAdminById,
findServerById,
findServersByAdminId,
haveActiveServices,
removeDeploymentsByServerId,
serverSetup,
updateServerById,
} from "@dokploy/server";
// import { serverSetup } from "@/server/setup/server-setup";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
@@ -32,6 +35,14 @@ export const serverRouter = createTRPCRouter({
.input(apiCreateServer)
.mutation(async ({ ctx, input }) => {
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);
return project;
} catch (error) {
@@ -77,13 +88,17 @@ export const serverRouter = createTRPCRouter({
return result;
}),
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
return await db.query.server.findMany({
const result = await db.query.server.findMany({
orderBy: desc(server.createdAt),
where: and(
isNotNull(server.sshKeyId),
eq(server.adminId, ctx.user.adminId),
),
where: IS_CLOUD
? and(
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
.input(apiFindOneServer)
@@ -125,6 +140,15 @@ export const serverRouter = createTRPCRouter({
await removeDeploymentsByServerId(currentServer);
await deleteServer(input.serverId);
if (IS_CLOUD) {
const admin = await findAdminById(ctx.user.adminId);
await updateServersBasedOnQuantity(
admin.adminId,
admin.serversQuantity,
);
}
return currentServer;
} catch (error) {
throw error;
@@ -141,6 +165,13 @@ export const serverRouter = createTRPCRouter({
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, {
...input,
});

View File

@@ -221,6 +221,13 @@ export const settingsRouter = createTRPCRouter({
}
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) {
await schedule({
cronSchedule: "0 0 * * *",
@@ -503,7 +510,7 @@ export const settingsRouter = createTRPCRouter({
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else {
} else if (!IS_CLOUD) {
const result = await execAsync(
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
);
@@ -635,7 +642,7 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
isCloud: adminProcedure.query(async () => {
isCloud: protectedProcedure.query(async () => {
return IS_CLOUD;
}),
health: publicProcedure.query(async () => {

View File

@@ -0,0 +1,130 @@
import { WEBSITE_URL, getStripeItems } from "@/server/utils/stripe";
import {
IS_CLOUD,
findAdminById,
findServersByAdminId,
updateAdmin,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import Stripe from "stripe";
import { z } from "zod";
import { adminProcedure, createTRPCRouter } from "../trpc";
export const stripeRouter = createTRPCRouter({
getProducts: adminProcedure.query(async ({ ctx }) => {
const admin = await findAdminById(ctx.user.adminId);
const stripeCustomerId = admin.stripeCustomerId;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
});
const products = await stripe.products.list({
expand: ["data.default_price"],
active: true,
});
if (!stripeCustomerId) {
return {
products: products.data,
subscriptions: [],
};
}
const subscriptions = await stripe.subscriptions.list({
customer: stripeCustomerId,
status: "active",
expand: ["data.items.data.price"],
});
return {
products: products.data,
subscriptions: subscriptions.data,
};
}),
createCheckoutSession: adminProcedure
.input(
z.object({
productId: z.string(),
serverQuantity: z.number().min(1),
isAnnual: z.boolean(),
}),
)
.mutation(async ({ ctx, input }) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
});
const items = getStripeItems(input.serverQuantity, input.isAnnual);
const admin = await findAdminById(ctx.user.adminId);
let stripeCustomerId = admin.stripeCustomerId;
if (stripeCustomerId) {
const customer = await stripe.customers.retrieve(stripeCustomerId);
if (customer.deleted) {
await updateAdmin(admin.authId, {
stripeCustomerId: null,
});
stripeCustomerId = null;
}
}
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: items,
...(stripeCustomerId && {
customer: stripeCustomerId,
}),
metadata: {
adminId: admin.adminId,
},
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
});
return { sessionId: session.id };
}),
createCustomerPortalSession: adminProcedure.mutation(
async ({ ctx, input }) => {
const admin = await findAdminById(ctx.user.adminId);
if (!admin.stripeCustomerId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Stripe Customer ID not found",
});
}
const stripeCustomerId = admin.stripeCustomerId;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
apiVersion: "2024-09-30.acacia",
});
try {
const session = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: `${WEBSITE_URL}/dashboard/settings/billing`,
});
return { url: session.url };
} catch (error) {
return {
url: "",
};
}
},
),
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
const admin = await findAdminById(ctx.user.adminId);
const servers = await findServersByAdminId(admin.adminId);
if (!IS_CLOUD) {
return true;
}
return servers.length < admin.serversQuantity;
}),
});

View File

@@ -1 +1 @@
export * from "@dokploy/server/dist/db/schema";
export * from "@dokploy/server/db/schema";

View File

@@ -27,7 +27,7 @@ import {
config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const app = next({ dev, turbopack: dev });
const handle = app.getRequestHandler();
void app.prepare().then(async () => {
try {
@@ -40,7 +40,9 @@ void app.prepare().then(async () => {
setupDockerContainerLogsWebSocketServer(server);
setupDockerContainerTerminalWebSocketServer(server);
setupTerminalWebSocketServer(server);
setupDockerStatsMonitoringSocketServer(server);
if (!IS_CLOUD) {
setupDockerStatsMonitoringSocketServer(server);
}
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
setupDirectories();

View File

@@ -1,7 +1,12 @@
import { findServerById } from "@dokploy/server";
import type { DeploymentJob } from "../queues/deployments-queue";
export const deploy = async (jobData: DeploymentJob) => {
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`, {
method: "POST",
headers: {

View File

@@ -0,0 +1,27 @@
export const WEBSITE_URL =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: "https://app.dokploy.com";
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID || ""; // $4.00
const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID || ""; // $7.99
export const getStripeItems = (serverQuantity: number, isAnnual: boolean) => {
const items = [];
if (isAnnual) {
items.push({
price: BASE_ANNUAL_MONTHLY_ID,
quantity: serverQuantity,
});
return items;
}
items.push({
price: BASE_PRICE_MONTHLY_ID,
quantity: serverQuantity,
});
return items;
};

View File

@@ -3,15 +3,15 @@ import {
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeTraefik,
} from "@dokploy/server/dist/setup/traefik-setup";
} from "@dokploy/server/setup/traefik-setup";
import { setupDirectories } from "@dokploy/server/dist/setup/config-paths";
import { initializePostgres } from "@dokploy/server/dist/setup/postgres-setup";
import { initializeRedis } from "@dokploy/server/dist/setup/redis-setup";
import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
import {
initializeNetwork,
initializeSwarm,
} from "@dokploy/server/dist/setup/setup";
} from "@dokploy/server/setup/setup";
(async () => {
try {
setupDirectories();

View File

@@ -26,7 +26,8 @@
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"@dokploy/server/*": ["../../packages/server/src/*"]
}
},

View File

@@ -9,7 +9,8 @@
"moduleResolution": "Node",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"@dokploy/server/*": ["../../packages/server/src/*"]
}
},
"include": ["next-env.d.ts", "./server/**/*"]