From aa7afd055016223f28dd5d67ba20583ca7173125 Mon Sep 17 00:00:00 2001 From: Javier Ruiz Date: Mon, 16 Dec 2024 18:29:20 +0100 Subject: [PATCH 1/3] feat: return jwt in auth --- apps/auth/src/main.ts | 16 +++++++--------- apps/auth/src/routes.ts | 21 ++++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/apps/auth/src/main.ts b/apps/auth/src/main.ts index 462ca92..897df0a 100644 --- a/apps/auth/src/main.ts +++ b/apps/auth/src/main.ts @@ -1,28 +1,26 @@ import koa from 'koa'; -import jwt from "koa-jwt"; import router from "./routes"; import bodyParser from 'koa-bodyparser'; import cors from "koa-cors" +import jwtMiddleware from 'koa-jwt'; const host = process.env.HOST ?? 'localhost'; const port = process.env.PORT ? Number(process.env.PORT) : 8080; +const secretKey = 'your-secret-key'; const app = new koa(); app.use(cors({ - origin: '*', // or specify a specific origin, e.g., 'http://example.com' + origin: '*', credentials: true, headers: ['Content-Type', 'Authorization'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] })); -app.use(bodyParser()) - -app.use(jwt({ - secret: 'your-shared-secret', - passthrough: true +app.use(jwtMiddleware({ + secret: secretKey, + passthrough: true, })); - - +app.use(bodyParser()) app.use(router.routes()).use(router.allowedMethods()); app.listen(port, host, () => { diff --git a/apps/auth/src/routes.ts b/apps/auth/src/routes.ts index 6144230..a720da6 100644 --- a/apps/auth/src/routes.ts +++ b/apps/auth/src/routes.ts @@ -1,6 +1,6 @@ import koaRouter from "koa-router"; import crypto from "crypto"; -const jwt = require('koa-jwt'); +import jwt from 'jsonwebtoken'; const router = new koaRouter(); @@ -12,21 +12,24 @@ router.post('/otp/generate', async (ctx, next) => { const otp = crypto.randomBytes(2).toString('hex'); const email = ctx.request.body; //set in context state temporally - ctx.state[email] = otp; + ctx.state.users = email; ctx.body = { otp }; - console.log("hola", ctx.state[email]); }); router.post('/otp/verify', async (ctx, next) => { - const userOtp = ctx.request.body.otp; - const storedOtp = "927100b8f1ee"; //await getStoredOtp(ctx.state.user.id); // retrieve stored OTP - if (userOtp === storedOtp) { - const token = jwt.sign({ userId: ctx.state.user.id }, 'your-secret-key', { expiresIn: '1h' }); - ctx.body = { token }; + const { otp: userOtp, email } = ctx.request.body; + + /*const storedOtp = ctx.state.users; // retrieve stored OTP + console.log("🚀 ~ router.post ~ storedOtp:", storedOtp)*/ + + /*if (userOtp === storedOtp) { + } else { ctx.status = 401; ctx.body = { error: 'Invalid OTP' }; - } + }*/ + const token = jwt.sign({ user: email }, 'your-secret-key', { expiresIn: '1h' }); + ctx.body = { token }; }); export default router; From 18da6cfdbe48a4b9f37628891549eaad2553a885 Mon Sep 17 00:00:00 2001 From: Javier Ruiz Date: Tue, 17 Dec 2024 09:12:14 +0100 Subject: [PATCH 2/3] feat: valid otp auth --- apps/auth/src/routes.ts | 50 +++++++++++-------- .../validate-token-form.tsx | 1 - 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/apps/auth/src/routes.ts b/apps/auth/src/routes.ts index a720da6..b851c64 100644 --- a/apps/auth/src/routes.ts +++ b/apps/auth/src/routes.ts @@ -1,35 +1,41 @@ -import koaRouter from "koa-router"; -import crypto from "crypto"; +import koaRouter from 'koa-router'; +import crypto from 'crypto'; import jwt from 'jsonwebtoken'; const router = new koaRouter(); -router.get("hello", "/", (ctx) => { - ctx.body = "

Hello

"; +router.get('hello', '/', (ctx) => { + ctx.body = '

Hello

'; }); router.post('/otp/generate', async (ctx, next) => { - const otp = crypto.randomBytes(2).toString('hex'); - const email = ctx.request.body; - //set in context state temporally - ctx.state.users = email; - ctx.body = { otp }; + const otp = crypto.randomBytes(2).toString('hex'); + const email = ctx.request.body; + + ctx.app.user = { email: email.email, otp }; + + ctx.body = { otp }; + + next(); }); router.post('/otp/verify', async (ctx, next) => { - const { otp: userOtp, email } = ctx.request.body; - - /*const storedOtp = ctx.state.users; // retrieve stored OTP - console.log("🚀 ~ router.post ~ storedOtp:", storedOtp)*/ - - /*if (userOtp === storedOtp) { - - } else { - ctx.status = 401; - ctx.body = { error: 'Invalid OTP' }; - }*/ - const token = jwt.sign({ user: email }, 'your-secret-key', { expiresIn: '1h' }); - ctx.body = { token }; + const { otp: userOtp, email } = ctx.request.body; + console.log('users', ctx.app.user); + + const storedOtp = ctx.app.user?.otp ?? ''; // retrieve stored OTP + + if (userOtp === storedOtp) { + const token = jwt.sign({ user: email }, 'your-secret-key', { + expiresIn: '1h', + }); + ctx.body = { token }; + } else { + ctx.status = 401; + ctx.body = { error: 'Invalid OTP' }; + } + + next(); }); export default router; diff --git a/apps/ui/src/app/components/validate-token-form/validate-token-form.tsx b/apps/ui/src/app/components/validate-token-form/validate-token-form.tsx index 205f00b..3cee0d2 100644 --- a/apps/ui/src/app/components/validate-token-form/validate-token-form.tsx +++ b/apps/ui/src/app/components/validate-token-form/validate-token-form.tsx @@ -1,5 +1,4 @@ import { Typography } from '@mui/material'; -import React from 'react' const ValidateTokenForm = () => { return ( From 2d520ecf6cb339f33fb427266c6598d4de4d3ecf Mon Sep 17 00:00:00 2001 From: Javier Ruiz Date: Tue, 17 Dec 2024 12:12:12 +0100 Subject: [PATCH 3/3] feat: verify token --- apps/ui/src/app/app.tsx | 5 +- .../app/components/otp-input/otp-input.tsx | 52 ++++++++----------- .../request-token-form/request-token-form.tsx | 9 ++-- .../validate-token-form.tsx | 41 ++++++++++++++- .../src/app/context/authentication-context.ts | 15 ++++-- .../app/context/authentication-provider.tsx | 9 ++-- apps/ui/src/app/hooks/use-validate-token.ts | 28 ++++++++++ .../ui/src/app/services/otp-authentication.ts | 21 ++++++-- package-lock.json | 13 ++++- package.json | 3 +- 10 files changed, 146 insertions(+), 50 deletions(-) create mode 100644 apps/ui/src/app/hooks/use-validate-token.ts diff --git a/apps/ui/src/app/app.tsx b/apps/ui/src/app/app.tsx index f8758f4..4ab8c3a 100644 --- a/apps/ui/src/app/app.tsx +++ b/apps/ui/src/app/app.tsx @@ -4,8 +4,7 @@ import { AuthenticationContext } from './context/authentication-context'; import ValidateTokenForm from './components/validate-token-form/validate-token-form'; export function App() { - const { otpToken } = useContext(AuthenticationContext); - console.log("🚀 ~ App ~ otpToken:", otpToken) + const { isWaitingForValidation } = useContext(AuthenticationContext); const appContainerStyles = { height: '100vh', @@ -16,7 +15,7 @@ export function App() { return (
- {otpToken ? ( + {isWaitingForValidation ? (
diff --git a/apps/ui/src/app/components/otp-input/otp-input.tsx b/apps/ui/src/app/components/otp-input/otp-input.tsx index 2143f22..8773474 100644 --- a/apps/ui/src/app/components/otp-input/otp-input.tsx +++ b/apps/ui/src/app/components/otp-input/otp-input.tsx @@ -1,32 +1,26 @@ -import { makeStyles, useTheme } from '@mui/material'; -import React from 'react' +import React, { Dispatch, SetStateAction } from 'react'; +import OtpInput from 'react-otp-input'; -const useStyles = makeStyles(theme => ({ - grid: { - backgroundColor: "grey", - height: "50vh", - textAlign: "center" - }, - avatar: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main - }, - submit: { - margin: theme.spacing(3, 0, 2) - }, - paper: { - marginTop: theme.spacing(8), - display: "flex", - flexDirection: "column", - alignItems: "center" - } - })); - -const OtpInput = () => { - - return ( -
OtpInput
- ) +type OtpInputProps = { + userToken: string; + setUserToken: Dispatch>; } -export default OtpInput \ No newline at end of file +export const OtpInputComp: React.FC = ({ + userToken, setUserToken +}) =>{ + + return ( + -} + renderInput={(props) => } + inputStyle={{ + height: "3em", + width: "3em" + }} + /> + ); +} \ No newline at end of file diff --git a/apps/ui/src/app/components/request-token-form/request-token-form.tsx b/apps/ui/src/app/components/request-token-form/request-token-form.tsx index 1759b9b..d088bde 100644 --- a/apps/ui/src/app/components/request-token-form/request-token-form.tsx +++ b/apps/ui/src/app/components/request-token-form/request-token-form.tsx @@ -29,7 +29,7 @@ const RequestTokenForm: React.FC = (props) => { }, }); - const { setOtpToken } = useContext(AuthenticationContext); + const { setIsWaitingForValidation, setEmail } = useContext(AuthenticationContext); const {mutate: fetchNewToken, isSuccess, data} = useRequestToken(); const onSubmit: SubmitHandler = (data, e) => { @@ -38,8 +38,11 @@ const RequestTokenForm: React.FC = (props) => { }; useEffect(() => { - setOtpToken(data?.otp ?? null); - }, [isSuccess, setOtpToken, data]) + if(data?.otp) { + setIsWaitingForValidation(true); + setEmail(data.emailRequired); + } + }, [isSuccess, data, setIsWaitingForValidation, setEmail]) return ( diff --git a/apps/ui/src/app/components/validate-token-form/validate-token-form.tsx b/apps/ui/src/app/components/validate-token-form/validate-token-form.tsx index 3cee0d2..1ca5e41 100644 --- a/apps/ui/src/app/components/validate-token-form/validate-token-form.tsx +++ b/apps/ui/src/app/components/validate-token-form/validate-token-form.tsx @@ -1,9 +1,46 @@ -import { Typography } from '@mui/material'; +import { Button, Typography } from '@mui/material'; +import { useContext, useEffect, useState } from 'react'; +import { OtpInputComp } from '../otp-input/otp-input'; +import useValidateToken from '../../hooks/use-validate-token'; +import { AuthenticationContext } from '../../context/authentication-context'; +import { Snackbar } from '@mui/material'; const ValidateTokenForm = () => { + const [userToken, setUserToken] = useState(""); + const [openErrorMessage, setOpenErrorMessage] = useState(false); + + + const { mutate: fetchValidation, isSuccess, data: jwtToken, error } = useValidateToken(); + const {email} = useContext(AuthenticationContext); + + + const handleValidateToken = () => { + fetchValidation({email: email ?? "", otp: userToken}); + } + + useEffect(() => { + if(error) { + setOpenErrorMessage(true); + } + }, [error]) + return ( -
+
Introduce your token + + + setOpenErrorMessage(false)} + />
) } diff --git a/apps/ui/src/app/context/authentication-context.ts b/apps/ui/src/app/context/authentication-context.ts index 76f5113..e6529b6 100644 --- a/apps/ui/src/app/context/authentication-context.ts +++ b/apps/ui/src/app/context/authentication-context.ts @@ -1,16 +1,21 @@ import { createContext, Dispatch, SetStateAction } from "react"; interface IAuthenticationContext { - otpToken: string | null; - setOtpToken: Dispatch>; - + email: string | null; + setEmail: Dispatch>; + isWaitingForValidation: boolean; + setIsWaitingForValidation: Dispatch>; } export const authenticationContextInitialValue: IAuthenticationContext = { - otpToken: null, - setOtpToken: () => { + email: null, + setEmail: () => { /** do nothing */ }, + isWaitingForValidation: false, + setIsWaitingForValidation: () => { + /** do nothing */ + } }; export const AuthenticationContext = createContext(authenticationContextInitialValue); diff --git a/apps/ui/src/app/context/authentication-provider.tsx b/apps/ui/src/app/context/authentication-provider.tsx index e7450dd..a636908 100644 --- a/apps/ui/src/app/context/authentication-provider.tsx +++ b/apps/ui/src/app/context/authentication-provider.tsx @@ -2,13 +2,16 @@ import { ReactNode, useState } from "react"; import { AuthenticationContext } from "./authentication-context"; export const AuthenticationProvider = ({ children }: {children: ReactNode}) => { - const [otpToken, setOtpToken] = useState(null); + const [email, setEmail] = useState(null); + const [isWaitingForValidation, setIsWaitingForValidation] = useState(false); return ( {children} diff --git a/apps/ui/src/app/hooks/use-validate-token.ts b/apps/ui/src/app/hooks/use-validate-token.ts new file mode 100644 index 0000000..3cafe55 --- /dev/null +++ b/apps/ui/src/app/hooks/use-validate-token.ts @@ -0,0 +1,28 @@ +import { useMutation } from '@tanstack/react-query'; +import { postValidateToken } from '../services'; + +const VALIDATE_TOKEN_KEY = 'validate_token'; + +interface IValidateMutation { + email: string; + otp: string; +} + +const useValidateToken = () => { + const { data, mutate, error, isSuccess } = useMutation({ + mutationKey: [VALIDATE_TOKEN_KEY], + mutationFn: async ({email, otp}: IValidateMutation) => { + const response = await postValidateToken(email, otp); + return response.token; + }, + }); + + return { + data, + mutate, + error, + isSuccess + }; +}; + +export default useValidateToken; diff --git a/apps/ui/src/app/services/otp-authentication.ts b/apps/ui/src/app/services/otp-authentication.ts index fe803b9..b68c4a8 100644 --- a/apps/ui/src/app/services/otp-authentication.ts +++ b/apps/ui/src/app/services/otp-authentication.ts @@ -1,12 +1,12 @@ import axios from 'axios'; const apiUrlGenerateToken = 'http://localhost:8080/otp/generate'; +const apiURLValidateToken = 'http://localhost:8080/otp/verify'; export const postRequestToken = (email: string) => { - const body = { email: email, - } + }; return axios .post(apiUrlGenerateToken, body) @@ -17,4 +17,19 @@ export const postRequestToken = (email: string) => { .catch((error) => { console.error(error); }); -}; \ No newline at end of file +}; + +export const postValidateToken = (email: string, otp: string) => { + const body = { + email: email, + otp: otp, + }; + return axios + .post(apiURLValidateToken, body) + .then((response) => { + return response.data; + }) + .catch((error) => { + console.error(error); + }); +}; diff --git a/package-lock.json b/package-lock.json index 9c6512e..ab0d923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "koa-router": "13.0.1", "react": "18.3.1", "react-dom": "18.3.1", - "react-hook-form": "^7.54.1" + "react-hook-form": "^7.54.1", + "react-otp-input": "^3.1.1" }, "devDependencies": { "@eslint/js": "^9.8.0", @@ -16321,6 +16322,16 @@ "dev": true, "license": "MIT" }, + "node_modules/react-otp-input": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-otp-input/-/react-otp-input-3.1.1.tgz", + "integrity": "sha512-bjPavgJ0/Zmf/AYi4onj8FbH93IjeD+e8pWwxIJreDEWsU1ILR5fs8jEJmMGWSBe/yyvPP6X/W6Mk9UkOCkTPw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.6 || ^17.0.0 || ^18.0.0", + "react-dom": ">=16.8.6 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", diff --git a/package.json b/package.json index 7c50356..623f265 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "koa-router": "13.0.1", "react": "18.3.1", "react-dom": "18.3.1", - "react-hook-form": "^7.54.1" + "react-hook-form": "^7.54.1", + "react-otp-input": "^3.1.1" }, "devDependencies": { "@eslint/js": "^9.8.0",