diff --git a/app/components/boundaries/BlockedDevicesBoundarie.client.tsx b/app/components/boundaries/BlockedDevices/BlockedDevicesBoundarie.client.tsx similarity index 100% rename from app/components/boundaries/BlockedDevicesBoundarie.client.tsx rename to app/components/boundaries/BlockedDevices/BlockedDevicesBoundarie.client.tsx diff --git a/app/components/boundaries/BlockedDevicesBoundarie.server.tsx b/app/components/boundaries/BlockedDevices/BlockedDevicesBoundarie.server.tsx similarity index 100% rename from app/components/boundaries/BlockedDevicesBoundarie.server.tsx rename to app/components/boundaries/BlockedDevices/BlockedDevicesBoundarie.server.tsx diff --git a/app/components/boundaries/ConnectedDevices/ConnectedDevicesBoundarie.client.tsx b/app/components/boundaries/ConnectedDevices/ConnectedDevicesBoundarie.client.tsx new file mode 100644 index 0000000..95131a8 --- /dev/null +++ b/app/components/boundaries/ConnectedDevices/ConnectedDevicesBoundarie.client.tsx @@ -0,0 +1,27 @@ +'use client'; +import { useSetAtom, atom } from 'jotai'; +import { useEffect, useMemo } from 'react'; +import { getConnectedDevices } from '@/lib/get-connected-devices'; +export const allConnectedDevices = atom>([]); + +export default function ConnectedDevicesWrapper({ + children, + connectedDevices: connectedDevices +}: { + children: React.ReactNode; + connectedDevices: string[]; +}) { + const setConnectedDevices = useSetAtom(allConnectedDevices); + useMemo(() => { + setConnectedDevices(connectedDevices); + }, []); + useEffect(() => { + setInterval(() => { + getConnectedDevices().then((connectedDevices) => { + if ('error' in connectedDevices) return; + setConnectedDevices(connectedDevices); + }); + }, 5000); + }); + return <>{children}; +} diff --git a/app/components/boundaries/ConnectedDevices/ConnectedDevicesBoundarie.server.tsx b/app/components/boundaries/ConnectedDevices/ConnectedDevicesBoundarie.server.tsx new file mode 100644 index 0000000..af2013e --- /dev/null +++ b/app/components/boundaries/ConnectedDevices/ConnectedDevicesBoundarie.server.tsx @@ -0,0 +1,21 @@ +import { getConnectedDevices } from '@/lib/get-connected-devices'; +import ConnectedDevicesWrapper from './ConnectedDevicesBoundarie.client'; + +export default async function ConnectedDevicesBoundarie({ + children +}: { + children: React.ReactNode; +}) { + const connectedDevices = await getConnectedDevices(); + if ('error' in connectedDevices) + return ( + + {children} + + ); + return ( + + {children} + + ); +} diff --git a/app/components/dashboard/DashboardCardNetwork/DashboardCardNetwork.server.tsx b/app/components/dashboard/DashboardCardNetwork/DashboardCardNetwork.server.tsx index bfdf8cc..df4ec7b 100644 --- a/app/components/dashboard/DashboardCardNetwork/DashboardCardNetwork.server.tsx +++ b/app/components/dashboard/DashboardCardNetwork/DashboardCardNetwork.server.tsx @@ -1,13 +1,25 @@ import SpeedMeter from './SpeedMeter.client'; import { getUptime } from '@/lib/get-uptime'; import DashboardUptime from './DashboardUptime.client'; +import { db } from '@/lib/db'; +import PerUserSpeedDetails from './PerUserSpeedDetails.client'; + +type ipToName = { + index_number: number; + ip: string; + name: string; + display_name: string; +}[]; export default async function DashboardCardNetWork() { let uptime = await getUptime(); + let allUsers = db + .prepare('SELECT index_number, ip, name, display_name FROM users') + .all() as ipToName; return ( <>
-
+
@@ -24,6 +36,7 @@ export default async function DashboardCardNetWork() { maxSpeed={parseInt(process.env.MAX_UPLOAD_SPEED || '15')} />
+
diff --git a/app/components/dashboard/DashboardCardNetwork/PerUserSpeedDetails.client.tsx b/app/components/dashboard/DashboardCardNetwork/PerUserSpeedDetails.client.tsx new file mode 100644 index 0000000..b282107 --- /dev/null +++ b/app/components/dashboard/DashboardCardNetwork/PerUserSpeedDetails.client.tsx @@ -0,0 +1,147 @@ +'use client'; +import { useAtomValue } from 'jotai'; +import { allSpeedStates } from '../../boundaries/SpeedBoundarie.client'; +import { useRef } from 'react'; + +export default function PerUserSpeedDetails({ + allUsers +}: { + allUsers: { + index_number: number; + ip: string; + name: string; + display_name: string; + }[]; +}) { + const perUserSpeedModal = useRef( + null + ) as unknown as React.MutableRefObject; + return ( + <> +
+
perUserSpeedModal.current?.showModal()} + className="mt-2 w-5/6" + > + Show Details +
+
+ + + ); +} + +function PerUserSpeedDetailsPopUp({ + allUsers, + perUserSpeedModal +}: { + allUsers: { + index_number: number; + ip: string; + name: string; + display_name: string; + }[]; + perUserSpeedModal: React.MutableRefObject; +}) { + function closePopUp() { + perUserSpeedModal.current?.close(); + } + const allSpeeds = useAtomValue(allSpeedStates); + if (!allSpeeds[0].length) + return ( + <> + +
+

Per User Speed

+ +
+ + + + + + + + + +
NameDownloadUpload
+
+ + +
+
+ +
+
+ + ); + const ipToName = allSpeeds[0].map((user) => { + const userDetails = allUsers.filter((u) => u.ip === user.ip)[0]; + if (!userDetails) return; + if (Number(user.in) <= 0 && Number(user.out) <= 0) return; + let userName = + userDetails.display_name || + (userDetails.name != 'Unknown' ? userDetails.name : user.ip); + return { + index_number: userDetails.index_number, + name: userName, + in: user.in, + out: user.out + }; + }); + const ipToNameFiltered = ipToName.filter((u) => u !== undefined) as { + index_number: number; + name: string; + in: string; + out: string; + }[]; + ipToNameFiltered.sort((a, b) => a.index_number - b.index_number); + return ( + <> + +
+

Per User Speed

+ +
+ + + + + + + + + + {ipToNameFiltered.map((user) => ( + + + + + + ))} + +
NameDownloadUpload
{user.name}{user.in} Mbps{user.out} Mbps
+
+ + +
+
+ +
+
+ + ); +} diff --git a/app/components/user-cards/UserCard.client.tsx b/app/components/user-cards/UserCard.client.tsx index a1f6f2d..3d1508f 100644 --- a/app/components/user-cards/UserCard.client.tsx +++ b/app/components/user-cards/UserCard.client.tsx @@ -12,13 +12,16 @@ import { getAllBlockedDevices, unblockDevice as unblockUser } from '@/lib/block-user-script.server'; -import { allBlockedDevices as allBlockedDevicesState } from '../boundaries/BlockedDevicesBoundarie.client'; +import { allBlockedDevices as allBlockedDevicesState } from '../boundaries/BlockedDevices/BlockedDevicesBoundarie.client'; +import { allConnectedDevices as allConnectedDevicesState } from '../boundaries/ConnectedDevices/ConnectedDevicesBoundarie.client'; export default function UserCard({ user }: { user: userReturnType }) { const [localUpdateTime, setLocalUpdateTime] = useState(''); const [showDetails, setShowDetails] = useState(false); const [showToast, setShowToast] = useState(false); const allBlockedDevices = useAtomValue(allBlockedDevicesState); const isBlocked = allBlockedDevices.includes(user.mac_address); + const allConnectedDevices = useAtomValue(allConnectedDevicesState); + const isConnected = allConnectedDevices.includes(user.mac_address); function copyText(text: string) { // TODO: This whole copy/toast logic is duplicate of DashboardCardCurrentStatus.client.tsx. Find a way to merge? @@ -45,6 +48,11 @@ export default function UserCard({ user }: { user: userReturnType }) {
+
+ {isConnected && ( + + )} +
void; }) { - let router = useRouter(); - let blockDeviceModal = useRef(null); + const router = useRouter(); + const blockDeviceModal = useRef(null); + const buttonRef = useRef(null); + const isRunning = useRef(false); const setBlockedDevices = useSetAtom(allBlockedDevicesState); async function blockDevice() { - (async () => { - blockUser(macAddress); - setBlockedDevices((prev) => [...prev, macAddress]); - const allBlockedDevices = await getAllBlockedDevices(); - if (!('error' in allBlockedDevices)) setBlockedDevices(allBlockedDevices); - })(); + isRunning.current = true; + if (!buttonRef.current) return; + buttonRef.current.setAttribute('disabled', 'true'); + buttonRef.current.classList.add('after:loading'); + buttonRef.current.innerText = ''; + await blockUser(macAddress); + const allBlockedDevices = await getAllBlockedDevices(); + if (!('error' in allBlockedDevices)) setBlockedDevices(allBlockedDevices); + buttonRef.current.classList.remove('after:loading'); + buttonRef.current.removeAttribute('disabled'); + isRunning.current = false; closePopUp(); router.refresh(); } async function closePopUp() { + if (isRunning.current) return; blockDeviceModal.current?.close(); await new Promise((resolve) => setTimeout(resolve, 100)); setBlockDeviceModalIsOpen(false); @@ -517,7 +533,11 @@ function BlockDevicePopUp({

Block Device

Are you sure you want to block this device internet access?

-
- - - + + + + +
diff --git a/lib/get-connected-devices.tsx b/lib/get-connected-devices.tsx new file mode 100644 index 0000000..5547c52 --- /dev/null +++ b/lib/get-connected-devices.tsx @@ -0,0 +1,60 @@ +'use server'; +import { getToken } from './get-token'; + +type connectedDeviceResponseType = { + id: number; + result?: [0, { stdout: string }?]; +}; + +export async function getConnectedDevices() { + let token = (await getToken()) as string; + let getConnectedDevicesResponse = await getConnectedDevicesRequest(token); + if ( + !getConnectedDevicesResponse.result || + !getConnectedDevicesResponse.result[1] + ) { + let newToken = await getToken(true); + if (!newToken) { + console.log('getConnectedDevices: Token not found'); + return { error: 'Token not found' }; + } + getConnectedDevicesResponse = await getConnectedDevicesRequest(newToken); + if ( + !getConnectedDevicesResponse.result || + !getConnectedDevicesResponse.result[1] + ) { + console.log('getConnectedDevices: Something went wrong'); + return { error: 'Something went wrong' }; + } + } + const connectedDevicesSplit = + getConnectedDevicesResponse.result[1].stdout.split('\n'); + let connectedDevices = connectedDevicesSplit + .map((device) => { + if (!device) return; + const deviceSplit = device.split(' '); + return deviceSplit[4]; + }) + .filter((device) => device !== undefined) as string[]; + return connectedDevices; +} + +async function getConnectedDevicesRequest(token: string) { + let requestBody = { + jsonrpc: '2.0', + id: 1, + method: 'call', + params: [token, 'file', 'exec', { command: '/etc/neigh-probe' }] + }; + + let makeRequest = await fetch(`${process.env.ROUTER_URL}/ubus`, { + method: 'POST', + body: JSON.stringify(requestBody), + headers: { + 'Content-Type': 'application/json' + }, + cache: 'no-cache' + }); + + return (await makeRequest.json()) as connectedDeviceResponseType; +} diff --git a/lib/get-speed.ts b/lib/get-speed.ts index dbfc1c7..bbc9c32 100644 --- a/lib/get-speed.ts +++ b/lib/get-speed.ts @@ -56,7 +56,7 @@ function formatSpeedOutput(speed: string) { for (let i = 1; i < lines.length; i++) { const columns = lines[i].split('\t'); - if (!columns.includes('br-lan')) continue; + if (!columns[2].match('br-lan')) continue; const rowData: { [key: string]: string } = {}; for (let j = 0; j < headers.length; j++) { diff --git a/router_setup.sh b/router_setup.sh index 81b8fc2..e4ac395 100644 --- a/router_setup.sh +++ b/router_setup.sh @@ -89,7 +89,8 @@ json_content='{ "/etc/wrtbwmon-update": ["exec"], "/proc/uptime": ["read"], "/etc/pppoe-status": ["exec"], - "/etc/block-device": ["exec"] + "/etc/block-device": ["exec"], + "/etc/neigh-probe": ["exec"] } } } @@ -150,6 +151,19 @@ echo "Script created /etc/wrtbwmon-update" ############################# +# Create connected device script +############################# +content=$(cat <<'END_SCRIPT' +#!/bin/sh + +ip -4 neigh show nud reachable nud stale nud permanent nud delay +END_SCRIPT +) +echo "$content" > /etc/neigh-probe +chmod 755 /etc/neigh-probe +echo "Script created /etc/neigh-probe" +############################# + # Create user block script #############################