diff --git a/apps/http-backend/src/middleware/index.ts b/apps/http-backend/src/middleware/index.ts index da46b2d..4db458b 100644 --- a/apps/http-backend/src/middleware/index.ts +++ b/apps/http-backend/src/middleware/index.ts @@ -49,7 +49,7 @@ export async function otpRateLimitter(req : Request, res : Response, next :Next const key = `otp_limit:${ip}` const maxRequest = 5; const expireTime = 5*60; - if(process.env.NODE_ENV==="dev"){ + if(process.env.NODE_ENV !=="production"){ next() return } @@ -79,7 +79,7 @@ export async function otpVerifyRateLimiter(req: Request, res: Response, next: Ne const maxAttempts = 7; // Allow more attempts than OTP requests const expireTime = 5 * 60; - if (process.env.NODE_ENV === "dev") { + if (process.env.NODE_ENV !== "production") { next(); return; } diff --git a/apps/http-backend/src/routes/v1/user/index.ts b/apps/http-backend/src/routes/v1/user/index.ts index e38507a..181e1cf 100644 --- a/apps/http-backend/src/routes/v1/user/index.ts +++ b/apps/http-backend/src/routes/v1/user/index.ts @@ -77,18 +77,12 @@ router.post("/signup/verify",otpVerifyRateLimiter, async (req, res) => { } }) - const sessionId= crypto.randomUUID(); - const token = jwt.sign({ - userId: user.id, - plan : user.plan, - sessionId - }, JWT_PASSWORD) - + const sessionId= crypto.randomUUID() // Set user sessions to validate login based on plan const sessionKey = `session:${user.id}` await setUserSessionsByPlan(sessionKey , user.plan , sessionId) - setCookie(res, token, 200, "LOGIN"); + setCookie(res, 200, sessionId , user.id, user.plan, "LOGIN"); }); @@ -157,16 +151,9 @@ router.post("/signin/verify",otpVerifyRateLimiter, async (req, res) => { } }) const sessionId= crypto.randomUUID(); - const token = jwt.sign({ - userId: user.id, - plan : user.plan, - sessionId - - }, JWT_PASSWORD) - const sessionKey = `session:${user.id}` await setUserSessionsByPlan(sessionKey , user.plan, sessionId) - setCookie(res,token, 200, "VERIFY") + setCookie(res, 200, sessionId , user.id, user.plan, "VERIFY"); }); diff --git a/apps/http-backend/src/utils/cookie.ts b/apps/http-backend/src/utils/cookie.ts index 063e32f..9e6d2d9 100644 --- a/apps/http-backend/src/utils/cookie.ts +++ b/apps/http-backend/src/utils/cookie.ts @@ -3,14 +3,18 @@ import { JWT_PASSWORD } from "../config"; import { Response } from "express"; -export const setCookie = (res: Response, token: string, statusCode: number, cookieType: "SIGNUP" | "LOGIN" | "VERIFY") => { - +export const setCookie = (res: Response, statusCode: number, sessionId : string,userId :string, plan : string, cookieType: "SIGNUP" | "LOGIN" | "VERIFY") => { + const token = jwt.sign({ + userId, + plan , + sessionId + }, JWT_PASSWORD) res .status(statusCode) .cookie("token", token, { httpOnly: true, maxAge: 10*24*60*60*1000, - sameSite: "lax", + sameSite: process.env.NODE_ENV === "production" ? "none" : "lax", secure: process.env.NODE_ENV === "production", }) .json({ diff --git a/apps/user-fe/app/_common/navbar.tsx b/apps/user-fe/app/_common/navbar.tsx index b6d7478..202cce1 100644 --- a/apps/user-fe/app/_common/navbar.tsx +++ b/apps/user-fe/app/_common/navbar.tsx @@ -4,24 +4,33 @@ import { NavLogo } from "../assets"; import Image from "next/image"; import { figtree, manrope } from "../lib/fonts"; import { cn } from "@repo/ui/utils"; -import { usePathname } from "next/navigation"; -import { useState, useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; import { LoginDialog } from "../_components/signup/singupDialog"; -import Cookies from "js-cookie"; // Make sure to install this package -import { getCookies } from "../../actions/auth"; +import { Button } from "@repo/ui/button"; +import { useDispatch, useSelector } from "react-redux"; +import { IDispatchType, IRootType } from "../lib/redux/store"; +import { setIsLoginOpen, setIsSignup } from "../lib/redux/signInDialog"; +import { logout } from "../lib/actions/getUser"; +import { logoutState } from "../lib/redux/authSlice"; +import { toast } from "sonner"; export default function Navbar() { const pathname = usePathname(); - const [isLoginOpen, setIsLoginOpen] = useState(false); - const [isAuthenticated, setIsAuthenticated] = useState(false); - - - - useEffect(() => { - const cookie = getCookies(); - setIsAuthenticated(!!cookie); - }, []); + const dispatch = useDispatch() + const isAuthorized = useSelector((state: IRootType) => state.auth.isAuthorized); + const isLoading = useSelector((state: IRootType) => state.auth.isLoading); + const router = useRouter() + //Handle Logout + const handleLogout =async()=>{ + try{ + await logout() + dispatch(logoutState()) + router.push('/') + }catch(e){ + toast.error('Cannot logout') + } + } return ( ); diff --git a/apps/user-fe/app/_components/signup/otp-dialog.tsx b/apps/user-fe/app/_components/signup/otp-dialog.tsx index bcd15a0..4f8320d 100644 --- a/apps/user-fe/app/_components/signup/otp-dialog.tsx +++ b/apps/user-fe/app/_components/signup/otp-dialog.tsx @@ -1,64 +1,82 @@ -import React, { useState } from "react"; -import { Dialog, DialogContent } from "@repo/ui/dialog"; +import { Dialog, DialogContent, DialogTitle } from "@repo/ui/dialog"; import Image from "next/image"; import { cn } from "@repo/ui/utils"; import { manrope } from "../../lib/fonts"; import { OtpGirl } from "../../assets"; -import { InputOTP, InputOTPGroup, InputOTPSlot } from "@repo/ui/input"; -import axios from "axios"; -import toast from "react-hot-toast"; -import { useRouter } from "next/navigation"; +import { useRef } from "react"; +import { InputOTP, InputOTPGroup, InputOTPSlot } from "@repo/ui/otp-input"; +import { useDispatch, useSelector } from "react-redux"; +import { IDispatchType, IRootType } from "../../lib/redux/store"; +import { signInVerify, signUpVerify } from "../../lib/redux/authSlice"; +import { toast } from "sonner"; +import { setIsLoginOpen, setIsProcessing, setShowOtp } from "../../lib/redux/signInDialog"; interface OtpDialogProps { - isOpen: boolean; - phoneNumber: string; - onClose: () => void; + name?: string + number: string } -export function OtpDialog({ isOpen, onClose, phoneNumber }: OtpDialogProps) { - const [otp, setOtp] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - const router = useRouter(); - - const handleOtpChange = (otpValue: string) => { - setOtp(otpValue); - }; +// app/_components/auth/otp-dialog.tsx +export function OtpDialog({ name, number }: OtpDialogProps) { + const otp = useRef('') + const otpInputRef = useRef(null); + const dispatch = useDispatch() + const signInDialog = useSelector((state: IRootType) => state.signInDialog) + const isOpen = signInDialog.showOtp + const isSignup = signInDialog.isSignup + const isProcessing = signInDialog.isProcessing + // onClose + const onClose = () => { + dispatch(setShowOtp(false)) + } + // Close on after successfull signin or signup + const onSubmitClose = () => { + dispatch(setShowOtp(false)) + dispatch(setIsLoginOpen(false)) + } + //Handle submit const handleSubmit = async () => { - if (otp.length === 6) { - setIsSubmitting(true); + dispatch(setIsProcessing()) + + //Checks otp size + if (otp.current.length < 4) { + toast.error('Enter a valid otp', { + duration: 2000, + className: 'text-red-500' + }) + dispatch(setIsProcessing()) + return } - console.log("otp", otp, " phone", phoneNumber); - const response = await axios.post( - process.env.NEXT_PUBLIC_BACKEND_URL + "user/signup/verify", - { - number: phoneNumber, - totp: otp, - name: "GUest User", - }, - { - withCredentials: true, - } - ); - console.log("Response in the otp", response.data); - if (response.status === 200) { - toast.success("Singin Successful."); + + //If otp of length 4 + let res; + if (name && isSignup) { + res = await dispatch(signUpVerify({ name, number, totp: otp.current })) } else { - toast.error("Something went wrong."); + res = await dispatch(signInVerify({ number, totp: otp.current })) } - setIsSubmitting(false); - router.push("/"); - }; + if (res.meta.requestStatus === 'rejected') { + toast.error(res.payload, { + duration: 2000, + className: 'text-red-500' + }) + } else { + toast.success(res.payload.message, { + duration: 2000, + className: 'text-green-500' + }) + onSubmitClose() + } + dispatch(setIsProcessing()) + } - const handleResend = () => { - // Implement resend OTP logic - console.log("Resend OTP"); - }; return (
+ {/* Back Button */}
-
-

+ {/* Resend Link */} +
+

Enter your OTP.{" "} -

- - - - - - - - - - + {/* OTP Input Fields */} +
+ { otp.current = e }}> + + {[...Array(4)].map((_, index) => ( + + ))} + + +
+ {/* Verify Button */}

); -} +} \ No newline at end of file diff --git a/apps/user-fe/app/_components/signup/singupDialog.tsx b/apps/user-fe/app/_components/signup/singupDialog.tsx index 6e97bbf..20e8926 100644 --- a/apps/user-fe/app/_components/signup/singupDialog.tsx +++ b/apps/user-fe/app/_components/signup/singupDialog.tsx @@ -1,76 +1,94 @@ import { Dialog, DialogContent } from "@repo/ui/dialog"; +import { Input } from "@repo/ui/input"; +import { Button } from "@repo/ui/button"; import Image from "next/image"; import { cn } from "@repo/ui/utils"; import { figtree, manrope } from "../../lib/fonts"; -import { useState } from "react"; +import { useEffect, useRef } from "react"; import { MaheepSingh } from "../../assets"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { UserSignUpSchema } from "@repo/common/types"; -import Link from "next/link"; -import axios from "axios"; -import { Loader } from 'lucide-react'; import { OtpDialog } from "./otp-dialog"; -import { z } from "zod"; +import Link from "next/link"; +import { useDispatch, useSelector } from "react-redux"; +import { IDispatchType, IRootType } from "../../lib/redux/store"; +import { matchRegex } from "../../lib/regexPhone"; +import { toast } from "sonner"; +import { signIn, signUp } from "../../lib/loginRoutes/user"; +import { setIsLoginOpen, setIsProcessing, setIsSignup, setShowOtp } from "../../lib/redux/signInDialog"; -interface LoginDialogProps { - isOpen: boolean; - onClose: () => void; -} -export function LoginDialog({ isOpen, onClose }: LoginDialogProps) { - const [showOtp, setShowOtp] = useState(false); - const [loading, setLoading] = useState(false); - const apiEndPoint = process.env.NEXT_PUBLIC_BACKEND_URL + "user/signup"; - const { - register, - handleSubmit, - formState: { errors }, - getValues, - } = useForm>({ - resolver: zodResolver(UserSignUpSchema), - mode: "onChange", - }); +export function LoginDialog() { + const signInDialog = useSelector((state : IRootType)=> state.signInDialog) + const dispatch = useDispatch() + const isOpen = signInDialog.isLoginOpen + const showOtp = signInDialog.showOtp + const isSignup = signInDialog.isSignup + const isProcessing = signInDialog.isProcessing + const phone = useRef(null); + const name = useRef(null); - const onSubmit = async () => { - const phoneNumber = getValues("number"); - setLoading(true); - try { - const response = await axios.post( - apiEndPoint, - { - number: process.env.NEXT_PUBLIC_PHONE_PREFIX + phoneNumber, - }, - { - withCredentials: true, - } - ); + //onClose + const onClose=() => { + dispatch(setIsLoginOpen(false)) + } + // Handle closing both dialogs + const handleClose = () => { + dispatch(setShowOtp(false)) + onClose(); + }; + + const onSignupClose = ()=>{ + dispatch(setIsSignup(!isSignup)) + } - if (response.status === 200) { - setShowOtp(true); + // Handle next button click + const handleNextClick = async () => { + const phoneValue = phone.current?.value || ''; + const nameValue = name.current?.value || ''; + //setting isLoading to true + dispatch(setIsProcessing()) + if (!matchRegex(phone.current?.value || '')) { + toast.error('Enter a valid phone', { + duration: 2000, + className: 'text-red-500' + }) + dispatch(setIsProcessing()) + return + } + let res; + if(isSignup) { + res = await signUp(phone.current?.value|| '') + }else{ + res = await signIn(phoneValue) } - } catch (error) { - alert("Something went wrong"); - } finally { - setLoading(false); + if(res.success) { + //Reset the enteries + toast.success(res.message, { + duration: 2000, + className: 'text-green-500' + }) + dispatch(setShowOtp(true)); + } else { + toast.error(res.message, { + duration: 2000, + className: 'text-red-500' + }) + } + //setting isLoading to false + dispatch(setIsProcessing()) + }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault() + handleNextClick() } - }; - + } return ( <> - { - setShowOtp(false); - onClose(); - }} - > - -
+ + +
+ {/* Maheep Singh Image with Speech Bubble */}
@@ -83,11 +101,13 @@ export function LoginDialog({ isOpen, onClose }: LoginDialogProps) { priority />
+ {/* Speech Bubble */}
Likho 98..
+ {/* Text */}

Enter your phone number or email,{" "} @@ -97,40 +117,52 @@ export function LoginDialog({ isOpen, onClose }: LoginDialogProps) {

- - {errors.number && ( -

- {errors.number.message} -

- )} + {/* Input Field */} + {isSignup && ( + + )} + {/* Phone Input */} + + + {/* Buttons with Separator */}
-
- + {isSignup && } + {!isSignup && } + {/* Terms Text */}

- +
- setShowOtp(false)} - /> + ); } + +// Usage in your component: \ No newline at end of file diff --git a/apps/user-fe/app/api/user/route.ts b/apps/user-fe/app/api/user/route.ts new file mode 100644 index 0000000..1b770d7 --- /dev/null +++ b/apps/user-fe/app/api/user/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { isValidSession, removeSession } from '@repo/redis/client' +import jwt, { JwtPayload } from 'jsonwebtoken' + +export async function isSessionValid(key: string, sessionId: string) { + const findValidSession = await isValidSession(key, sessionId) + return findValidSession +} +export async function GET(req: NextRequest) { + const url = new URL(req.url); + const token = url.searchParams.get('token'); + if(!token){ + return NextResponse.redirect(new URL('/invalidsession' , req.url)) + } + try{ + const payload = jwt.verify(token!, process.env.JWT_SECRET!) as JwtPayload + const key = `session:${payload.userId}` + const sessionId = payload.sessionId + const isValid =await isSessionValid(key!, sessionId!); + return NextResponse.json({ + isValid + }); + }catch(e){ + return NextResponse.redirect(new URL('/invalidsession' , req.url)) + } +} \ No newline at end of file diff --git a/apps/user-fe/app/invalidsession/page.tsx b/apps/user-fe/app/invalidsession/page.tsx new file mode 100644 index 0000000..5ef0a2a --- /dev/null +++ b/apps/user-fe/app/invalidsession/page.tsx @@ -0,0 +1,23 @@ +'use client' + +import { useRouter } from "next/navigation" +import { useEffect } from "react" +import { toast } from "sonner" +import { logout } from "../lib/actions/getUser" +import { useDispatch } from "react-redux" +import { IDispatchType } from "../lib/redux/store" +import { logoutState } from "../lib/redux/authSlice" + +export default function page(){ + const dispatch = useDispatch() + const router = useRouter() + useEffect(()=>{ + (async ()=> { + toast("Multiple Device logged in"); + await logout(); + dispatch(logoutState()); + router.push("/"); + })() + } , []) + return null +} \ No newline at end of file diff --git a/apps/user-fe/app/layout.tsx b/apps/user-fe/app/layout.tsx index 4532ff1..6b68d27 100644 --- a/apps/user-fe/app/layout.tsx +++ b/apps/user-fe/app/layout.tsx @@ -1,21 +1,28 @@ -import { Toaster } from "react-hot-toast"; +import { Toaster } from "sonner"; import "./globals.css"; +import { Providers } from "./providers"; +import Navbar from "./_common/navbar"; +import { getUser } from "./lib/actions/getUser"; export const metadata = { title: "India's Got Latent", description: "A talent show for the latently talented", }; -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode; }) { + const session = await getUser() return ( - + + + + {children} - + ); diff --git a/apps/user-fe/app/lib/actions/getUser.ts b/apps/user-fe/app/lib/actions/getUser.ts new file mode 100644 index 0000000..364979b --- /dev/null +++ b/apps/user-fe/app/lib/actions/getUser.ts @@ -0,0 +1,31 @@ +'use server' +import jwt, { JwtPayload } from 'jsonwebtoken' +import { cookies } from 'next/headers' +import { removeSession} from '@repo/redis/client' + + +export async function getUser() { + const getCookies = cookies() + const token = getCookies.get('token')?.value || '' + if (!token) { + return null + } + try { + //verify token + const jwtVerify = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload + return jwtVerify + } catch (e) { + return null + } +} + +export const logout =async ()=>{ + const getCookies = cookies() + const token = getCookies.get('token')?.value || '' + if(!token){ + return 'Already logged out' + } + const {userId , sessionId} =jwt.verify(token , process.env.JWT_SECRET!) as JwtPayload + await removeSession(userId , sessionId) + cookies().delete('token') +} \ No newline at end of file diff --git a/apps/user-fe/app/lib/api.ts b/apps/user-fe/app/lib/api.ts new file mode 100644 index 0000000..77ba838 --- /dev/null +++ b/apps/user-fe/app/lib/api.ts @@ -0,0 +1,19 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: process.env.NEXT_PUBLIC_BACKEND_URL, + headers: { + 'Content-Type': 'application/json' + }, + withCredentials: true +}); + +api.interceptors.response.use( + (response) => response, + (error) => { + const message = error.response?.data?.message || error.message || 'An error occurred'; + return Promise.reject(message); + } +); + +export default api diff --git a/apps/user-fe/app/lib/loginRoutes/user.ts b/apps/user-fe/app/lib/loginRoutes/user.ts new file mode 100644 index 0000000..7e02e5c --- /dev/null +++ b/apps/user-fe/app/lib/loginRoutes/user.ts @@ -0,0 +1,36 @@ +import api from "../api" + +export const signUp = async(number: string )=>{ + try{ + const res = await api.post(`/api/v1/user/signup` , { + number : number + }) + return { + success: true, + message : res.data.message + } + }catch(e){ + return { + success : false, + message : e + } + } +} + + +export const signIn = async(number: string)=>{ + try{ + const res = await api.post(`/api/v1/user/signin` , { + number: number + }) + return { + success: true, + message : res.data.message + } + }catch(e){ + return { + success : false, + message : e + } + } +} diff --git a/apps/user-fe/app/lib/redux/authSlice.ts b/apps/user-fe/app/lib/redux/authSlice.ts new file mode 100644 index 0000000..edaf5bb --- /dev/null +++ b/apps/user-fe/app/lib/redux/authSlice.ts @@ -0,0 +1,92 @@ +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +import api from "../api"; +import { SignInVerifyType, SignUpVerifyType } from "@repo/common/types"; +import { JwtPayload } from "jsonwebtoken"; + +const backend_url = process.env.NEXT_PUBLIC_BACKEND_URL; + +export const fetchUser = createAsyncThunk('/api/v1/user', async(user : JwtPayload | null, {rejectWithValue})=>{ + if(!user){ + return rejectWithValue('Not Authenticated') + }else{ + return user + } +}) + +//Signup verify +export const signUpVerify = createAsyncThunk('api/v1/user/signup/verify' ,async ({name ,number , totp} : SignUpVerifyType, {rejectWithValue})=>{ + try{ + const res =await api.post('/api/v1/user/signup/verify' , { + name, + number, + totp + }) + return res.data + }catch(e){ + return rejectWithValue('Cannot verify') + } +}) + +//Signin Verify +export const signInVerify = createAsyncThunk('api/v1/user/signin/verify' , async({number , totp} : SignInVerifyType, {rejectWithValue})=>{ + try{ + const res = await api.post('/api/v1/user/signin/verify' , { + number, + totp + }) + return res.data + }catch(e){ + return rejectWithValue('Cannot Verify') + } +}) + + + +const initialState = { + userid : '', + isVerified : false, + isAuthorized : false, + isLoading : true, + plan: 'start' +} + +const authSlice = createSlice({ + name : 'authSlice', + initialState , + reducers : { + logoutState : (state)=>{ + state.isAuthorized = false + state.isVerified= false + state.plan = '' + } + }, + extraReducers : (builder)=>{ + builder.addCase(fetchUser.fulfilled ,(state , action)=>{ + state.isAuthorized = true + state.userid = action.payload?.userId + state.isLoading = false + state.plan = action.payload?.plan + }) + builder.addCase(signUpVerify.fulfilled , (state, action)=>{ + state.isAuthorized = true + state.isVerified = true + state.userid = action.payload.userId + state.isLoading = false + state.plan = action.payload.plan + }) + builder.addCase(fetchUser.rejected , (state)=>{ + state.isLoading = false + }) + builder.addCase(signInVerify.fulfilled , (state , action)=>{ + state.isAuthorized = true + state.isVerified = true + state.userid = action.payload.userId + state.isLoading = false + state.plan = action.payload.plan + }) + }, + +}) + +export const {logoutState } = authSlice.actions +export default authSlice.reducer \ No newline at end of file diff --git a/apps/user-fe/app/lib/redux/signInDialog.ts b/apps/user-fe/app/lib/redux/signInDialog.ts new file mode 100644 index 0000000..877120b --- /dev/null +++ b/apps/user-fe/app/lib/redux/signInDialog.ts @@ -0,0 +1,30 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + isSignup : false, + isLoginOpen : false, + showOtp : false, + isProcessing : false +} +const signInDialog = createSlice({ + name : 'loginSlice', + initialState , + reducers : { + setIsSignup : (state , action)=>{ + state.isSignup = action.payload + }, + setIsLoginOpen : (state, action)=>{ + state.isLoginOpen = action.payload + }, + setShowOtp : (state , action)=>{ + state.showOtp = action.payload + }, + setIsProcessing : (state)=>{ + state.isProcessing = !state.isProcessing + } + + } +}) + +export const {setIsProcessing , setIsSignup , setIsLoginOpen , setShowOtp} = signInDialog.actions +export default signInDialog.reducer \ No newline at end of file diff --git a/apps/user-fe/app/lib/redux/store.ts b/apps/user-fe/app/lib/redux/store.ts new file mode 100644 index 0000000..f6f4c1b --- /dev/null +++ b/apps/user-fe/app/lib/redux/store.ts @@ -0,0 +1,13 @@ +import { configureStore } from "@reduxjs/toolkit"; +import authReducer from './authSlice' +import signInDialogReducer from './signInDialog' + +export const store = configureStore({ + reducer : { + auth : authReducer, + signInDialog : signInDialogReducer + } +}) + +export type IRootType = ReturnType +export type IDispatchType = typeof store.dispatch \ No newline at end of file diff --git a/apps/user-fe/app/lib/regexPhone.tsx b/apps/user-fe/app/lib/regexPhone.tsx new file mode 100644 index 0000000..00ea529 --- /dev/null +++ b/apps/user-fe/app/lib/regexPhone.tsx @@ -0,0 +1,4 @@ +export function matchRegex(number: string){ + const regexPattern = new RegExp("^[0-9]{10}$"); + return regexPattern.test(number) +} \ No newline at end of file diff --git a/apps/user-fe/app/providers.tsx b/apps/user-fe/app/providers.tsx new file mode 100644 index 0000000..c7ed2fd --- /dev/null +++ b/apps/user-fe/app/providers.tsx @@ -0,0 +1,24 @@ +'use client' + +import { Provider, useDispatch } from "react-redux" +import { IDispatchType, store } from "./lib/redux/store" +import { ReactNode, useEffect } from "react" +import { JwtPayload } from "jsonwebtoken" +import { fetchUser } from "./lib/redux/authSlice" + +export function Providers({children , session} : {children : ReactNode , session : JwtPayload | null}){ + return + + {children} + +} + +function AuthInitializer({ session }: { session: JwtPayload | null }) { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchUser(session)); + }, [dispatch, session]); + + return null; +} \ No newline at end of file diff --git a/apps/user-fe/middleware.ts b/apps/user-fe/middleware.ts new file mode 100644 index 0000000..da99bad --- /dev/null +++ b/apps/user-fe/middleware.ts @@ -0,0 +1,29 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +export const config = { + matcher: ['/', '/upgrade', '/episode/:path*','/episodes'], + }; + +export async function middleware(request: NextRequest) { + const token = request.cookies.get('token')?.value + if (!token) { + return NextResponse.next() + } + + try { + //Checking if the token is valid and checks if multiple sessions are logged in + const isValid = await fetch(`${process.env.LOCAL_BASE_URL}/api/user?token=${token}`) + const json = await isValid.json() + if (!json.isValid) { + const response = NextResponse.redirect(new URL('/invalidsession' , request.url)) + response.cookies.delete('token') + return response + } + return NextResponse.next() + } catch (error) { + const response =NextResponse.redirect(new URL('/invalidsession' , request.url)) + response.cookies.delete('token') + return response + } +} \ No newline at end of file diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 1c9264e..a579275 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -67,7 +67,7 @@ export const UserSignUpSchema = z.object({ export const UserSignUpVerifySchema = z.object({ number: z.string().min(9).max(13), - totp: z.string().min(6).max(6), + totp: z.string().min(4).max(6), name: z.string().min(1).max(255) }) @@ -77,6 +77,8 @@ export const SignInSchema = z.object({ export const SignInVerifySchema = z.object({ number: z.string().min(9).max(13), - totp: z.string().min(6).max(6) + totp: z.string().min(4).max(6) }) +export type SignInVerifyType = z.infer +export type SignUpVerifyType = z.infer \ No newline at end of file diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index a9388ac..18b115d 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -3,6 +3,10 @@ import { RedisClientType, createClient } from "redis"; export const client: RedisClientType = createClient(); +(async function connect(){ + await client.connect() +})() + export function getRedisKey(key: string) { return `latent:${key}`; } @@ -10,3 +14,34 @@ export function getRedisKey(key: string) { export function incrCount(key: string) { return client.incr(getRedisKey(key)); } + +export async function getUserSessions( key : string){ + return await client.lRange(key , 0 ,-1) +} + +//Check if the session exists +export async function isValidSession(key : string, sessionId : string){ + const sessions = await getUserSessions(key) + return sessions.includes(sessionId) +} + + +//Sets the session as per the plan +export async function setUserSessionsByPlan(key : string ,plan : string, sessionId : string){ + const userSessions = await getUserSessions(key) + const length =userSessions.length + const multi = client.multi() + {/* Allowed sessions based on the active plans */} + if(length>0 && (plan==='start' || plan==='basic')){ + multi.lPop(key) + }else if(length>1 && (plan==='standard' || plan==='premium')){ + multi.lPop(key) + } + multi.rPush(key , sessionId) + await multi.exec() +} + +//Remove the session key +export async function removeSession(key :string , sessionId : string){ + await client.lRem(key , 0 , sessionId) +} \ No newline at end of file diff --git a/packages/ui/src/input.tsx b/packages/ui/src/input.tsx index b8c1d23..325cb5d 100644 --- a/packages/ui/src/input.tsx +++ b/packages/ui/src/input.tsx @@ -1,71 +1,22 @@ -"use client" - import * as React from "react" -import { OTPInput, OTPInputContext } from "input-otp" -import { Minus } from "lucide-react" import { cn } from "./lib/utils" -const InputOTP = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, containerClassName, ...props }, ref) => ( - -)) -InputOTP.displayName = "InputOTP" - -const InputOTPGroup = React.forwardRef< - React.ElementRef<"div">, - React.ComponentPropsWithoutRef<"div"> ->(({ className, ...props }, ref) => ( -
-)) -InputOTPGroup.displayName = "InputOTPGroup" - -const InputOTPSlot = React.forwardRef< - React.ElementRef<"div">, - React.ComponentPropsWithoutRef<"div"> & { index: number } ->(({ index, className, ...props }, ref) => { - const inputOTPContext = React.useContext(OTPInputContext) - const { char, hasFakeCaret, isActive } : any | undefined= inputOTPContext.slots[index] - - return ( -
- {char} - {hasFakeCaret && ( -
-
-
- )} -
- ) -}) -InputOTPSlot.displayName = "InputOTPSlot" - -const InputOTPSeparator = React.forwardRef< - React.ElementRef<"div">, - React.ComponentPropsWithoutRef<"div"> ->(({ ...props }, ref) => ( -
- -
-)) -InputOTPSeparator.displayName = "InputOTPSeparator" - -export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/packages/ui/src/otp-input.tsx b/packages/ui/src/otp-input.tsx new file mode 100644 index 0000000..8364100 --- /dev/null +++ b/packages/ui/src/otp-input.tsx @@ -0,0 +1,75 @@ +"use client" + +import * as React from "react" +import { OTPInput, OTPInputContext, SlotProps } from "input-otp" +import { Dot } from "lucide-react" + +import { cn } from "./lib/utils" + +const InputOTP = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( +
+)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = (React.useContext(OTPInputContext) ) + + + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[ + index + ] as SlotProps + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( +
+ +
+)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }