mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(dokploy): add reset password for cloud
This commit is contained in:
2
apps/dokploy/drizzle/0044_wandering_butterfly.sql
Normal file
2
apps/dokploy/drizzle/0044_wandering_butterfly.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "auth" ADD COLUMN "resetPasswordToken" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth" ADD COLUMN "resetPasswordExpiresAt" text;
|
||||||
3956
apps/dokploy/drizzle/meta/0044_snapshot.json
Normal file
3956
apps/dokploy/drizzle/meta/0044_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -309,6 +309,13 @@
|
|||||||
"when": 1729472842572,
|
"when": 1729472842572,
|
||||||
"tag": "0043_legal_power_pack",
|
"tag": "0043_legal_power_pack",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 44,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1729580119063,
|
||||||
|
"tag": "0044_wandering_butterfly",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,10 @@ type AuthResponse = {
|
|||||||
authId: string;
|
authId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Home() {
|
interface Props {
|
||||||
|
IS_CLOUD: boolean;
|
||||||
|
}
|
||||||
|
export default function Home({ IS_CLOUD }: Props) {
|
||||||
const [temp, setTemp] = useState<AuthResponse>({
|
const [temp, setTemp] = useState<AuthResponse>({
|
||||||
is2FAEnabled: false,
|
is2FAEnabled: false,
|
||||||
authId: "",
|
authId: "",
|
||||||
@@ -176,13 +179,22 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
||||||
<Link
|
{IS_CLOUD ? (
|
||||||
className="hover:underline text-muted-foreground"
|
<Link
|
||||||
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
className="hover:underline text-muted-foreground"
|
||||||
target="_blank"
|
href="/send-reset-password"
|
||||||
>
|
>
|
||||||
Lost your password?
|
Lost your password?
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
<div className="p-2" />
|
<div className="p-2" />
|
||||||
@@ -212,7 +224,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
IS_CLOUD: IS_CLOUD,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const hasAdmin = await isAdminPresent();
|
const hasAdmin = await isAdminPresent();
|
||||||
|
|||||||
229
apps/dokploy/pages/reset-password.tsx
Normal file
229
apps/dokploy/pages/reset-password.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
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;
|
||||||
|
console.log(token);
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
172
apps/dokploy/pages/send-reset-password.tsx
Normal file
172
apps/dokploy/pages/send-reset-password.tsx
Normal 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: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
apiUpdateAuthByAdmin,
|
apiUpdateAuthByAdmin,
|
||||||
apiVerify2FA,
|
apiVerify2FA,
|
||||||
apiVerifyLogin2FA,
|
apiVerifyLogin2FA,
|
||||||
|
auth,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -18,12 +19,17 @@ import {
|
|||||||
getUserByToken,
|
getUserByToken,
|
||||||
lucia,
|
lucia,
|
||||||
luciaToken,
|
luciaToken,
|
||||||
|
sendEmailNotification,
|
||||||
updateAuthById,
|
updateAuthById,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
verify2FA,
|
verify2FA,
|
||||||
} 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 { eq } from "drizzle-orm";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
@@ -233,4 +239,101 @@ export const authRouter = createTRPCRouter({
|
|||||||
verifyToken: protectedProcedure.mutation(async () => {
|
verifyToken: protectedProcedure.mutation(async () => {
|
||||||
return true;
|
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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export const auth = pgTable("auth", {
|
|||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
resetPasswordToken: text("resetPasswordToken"),
|
||||||
|
resetPasswordExpiresAt: text("resetPasswordExpiresAt"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authRelations = relations(auth, ({ many }) => ({
|
export const authRelations = relations(auth, ({ many }) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user