diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json index b98d1f68b..c033be28b 100644 --- a/dashboard/package-lock.json +++ b/dashboard/package-lock.json @@ -12,9 +12,11 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.14", + "@mui/x-date-pickers": "^7.2.0", "@rainbow-me/rainbowkit": "^2.0.2", "@tanstack/react-query": "^5.28.4", "classnames": "^2.5.0", + "dayjs": "^1.11.10", "ethers": "^6.10.0", "express": "^4.18.2", "micromodal": "^0.4.10", @@ -2190,9 +2192,9 @@ "peer": true }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3488,6 +3490,71 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/@mui/x-date-pickers": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.2.0.tgz", + "integrity": "sha512-hsXugZ+n1ZnHRYzf7+PFrjZ44T+FyGZmTreBmH0M2RUaAblgK+A1V3KNLT+r4Y9gJLH+92LwePxQ9xyfR+E51A==", + "dependencies": { + "@babel/runtime": "^7.24.0", + "@mui/base": "^5.0.0-beta.40", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "@types/react-transition-group": "^4.4.10", + "clsx": "^2.1.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14", + "date-fns": "^2.25.0 || ^3.2.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@next/env": { "version": "14.0.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz", @@ -7480,8 +7547,7 @@ "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", - "peer": true + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debug": { "version": "4.3.4", diff --git a/dashboard/package.json b/dashboard/package.json index 8d081ad20..c9f5bf002 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -14,9 +14,11 @@ "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.14", + "@mui/x-date-pickers": "^7.2.0", "@rainbow-me/rainbowkit": "^2.0.2", "@tanstack/react-query": "^5.28.4", "classnames": "^2.5.0", + "dayjs": "^1.11.10", "ethers": "^6.10.0", "express": "^4.18.2", "micromodal": "^0.4.10", diff --git a/dashboard/src/components/Admin/DownloadLogs.tsx b/dashboard/src/components/Admin/DownloadLogs.tsx index 155872c25..7a26cbbe9 100644 --- a/dashboard/src/components/Admin/DownloadLogs.tsx +++ b/dashboard/src/components/Admin/DownloadLogs.tsx @@ -1,55 +1,162 @@ -import React, { useState } from 'react' +import React, { useCallback, useState } from 'react' +import Image from 'next/image' +import { useAdminContext } from '@context/AdminProvider' +import { + Button, + TextField, + Select, + MenuItem, + FormControl, + InputLabel +} from '@mui/material' +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' +import { LocalizationProvider, DateTimePicker } from '@mui/x-date-pickers' +import dayjs, { Dayjs } from 'dayjs' + +import DownloadSVG from '../../assets/download.svg' import styles from './index.module.css' -import Button from '@mui/material/Button' -import { useAdminContext } from '@context/AdminProvider' // Assuming the context is available -export default function DownloadLogs() { +export default function DownloadButton() { + const [showFilters, setShowFilters] = useState(false) const [isLoading, setLoading] = useState(false) + const [startDate, setStartDate] = useState(dayjs()) + const [endDate, setEndDate] = useState(dayjs()) + const [maxLogs, setMaxLogs] = useState('') + const [moduleName, setModuleName] = useState('') + const [level, setLevel] = useState('') const { signature, expiryTimestamp } = useAdminContext() - const Spinner = () => { - return - } - - async function downloadLogs() { - if (!expiryTimestamp || !signature) { - console.error('Missing expiryTimestamp or signature') - return - } + const downloadLogs = useCallback(async () => { + const startDateParam = startDate ? `&startTime=${startDate.toISOString()}` : '' + const endDateParam = endDate ? `&endTime=${endDate.toISOString()}` : '' + const maxLogsParam = maxLogs ? `&maxLogs=${maxLogs}` : '' + const moduleNameParam = + moduleName && moduleName !== 'all' ? `&moduleName=${moduleName}` : '' + const levelParam = level && level !== 'all' ? `&level="${level}"` : '' setLoading(true) try { - const response = await fetch(`/logs`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ expiryTimestamp, signature }) - }) - - if (!response.ok) { - throw new Error('Network response was not ok') + if (!expiryTimestamp || !signature) { + console.error('Missing expiryTimestamp or signature') + return } - + const response = await fetch( + `/logs?${startDateParam}${endDateParam}${maxLogsParam}${moduleNameParam}${levelParam}`, + { + headers: { + 'Content-Type': 'application/json' + }, + method: 'POST', + body: JSON.stringify({ expiryTimestamp, signature }) + } + ) const data = await response.json() - const dataStr = - 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(data)) - const download = document.createElement('a') - download.setAttribute('href', dataStr) - download.setAttribute('download', 'LogsData.json') - document.body.appendChild(download) - download.click() - download.remove() + if (data) { + const dataStr = + 'data:application/json;charset=utf-8,' + + encodeURIComponent(JSON.stringify(data)) + const download = document.createElement('a') + download.setAttribute('href', dataStr) + download.setAttribute('download', 'LogsData.json') + document.body.appendChild(download) + download.click() + download.remove() + } + setLoading(false) } catch (error) { - console.error('Error downloading logs:', error) - } finally { + console.error(error) setLoading(false) } - } + }, [startDate, endDate, maxLogs, moduleName, level]) return ( - +
+ + + {showFilters && ( +
+ + + setStartDate(newDate)} + /> + + + setEndDate(newDate)} + /> + + + setMaxLogs(e.target.value)} + fullWidth + margin="normal" + variant="outlined" + /> + + Module Name + + + + + Level + + + + +
+ )} +
) } diff --git a/dashboard/src/components/Admin/index.module.css b/dashboard/src/components/Admin/index.module.css index dfa206a90..5bfe82886 100644 --- a/dashboard/src/components/Admin/index.module.css +++ b/dashboard/src/components/Admin/index.module.css @@ -107,7 +107,7 @@ min-width: 245px; display: flex; flex-direction: column; - padding: 40px; + padding: 20px; } .title { diff --git a/dashboard/src/components/Admin/index.tsx b/dashboard/src/components/Admin/index.tsx index 71dbad5c0..26f2fac5b 100644 --- a/dashboard/src/components/Admin/index.tsx +++ b/dashboard/src/components/Admin/index.tsx @@ -19,12 +19,12 @@ export default function AdminActions() {
Your account does not have admin access
)} - {(!signature || !validTimestamp) && admin && ( + {(!signature || !validTimestamp) && isConnected && admin && ( )} - {isConnected && signature && validTimestamp && admin && ( + {isConnected && signature && validTimestamp && isConnected && admin && ( diff --git a/dashboard/src/components/Dashboard/Menu.module.css b/dashboard/src/components/Dashboard/Menu.module.css new file mode 100644 index 000000000..f0ea8dcee --- /dev/null +++ b/dashboard/src/components/Dashboard/Menu.module.css @@ -0,0 +1,28 @@ +.root { + border-radius: 12px; + background: #FFF; + max-width: 260px; + display: flex; + flex-direction: column; + padding: 40px 28px; + min-width: 260px; +} + +.title { + color: #3D4551; + font-family: Helvetica; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: 140%; + margin-bottom: 47px; +} + +@media screen and (max-width: 700px) { + .root { + max-width: none; + width: 90vw; + margin: 0 auto; + padding: 20px; + } +} \ No newline at end of file diff --git a/dashboard/src/components/Dashboard/index.module.css b/dashboard/src/components/Dashboard/index.module.css index 2a08b46ac..6aaf97cd7 100644 --- a/dashboard/src/components/Dashboard/index.module.css +++ b/dashboard/src/components/Dashboard/index.module.css @@ -3,6 +3,7 @@ flex-direction: row; gap: 28px; position: relative; + min-height: 550px; } .bodyContainer { diff --git a/dashboard/src/components/Dashboard/index.tsx b/dashboard/src/components/Dashboard/index.tsx index 7f78516c8..4f47dfa2e 100644 --- a/dashboard/src/components/Dashboard/index.tsx +++ b/dashboard/src/components/Dashboard/index.tsx @@ -81,6 +81,7 @@ export default function Dashboard() { setLoading(false) }) } catch (error) { + setLoading(false) console.log('error', error) } }, []) @@ -123,12 +124,8 @@ export default function Dashboard() {
NODE ID
{nodeData.map((node) => { return ( -
-
setNode(node)} - > +
+
setNode(node)}>
{truncateString(node.id, 12)}
diff --git a/dashboard/src/components/NodePeers/index.tsx b/dashboard/src/components/NodePeers/index.tsx index 8989f4966..6162575fc 100644 --- a/dashboard/src/components/NodePeers/index.tsx +++ b/dashboard/src/components/NodePeers/index.tsx @@ -42,7 +42,7 @@ export default function NodePeers() { {nodePeers.length > 0 ? ( nodePeers.map((address) => { return ( -
+
{truncateString(address, 12)}
) diff --git a/dashboard/src/components/Search/style.module.css b/dashboard/src/components/Search/style.module.css index ca97f26bb..03f9a3f3a 100644 --- a/dashboard/src/components/Search/style.module.css +++ b/dashboard/src/components/Search/style.module.css @@ -1,6 +1,6 @@ .searchOutterContainer { display: flex; - justify-content: end; + justify-content: flex-end; align-items: center; } diff --git a/dashboard/src/components/Spinner/style.module.css b/dashboard/src/components/Spinner/style.module.css index 18837ce87..c2d20073c 100644 --- a/dashboard/src/components/Spinner/style.module.css +++ b/dashboard/src/components/Spinner/style.module.css @@ -8,6 +8,7 @@ box-sizing: border-box; animation: rotation 1s linear infinite; } + .loader::after, .loader::before { content: ''; @@ -21,10 +22,20 @@ transform: translate(150%, 150%); border-radius: 50%; } + .loader::before { left: auto; top: auto; right: 0; bottom: 0; transform: translate(-150%, -150%); -} \ No newline at end of file +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/dashboard/src/context/AdminProvider.tsx b/dashboard/src/context/AdminProvider.tsx index 61a2823bd..616fe85bf 100644 --- a/dashboard/src/context/AdminProvider.tsx +++ b/dashboard/src/context/AdminProvider.tsx @@ -9,7 +9,7 @@ import { useEffect } from 'react' import { useAccount, useSignMessage } from 'wagmi' -import { sha256, toUtf8Bytes } from 'ethers' +import { verifyMessage } from 'ethers' interface AdminContextType { admin: boolean @@ -46,29 +46,26 @@ export const AdminProvider: FunctionComponent<{ children: ReactNode }> = ({ } }, [address, isConnected]) + // Get expiryTimestamp and signature from localStorage useEffect(() => { const storedExpiry = localStorage.getItem('expiryTimestamp') - if (storedExpiry) { - setExpiryTimestamp(parseInt(storedExpiry, 10)) - } - - const storedSignature = localStorage.getItem('signature') - if (storedSignature) { - setSignature(storedSignature) + const storedExpiryTimestamp = storedExpiry ? parseInt(storedExpiry, 10) : null + if (storedExpiryTimestamp && storedExpiryTimestamp > Date.now()) { + setExpiryTimestamp(storedExpiryTimestamp) + const storedSignature = localStorage.getItem('signature') + if (storedSignature) { + setSignature(storedSignature) + } } }, [address, isConnected]) + // Store signature and expiryTimestamp in localStorage useEffect(() => { - if (expiryTimestamp) { + if (expiryTimestamp && expiryTimestamp > Date.now()) { localStorage.setItem('expiryTimestamp', expiryTimestamp.toString()) + signature && localStorage.setItem('signature', signature) } - }, [expiryTimestamp, address, isConnected]) - - useEffect(() => { - if (signature) { - localStorage.setItem('signature', signature) - } - }, [signature, address, isConnected]) + }, [expiryTimestamp, signature, address, isConnected]) useEffect(() => { if (signMessageData) { @@ -88,15 +85,27 @@ export const AdminProvider: FunctionComponent<{ children: ReactNode }> = ({ }, [expiryTimestamp, address, isConnected]) const generateSignature = () => { - if (isConnected && (!expiryTimestamp || Date.now() >= expiryTimestamp)) { - const newExpiryTimestamp = Date.now() + 12 * 60 * 60 * 1000 // 12 hours ahead in milliseconds - signMessage({ - message: sha256(toUtf8Bytes(newExpiryTimestamp.toString())) - }) - setExpiryTimestamp(newExpiryTimestamp) - } + const newExpiryTimestamp = Date.now() + 12 * 60 * 60 * 1000 // 12 hours ahead in milliseconds + signMessage({ + message: newExpiryTimestamp.toString() + }) + setExpiryTimestamp(newExpiryTimestamp) } + // Remove signature and expiryTimestamp from state if they are not from the currently connected account + useEffect(() => { + if (expiryTimestamp && signature) { + const signerAddress = verifyMessage( + expiryTimestamp.toString(), + signature + ).toLowerCase() + if (signerAddress !== address?.toLowerCase()) { + setExpiryTimestamp(undefined) + setSignature(undefined) + } + } + }, [address, expiryTimestamp, signature]) + const value: AdminContextType = { admin, setAdmin, diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 2d4dbe187..958a871c2 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -18,15 +18,17 @@ export function validateAdminSignature( return { valid: false, error: errorMsg } } const currentTimestamp = new Date().getTime() + if (currentTimestamp > expiryTimestamp) { + const errorMsg = `The expiryTimestamp ${expiryTimestamp} sent for validation is in the past. Therefore signature ${signature} is rejected` + CORE_LOGGER.logMessage(errorMsg) + return { valid: false, error: errorMsg } + } for (const address of allowedAdmins) { - if ( - ethers.getAddress(address) === ethers.getAddress(signerAddress) && - currentTimestamp < expiryTimestamp - ) { + if (ethers.getAddress(address) === ethers.getAddress(signerAddress)) { return { valid: true, error: '' } } } - const errorMsg = `Signature ${signature} is invalid` + const errorMsg = `The address which signed the message is not on the allowed admins list. Therefore signature ${signature} is rejected` CORE_LOGGER.logMessage(errorMsg) return { valid: false, error: errorMsg } } catch (e) {