From a4d0e532c4d8b709550919ece306b1ecf4b339a1 Mon Sep 17 00:00:00 2001 From: NAVAL JANGIR Date: Sun, 2 Feb 2025 17:54:29 +0530 Subject: [PATCH] (signin, signup, logout working),added middleware, multiple session handling --- apps/user-fe/app/_common/navbar.tsx | 108 ++++-- .../app/_components/signup/otp-dialog.tsx | 171 +++++---- .../app/_components/signup/singupDialog.tsx | 204 +++++----- apps/user-fe/app/api/user/route.ts | 26 ++ apps/user-fe/app/invalidsession/page.tsx | 23 ++ apps/user-fe/app/layout.tsx | 15 +- apps/user-fe/app/lib/actions/getUser.ts | 31 ++ apps/user-fe/app/lib/api.ts | 19 + apps/user-fe/app/lib/loginRoutes/user.ts | 36 ++ apps/user-fe/app/lib/redux/authSlice.ts | 92 +++++ apps/user-fe/app/lib/redux/signInDialog.ts | 30 ++ apps/user-fe/app/lib/redux/store.ts | 13 + apps/user-fe/app/lib/regexPhone.tsx | 4 + apps/user-fe/app/providers.tsx | 24 ++ apps/user-fe/middleware.ts | 29 ++ packages/common/src/types.ts | 6 +- packages/redis/src/index.ts | 35 ++ packages/ui/src/input.tsx | 85 +--- packages/ui/src/otp-input.tsx | 75 ++++ pnpm-lock.yaml | 363 +++++++++++++++++- 20 files changed, 1100 insertions(+), 289 deletions(-) create mode 100644 apps/user-fe/app/api/user/route.ts create mode 100644 apps/user-fe/app/invalidsession/page.tsx create mode 100644 apps/user-fe/app/lib/actions/getUser.ts create mode 100644 apps/user-fe/app/lib/api.ts create mode 100644 apps/user-fe/app/lib/loginRoutes/user.ts create mode 100644 apps/user-fe/app/lib/redux/authSlice.ts create mode 100644 apps/user-fe/app/lib/redux/signInDialog.ts create mode 100644 apps/user-fe/app/lib/redux/store.ts create mode 100644 apps/user-fe/app/lib/regexPhone.tsx create mode 100644 apps/user-fe/app/providers.tsx create mode 100644 apps/user-fe/middleware.ts create mode 100644 packages/ui/src/otp-input.tsx 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 } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a447911..49264c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,12 @@ importers: authenticator: specifier: ^1.1.5 version: 1.1.5 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + cors: + specifier: ^2.8.5 + version: 2.8.5 dotenv: specifier: ^16.4.7 version: 16.4.7 @@ -91,12 +97,45 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config + '@types/cookie-parser': + specifier: ^1.4.8 + version: 1.4.8(@types/express@5.0.0) + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 + esbuild: + specifier: 0.24.2 + version: 0.24.2 apps/user-fe: dependencies: + '@hookform/resolvers': + specifier: ^3.10.0 + version: 3.10.0(react-hook-form@7.54.2(react@18.3.1)) + '@radix-ui/react-switch': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reduxjs/toolkit': + specifier: ^2.5.1 + version: 2.5.1(react-redux@9.2.0(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@repo/redis': + specifier: workspace:* + version: link:../../packages/redis '@repo/ui': specifier: workspace:* version: link:../../packages/ui + '@types/jsonwebtoken': + specifier: ^9.0.7 + version: 9.0.7 + axios: + specifier: ^1.7.9 + version: 1.7.9 + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 lucide-react: specifier: ^0.323.0 version: 0.323.0(react@18.3.1) @@ -109,7 +148,25 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.54.2 + version: 7.54.2(react@18.3.1) + react-hot-toast: + specifier: ^2.5.1 + version: 2.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-redux: + specifier: ^9.2.0 + version: 9.2.0(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1) + sonner: + specifier: ^1.7.3 + version: 1.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: ^3.24.1 + version: 3.24.1 devDependencies: + '@repo/common': + specifier: workspace:* + version: link:../../packages/common '@repo/eslint-config': specifier: workspace:* version: link:../../packages/eslint-config @@ -119,6 +176,9 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:../../packages/typescript-config + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 '@types/node': specifier: ^20.10.6 version: 20.17.6 @@ -168,6 +228,9 @@ importers: '@prisma/client': specifier: 6.2.1 version: 6.2.1(prisma@6.2.1) + dotenv: + specifier: ^16.4.7 + version: 16.4.7 prisma: specifier: ^6.2.1 version: 6.2.1 @@ -266,18 +329,27 @@ importers: packages/ui: dependencies: + '@hookform/resolvers': + specifier: ^3.10.0 + version: 3.10.0(react-hook-form@7.54.2(react@18.3.1)) '@radix-ui/react-dialog': specifier: ^1.0.5 version: 1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.1 + version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': - specifier: ^1.0.2 + specifier: ^1.1.1 version: 1.1.1(@types/react@18.3.1)(react@18.3.1) class-variance-authority: - specifier: ^0.7.0 + specifier: ^0.7.1 version: 0.7.1 clsx: - specifier: ^2.1.0 + specifier: ^2.1.1 version: 2.1.1 + input-otp: + specifier: ^1.4.2 + version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lucide-react: specifier: ^0.323.0 version: 0.323.0(react@18.3.1) @@ -287,12 +359,18 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.54.2 + version: 7.54.2(react@18.3.1) tailwind-merge: - specifier: ^2.2.1 + specifier: ^2.6.0 version: 2.6.0 tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.5.4))) + zod: + specifier: ^3.24.1 + version: 3.24.1 devDependencies: '@repo/eslint-config': specifier: workspace:* @@ -533,6 +611,11 @@ packages: resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hookform/resolvers@3.10.0': + resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -912,6 +995,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.1': + resolution: {integrity: sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.3': resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} peerDependencies: @@ -960,6 +1056,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.1.2': + resolution: {integrity: sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: @@ -996,6 +1105,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@redis/bloom@1.2.0': resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} peerDependencies: @@ -1025,6 +1152,17 @@ packages: peerDependencies: '@redis/client': ^1.0.0 + '@reduxjs/toolkit@2.5.1': + resolution: {integrity: sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rollup/rollup-android-arm-eabi@4.30.1': resolution: {integrity: sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==} cpu: [arm] @@ -1161,6 +1299,14 @@ packages: '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/cookie-parser@1.4.8': + resolution: {integrity: sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==} + peerDependencies: + '@types/express': '*' + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1179,6 +1325,9 @@ packages: '@types/inquirer@6.5.0': resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==} + '@types/js-cookie@3.0.6': + resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1225,6 +1374,9 @@ packages: '@types/tinycolor2@1.4.6': resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.15.0': resolution: {integrity: sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1650,6 +1802,10 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -1657,9 +1813,17 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-js-pure@3.39.0: resolution: {integrity: sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -2144,6 +2308,11 @@ packages: resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} engines: {node: '>=8'} + goober@2.1.16: + resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} + peerDependencies: + csstype: ^3.0.10 + gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} @@ -2226,6 +2395,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2248,6 +2420,12 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + input-otp@1.4.2: + resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + inquirer@7.3.3: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} engines: {node: '>=8.0.0'} @@ -2423,6 +2601,10 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2479,6 +2661,7 @@ packages: lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -2965,9 +3148,34 @@ packages: peerDependencies: react: ^19.0.0 + react-hook-form@7.54.2: + resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-hot-toast@2.5.1: + resolution: {integrity: sha512-54Gq1ZD1JbmAb4psp9bvFHjS7lje+8ubboUmvKZkCsQBLH6AOpZ9JemfRvIdHcfb9AZXRaFLrb3qUobGYDJhFQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -3020,6 +3228,14 @@ packages: redis@4.7.0: resolution: {integrity: sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -3038,6 +3254,9 @@ packages: resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} engines: {node: '>=0.10.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3184,6 +3403,12 @@ packages: resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sonner@1.7.3: + resolution: {integrity: sha512-KXLWQfyR6AHpYZuQk8eO8fCbZSJY3JOpgsu/tbGc++jgPjj8JsR1ZpO8vFhqR/OxvWMQCSAmnSShY0gr4FPqHg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3537,6 +3762,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.4.0: + resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3832,6 +4062,10 @@ snapshots: dependencies: levn: 0.4.1 + '@hookform/resolvers@3.10.0(react-hook-form@7.54.2(react@18.3.1))': + dependencies: + react-hook-form: 7.54.2(react@18.3.1) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4123,6 +4357,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-label@2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -4159,6 +4402,21 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-switch@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.0 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.1)(react@18.3.1)': dependencies: react: 18.3.1 @@ -4185,6 +4443,19 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-use-previous@1.1.0(@types/react@18.3.1)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.1 + + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.1)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.1 + '@redis/bloom@1.2.0(@redis/client@1.6.0)': dependencies: '@redis/client': 1.6.0 @@ -4211,6 +4482,16 @@ snapshots: dependencies: '@redis/client': 1.6.0 + '@reduxjs/toolkit@2.5.1(react-redux@9.2.0(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 18.3.1 + react-redux: 9.2.0(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1) + '@rollup/rollup-android-arm-eabi@4.30.1': optional: true @@ -4335,6 +4616,14 @@ snapshots: dependencies: '@types/node': 20.17.6 + '@types/cookie-parser@1.4.8(@types/express@5.0.0)': + dependencies: + '@types/express': 5.0.0 + + '@types/cors@2.8.17': + dependencies: + '@types/node': 20.17.6 + '@types/estree@1.0.6': {} '@types/express-serve-static-core@5.0.5': @@ -4363,6 +4652,8 @@ snapshots: '@types/through': 0.0.33 rxjs: 6.6.7 + '@types/js-cookie@3.0.6': {} + '@types/json-schema@7.0.15': {} '@types/jsonwebtoken@9.0.7': @@ -4413,6 +4704,8 @@ snapshots: '@types/tinycolor2@1.4.6': {} + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.15.0(@typescript-eslint/parser@8.15.0(eslint@9.15.0(jiti@1.21.7))(typescript@5.5.4))(eslint@9.15.0(jiti@1.21.7))(typescript@5.5.4)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -4931,12 +5224,24 @@ snapshots: content-type@1.0.5: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + cookie-signature@1.0.6: {} cookie@0.7.1: {} + cookie@0.7.2: {} + core-js-pure@3.39.0: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + create-require@1.1.1: {} cross-spawn@7.0.6: @@ -5571,6 +5876,10 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + goober@2.1.16(csstype@3.1.3): + dependencies: + csstype: 3.1.3 + gopd@1.0.1: dependencies: get-intrinsic: 1.2.4 @@ -5659,6 +5968,8 @@ snapshots: ignore@5.3.2: {} + immer@10.1.1: {} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -5677,6 +5988,11 @@ snapshots: ini@1.3.8: {} + input-otp@1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + inquirer@7.3.3: dependencies: ansi-escapes: 4.3.2 @@ -5863,6 +6179,8 @@ snapshots: jiti@1.21.7: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -6410,8 +6728,28 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-hook-form@7.54.2(react@18.3.1): + dependencies: + react: 18.3.1 + + react-hot-toast@2.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + csstype: 3.1.3 + goober: 2.1.16(csstype@3.1.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is@16.13.1: {} + react-redux@9.2.0(@types/react@18.3.1)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + use-sync-external-store: 1.4.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + redux: 5.0.1 + react-remove-scroll-bar@2.3.8(@types/react@18.3.1)(react@18.3.1): dependencies: react: 18.3.1 @@ -6468,6 +6806,12 @@ snapshots: '@redis/search': 1.2.0(@redis/client@1.6.0) '@redis/time-series': 1.1.0(@redis/client@1.6.0) + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.6: dependencies: call-bind: 1.0.7 @@ -6496,6 +6840,8 @@ snapshots: dependencies: rc: 1.2.8 + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve@1.22.8: @@ -6711,6 +7057,11 @@ snapshots: ip-address: 9.0.5 smart-buffer: 4.2.0 + sonner@1.7.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + source-map-js@1.2.1: {} source-map@0.6.1: {} @@ -7083,6 +7434,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + use-sync-external-store@1.4.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {}