diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index 7e4a3c82a..c72d72542 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -51,20 +51,9 @@ const baseAdmin: User = { serversQuantity: 0, stripeCustomerId: "", stripeSubscriptionId: "", - accessedProjects: [], - accessedServices: [], banExpires: new Date(), banned: true, banReason: "", - canAccessToAPI: false, - canCreateProjects: false, - canDeleteProjects: false, - canDeleteServices: false, - canAccessToDocker: false, - canAccessToSSHKeys: false, - canCreateServices: false, - canAccessToTraefikFiles: false, - canAccessToGitProviders: false, email: "", expirationDate: "", id: "", @@ -73,7 +62,6 @@ const baseAdmin: User = { createdAt2: new Date().toISOString(), emailVerified: false, image: "", - token: "", updatedAt: new Date(), twoFactorEnabled: false, }; diff --git a/apps/dokploy/components/auth/login-2fa.tsx b/apps/dokploy/components/auth/login-2fa.tsx deleted file mode 100644 index 634f28146..000000000 --- a/apps/dokploy/components/auth/login-2fa.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; - -import { CardTitle } from "@/components/ui/card"; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot, -} from "@/components/ui/input-otp"; -import { api } from "@/utils/api"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { REGEXP_ONLY_DIGITS } from "input-otp"; -import { AlertTriangle } from "lucide-react"; -import { useRouter } from "next/router"; -import { useEffect } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -const Login2FASchema = z.object({ - pin: z.string().min(6, { - message: "Pin is required", - }), -}); - -type Login2FA = z.infer; - -interface Props { - authId: string; -} - -export const Login2FA = ({ authId }: Props) => { - const { push } = useRouter(); - - const { mutateAsync, isLoading, isError, error } = - api.auth.verifyLogin2FA.useMutation(); - - const form = useForm({ - defaultValues: { - pin: "", - }, - resolver: zodResolver(Login2FASchema), - }); - - useEffect(() => { - form.reset({ - pin: "", - }); - }, [form, form.reset, form.formState.isSubmitSuccessful]); - - const onSubmit = async (data: Login2FA) => { - await mutateAsync({ - pin: data.pin, - id: authId, - }) - .then(() => { - toast.success("Signin successfully", { - duration: 2000, - }); - - push("/dashboard/projects"); - }) - .catch(() => { - toast.error("Signin failed", { - duration: 2000, - }); - }); - }; - return ( -
- - {isError && ( -
- - - {error?.message} - -
- )} - 2FA Login - - ( - - Pin - -
- - - - - - - - - - -
-
- - Please enter the 6 digits code provided by your authenticator - app. - - -
- )} - /> - - - - ); -}; diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx index e299cfb38..e92ce03fc 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx @@ -72,7 +72,7 @@ export const ShowPaidMonitoring = ({ data, isLoading, error: queryError, - } = api.user.getServerMetrics.useQuery( + } = api.server.getServerMetrics.useQuery( { url: BASE_URL, token, diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index 3ececb649..28fba67df 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -44,6 +44,12 @@ const PinSchema = z.object({ }), }); +type TwoFactorSetupData = { + qrCodeUrl: string; + secret: string; + totpURI: string; +}; + type PasswordForm = z.infer; type PinForm = z.infer; diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server-ip.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server-ip.tsx index 3a511d8ea..afe1e4c36 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server-ip.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server-ip.tsx @@ -80,7 +80,7 @@ export const UpdateServerIp = ({ children }: Props) => { }) .then(async () => { toast.success("Server IP Updated"); - await utils.admin.one.invalidate(); + await utils.user.get.invalidate(); setIsOpen(false); }) .catch(() => { diff --git a/apps/dokploy/pages/api/stripe/webhook.ts b/apps/dokploy/pages/api/stripe/webhook.ts index 592803b15..9e8c9da5e 100644 --- a/apps/dokploy/pages/api/stripe/webhook.ts +++ b/apps/dokploy/pages/api/stripe/webhook.ts @@ -1,7 +1,7 @@ import { buffer } from "node:stream/consumers"; import { db } from "@/server/db"; import { organization, server, users_temp } from "@/server/db/schema"; -import { findUserById, type Server } from "@dokploy/server"; +import { type Server, findUserById } from "@dokploy/server"; import { asc, eq } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; import Stripe from "stripe"; diff --git a/apps/dokploy/pages/confirm-email.tsx b/apps/dokploy/pages/confirm-email.tsx deleted file mode 100644 index 2910a2677..000000000 --- a/apps/dokploy/pages/confirm-email.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { OnboardingLayout } from "@/components/layouts/onboarding-layout"; -import { Logo } from "@/components/shared/logo"; -import { CardDescription, CardTitle } from "@/components/ui/card"; -import { db } from "@/server/db"; -import { auth } from "@/server/db/schema"; -import { IS_CLOUD, updateAuthById } from "@dokploy/server"; -import { isBefore } from "date-fns"; -import { eq } from "drizzle-orm"; -import type { GetServerSidePropsContext } from "next"; -import Link from "next/link"; -import type { ReactElement } from "react"; - -export default function Home() { - return ( -
-
- - - Dokploy - - Email Confirmed - - Congratulations, your email is confirmed. - -
- - Click here to login - -
-
-
- ); -} - -Home.getLayout = (page: ReactElement) => { - return {page}; -}; -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.confirmationToken, token), - }); - - if ( - !authR || - authR?.confirmationToken === null || - authR?.confirmationExpiresAt === null - ) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - - const isExpired = isBefore(new Date(authR.confirmationExpiresAt), new Date()); - - if (isExpired) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - - await updateAuthById(authR.id, { - confirmationToken: null, - confirmationExpiresAt: null, - }); - - return { - props: { - token: authR.confirmationToken, - }, - }; -} diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index 783ec651d..4a85952e6 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -85,6 +85,7 @@ export default function Home({ IS_CLOUD }: Props) { return; } + // @ts-ignore if (data?.twoFactorRedirect as boolean) { setTwoFactorCode(""); setIsTwoFactor(true); diff --git a/apps/dokploy/pages/send-reset-password.tsx b/apps/dokploy/pages/send-reset-password.tsx index 0ea59cf85..8f6902f6e 100644 --- a/apps/dokploy/pages/send-reset-password.tsx +++ b/apps/dokploy/pages/send-reset-password.tsx @@ -1,4 +1,3 @@ -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"; @@ -126,9 +125,7 @@ export default function Home() { - ) : ( - - )} + ) : null}
diff --git a/apps/dokploy/reset-password.ts b/apps/dokploy/reset-password.ts index 43b11fdf6..32cab4334 100644 --- a/apps/dokploy/reset-password.ts +++ b/apps/dokploy/reset-password.ts @@ -1,6 +1,8 @@ import { findAdmin } from "@dokploy/server"; -import { updateAuthById } from "@dokploy/server"; import { generateRandomPassword } from "@dokploy/server"; +import { db } from "@dokploy/server/db"; +import { account } from "@dokploy/server/db/schema"; +import { eq } from "drizzle-orm"; (async () => { try { @@ -8,9 +10,12 @@ import { generateRandomPassword } from "@dokploy/server"; const result = await findAdmin(); - const update = await updateAuthById(result.authId, { - password: randomPassword.hashedPassword, - }); + const update = await db + .update(account) + .set({ + password: randomPassword.hashedPassword, + }) + .where(eq(account.userId, result.userId)); if (update) { console.log("Password reset successful"); diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index 31a50c67b..1a6c046e3 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -14,13 +14,11 @@ import { IS_CLOUD, findUserById, getUserByToken, - sendDiscordNotification, sendEmailNotification, validateRequest, } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; -import { isBefore } from "date-fns"; import { and, eq } from "drizzle-orm"; import { nanoid } from "nanoid"; import { z } from "zod"; @@ -321,157 +319,64 @@ export const authRouter = createTRPCRouter({ `, ); }), - - resetPassword: publicProcedure - .input( - z.object({ - resetPasswordToken: z.string().min(1), - password: z.string().min(1), - }), - ) - .mutation(async ({ 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; - }), - confirmEmail: adminProcedure - .input( - z.object({ - confirmationToken: z.string().min(1), - }), - ) - .mutation(async ({ input }) => { - if (!IS_CLOUD) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Functionality not available in cloud version", - }); - } - const authR = await db.query.auth.findFirst({ - where: eq(auth.confirmationToken, input.confirmationToken), - }); - if (!authR || authR.confirmationExpiresAt === null) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Token not found", - }); - } - if (authR.confirmationToken !== input.confirmationToken) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Confirmation Token not found", - }); - } - - const isExpired = isBefore( - new Date(authR.confirmationExpiresAt), - new Date(), - ); - - if (isExpired) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Confirmation Token expired", - }); - } - 1; - await updateAuthById(authR.id, { - confirmationToken: null, - confirmationExpiresAt: null, - }); - return true; - }), }); -export const sendVerificationEmail = async (authId: string) => { - const token = nanoid(); - const result = await updateAuthById(authId, { - confirmationToken: token, - confirmationExpiresAt: new Date( - new Date().getTime() + 24 * 60 * 60 * 1000, - ).toISOString(), - }); +// export const sendVerificationEmail = async (authId: string) => { +// const token = nanoid(); +// const result = await updateAuthById(authId, { +// confirmationToken: token, +// confirmationExpiresAt: new Date( +// new Date().getTime() + 24 * 60 * 60 * 1000, +// ).toISOString(), +// }); - if (!result) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "User not found", - }); - } - await sendEmailNotification( - { - fromAddress: process.env.SMTP_FROM_ADDRESS || "", - toAddresses: [result?.email], - smtpServer: process.env.SMTP_SERVER || "", - smtpPort: Number(process.env.SMTP_PORT), - username: process.env.SMTP_USERNAME || "", - password: process.env.SMTP_PASSWORD || "", - }, - "Confirm your email | Dokploy", - ` - Welcome to Dokploy! - Please confirm your email by clicking the link below: - - Confirm Email - - `, - ); +// if (!result) { +// throw new TRPCError({ +// code: "BAD_REQUEST", +// message: "User not found", +// }); +// } +// await sendEmailNotification( +// { +// fromAddress: process.env.SMTP_FROM_ADDRESS || "", +// toAddresses: [result?.email], +// smtpServer: process.env.SMTP_SERVER || "", +// smtpPort: Number(process.env.SMTP_PORT), +// username: process.env.SMTP_USERNAME || "", +// password: process.env.SMTP_PASSWORD || "", +// }, +// "Confirm your email | Dokploy", +// ` +// Welcome to Dokploy! +// Please confirm your email by clicking the link below: +// +// Confirm Email +// +// `, +// ); - return true; -}; +// return true; +// }; -export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => { - await sendDiscordNotification( - { - webhookUrl: process.env.DISCORD_WEBHOOK_URL || "", - }, - { - title: "New User Registered", - color: 0x00ff00, - fields: [ - { - name: "Email", - value: newAdmin.email, - inline: true, - }, - ], - timestamp: newAdmin.createdAt, - footer: { - text: "Dokploy User Registration Notification", - }, - }, - ); -}; +// export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => { +// await sendDiscordNotification( +// { +// webhookUrl: process.env.DISCORD_WEBHOOK_URL || "", +// }, +// { +// title: "New User Registered", +// color: 0x00ff00, +// fields: [ +// { +// name: "Email", +// value: newAdmin.email, +// inline: true, +// }, +// ], +// timestamp: newAdmin.createdAt, +// footer: { +// text: "Dokploy User Registration Notification", +// }, +// }, +// ); +// }; diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index 1ebb161a4..1a9ebc0ac 100644 --- a/apps/dokploy/server/api/routers/server.ts +++ b/apps/dokploy/server/api/routers/server.ts @@ -37,6 +37,7 @@ import { import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm"; +import { z } from "zod"; export const serverRouter = createTRPCRouter({ create: protectedProcedure @@ -378,4 +379,62 @@ export const serverRouter = createTRPCRouter({ const ip = await getPublicIpWithFallback(); return ip; }), + getServerMetrics: protectedProcedure + .input( + z.object({ + url: z.string(), + token: z.string(), + dataPoints: z.string(), + }), + ) + .query(async ({ input }) => { + try { + const url = new URL(input.url); + url.searchParams.append("limit", input.dataPoints); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${input.token}`, + }, + }); + if (!response.ok) { + throw new Error( + `Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`, + ); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error( + [ + "No monitoring data available. This could be because:", + "", + "1. You don't have setup the monitoring service, you can do in web server section.", + "2. If you already have setup the monitoring service, wait a few minutes and refresh the page.", + ].join("\n"), + ); + } + return data as { + cpu: string; + cpuModel: string; + cpuCores: number; + cpuPhysicalCores: number; + cpuSpeed: number; + os: string; + distro: string; + kernel: string; + arch: string; + memUsed: string; + memUsedGB: string; + memTotal: string; + uptime: number; + diskUsed: string; + totalDisk: string; + networkIn: string; + networkOut: string; + timestamp: string; + }[]; + } catch (error) { + throw error; + } + }), }); diff --git a/apps/dokploy/server/api/routers/user.ts b/apps/dokploy/server/api/routers/user.ts index 43cffd9e1..5717d59bf 100644 --- a/apps/dokploy/server/api/routers/user.ts +++ b/apps/dokploy/server/api/routers/user.ts @@ -143,4 +143,63 @@ export const userRouter = createTRPCRouter({ }, }); }), + + getContainerMetrics: protectedProcedure + .input( + z.object({ + url: z.string(), + token: z.string(), + appName: z.string(), + dataPoints: z.string(), + }), + ) + .query(async ({ input }) => { + try { + if (!input.appName) { + throw new Error( + [ + "No Application Selected:", + "", + "Make Sure to select an application to monitor.", + ].join("\n"), + ); + } + const url = new URL(`${input.url}/metrics/containers`); + url.searchParams.append("limit", input.dataPoints); + url.searchParams.append("appName", input.appName); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${input.token}`, + }, + }); + if (!response.ok) { + throw new Error( + `Error ${response.status}: ${response.statusText}. Please verify that the application "${input.appName}" is running and this service is included in the monitoring configuration.`, + ); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error( + [ + `No monitoring data available for "${input.appName}". This could be because:`, + "", + "1. The container was recently started - wait a few minutes for data to be collected", + "2. The container is not running - verify its status", + "3. The service is not included in your monitoring configuration", + ].join("\n"), + ); + } + return data as { + containerId: string; + containerName: string; + containerImage: string; + containerLabels: string; + containerCommand: string; + containerCreated: string; + }[]; + } catch (error) { + throw error; + } + }), }); diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index 78df14aa4..099018f2b 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -108,6 +108,23 @@ export const isAdminPresent = async () => { return true; }; +export const findAdmin = async () => { + const admin = await db.query.member.findFirst({ + where: eq(member.role, "owner"), + with: { + user: true, + }, + }); + + if (!admin) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Admin not found", + }); + } + return admin; +}; + export const getUserByToken = async (token: string) => { const user = await db.query.invitation.findFirst({ where: eq(invitation.id, token), @@ -154,8 +171,8 @@ export const getDokployUrl = async () => { } const admin = await findAdmin(); - if (admin.host) { - return `https://${admin.host}`; + if (admin.user.host) { + return `https://${admin.user.host}`; } - return `http://${admin.serverIp}:${process.env.PORT}`; + return `http://${admin.user.serverIp}:${process.env.PORT}`; };