mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: update reset password and authentication flows
This commit removes several authentication-related components and simplifies the password reset process: - Removed login-2fa component - Deleted confirm-email page - Updated reset password logic to use Drizzle ORM directly - Removed unused authentication-related functions - Simplified server-side authentication routes
This commit is contained in:
@@ -51,20 +51,9 @@ const baseAdmin: User = {
|
|||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
accessedProjects: [],
|
|
||||||
accessedServices: [],
|
|
||||||
banExpires: new Date(),
|
banExpires: new Date(),
|
||||||
banned: true,
|
banned: true,
|
||||||
banReason: "",
|
banReason: "",
|
||||||
canAccessToAPI: false,
|
|
||||||
canCreateProjects: false,
|
|
||||||
canDeleteProjects: false,
|
|
||||||
canDeleteServices: false,
|
|
||||||
canAccessToDocker: false,
|
|
||||||
canAccessToSSHKeys: false,
|
|
||||||
canCreateServices: false,
|
|
||||||
canAccessToTraefikFiles: false,
|
|
||||||
canAccessToGitProviders: false,
|
|
||||||
email: "",
|
email: "",
|
||||||
expirationDate: "",
|
expirationDate: "",
|
||||||
id: "",
|
id: "",
|
||||||
@@ -73,7 +62,6 @@ const baseAdmin: User = {
|
|||||||
createdAt2: new Date().toISOString(),
|
createdAt2: new Date().toISOString(),
|
||||||
emailVerified: false,
|
emailVerified: false,
|
||||||
image: "",
|
image: "",
|
||||||
token: "",
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
twoFactorEnabled: false,
|
twoFactorEnabled: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<typeof Login2FASchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
authId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Login2FA = ({ authId }: Props) => {
|
|
||||||
const { push } = useRouter();
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, isError, error } =
|
|
||||||
api.auth.verifyLogin2FA.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<Login2FA>({
|
|
||||||
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 (
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center 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>
|
|
||||||
)}
|
|
||||||
<CardTitle className="text-xl font-bold">2FA Login</CardTitle>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="pin"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col max-sm:items-center">
|
|
||||||
<FormLabel>Pin</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="flex">
|
|
||||||
<InputOTP
|
|
||||||
maxLength={6}
|
|
||||||
{...field}
|
|
||||||
pattern={REGEXP_ONLY_DIGITS}
|
|
||||||
>
|
|
||||||
<InputOTPGroup>
|
|
||||||
<InputOTPSlot index={0} className="border-border" />
|
|
||||||
<InputOTPSlot index={1} className="border-border" />
|
|
||||||
<InputOTPSlot index={2} className="border-border" />
|
|
||||||
<InputOTPSlot index={3} className="border-border" />
|
|
||||||
<InputOTPSlot index={4} className="border-border" />
|
|
||||||
<InputOTPSlot index={5} className="border-border" />
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Please enter the 6 digits code provided by your authenticator
|
|
||||||
app.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button isLoading={isLoading} type="submit">
|
|
||||||
Submit 2FA
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -72,7 +72,7 @@ export const ShowPaidMonitoring = ({
|
|||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
error: queryError,
|
error: queryError,
|
||||||
} = api.user.getServerMetrics.useQuery(
|
} = api.server.getServerMetrics.useQuery(
|
||||||
{
|
{
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ const PinSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type TwoFactorSetupData = {
|
||||||
|
qrCodeUrl: string;
|
||||||
|
secret: string;
|
||||||
|
totpURI: string;
|
||||||
|
};
|
||||||
|
|
||||||
type PasswordForm = z.infer<typeof PasswordSchema>;
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
type PinForm = z.infer<typeof PinSchema>;
|
type PinForm = z.infer<typeof PinSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Server IP Updated");
|
toast.success("Server IP Updated");
|
||||||
await utils.admin.one.invalidate();
|
await utils.user.get.invalidate();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { buffer } from "node:stream/consumers";
|
import { buffer } from "node:stream/consumers";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { organization, server, users_temp } from "@/server/db/schema";
|
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 { asc, eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<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">Email Confirmed</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Congratulations, your email is confirmed.
|
|
||||||
</CardDescription>
|
|
||||||
<div>
|
|
||||||
<Link href="/" className="w-full text-primary">
|
|
||||||
Click here to login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</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.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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -85,6 +85,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
if (data?.twoFactorRedirect as boolean) {
|
if (data?.twoFactorRedirect as boolean) {
|
||||||
setTwoFactorCode("");
|
setTwoFactorCode("");
|
||||||
setIsTwoFactor(true);
|
setIsTwoFactor(true);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Login2FA } from "@/components/auth/login-2fa";
|
|
||||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
@@ -126,9 +125,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
) : (
|
) : null}
|
||||||
<Login2FA authId={temp.authId} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-row justify-between flex-wrap">
|
<div className="flex flex-row justify-between flex-wrap">
|
||||||
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { findAdmin } from "@dokploy/server";
|
import { findAdmin } from "@dokploy/server";
|
||||||
import { updateAuthById } from "@dokploy/server";
|
|
||||||
import { generateRandomPassword } 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 () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -8,9 +10,12 @@ import { generateRandomPassword } from "@dokploy/server";
|
|||||||
|
|
||||||
const result = await findAdmin();
|
const result = await findAdmin();
|
||||||
|
|
||||||
const update = await updateAuthById(result.authId, {
|
const update = await db
|
||||||
password: randomPassword.hashedPassword,
|
.update(account)
|
||||||
});
|
.set({
|
||||||
|
password: randomPassword.hashedPassword,
|
||||||
|
})
|
||||||
|
.where(eq(account.userId, result.userId));
|
||||||
|
|
||||||
if (update) {
|
if (update) {
|
||||||
console.log("Password reset successful");
|
console.log("Password reset successful");
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ import {
|
|||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
findUserById,
|
findUserById,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
sendDiscordNotification,
|
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { isBefore } from "date-fns";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
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) => {
|
// export const sendVerificationEmail = async (authId: string) => {
|
||||||
const token = nanoid();
|
// const token = nanoid();
|
||||||
const result = await updateAuthById(authId, {
|
// const result = await updateAuthById(authId, {
|
||||||
confirmationToken: token,
|
// confirmationToken: token,
|
||||||
confirmationExpiresAt: new Date(
|
// confirmationExpiresAt: new Date(
|
||||||
new Date().getTime() + 24 * 60 * 60 * 1000,
|
// new Date().getTime() + 24 * 60 * 60 * 1000,
|
||||||
).toISOString(),
|
// ).toISOString(),
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (!result) {
|
// if (!result) {
|
||||||
throw new TRPCError({
|
// throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
// code: "BAD_REQUEST",
|
||||||
message: "User not found",
|
// message: "User not found",
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
await sendEmailNotification(
|
// await sendEmailNotification(
|
||||||
{
|
// {
|
||||||
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
|
// fromAddress: process.env.SMTP_FROM_ADDRESS || "",
|
||||||
toAddresses: [result?.email],
|
// toAddresses: [result?.email],
|
||||||
smtpServer: process.env.SMTP_SERVER || "",
|
// smtpServer: process.env.SMTP_SERVER || "",
|
||||||
smtpPort: Number(process.env.SMTP_PORT),
|
// smtpPort: Number(process.env.SMTP_PORT),
|
||||||
username: process.env.SMTP_USERNAME || "",
|
// username: process.env.SMTP_USERNAME || "",
|
||||||
password: process.env.SMTP_PASSWORD || "",
|
// password: process.env.SMTP_PASSWORD || "",
|
||||||
},
|
// },
|
||||||
"Confirm your email | Dokploy",
|
// "Confirm your email | Dokploy",
|
||||||
`
|
// `
|
||||||
Welcome to Dokploy!
|
// Welcome to Dokploy!
|
||||||
Please confirm your email by clicking the link below:
|
// Please confirm your email by clicking the link below:
|
||||||
<a href="${WEBSITE_URL}/confirm-email?token=${result?.confirmationToken}">
|
// <a href="${WEBSITE_URL}/confirm-email?token=${result?.confirmationToken}">
|
||||||
Confirm Email
|
// Confirm Email
|
||||||
</a>
|
// </a>
|
||||||
`,
|
// `,
|
||||||
);
|
// );
|
||||||
|
|
||||||
return true;
|
// return true;
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
|
// export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
|
||||||
await sendDiscordNotification(
|
// await sendDiscordNotification(
|
||||||
{
|
// {
|
||||||
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
// webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
title: "New User Registered",
|
// title: "New User Registered",
|
||||||
color: 0x00ff00,
|
// color: 0x00ff00,
|
||||||
fields: [
|
// fields: [
|
||||||
{
|
// {
|
||||||
name: "Email",
|
// name: "Email",
|
||||||
value: newAdmin.email,
|
// value: newAdmin.email,
|
||||||
inline: true,
|
// inline: true,
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
timestamp: newAdmin.createdAt,
|
// timestamp: newAdmin.createdAt,
|
||||||
footer: {
|
// footer: {
|
||||||
text: "Dokploy User Registration Notification",
|
// text: "Dokploy User Registration Notification",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export const serverRouter = createTRPCRouter({
|
export const serverRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -378,4 +379,62 @@ export const serverRouter = createTRPCRouter({
|
|||||||
const ip = await getPublicIpWithFallback();
|
const ip = await getPublicIpWithFallback();
|
||||||
return ip;
|
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;
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,6 +108,23 @@ export const isAdminPresent = async () => {
|
|||||||
return true;
|
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) => {
|
export const getUserByToken = async (token: string) => {
|
||||||
const user = await db.query.invitation.findFirst({
|
const user = await db.query.invitation.findFirst({
|
||||||
where: eq(invitation.id, token),
|
where: eq(invitation.id, token),
|
||||||
@@ -154,8 +171,8 @@ export const getDokployUrl = async () => {
|
|||||||
}
|
}
|
||||||
const admin = await findAdmin();
|
const admin = await findAdmin();
|
||||||
|
|
||||||
if (admin.host) {
|
if (admin.user.host) {
|
||||||
return `https://${admin.host}`;
|
return `https://${admin.user.host}`;
|
||||||
}
|
}
|
||||||
return `http://${admin.serverIp}:${process.env.PORT}`;
|
return `http://${admin.user.serverIp}:${process.env.PORT}`;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user