Skip to content

Instantly share code, notes, and snippets.

@HectorBlisS
Last active April 26, 2023 14:25
Show Gist options
  • Save HectorBlisS/d9ca9a64e42c356cb48a16e3ed950691 to your computer and use it in GitHub Desktop.
Save HectorBlisS/d9ca9a64e42c356cb48a16e3ed950691 to your computer and use it in GitHub Desktop.
Youtube_forgot_password
export const action: ActionFunction = async ({ request }) => {
const url = new URL(request.url);
const session = await getSession(request.headers.get("Cookie"));
const formData = await request.formData();
const intent = formData.get("intent");
const token = url.searchParams.get("token");
const tokenResult = await verifyToken(token);
if (intent === "forgot-email") {
const email = formData.get("email") as string;
const exists = await db.user.findUnique({
where: {
email,
},
});
if (!exists) {
return { ok: false, error: "Email not found" };
}
// if so, send email with token
sendResetPassword(email as string);
return { ok: true, showEmailSent: true };
}
if (intent === "update-password") {
if (!tokenResult.isValid || !tokenResult.email) {
return {
ok: false,
error: "Invalid or expired token. Get a new email",
errorCode: 666,
};
}
const password = formData.get("password");
if (!password) return { ok: false, error: "No password sent" };
const validate = passwordSchema.safeParse(password);
if (!validate.success) return { ok: false, error: "Invalid password" };
// hash password
const hash = await hashPass(password as string);
const user = await db.user.update({
where: {
email: tokenResult.email,
},
data: {
hashString: hash,
},
});
// save cookie // redirect to apps
session.set("userId", user.id);
return redirect("/apps", {
headers: {
"Set-Cookie": await commitSession(session, {
expires: new Date("2050-12-12"),
}),
},
});
}
return null;
};
var regularExpression =
/^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{6,16}$/;
const passwordSchema = z.string().regex(regularExpression);
export const EmailForm = ({
isLoading,
error,
}: {
isLoading?: boolean;
error?: string | null;
}) => (
<section>
<FeedbugDialog
isOpen={true}
onClose={() => false}
footer={null}
title={
<>
<img
alt="logo"
src={`/assets/logo-sticky.png`}
className="w-[50%] rounded-lg mb-8 mx-auto"
/>
<h2 className="text-[#333333] text-4xl font-bold my-4">
<strong className="text-[#4966f6]">Enter your</strong> email
</h2>
<p className="text-[#888888] text-xl font-thin mb-8">
Get the instructions in your email.
</p>
</>
}
>
<Form method="post" className="flex flex-col">
<TextField
isDisabled={false}
name="email"
type="email"
placeholder="Email"
/>
<p className="text-red-500 text-sm px-4 mb-2">{error}</p>
<FormButton
isLoading={isLoading}
name="intent"
value="forgot-email"
type="submit"
>
Restore my password
</FormButton>
</Form>
<Link
to="/"
className="text-center text-xs pt-4 block text-brand-500 hover:text-brand-400 "
>
Did you remember your password?
</Link>
</FeedbugDialog>
</section>
);
export default function Forgot() {
const { show } = useLoaderData<loaderData>();
const actionData = useActionData();
const transition = useTransition();
const [error, setError] = useState(null);
useEffect(() => {
console.log("Action", actionData);
if (actionData?.error) {
setError(actionData.error);
}
}, [actionData]);
const form =
show === "input" ? (
<>
<PasswordForm isLoading={transition.state !== "idle"} error={error} />
</>
) : (
<EmailForm isLoading={transition.state !== "idle"} error={error} />
);
return (
<>
<section className="flex min-h-screen bg-gradient-to-r from-brand-500 to-violet-500"></section>
{actionData?.showEmailSent ? <SentMessage /> : form}
</>
);
}
export const FormButton = ({
isLoading,
children,
...props
}: {
children?: ReactNode;
isLoading?: boolean;
[x: string]: any;
}) => (
<button
{...props}
disabled={isLoading}
type="submit"
className="px-4 py-3 rounded-lg bg-[#4966f6] text-white active:scale-95 hover:bg-brand-600 active:bg-blue-400 hover:bg-blue-600 text-lg disabled:pointer-events-none disabled:bg-gray-300 w-full"
>
{isLoading ? (
<div className="border-4 border-t-blue-500 animate-spin w-8 h-8 rounded-full mx-auto" />
) : children ? (
children
) : (
"Start now"
)}
</button>
);
import {
type LoaderFunction,
type ActionFunction,
redirect,
} from "@remix-run/node";
import {
Form,
Link,
useActionData,
useLoaderData,
useTransition,
} from "@remix-run/react";
import { useState, type FormEventHandler, useEffect } from "react";
import { z } from "zod";
import Dialog from "Cualquier dialog que tu ocupes yo uso el de headless ui";
import { FormButton, TextField } from "Estos componentes también son mios pero cualquier input y form funcionan";
import { db } from "~/db/db.server";
import { hashPass, verifyToken } from "~/utils/tokens <= Estas funciónes también te la regalo";
import { commitSession, getSession } from "~/sessions";
import { sendResetPassword } from "~/utils/emailSenders"; // este es mi email sender, también te lo regalo
import bcrypt from "bcrypt";
type loaderData = {
show: "input" | "email";
ok?: boolean;
};
export const loader: LoaderFunction = async ({ request, params }) => {
const url = new URL(request.url);
const token = url.searchParams.get("token");
// validate token
const result = await verifyToken(token);
if (result.isValid) {
return {
ok: true,
show: "input",
};
}
return { show: "email" };
};
export const SentMessage = () => (
<FeedbugDialog
isOpen={true}
onClose={() => false}
footer={null}
title={
<>
<img
alt="logo"
src={`/assets/logo-sticky.png`}
className="w-[50%] rounded-lg mb-8 mx-auto"
/>
<h2 className="text-[#333333] text-4xl font-bold my-4">
<strong className="text-[#4966f6]">Check your </strong>email
</h2>
<p className="text-[#888888] text-lg font-thin mb-8">
We've sent the instructions to reset your password to your email
</p>
</>
}
>
<Link
to="/"
className="text-center text-xs pt-4 block text-brand-500 hover:text-brand-400 "
>
Go back
</Link>
</FeedbugDialog>
);
export const PasswordForm = ({
onSubmit,
isLoading,
error,
}: {
isLoading?: boolean;
error?: string | null;
onSubmit?: (arg0: FormEventHandler<HTMLFormElement>) => void;
}) => (
<section>
<FeedbugDialog
isOpen={true}
onClose={() => false}
footer={null}
title={
<>
<img
alt="logo"
src={`/assets/logo-sticky.png`}
className="w-[50%] rounded-lg mb-8 mx-auto"
/>
<h2 className="text-[#333333] text-4xl font-bold my-4">
<strong className="text-[#4966f6]">Enter your new</strong> password
</h2>
<p className="text-[#888888] text-lg font-thin mb-8">
Remember to use 6 characters 1 number, 1 special character and 1
case letter.
</p>
</>
}
>
<Form method="post" className="flex flex-col">
<TextField
isDisabled={false}
name="password"
type="password"
placeholder="New password"
/>
<p className="text-red-500 text-sm px-4 mb-2">{error}</p>
<FormButton isLoading={isLoading} name="intent" value="update-password">
Reset my password
</FormButton>
</Form>
<Link
to="/"
className="text-center text-xs pt-4 block text-brand-500 hover:text-brand-400 "
>
Did you remember your password?
</Link>
</FeedbugDialog>
</section>
);
import jwt from "jsonwebtoken";
const KEY = process.env.TOKEN_SECRET ?? "fixtergeek.com";
export const verifyToken = async (
token: string | null | undefined
): Promise<{ isValid: boolean; error?: any; email?: string }> => {
if (!token) return { isValid: false };
try {
return {
...jwt.verify(token, KEY),
isValid: true,
};
} catch (e) {
return { isValid: false, error: e };
}
};
const compareHash = async (password: string, hash: string) => {
return await bcrypt.compare(password, hash);
};
export const hashPass = async (password: string) => {
const saltRounds = 10;
return await bcrypt.hash(password, saltRounds);
};
export const genTokenWithEmail = (email: string) => {
return jwt.sign(
{
email,
},
KEY,
{ expiresIn: "12h" }
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment