diff --git a/src/handlers/session_handler.rs b/src/handlers/session_handler.rs index 3879a89..13c14af 100644 --- a/src/handlers/session_handler.rs +++ b/src/handlers/session_handler.rs @@ -182,6 +182,7 @@ pub async fn revoke_all( #[debug_handler] pub async fn delete( State(state): State, + header: HeaderMap, payload: Json, ) -> Result> { // revoke all the sessions diff --git a/ui/app/globals.css b/ui/app/globals.css index 5162a94..b4ec89b 100644 --- a/ui/app/globals.css +++ b/ui/app/globals.css @@ -30,7 +30,7 @@ @layer base { * { - @apply border-border; + @apply border-border overflow-x-clip; } body { diff --git a/ui/app/user/[userID]/page.tsx b/ui/app/user/[userID]/page.tsx index abcf120..d570e2b 100644 --- a/ui/app/user/[userID]/page.tsx +++ b/ui/app/user/[userID]/page.tsx @@ -4,834 +4,488 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { IUser } from "@/interfaces/IUser"; import { - AlertDialog, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import React, { useEffect } from "react"; +import React, { useCallback, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { FaRegCopy } from "react-icons/fa"; import { useRouter } from "next/navigation"; import { ISession } from "@/interfaces/ISession"; -import { ColumnDef } from "@tanstack/react-table"; -import { DataTable } from "@/components/ui/data-table"; -import { formatTimestampWithAddedDays } from "@/utils/date"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { IoMdMore } from "react-icons/io"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import useCopy from "@/hooks/useCopy"; import { toast } from "@/components/ui/use-toast"; import { Badge } from "@/components/ui/badge"; -import { capitalizeFirstLetter } from "@/utils/string"; import { GoClockFill } from "react-icons/go"; import { TiTick } from "react-icons/ti"; import { IoIosWarning } from "react-icons/io"; import { format } from "date-fns"; +import SessionTable from "@/components/session/SessionTable"; const UserDetails = ({ params }: any) => { - const { userID } = params; - const { copyHandler } = useCopy(); - const [loading, setLoading] = React.useState(true); - const [user, setUser] = React.useState(null); - const [role, setRole] = React.useState(""); - const [name, setName] = React.useState(""); - const [sessions, setSessions] = React.useState([] as ISession[]); - const [oldPassword, setOldPassword] = React.useState(""); - const [newPassword, setNewPassword] = React.useState(""); - const [confirmNewPassword, setConfirmNewPassword] = React.useState(""); - const router = useRouter(); + const { userID } = params; + const { copyHandler } = useCopy(); + const [loading, setLoading] = React.useState(true); + const [user, setUser] = React.useState(null); + const [role, setRole] = React.useState(""); + const [name, setName] = React.useState(""); + const [sessions, setSessions] = React.useState([] as ISession[]); + const [oldPassword, setOldPassword] = React.useState(""); + const [newPassword, setNewPassword] = React.useState(""); + const [confirmNewPassword, setConfirmNewPassword] = React.useState(""); + const router = useRouter(); - // function to update is_active - const updateUserActive = async (is_active: boolean) => { - try { - setLoading(true); - await fetch( - `${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/toggle-active-status`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: user?.email, - is_active: is_active, - }), + // function to update is_active + const updateUserActive = async (is_active: boolean) => { + try { + setLoading(true); + await fetch( + `${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/toggle-active-status`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: user?.email, + is_active: is_active, + }), + } + ); + await getUser(); + } catch (error) { + console.error("Error during POST request:", error); } - ); - await getUser(); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; + setLoading(false); + }; - const getUser = async () => { - try { - setLoading(true); - const res = await fetch( - `${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/get-from-uid`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - uid: userID, - }), + const getUser = useCallback(async () => { + try { + setLoading(true); + const res = await fetch( + `${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/get-from-uid`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + uid: userID, + }), + } + ); + const { data } = await res.json(); + setUser(data); + setRole(data?.role || ""); + setName(data?.name || ""); + } catch (error) { + console.error("Error during POST request:", error); } - ); - const { data } = await res.json(); - setUser(data); - setRole(data?.role || ""); - setName(data?.name || ""); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; + setLoading(false); + }, [userID]); - // fetch all sessions - const fetchAllSessions = async () => { - try { - setLoading(true); - const res = await fetch( - `${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/get-all-from-uid`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - uid: userID, - }), - cache: "no-cache", + // delete user function + const deleteUser = async (email: string) => { + try { + setLoading(true); + await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/delete`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + }), + }); + router.push("/"); + } catch (error) { + console.error("Error during POST request:", error); } - ); - const { data } = await res.json(); - setSessions(data); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; + setLoading(false); + }; - // delete user function - const deleteUser = async (email: string) => { - try { - setLoading(true); - await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/delete`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - }), - }); - router.push("/"); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; - - // revoke session function - const revokeSession = async (session_id: string) => { - try { - setLoading(true); - await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/revoke`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - session_id, - uid: userID, - }), - }); - await fetchAllSessions(); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; - - // delete session function - const deleteSession = async (session_id: string) => { - try { - setLoading(true); - await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/delete`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - session_id, - uid: userID, - }), - }); - await fetchAllSessions(); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; - - // delete all sessions function - const deleteAllSessions = async () => { - try { - setLoading(true); - await fetch( - `${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/delete-all`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - uid: userID, - }), + // reset password function + const resetPassword = async ( + email: string, + old_password: string, + new_password: string + ) => { + if ( + old_password === "" || + new_password === "" || + confirmNewPassword === "" + ) { + toast({ + title: "Error", + description: "Fill all the fields correctly.", + variant: "destructive", + }); + return; + } else { + if (new_password !== confirmNewPassword) { + toast({ + title: "Error", + description: "New and Confirm New Passwords do not match.", + variant: "destructive", + }); + return; + } } - ); - await fetchAllSessions(); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; - - // revoke all sessions function - const revokeAllSessions = async () => { - try { - setLoading(true); - await fetch( - `${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/revoke-all`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - uid: userID, - }), + try { + setLoading(true); + await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/password/reset`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + old_password, + new_password, + }), + }); + } catch (error) { + console.error("Error during POST request:", error); + } finally { + setOldPassword(""); + setNewPassword(""); + setConfirmNewPassword(""); } - ); - await fetchAllSessions(); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; + setLoading(false); + }; - // reset password function - const resetPassword = async ( - email: string, - old_password: string, - new_password: string - ) => { - if ( - old_password === "" || - new_password === "" || - confirmNewPassword === "" - ) { - toast({ - title: "Error", - description: "Fill all the fields correctly.", - variant: "destructive", - }); - return; - } else { - if (new_password !== confirmNewPassword) { - toast({ - title: "Error", - description: "New and Confirm New Passwords do not match.", - variant: "destructive", - }); - return; - } - } - try { - setLoading(true); - await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/password/reset`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - old_password, - new_password, - }), - }); - } catch (error) { - console.error("Error during POST request:", error); - } finally { - setOldPassword(""); - setNewPassword(""); - setConfirmNewPassword(""); - } - setLoading(false); - }; - - // forget password request function - const forgetPassword = async (email: string) => { - try { - setLoading(true); - await fetch( - `${process.env.NEXT_PUBLIC_ENDPOINT}/api/password/forget-request`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - }), + // forget password request function + const forgetPassword = async (email: string) => { + try { + setLoading(true); + await fetch( + `${process.env.NEXT_PUBLIC_ENDPOINT}/api/password/forget-request`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + }), + } + ); + } catch (error) { + console.error("Error during POST request:", error); } - ); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; - - // edit user function - const editUser = async (email: string, name: string, role: string) => { - if (name === "" || role === "") { - toast({ - title: "Error", - description: "Fill all the fields correctly.", - variant: "destructive", - }); - return; - } - try { - setLoading(true); - if (role !== user?.role) { - await fetch( - `${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/update-role`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email: user?.email, - role, - }), - } - ); - } - if (name !== user?.name) { - await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/update`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - name, - }), - }); - } - await getUser(); - } catch (error) { - console.error("Error during POST request:", error); - } - setLoading(false); - }; - - const sessionColumns: ColumnDef[] = [ - { - accessorKey: "session_id", - header: "Session ID", - cell: ({ row }) => { - const session_id: string = row.getValue("session_id") as string; + setLoading(false); + }; - return ( -
-

- {session_id} -

- copyHandler(session_id, "Session ID")} /> -
- ); - }, - }, - { - accessorKey: "user_agent", - header: "User Agent", - }, - { - accessorKey: "is_revoked", - header: "Is Revoked", - cell: ({ row }) => { - return ( -
{capitalizeFirstLetter(row.original.is_revoked.toString())}
- ); - }, - }, - { - accessorKey: "updated_at", - header: "Updated At", - cell: ({ row }) => { - return ( -
- {new Date( - parseInt(row.original.updated_at.$date.$numberLong) - ).toLocaleString()} -
- ); - }, - }, - { - accessorKey: "created_at", - header: "Expires At", - cell: ({ row }) => { - return ( -
- {formatTimestampWithAddedDays( - parseInt(row.original.created_at.$date.$numberLong), - 45 - )} -
- ); - }, - }, - { - accessorKey: "updated_at", - header: "Action", - cell: ({ row }) => { - const session = row.original; - return ( -
- - - - - - {!row.original.is_revoked && ( - - - - Revoke - - - - - Are you absolutely sure? - - - This action cannot be undone. - - - - Cancel - - - - - - )} - - - - Delete - - - - - Are you absolutely sure? - - - This action cannot be undone. - - - - Cancel - - - - - - - -
- ); - }, - }, - ]; + // edit user function + const editUser = async (email: string, name: string, role: string) => { + if (name === "" || role === "") { + toast({ + title: "Error", + description: "Fill all the fields correctly.", + variant: "destructive", + }); + return; + } + try { + setLoading(true); + if (role !== user?.role) { + await fetch( + `${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/update-role`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email: user?.email, + role, + }), + } + ); + } + if (name !== user?.name) { + await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/update`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + name, + }), + }); + } + await getUser(); + } catch (error) { + console.error("Error during POST request:", error); + } + setLoading(false); + }; - useEffect(() => { - getUser(); - fetchAllSessions(); - }, []); + useEffect(() => { + getUser(); + }, [getUser]); - return ( -
-
- {loading ? ( -
- -
- ) : ( -
- - - -

{user?.name}

- {/* */} -
-
- - - - - - - - Update User Name - - -

Name

- setName(e.target.value)} - /> -

Role

- setRole(e.target.value)} - /> -
-
- - Cancel - - -
-
- - - - - - - - Are you absolutely sure? - - - This action cannot be undone. - - - - Cancel - - - - - - - - - - - - - Reset Password - - - - - Reset Password - - - - setOldPassword(e.target.value) - } - /> - - setNewPassword(e.target.value) - } - /> - - setConfirmNewPassword(e.target.value) - } - /> - - - - Cancel - - - - - - - - - Forget Password - - - - - Forget Password - - - This action cannot be undone. - - - - Cancel - - - - - - - -
-
-
-
- -
-
-

Email

-

{user?.email}

-
-
-

Role

-

{user?.role}

-
-
-

- Email Verification -

- - {user?.email_verified ? : } +
+
+ + + + + + + + Update User Name + + +

Name

+ setName(e.target.value)} + /> +

Role

+ setRole(e.target.value)} + /> +
+
+ + Cancel + + +
+
+ + + + + + + + Are you absolutely sure? + + + This action cannot be undone. + + + + Cancel + + + + + + + + + + + + + Reset Password + + + + + Reset Password + + + + setOldPassword(e.target.value) + } + /> + + setNewPassword(e.target.value) + } + /> + + setConfirmNewPassword(e.target.value) + } + /> + + + + Cancel + + + + + + + + + Forget Password + + + + + Forget Password + + + This action cannot be undone. + + + + Cancel + + + + + + + +
+
+ + + +
+
+

Email

+

{user?.email}

+
+
+

Role

+

{user?.role}

+
+
+

+ Email Verification +

+ + {user?.email_verified ? : } - {user?.email_verified ? "Verified" : "Pending"} - -
-
-

Account Status

- - {user?.is_active ? : } + {user?.email_verified ? "Verified" : "Pending"} + +
+
+

Account Status

+ + {user?.is_active ? : } - {user?.is_active ? "Active" : "Suspended"} - -
-
-

Created At

-

- {format( - new Date(parseInt(user?.created_at.$date.$numberLong!)), - "PP - p" - )} -

-
-
-

Updated At

-

- {format( - new Date(parseInt(user?.updated_at.$date.$numberLong!)), - "PP - p" - )} -

-
-
-
- - - - -

Sessions

- - - - - - - - - Revoke All - - - - - Are you absolutely sure? - - - This action cannot be undone. - - - - Cancel - - - - - - - - - Delete All - - - - - Are you absolutely sure? - - - This action cannot be undone. - - - - Cancel - - - - - - - -
-
- - - -
-
- )} -
-
- ); + {user?.is_active ? "Active" : "Suspended"} + +
+
+

Created At

+

+ {format( + new Date(parseInt(user?.created_at.$date.$numberLong!)), + "PP - p" + )} +

+
+
+

Updated At

+

+ {format( + new Date(parseInt(user?.updated_at.$date.$numberLong!)), + "PP - p" + )} +

+
+
+ + + + + )} + + + ); }; export default UserDetails; diff --git a/ui/components/session/SessionTable.tsx b/ui/components/session/SessionTable.tsx new file mode 100644 index 0000000..21415f7 --- /dev/null +++ b/ui/components/session/SessionTable.tsx @@ -0,0 +1,440 @@ +/* eslint-disable @next/next/no-img-element */ +import { ISession } from "@/interfaces/ISession"; +import { formatTimestampWithAddedDays } from "@/utils/date"; +import { capitalizeFirstLetter } from "@/utils/string"; +import { + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogCancel, +} from "@radix-ui/react-alert-dialog"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@radix-ui/react-dropdown-menu"; +import { ColumnDef } from "@tanstack/react-table"; +import { Loader } from "lucide-react"; +import React, { useCallback, useEffect } from "react"; +import { FaRegCopy } from "react-icons/fa"; +import { IoMdMore } from "react-icons/io"; +import { AlertDialogHeader, AlertDialogFooter } from "../ui/alert-dialog"; +import { Button } from "../ui/button"; +import useCopy from "@/hooks/useCopy"; +import { Card, CardHeader, CardTitle, CardContent } from "../ui/card"; +import { DataTable } from "../ui/data-table"; +import UAParser from "ua-parser-js"; +import { addDays, format } from "date-fns"; + +interface SessionTableProps { + userID: string; +} + +const SessionTable: React.FC = ({ userID }) => { + const [loading, setLoading] = React.useState(false); + const [sessions, setSessions] = React.useState([]); + const { copyHandler } = useCopy(); + + // fetch all sessions + const fetchAllSessions = useCallback(async () => { + try { + setLoading(true); + const res = await fetch( + `${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/get-all-from-uid`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + uid: userID, + }), + cache: "no-cache", + } + ); + const { data } = await res.json(); + setSessions(data); + } catch (error) { + console.error("Error during POST request:", error); + } + setLoading(false); + }, [userID]); + + // revoke session function + const revokeSession = async (session_id: string) => { + try { + setLoading(true); + await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/revoke`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + session_id, + uid: userID, + }), + }); + await fetchAllSessions(); + } catch (error) { + console.error("Error during POST request:", error); + } + setLoading(false); + }; + + // delete session function + const deleteSession = async (session_id: string) => { + try { + setLoading(true); + await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/delete`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + session_id, + uid: userID, + }), + }); + await fetchAllSessions(); + } catch (error) { + console.error("Error during POST request:", error); + } + setLoading(false); + }; + + // delete all sessions function + const deleteAllSessions = async () => { + try { + setLoading(true); + await fetch( + `${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/delete-all`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + uid: userID, + }), + } + ); + await fetchAllSessions(); + } catch (error) { + console.error("Error during POST request:", error); + } + setLoading(false); + }; + + // revoke all sessions function + const revokeAllSessions = async () => { + try { + setLoading(true); + await fetch( + `${process.env.NEXT_PUBLIC_ENDPOINT}/api/session/revoke-all`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + uid: userID, + }), + } + ); + await fetchAllSessions(); + } catch (error) { + console.error("Error during POST request:", error); + } + setLoading(false); + }; + + useEffect(() => { + fetchAllSessions(); + }, [fetchAllSessions]); + + const parser = new UAParser("user-agent"); + + const sessionColumns: ColumnDef[] = [ + { + accessorKey: "user_agent", + header: "User Agent", + cell: ({ row }) => { + parser.setUA(row.original.user_agent); + const result = parser.getResult(); + + console.log(result); + + return ( + // render device type and browser name with logo +
+
+
+ {result.device.type === "mobile" && + ( + device-logo + )} + {result.device.type === undefined && + ( + device-logo + )} +
+
+

{result.device?.vendor} - {result.device?.model}

+

{result.os?.name} - Version: {result.os?.version ? result.os.version : "Unknown"}

+
+ {result.browser.name?.includes("Chrome") && ( + browser-logo + )} + {result.browser.name?.includes("Mozilla") && ( + browser-logo + )} + {result.browser.name?.includes("Safari") && ( + browser-logo + )} +

{result.browser?.name} - Version: {result.browser?.version}

+
+
+
+ +
+ ); + }, + }, + { + accessorKey: "updated_at", + header: "Updated At", + cell: ({ row }) => { + return ( +
+ { + format( + parseInt(row.original.updated_at.$date.$numberLong) + , "PP - p" + ) + } +
+ ); + }, + }, + { + accessorKey: "created_at", + header: "Expires At", + cell: ({ row }) => { + return ( +
+ { + format(addDays( + + parseInt(row.original.created_at.$date.$numberLong) + , 45), "PP - p") + } +
+ ); + }, + }, + { + accessorKey: "is_revoked", + header: "Revoked", + cell: ({ row }) => { + return ( +
+ {row.original.is_revoked ? "Yes" : "No"} +
+ ); + }, + }, + { + accessorKey: "action", + header: "Action", + cell: ({ row }) => { + const session = row.original; + return ( +
+ + + + + + {!row.original.is_revoked && ( + + + + Revoke + + + + + Are you absolutely sure? + + + This action cannot be undone. + + + + Cancel + + + + + + )} + + + + Delete + + + + + Are you absolutely sure? + + + This action cannot be undone. + + + + Cancel + + + + + + + +
+ ); + }, + }, + ]; + + return ( + + + +

Sessions

+ + + + + + + + + Revoke All + + + + + Are you absolutely sure? + + + This action cannot be undone. + + + + Cancel + + + + + + + + + Delete All + + + + + Are you absolutely sure? + + + This action cannot be undone. + + + + Cancel + + + + + + + +
+
+ + + +
+ ); +}; + +export default SessionTable; diff --git a/ui/package-lock.json b/ui/package-lock.json index c14b141..2c05594 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-table": "^8.17.3", + "@types/ua-parser-js": "^0.7.39", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", @@ -26,7 +27,8 @@ "react-dom": "^18", "react-icons": "^5.2.1", "tailwind-merge": "^2.3.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "ua-parser-js": "^1.0.38" }, "devDependencies": { "@types/node": "^20", @@ -1332,6 +1334,11 @@ "@types/react": "*" } }, + "node_modules/@types/ua-parser-js": { + "version": "0.7.39", + "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz", + "integrity": "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==" + }, "node_modules/@typescript-eslint/parser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", @@ -5475,6 +5482,28 @@ "node": ">=14.17" } }, + "node_modules/ua-parser-js": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", + "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index a93c10e..f012d74 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@tanstack/react-table": "^8.17.3", + "@types/ua-parser-js": "^0.7.39", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "date-fns": "^3.6.0", @@ -27,7 +28,8 @@ "react-dom": "^18", "react-icons": "^5.2.1", "tailwind-merge": "^2.3.0", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "ua-parser-js": "^1.0.38" }, "devDependencies": { "@types/node": "^20", diff --git a/ui/public/user-agent/browsers/chrome.png b/ui/public/user-agent/browsers/chrome.png new file mode 100644 index 0000000..8558310 Binary files /dev/null and b/ui/public/user-agent/browsers/chrome.png differ diff --git a/ui/public/user-agent/browsers/mozilla.png b/ui/public/user-agent/browsers/mozilla.png new file mode 100644 index 0000000..b1dfa7d Binary files /dev/null and b/ui/public/user-agent/browsers/mozilla.png differ diff --git a/ui/public/user-agent/browsers/safari.png b/ui/public/user-agent/browsers/safari.png new file mode 100644 index 0000000..53c0e4a Binary files /dev/null and b/ui/public/user-agent/browsers/safari.png differ diff --git a/ui/public/user-agent/devices/desktop.svg b/ui/public/user-agent/devices/desktop.svg new file mode 100644 index 0000000..806f8bd --- /dev/null +++ b/ui/public/user-agent/devices/desktop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/public/user-agent/devices/phone.svg b/ui/public/user-agent/devices/phone.svg new file mode 100644 index 0000000..51dd21e --- /dev/null +++ b/ui/public/user-agent/devices/phone.svg @@ -0,0 +1 @@ + \ No newline at end of file