refactor: migrate authentication routes to user router and update related components

This commit continues the refactoring of authentication-related code by:

- Moving authentication routes from `auth.ts` to `user.ts`
- Updating import paths and function calls across components
- Removing commented-out authentication code
- Simplifying user-related queries and mutations
- Updating server-side authentication handling
This commit is contained in:
Mauricio Siu
2025-02-22 22:02:12 -06:00
parent b00c12965a
commit 0478419f7c
30 changed files with 394 additions and 615 deletions

View File

@@ -61,7 +61,7 @@ export const Disable2FA = () => {
} }
toast.success("2FA disabled successfully"); toast.success("2FA disabled successfully");
utils.auth.get.invalidate(); utils.user.get.invalidate();
setIsOpen(false); setIsOpen(false);
} catch (_error) { } catch (_error) {
form.setError("password", { form.setError("password", {

View File

@@ -125,7 +125,7 @@ export const Enable2FA = () => {
} }
toast.success("2FA configured successfully"); toast.success("2FA configured successfully");
utils.auth.get.invalidate(); utils.user.get.invalidate();
setIsDialogOpen(false); setIsDialogOpen(false);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {

View File

@@ -17,7 +17,7 @@ export const GenerateToken = () => {
const { data, refetch } = api.user.get.useQuery(); const { data, refetch } = api.user.get.useQuery();
const { mutateAsync: generateToken, isLoading: isLoadingToken } = const { mutateAsync: generateToken, isLoading: isLoadingToken } =
api.auth.generateToken.useMutation(); api.user.generateToken.useMutation();
return ( return (
<div className="w-full"> <div className="w-full">
@@ -51,7 +51,7 @@ export const GenerateToken = () => {
<Label>Token</Label> <Label>Token</Label>
<ToggleVisibilityInput <ToggleVisibilityInput
placeholder="Token" placeholder="Token"
value={data?.user?.token || ""} value={data || ""}
disabled disabled
/> />
</div> </div>

View File

@@ -1,138 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
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 { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "next-i18next";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const profileSchema = z.object({
password: z.string().min(1, {
message: "Password is required",
}),
});
type Profile = z.infer<typeof profileSchema>;
export const RemoveSelfAccount = () => {
const { data } = api.user.get.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.auth.removeSelfAccount.useMutation();
const { t } = useTranslation("settings");
const router = useRouter();
const form = useForm<Profile>({
defaultValues: {
password: "",
},
resolver: zodResolver(profileSchema),
});
useEffect(() => {
if (data) {
form.reset({
password: "",
});
}
form.reset();
}, [form, form.reset, data]);
const onSubmit = async (values: Profile) => {
await mutateAsync({
password: values.password,
})
.then(async () => {
toast.success("Profile Deleted");
router.push("/");
})
.catch(() => {});
};
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div>
<CardTitle className="text-xl">Remove Self Account</CardTitle>
<CardDescription>
If you want to remove your account, you can do it here
</CardDescription>
</div>
</CardHeader>
<CardContent className="space-y-2">
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
onSubmit={(e) => e.preventDefault()}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
}
}}
className="grid gap-4"
>
<div className="space-y-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.profile.password")}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t("settings.profile.password")}
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</form>
</Form>
<div className="flex justify-end pt-2">
<DialogAction
title="Are you sure you want to remove your account?"
description="This action cannot be undone, all your projects/servers will be deleted."
onClick={() => form.handleSubmit(onSubmit)()}
>
<Button
type="button"
isLoading={isLoading}
variant="destructive"
>
Remove
</Button>
</DialogAction>
</div>
</CardContent>
</div>
</Card>
</div>
);
};

View File

@@ -89,7 +89,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
enabled: !!serverId, enabled: !!serverId,
}, },
) )
: api.user.get.useQuery(); : api.user.getServerMetrics.useQuery();
const url = useUrl(); const url = useUrl();

View File

@@ -89,7 +89,7 @@ import { UpdateServerButton } from "./update-server";
import { UserNav } from "./user-nav"; import { UserNav } from "./user-nav";
// The types of the queries we are going to use // The types of the queries we are going to use
type AuthQueryOutput = inferRouterOutputs<AppRouter>["auth"]["get"]; type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
type SingleNavItem = { type SingleNavItem = {
isSingle?: true; isSingle?: true;

View File

@@ -1,17 +1,11 @@
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc"; import { createTRPCContext } from "@/server/api/trpc";
import { validateBearerToken, validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createOpenApiNextHandler } from "@dokploy/trpc-openapi"; import { createOpenApiNextHandler } from "@dokploy/trpc-openapi";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
let { session, user } = await validateBearerToken(req); const { session, user } = await validateRequest(req);
if (!session) {
const cookieResult = await validateRequest(req, res);
session = cookieResult.session;
user = cookieResult.user;
}
if (!user || !session) { if (!user || !session) {
res.status(401).json({ message: "Unauthorized" }); res.status(401).json({ message: "Unauthorized" });

View File

@@ -52,7 +52,7 @@ export async function getServerSideProps(
}); });
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
if (!user) { if (!user) {
return { return {
redirect: { redirect: {

View File

@@ -52,7 +52,7 @@ export async function getServerSideProps(
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();

View File

@@ -45,7 +45,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();
return { return {

View File

@@ -53,7 +53,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
return { return {
props: { props: {

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();
return { return {

View File

@@ -45,7 +45,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
try { try {
await helpers.project.all.prefetch(); await helpers.project.all.prefetch();
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();

View File

@@ -209,7 +209,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
return { return {
props: { props: {

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();
return { return {

View File

@@ -1,6 +1,5 @@
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token"; import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form"; import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { RemoveSelfAccount } from "@/components/dashboard/settings/profile/remove-self-account";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
@@ -15,14 +14,14 @@ import superjson from "superjson";
const Page = () => { const Page = () => {
const { data } = api.user.get.useQuery(); const { data } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); // const { data: isCloud } = api.settings.isCloud.useQuery();
return ( return (
<div className="w-full"> <div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4"> <div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
<ProfileForm /> <ProfileForm />
{(data?.canAccessToAPI || data?.role === "owner") && <GenerateToken />} {(data?.canAccessToAPI || data?.role === "owner") && <GenerateToken />}
{isCloud && <RemoveSelfAccount />} {/* {isCloud && <RemoveSelfAccount />} */}
</div> </div>
</div> </div>
); );
@@ -53,15 +52,7 @@ export async function getServerSideProps(
}); });
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
if (user?.role === "member") {
// const userR = await helpers.user.one.fetch({
// userId: user.id,
// });
// await helpers.user.byAuthId.prefetch({
// authId: user.authId,
// });
}
if (!user) { if (!user) {
return { return {

View File

@@ -45,7 +45,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();
return { return {

View File

@@ -110,7 +110,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
return { return {
props: { props: {

View File

@@ -56,7 +56,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();
return { return {

View File

@@ -50,7 +50,7 @@ export async function getServerSideProps(
}, },
transformer: superjson, transformer: superjson,
}); });
await helpers.auth.get.prefetch(); await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch(); await helpers.settings.isCloud.prefetch();
return { return {

View File

@@ -12,17 +12,13 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { db } from "@/server/db"; import { authClient } from "@/lib/auth-client";
import { auth } from "@/server/db/schema";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server"; import { IS_CLOUD } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { type ReactElement, useEffect } from "react"; import { type ReactElement, useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -54,11 +50,12 @@ const loginSchema = z
type Login = z.infer<typeof loginSchema>; type Login = z.infer<typeof loginSchema>;
interface Props { interface Props {
token: string; tokenResetPassword: string;
} }
export default function Home({ token }: Props) { export default function Home({ tokenResetPassword }: Props) {
const { mutateAsync, isLoading, isError, error } = const [token, setToken] = useState<string | null>(tokenResetPassword);
api.auth.resetPassword.useMutation(); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter(); const router = useRouter();
const form = useForm<Login>({ const form = useForm<Login>({
defaultValues: { defaultValues: {
@@ -68,26 +65,32 @@ export default function Home({ token }: Props) {
resolver: zodResolver(loginSchema), resolver: zodResolver(loginSchema),
}); });
useEffect(() => {
const token = new URLSearchParams(window.location.search).get("token");
if (token) {
setToken(token);
}
}, [token]);
useEffect(() => { useEffect(() => {
form.reset(); form.reset();
}, [form, form.reset, form.formState.isSubmitSuccessful]); }, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => { const onSubmit = async (values: Login) => {
await mutateAsync({ setIsLoading(true);
resetPasswordToken: token, const { error } = await authClient.resetPassword({
password: values.password, newPassword: values.password,
}) token: token || "",
.then((_data) => { });
toast.success("Password reset successfully", {
duration: 2000, if (error) {
}); setError(error.message || "An error occurred");
router.push("/"); } else {
}) toast.success("Password reset successfully");
.catch(() => { router.push("/");
toast.error("Error resetting password", { }
duration: 2000, setIsLoading(false);
});
});
}; };
return ( return (
<div className="flex h-screen w-full items-center justify-center "> <div className="flex h-screen w-full items-center justify-center ">
@@ -104,9 +107,9 @@ export default function Home({ token }: Props) {
<div className="w-full"> <div className="w-full">
<CardContent className="p-0"> <CardContent className="p-0">
{isError && ( {error && (
<AlertBlock type="error" className="my-2"> <AlertBlock type="error" className="my-2">
{error?.message} {error}
</AlertBlock> </AlertBlock>
)} )}
<Form {...form}> <Form {...form}>
@@ -194,35 +197,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
}; };
} }
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 { return {
props: { props: {
token: authR.resetPasswordToken, tokenResetPassword: token,
}, },
}; };
} }

View File

@@ -12,7 +12,7 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { authClient } from "@/lib/auth-client";
import { IS_CLOUD } from "@dokploy/server"; import { IS_CLOUD } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
@@ -46,8 +46,9 @@ export default function Home() {
is2FAEnabled: false, is2FAEnabled: false,
authId: "", authId: "",
}); });
const { mutateAsync, isLoading, isError, error } =
api.auth.sendResetPasswordEmail.useMutation(); const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const _router = useRouter(); const _router = useRouter();
const form = useForm<Login>({ const form = useForm<Login>({
defaultValues: { defaultValues: {
@@ -61,19 +62,20 @@ export default function Home() {
}, [form, form.reset, form.formState.isSubmitSuccessful]); }, [form, form.reset, form.formState.isSubmitSuccessful]);
const onSubmit = async (values: Login) => { const onSubmit = async (values: Login) => {
await mutateAsync({ setIsLoading(true);
const { error } = await authClient.forgetPassword({
email: values.email, email: values.email,
}) redirectTo: "/reset-password",
.then((_data) => { });
toast.success("Email sent", { if (error) {
duration: 2000, setError(error.message || "An error occurred");
}); setIsLoading(false);
}) } else {
.catch(() => { toast.success("Email sent", {
toast.error("Error sending email", { duration: 2000,
duration: 2000,
});
}); });
}
setIsLoading(false);
}; };
return ( return (
<div className="flex w-full items-center justify-center "> <div className="flex w-full items-center justify-center ">
@@ -89,9 +91,9 @@ export default function Home() {
<div className="mx-auto w-full max-w-lg bg-transparent "> <div className="mx-auto w-full max-w-lg bg-transparent ">
<CardContent className="p-0"> <CardContent className="p-0">
{isError && ( {error && (
<AlertBlock type="error" className="my-2"> <AlertBlock type="error" className="my-2">
{error?.message} {error}
</AlertBlock> </AlertBlock>
)} )}
{!temp.is2FAEnabled ? ( {!temp.is2FAEnabled ? (

View File

@@ -31,294 +31,268 @@ import {
} from "../trpc"; } from "../trpc";
export const authRouter = createTRPCRouter({ export const authRouter = createTRPCRouter({
createAdmin: publicProcedure.mutation(async ({ input }) => { // createAdmin: publicProcedure.mutation(async ({ input }) => {
try { // try {
if (!IS_CLOUD) { // if (!IS_CLOUD) {
const admin = await db.query.admins.findFirst({}); // const admin = await db.query.admins.findFirst({});
if (admin) { // if (admin) {
throw new TRPCError({ // throw new TRPCError({
code: "BAD_REQUEST", // code: "BAD_REQUEST",
message: "Admin already exists", // message: "Admin already exists",
}); // });
} // }
} // }
const newAdmin = await createAdmin(input); // const newAdmin = await createAdmin(input);
// if (IS_CLOUD) {
if (IS_CLOUD) { // await sendDiscordNotificationWelcome(newAdmin);
await sendDiscordNotificationWelcome(newAdmin); // await sendVerificationEmail(newAdmin.id);
await sendVerificationEmail(newAdmin.id); // return {
return { // status: "success",
status: "success", // type: "cloud",
type: "cloud", // };
}; // }
} // // const session = await lucia.createSession(newAdmin.id || "", {});
// const session = await lucia.createSession(newAdmin.id || "", {}); // // ctx.res.appendHeader(
// ctx.res.appendHeader( // // "Set-Cookie",
// "Set-Cookie", // // lucia.createSessionCookie(session.id).serialize(),
// lucia.createSessionCookie(session.id).serialize(), // // );
// ); // return {
return { // status: "success",
status: "success", // type: "selfhosted",
type: "selfhosted", // };
}; // } catch (error) {
} catch (error) { // throw new TRPCError({
throw new TRPCError({ // code: "BAD_REQUEST",
code: "BAD_REQUEST", // // @ts-ignore
// @ts-ignore // message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`,
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error creating admin"}`, // cause: error,
cause: error, // });
}); // }
} // }),
}), // createUser: publicProcedure.mutation(async ({ input }) => {
createUser: publicProcedure.mutation(async ({ input }) => { // try {
try { // const _token = await getUserByToken(input.token);
const _token = await getUserByToken(input.token); // // if (token.isExpired) {
// if (token.isExpired) { // // throw new TRPCError({
// throw new TRPCError({ // // code: "BAD_REQUEST",
// code: "BAD_REQUEST", // // message: "Invalid token",
// message: "Invalid token", // // });
// }); // // }
// } // // const newUser = await createUser(input);
// // if (IS_CLOUD) {
// const newUser = await createUser(input); // // await sendVerificationEmail(token.authId);
// // return true;
// if (IS_CLOUD) { // // }
// await sendVerificationEmail(token.authId); // // const session = await lucia.createSession(newUser?.authId || "", {});
// return true; // // ctx.res.appendHeader(
// } // // "Set-Cookie",
// const session = await lucia.createSession(newUser?.authId || "", {}); // // lucia.createSessionCookie(session.id).serialize(),
// ctx.res.appendHeader( // // );
// "Set-Cookie", // return true;
// lucia.createSessionCookie(session.id).serialize(), // } catch (error) {
// ); // throw new TRPCError({
return true; // code: "BAD_REQUEST",
} catch (error) { // message: "Error creating the user",
throw new TRPCError({ // cause: error,
code: "BAD_REQUEST", // });
message: "Error creating the user", // }
cause: error, // }),
}); // login: publicProcedure.mutation(async ({ input }) => {
} // try {
}), // const auth = await findAuthByEmail(input.email);
// const correctPassword = bcrypt.compareSync(
login: publicProcedure.mutation(async ({ input }) => { // input.password,
try { // auth?.password || "",
const auth = await findAuthByEmail(input.email); // );
// if (!correctPassword) {
const correctPassword = bcrypt.compareSync( // throw new TRPCError({
input.password, // code: "BAD_REQUEST",
auth?.password || "", // message: "Credentials do not match",
); // });
// }
if (!correctPassword) { // if (auth?.confirmationToken && IS_CLOUD) {
throw new TRPCError({ // await sendVerificationEmail(auth.id);
code: "BAD_REQUEST", // throw new TRPCError({
message: "Credentials do not match", // code: "BAD_REQUEST",
}); // message:
} // "Email not confirmed, we have sent you a confirmation email please check your inbox.",
// });
if (auth?.confirmationToken && IS_CLOUD) { // }
await sendVerificationEmail(auth.id); // if (auth?.is2FAEnabled) {
throw new TRPCError({ // return {
code: "BAD_REQUEST", // is2FAEnabled: true,
message: // authId: auth.id,
"Email not confirmed, we have sent you a confirmation email please check your inbox.", // };
}); // }
} // // const session = await lucia.createSession(auth?.id || "", {});
// // ctx.res.appendHeader(
if (auth?.is2FAEnabled) { // // "Set-Cookie",
return { // // lucia.createSessionCookie(session.id).serialize(),
is2FAEnabled: true, // // );
authId: auth.id, // return {
}; // is2FAEnabled: false,
} // authId: auth?.id,
// };
// const session = await lucia.createSession(auth?.id || "", {}); // } catch (error) {
// throw new TRPCError({
// ctx.res.appendHeader( // code: "BAD_REQUEST",
// "Set-Cookie", // message: `Error: ${error instanceof Error ? error.message : "Login error"}`,
// lucia.createSessionCookie(session.id).serialize(), // cause: error,
// ); // });
return { // }
is2FAEnabled: false, // }),
authId: auth?.id, // get: protectedProcedure.query(async ({ ctx }) => {
}; // const memberResult = await db.query.member.findFirst({
} catch (error) { // where: and(
throw new TRPCError({ // eq(member.userId, ctx.user.id),
code: "BAD_REQUEST", // eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
message: `Error: ${error instanceof Error ? error.message : "Login error"}`, // ),
cause: error, // with: {
}); // user: true,
} // },
}), // });
// return memberResult;
get: protectedProcedure.query(async ({ ctx }) => { // }),
const memberResult = await db.query.member.findFirst({ // logout: protectedProcedure.mutation(async ({ ctx }) => {
where: and( // const { req } = ctx;
eq(member.userId, ctx.user.id), // const { session } = await validateRequest(req);
eq(member.organizationId, ctx.session?.activeOrganizationId || ""), // if (!session) return false;
), // // await lucia.invalidateSession(session.id);
with: { // // res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
user: true, // return true;
}, // }),
}); // update: protectedProcedure.mutation(async ({ ctx, input }) => {
// const currentAuth = await findAuthByEmail(ctx.user.email);
return memberResult; // if (input.currentPassword || input.password) {
}), // const correctPassword = bcrypt.compareSync(
// input.currentPassword || "",
logout: protectedProcedure.mutation(async ({ ctx }) => { // currentAuth?.password || "",
const { req } = ctx; // );
const { session } = await validateRequest(req); // if (!correctPassword) {
if (!session) return false; // throw new TRPCError({
// code: "BAD_REQUEST",
// await lucia.invalidateSession(session.id); // message: "Current password is incorrect",
// res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); // });
return true; // }
}), // }
// // const auth = await updateAuthById(ctx.user.authId, {
update: protectedProcedure.mutation(async ({ ctx, input }) => { // // ...(input.email && { email: input.email.toLowerCase() }),
const currentAuth = await findAuthByEmail(ctx.user.email); // // ...(input.password && {
// // password: bcrypt.hashSync(input.password, 10),
if (input.currentPassword || input.password) { // // }),
const correctPassword = bcrypt.compareSync( // // ...(input.image && { image: input.image }),
input.currentPassword || "", // // });
currentAuth?.password || "", // return auth;
); // }),
if (!correctPassword) { // removeSelfAccount: protectedProcedure
throw new TRPCError({ // .input(
code: "BAD_REQUEST", // z.object({
message: "Current password is incorrect", // password: z.string().min(1),
}); // }),
} // )
} // .mutation(async ({ ctx, input }) => {
// const auth = await updateAuthById(ctx.user.authId, { // if (!IS_CLOUD) {
// ...(input.email && { email: input.email.toLowerCase() }), // throw new TRPCError({
// ...(input.password && { // code: "NOT_FOUND",
// password: bcrypt.hashSync(input.password, 10), // message: "This feature is only available in the cloud version",
// }), // });
// ...(input.image && { image: input.image }), // }
// }); // const currentAuth = await findAuthByEmail(ctx.user.email);
// const correctPassword = bcrypt.compareSync(
return auth; // input.password,
}), // currentAuth?.password || "",
removeSelfAccount: protectedProcedure // );
.input( // if (!correctPassword) {
z.object({ // throw new TRPCError({
password: z.string().min(1), // code: "BAD_REQUEST",
}), // message: "Password is incorrect",
) // });
.mutation(async ({ ctx, input }) => { // }
if (!IS_CLOUD) { // const { req } = ctx;
throw new TRPCError({ // const { session } = await validateRequest(req);
code: "NOT_FOUND", // if (!session) return false;
message: "This feature is only available in the cloud version", // // await lucia.invalidateSession(session.id);
}); // // res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
} // // if (ctx.user.rol === "owner") {
const currentAuth = await findAuthByEmail(ctx.user.email); // // await removeAdminByAuthId(ctx.user.authId);
// // } else {
const correctPassword = bcrypt.compareSync( // // await removeUserByAuthId(ctx.user.authId);
input.password, // // }
currentAuth?.password || "", // return true;
); // }),
// generateToken: protectedProcedure.mutation(async ({ ctx }) => {
if (!correctPassword) { // const auth = await findUserById(ctx.user.id);
throw new TRPCError({ // console.log(auth);
code: "BAD_REQUEST", // // if (auth.token) {
message: "Password is incorrect", // // await luciaToken.invalidateSession(auth.token);
}); // // }
} // // const session = await luciaToken.createSession(auth?.id || "", {
const { req } = ctx; // // expiresIn: 60 * 60 * 24 * 30,
const { session } = await validateRequest(req); // // });
if (!session) return false; // // await updateUser(auth.id, {
// // token: session.id,
// await lucia.invalidateSession(session.id); // // });
// res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize()); // return auth;
// }),
// if (ctx.user.rol === "owner") { // verifyToken: protectedProcedure.mutation(async () => {
// await removeAdminByAuthId(ctx.user.authId); // return true;
// } else { // }),
// await removeUserByAuthId(ctx.user.authId); // one: adminProcedure
// } // .input(z.object({ userId: z.string().min(1) }))
// .query(async ({ input }) => {
return true; // // TODO: Check if the user is admin or member
}), // const user = await findUserById(input.userId);
// return user;
generateToken: protectedProcedure.mutation(async ({ ctx }) => { // }),
const auth = await findUserById(ctx.user.id); // sendResetPasswordEmail: publicProcedure
console.log(auth); // .input(
// z.object({
// if (auth.token) { // email: z.string().min(1).email(),
// await luciaToken.invalidateSession(auth.token); // }),
// } // )
// const session = await luciaToken.createSession(auth?.id || "", { // .mutation(async ({ input }) => {
// expiresIn: 60 * 60 * 24 * 30, // if (!IS_CLOUD) {
// }); // throw new TRPCError({
// await updateUser(auth.id, { // code: "NOT_FOUND",
// token: session.id, // message: "This feature is only available in the cloud version",
// }); // });
return auth; // }
}), // const authR = await db.query.auth.findFirst({
verifyToken: protectedProcedure.mutation(async () => { // where: eq(auth.email, input.email),
return true; // });
}), // if (!authR) {
one: adminProcedure // throw new TRPCError({
.input(z.object({ userId: z.string().min(1) })) // code: "NOT_FOUND",
.query(async ({ input }) => { // message: "User not found",
// TODO: Check if the user is admin or member // });
const user = await findUserById(input.userId); // }
return user; // const token = nanoid();
}), // await updateAuthById(authR.id, {
sendResetPasswordEmail: publicProcedure // resetPasswordToken: token,
.input( // // Make resetPassword in 24 hours
z.object({ // resetPasswordExpiresAt: new Date(
email: z.string().min(1).email(), // new Date().getTime() + 24 * 60 * 60 * 1000,
}), // ).toISOString(),
) // });
.mutation(async ({ input }) => { // await sendEmailNotification(
if (!IS_CLOUD) { // {
throw new TRPCError({ // fromAddress: process.env.SMTP_FROM_ADDRESS!,
code: "NOT_FOUND", // toAddresses: [authR.email],
message: "This feature is only available in the cloud version", // smtpServer: process.env.SMTP_SERVER!,
}); // smtpPort: Number(process.env.SMTP_PORT),
} // username: process.env.SMTP_USERNAME!,
const authR = await db.query.auth.findFirst({ // password: process.env.SMTP_PASSWORD!,
where: eq(auth.email, input.email), // },
}); // "Reset Password",
if (!authR) { // `
throw new TRPCError({ // Reset your password by clicking the link below:
code: "NOT_FOUND", // The link will expire in 24 hours.
message: "User not found", // <a href="${WEBSITE_URL}/reset-password?token=${token}">
}); // Reset Password
} // </a>
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(),
});
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="${WEBSITE_URL}/reset-password?token=${token}">
Reset Password
</a>
`,
);
}),
}); });
// export const sendVerificationEmail = async (authId: string) => { // export const sendVerificationEmail = async (authId: string) => {

View File

@@ -65,6 +65,19 @@ export const userRouter = createTRPCRouter({
return memberResult; return memberResult;
}), }),
getServerMetrics: protectedProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(
eq(member.userId, ctx.user.id),
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
),
with: {
user: true,
},
});
return memberResult?.user;
}),
update: protectedProcedure update: protectedProcedure
.input(apiUpdateUser) .input(apiUpdateUser)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
@@ -112,7 +125,6 @@ export const userRouter = createTRPCRouter({
const { id, ...rest } = input; const { id, ...rest } = input;
console.log(rest);
await db await db
.update(member) .update(member)
.set({ .set({
@@ -202,4 +214,8 @@ export const userRouter = createTRPCRouter({
throw error; throw error;
} }
}), }),
generateToken: protectedProcedure.mutation(async () => {
return "token";
}),
}); });

View File

@@ -6,7 +6,7 @@ import { organization, twoFactor } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm"; import { and, desc, eq } from "drizzle-orm";
import { db } from "../db"; import { db } from "../db";
import * as schema from "../db/schema"; import * as schema from "../db/schema";
import { sendVerificationEmail } from "../verification/send-verification-email"; import { sendEmail } from "../verification/send-verification-email";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
export const auth = betterAuth({ export const auth = betterAuth({
@@ -30,7 +30,11 @@ export const auth = betterAuth({
autoSignInAfterVerification: true, autoSignInAfterVerification: true,
sendVerificationEmail: async ({ user, url }) => { sendVerificationEmail: async ({ user, url }) => {
console.log("Sending verification email to", user.email); console.log("Sending verification email to", user.email);
await sendVerificationEmail(user.email, url); await sendEmail({
email: user.email,
subject: "Verify your email",
text: `Click the link to verify your email: ${url}`,
});
}, },
}, },
emailAndPassword: { emailAndPassword: {
@@ -45,6 +49,13 @@ export const auth = betterAuth({
return bcrypt.compareSync(password, hash); return bcrypt.compareSync(password, hash);
}, },
}, },
sendResetPassword: async ({ user, url }) => {
await sendEmail({
email: user.email,
subject: "Reset your password",
text: `Click the link to reset your password: ${url}`,
});
},
}, },
databaseHooks: { databaseHooks: {
user: { user: {

View File

@@ -1,6 +1,5 @@
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { import {
type apiCreateUserInvitation,
invitation, invitation,
member, member,
organization, organization,
@@ -10,42 +9,6 @@ import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
export type User = typeof users_temp.$inferSelect;
export const createInvitation = async (
_input: typeof apiCreateUserInvitation._type,
_adminId: string,
) => {
// await db.transaction(async (tx) => {
// const result = await tx
// .insert(auth)
// .values({
// email: input.email.toLowerCase(),
// rol: "user",
// password: bcrypt.hashSync("01231203012312", 10),
// })
// .returning()
// .then((res) => res[0]);
// if (!result) {
// throw new TRPCError({
// code: "BAD_REQUEST",
// message: "Error creating the user",
// });
// }
// const expiresIn24Hours = new Date();
// expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1);
// const token = randomBytes(32).toString("hex");
// await tx
// .insert(users)
// .values({
// adminId: adminId,
// authId: result.id,
// token,
// expirationDate: expiresIn24Hours.toISOString(),
// })
// .returning();
// });
};
export const findUserById = async (userId: string) => { export const findUserById = async (userId: string) => {
const user = await db.query.users_temp.findFirst({ const user = await db.query.users_temp.findFirst({
where: eq(users_temp.id, userId), where: eq(users_temp.id, userId),
@@ -69,34 +32,6 @@ export const findOrganizationById = async (organizationId: string) => {
return organizationResult; return organizationResult;
}; };
export const updateUser = async (userId: string, userData: Partial<User>) => {
const user = await db
.update(users_temp)
.set({
...userData,
})
.where(eq(users_temp.id, userId))
.returning()
.then((res) => res[0]);
return user;
};
export const updateAdminById = async (
_adminId: string,
_adminData: Partial<User>,
) => {
// const admin = await db
// .update(admins)
// .set({
// ...adminData,
// })
// .where(eq(admins.adminId, adminId))
// .returning()
// .then((res) => res[0]);
// return admin;
};
export const isAdminPresent = async () => { export const isAdminPresent = async () => {
const admin = await db.query.member.findFirst({ const admin = await db.query.member.findFirst({
where: eq(member.role, "owner"), where: eq(member.role, "owner"),

View File

@@ -1,5 +1,5 @@
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { member, type users_temp } from "@dokploy/server/db/schema"; import { member, users_temp } from "@dokploy/server/db/schema";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
@@ -235,3 +235,16 @@ export const findMemberById = async (
} }
return result; return result;
}; };
export const updateUser = async (userId: string, userData: Partial<User>) => {
const user = await db
.update(users_temp)
.set({
...userData,
})
.where(eq(users_temp.id, userId))
.returning()
.then((res) => res[0]);
return user;
};

View File

@@ -30,6 +30,7 @@ class LogRotationManager {
private async getStateFromDB(): Promise<boolean> { private async getStateFromDB(): Promise<boolean> {
// const setting = await db.query.admins.findFirst({}); // const setting = await db.query.admins.findFirst({});
// return setting?.enableLogRotation ?? false; // return setting?.enableLogRotation ?? false;
return false;
} }
private async setStateInDB(_active: boolean): Promise<void> { private async setStateInDB(_active: boolean): Promise<void> {

View File

@@ -11,13 +11,14 @@ import { runMariadbBackup } from "./mariadb";
import { runMongoBackup } from "./mongo"; import { runMongoBackup } from "./mongo";
import { runMySqlBackup } from "./mysql"; import { runMySqlBackup } from "./mysql";
import { runPostgresBackup } from "./postgres"; import { runPostgresBackup } from "./postgres";
import { findAdmin } from "../../services/admin";
export const initCronJobs = async () => { export const initCronJobs = async () => {
console.log("Setting up cron jobs...."); console.log("Setting up cron jobs....");
const admin = await findAdmin(); const admin = await findAdmin();
if (admin?.enableDockerCleanup) { if (admin?.user.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`, `Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
@@ -25,7 +26,7 @@ export const initCronJobs = async () => {
await cleanUpUnusedImages(); await cleanUpUnusedImages();
await cleanUpDockerBuilder(); await cleanUpDockerBuilder();
await cleanUpSystemPrune(); await cleanUpSystemPrune();
await sendDockerCleanupNotifications(admin.adminId); await sendDockerCleanupNotifications(admin.user.id);
}); });
} }
@@ -42,7 +43,7 @@ export const initCronJobs = async () => {
await cleanUpDockerBuilder(serverId); await cleanUpDockerBuilder(serverId);
await cleanUpSystemPrune(serverId); await cleanUpSystemPrune(serverId);
await sendDockerCleanupNotifications( await sendDockerCleanupNotifications(
admin.adminId, admin.user.id,
`Docker cleanup for Server ${name} (${serverId})`, `Docker cleanup for Server ${name} (${serverId})`,
); );
}); });

View File

@@ -2,7 +2,15 @@ import {
sendDiscordNotification, sendDiscordNotification,
sendEmailNotification, sendEmailNotification,
} from "../utils/notifications/utils"; } from "../utils/notifications/utils";
export const sendVerificationEmail = async (email: string, url: string) => { export const sendEmail = async ({
email,
subject,
text,
}: {
email: string;
subject: string;
text: string;
}) => {
await sendEmailNotification( await sendEmailNotification(
{ {
fromAddress: process.env.SMTP_FROM_ADDRESS || "", fromAddress: process.env.SMTP_FROM_ADDRESS || "",
@@ -12,14 +20,8 @@ export const sendVerificationEmail = async (email: string, url: string) => {
username: process.env.SMTP_USERNAME || "", username: process.env.SMTP_USERNAME || "",
password: process.env.SMTP_PASSWORD || "", password: process.env.SMTP_PASSWORD || "",
}, },
"Confirm your email | Dokploy", subject,
` text,
Welcome to Dokploy!
Please confirm your email by clicking the link below:
<a href="${url}">
Confirm Email
</a>
`,
); );
return true; return true;