Skip to content

Commit

Permalink
Email verification done with otp
Browse files Browse the repository at this point in the history
  • Loading branch information
Dovakiin0 committed Sep 18, 2023
1 parent f7e3660 commit 3f3007c
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 25 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ jobs:
cd ~/TMpro
git pull origin master
git status
echo "$(cat ${{secrets.ENV_PROD}})" > .env.production
sh deploy.sh
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ web_modules/

# dotenv environment variable files

.env
.env.production
.env.development.local
.env.test.local
.env.production.local
Expand Down
7 changes: 6 additions & 1 deletion client/src/app/Register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ function Register({ }: Props) {
form.setErrors({ password: "Please enter a strong password" });
return;
}
register(values);
register(values, () => {
navigate(`/verify`, { state: { email: values.email } });
});
};

useEffect(() => {
Expand Down Expand Up @@ -149,11 +151,13 @@ function Register({ }: Props) {
<TextInput
withAsterisk
label="Username"
placeholder="Your Usernam"
{...form.getInputProps("username")}
/>
<TextInput
withAsterisk
label="Email"
placeholder="Your email address"
{...form.getInputProps("email")}
/>
<Popover
Expand Down Expand Up @@ -187,6 +191,7 @@ function Register({ }: Props) {
<PasswordInput
withAsterisk
label="Confirm Password"
placeholder="Confirm Password"
{...form.getInputProps("confirm_password")}
/>
<Button type="submit" color="red" loading={loading}>
Expand Down
111 changes: 111 additions & 0 deletions client/src/app/Verify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useEffect } from "react";
import {
Button,
Card,
Divider,
Group,
Image,
PinInput,
Text,
useMantineTheme,
} from "@mantine/core";
import { motion } from "framer-motion";
import { useNavigate, useLocation } from "react-router-dom";
import { useForm } from "@mantine/form";
import { IVerifyOtp } from "../types/IUser";
import useAuth from "../hooks/useAuth";

type Props = {};

function Verify({ }: Props) {
const theme = useMantineTheme();
const navigate = useNavigate();
const location = useLocation();

const form = useForm<IVerifyOtp>({
initialValues: {
otp: 0,
},
validate: {
otp: (value) => (value.toString().length === 6 ? null : "Invalid OTP"),
},
});

useEffect(() => {
if (!location.state) {
navigate("/login");
}
}, [location]);

const { verifyOTP, generateOTP } = useAuth();

const handleSubmit = (values: IVerifyOtp) => {
if (location.state) {
verifyOTP(location.state.email, values.otp, () => {
navigate("/login");
});
}
};

return (
<motion.div
initial={{ opacity: 0, scale: 0.5 }}
animate={{
opacity: 1,
scale: 1,
transition: { ease: "easeOut", delay: 0.5 },
}}
className="w-screen h-screen flex flex-col space-y-4 items-center justify-center"
>
<div className="w-32">
<Image src="/full.png" />
</div>
<Card
shadow="sm"
padding="lg"
radius="md"
withBorder
className={`w-full xl:w-1/4`}
>
<div className="m-2 flex flex-col space-y-2">
<Text size={25} weight={"bold"}>
Verify Your Account
</Text>
<div className="flex space-x-2">
<Text color={theme.colors.dark[1]}>
Didn't get verification code?{" "}
</Text>
<p
className="font-bold underline hover:cursor-pointer"
onClick={() => generateOTP(location.state.email)}
>
Resend
</p>
</div>
<Divider />
<form
onSubmit={form.onSubmit((values) => handleSubmit(values))}
className="flex flex-col space-y-5"
>
<Text weight="bold" size="xl">
Enter Verification Code
</Text>
<Group position="center">
<PinInput
size="xl"
type="number"
length={6}
{...form.getInputProps("otp")}
/>
</Group>
<Button color="red" type="submit">
Submit
</Button>
</form>
</div>
</Card>
</motion.div>
);
}

export default Verify;
51 changes: 46 additions & 5 deletions client/src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default function useAuth() {
const [errors, setErrors] = useState();
const { current } = useAppSelector((state) => state.auth);
const dispatch = useAppDispatch();
const { Error } = useToast();
const { Error, Success } = useToast();

useEffect(() => {
checkUser();
Expand All @@ -33,7 +33,7 @@ export default function useAuth() {
setLoading(true);
const response = await client.post("/api/auth", credentials);
if (response.status === 200) {
dispatch(loginSuccess(response.data));
dispatch(loginSuccess(response.data.user));
}
} catch (err: any) {
setErrors(err);
Expand All @@ -43,7 +43,7 @@ export default function useAuth() {
}
};

const register = async (userData: IRegisterUser) => {
const register = async (userData: IRegisterUser, cb?: () => void) => {
try {
setLoading(true);
const response = await client.post("/api/auth/register", {
Expand All @@ -52,7 +52,8 @@ export default function useAuth() {
password: userData.password,
});
if (response.status === 201) {
dispatch(loginSuccess(response.data));
await generateOTP(response.data.user.email);
cb?.();
}
} catch (err: any) {
setErrors(err);
Expand All @@ -62,6 +63,37 @@ export default function useAuth() {
}
};

const generateOTP = async (email: string) => {
try {
const response = await client.post("/api/auth/otp", { email });
if (response.status === 200) {
Success({
message: response.data.message,
});
}
} catch (err: any) {
Error({ message: err.response.data.message });
}
};

const verifyOTP = async (email: string, otp: number, cb: () => void) => {
try {
setLoading(true);
const response = await client.post("/api/auth/otp/verify", {
email,
otp,
});
if (response.status === 200) {
Success({
message: response.data.message,
});
cb();
}
} catch (err: any) {
Error({ message: err.response.data.message });
}
};

const logoutUser = async () => {
try {
setLoading(true);
Expand All @@ -77,5 +109,14 @@ export default function useAuth() {
}
};

return { login, loading, errors, register, logoutUser, current };
return {
login,
loading,
errors,
register,
logoutUser,
current,
verifyOTP,
generateOTP,
};
}
5 changes: 5 additions & 0 deletions client/src/routes/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const Home = React.lazy(() => import("../app/Home"));
const ProtectedRoute = React.lazy(() => import("./ProtectedRoute"));
const Login = React.lazy(() => import("../app/Login"));
const Register = React.lazy(() => import("../app/Register"));
const Verify = React.lazy(() => import("../app/Verify"));
const Error = React.lazy(() => import("../components/Error"));

// initialize route paths
Expand All @@ -28,4 +29,8 @@ export const router = createBrowserRouter([
path: "/register",
element: <Register />,
},
{
path: "/verify",
element: <Verify />,
},
]);
4 changes: 4 additions & 0 deletions client/src/types/IUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ export interface IUserResponse {
email: string;
username: string;
}

export interface IVerifyOtp {
otp: number;
}
3 changes: 3 additions & 0 deletions docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ services:
- MONGO_URI=mongodb://mongo_db:27017/TMpro
- JWT_SECRET=verysecretkey
- NODE_ENV=development
- EMAIL_HOST=smtp.ethereal.email
- EMAIL_PASS=TKJ64DySyD75dVnncu
- EMAIL_USER=mathew.terry98@ethereal.email
depends_on:
- mongo_db

Expand Down
7 changes: 2 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,8 @@ services:
restart: always
ports:
- "5001:5001"
environment:
- MONGO_URI=mongodb://mongo_db:27017/TMpro
- JWT_SECRET=verysecretkey
- PORT=5001
- NODE_ENV=production
env_file:
- .env.production
depends_on:
- mongo_db

Expand Down
27 changes: 19 additions & 8 deletions server/src/config/NodeMailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@ import * as path from "path";
import hbs from "nodemailer-express-handlebars";

export const Transporter = () => {
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST as string,
port: 587,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
let transporter;
if (process.env.NODE_ENV === "development") {
transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST,
port: 587,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
} else {
transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
}

const handleBarsOptions: hbs.NodemailerExpressHandlebarsOptions = {
viewEngine: {
Expand Down
13 changes: 8 additions & 5 deletions server/src/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const registerUser = asyncHandler(async (req: IRequest, res: Response) => {
success: true,
user: {
_id: user._id,
email: user.email,
},
});
});
Expand All @@ -105,9 +106,9 @@ const registerUser = asyncHandler(async (req: IRequest, res: Response) => {
*/
const generateOtpForUser = asyncHandler(
async (req: IRequest, res: Response) => {
const { id } = req.body;
const { email } = req.body;

const user = await User.findById({ id });
const user = await User.findOne({ email });
if (!user) {
res.status(400);
throw new Error("User does not exists");
Expand All @@ -134,7 +135,7 @@ const generateOtpForUser = asyncHandler(
},
});

res.status(400).json({
res.status(200).json({
success: true,
message: "OTP has been sent to your email address",
});
Expand All @@ -147,8 +148,8 @@ const generateOtpForUser = asyncHandler(
@Method POST
*/
const verifyOTP = asyncHandler(async (req: IRequest, res: Response) => {
const { otp, id } = req.body;
const user = await User.findById({ id });
const { otp, email } = req.body;
const user = await User.findOne({ email });

if (!user || !user.otp || !user.otpExpiration) {
res.status(400);
Expand All @@ -166,6 +167,8 @@ const verifyOTP = asyncHandler(async (req: IRequest, res: Response) => {
user.otp = null;
user.otpExpiration = null;

await user.save();

res
.status(200)
.json({ success: true, message: "Account verified, Please login!" });
Expand Down
5 changes: 5 additions & 0 deletions server/src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ export const isAuth = async (
) => {
try {
let token = req.cookies["access_token"];
if (typeof token === "undefined") {
next();
return;
}
const decoded = verifyJWT(token);

if (decoded === null) {
next();
return;
}

Expand Down

0 comments on commit 3f3007c

Please sign in to comment.