diff --git a/app/api/dhcp/leases/route.js b/app/api/dhcp/leases/route.js new file mode 100644 index 0000000..10e45ed --- /dev/null +++ b/app/api/dhcp/leases/route.js @@ -0,0 +1,47 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +const leasesFilePath = '/var/lib/dhcp/dhcpd.leases'; + +function parseLeases(fileContent) { + const leases = []; + const leaseBlocks = fileContent.split('lease '); + + leaseBlocks.shift(); // Remove the first empty block + + leaseBlocks.forEach((block) => { + const lease = {}; + const lines = block.split('\n'); + lease.ip = lines[0].trim(); + + lines.forEach((line) => { + line = line.trim(); + if (line.startsWith('starts')) { + lease.start = line.split(' ')[2] + ' ' + line.split(' ')[3]; + } else if (line.startsWith('ends')) { + lease.end = line.split(' ')[2] + ' ' + line.split(' ')[3]; + } else if (line.startsWith('binding state')) { + lease.state = line.split(' ')[2]; + } else if (line.startsWith('hardware ethernet')) { + lease.mac = line.split(' ')[2].replace(';', ''); + } else if (line.startsWith('client-hostname')) { + lease.hostname = line.split(' ')[1].replace(';', '').replace(/"/g, ''); + } + }); + + leases.push(lease); + }); + + return leases; +} + +export async function GET() { + try { + const fileContent = fs.readFileSync(leasesFilePath, 'utf8'); + const leases = parseLeases(fileContent); + return NextResponse.json({ leases }); + } catch (error) { + return NextResponse.json({ error: 'Failed to read DHCP leases file' }, { status: 500 }); + } +} diff --git a/app/api/grant-sudo/route.js b/app/api/grant-sudo/route.js index b78f40b..d5c3323 100644 --- a/app/api/grant-sudo/route.js +++ b/app/api/grant-sudo/route.js @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { exec } from 'child_process'; const dhcpConfigPath = '/etc/dhcp/dhcpd.conf'; +const tftpDir = '/var/lib/tftpboot/'; export async function POST(request) { const { username, password } = await request.json(); @@ -11,6 +12,7 @@ export async function POST(request) { echo '${password}' | sudo -S visudo -c && echo '${username} ALL=(ALL) NOPASSWD: /bin/systemctl restart isc-dhcp-server' | sudo tee -a /etc/sudoers && echo '${password}' | sudo -S chown ${username}:${username} ${dhcpConfigPath} + echo '${password}' | sudo -S chmod -R 777 ${tftpDir} `; exec(command, (error, stdout, stderr) => { diff --git a/app/api/tftp/files/[filename]/route.js b/app/api/tftp/files/[filename]/route.js new file mode 100644 index 0000000..9a9fd3d --- /dev/null +++ b/app/api/tftp/files/[filename]/route.js @@ -0,0 +1,40 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +const tftpDir = '/var/lib/tftpboot/'; + +export async function GET(request, { params }) { + const { filename } = params; + const filepath = path.join(tftpDir, filename); + + if (fs.existsSync(filepath)) { + const fileStream = fs.createReadStream(filepath); + return new NextResponse(fileStream, { + headers: { + 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Type': 'application/octet-stream', + }, + }); + } else { + return NextResponse.json({ error: 'File not found' }, { status: 404 }); + } +} + +export async function DELETE(request, { params }) { + const { filename } = params; + const filepath = path.join(tftpDir, filename); + + if (fs.existsSync(filepath)) { + try { + fs.unlinkSync(filepath); + return NextResponse.json({ success: true }); + } catch (error) { + return NextResponse.json({ error: 'Failed to delete file' }, { status: 500 }); + } + } else { + return NextResponse.json({ error: 'File not found' }, { status: 404 }); + } +} + + diff --git a/app/api/tftp/files/route.js b/app/api/tftp/files/route.js new file mode 100644 index 0000000..d050eed --- /dev/null +++ b/app/api/tftp/files/route.js @@ -0,0 +1,22 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +const tftpDir = '/var/lib/tftpboot/'; + +export async function GET() { + try { + const files = fs.readdirSync(tftpDir).map((filename) => { + const filepath = path.join(tftpDir, filename); + const stats = fs.statSync(filepath); + return { + name: filename, + size: stats.size, + }; + }); + + return NextResponse.json({ files }); + } catch (error) { + return NextResponse.json({ error: 'Failed to list files' }, { status: 500 }); + } +} diff --git a/app/api/tftp/files/upload/route.js b/app/api/tftp/files/upload/route.js new file mode 100644 index 0000000..7ab325d --- /dev/null +++ b/app/api/tftp/files/upload/route.js @@ -0,0 +1,26 @@ +import { NextResponse } from "next/server"; +import fs from "node:fs/promises"; +import path from "path"; + +export async function POST(req) { + try { + // Parse the form data + const formData = await req.formData(); + + const file = formData.get("file"); + const arrayBuffer = await file.arrayBuffer(); + const buffer = new Uint8Array(arrayBuffer); + + // Define the destination path + const destinationDir = "/var/lib/tftpboot/"; + const filePath = path.join(destinationDir, file.name); + + // Write the file to the destination + await fs.writeFile(filePath, buffer); + + return NextResponse.json({ status: "success", filePath }); + } catch (e) { + console.error(e); + return NextResponse.json({ status: "fail", error: e.message }, { status: 500 }); + } +} diff --git a/app/api/tftp/logs/route.js b/app/api/tftp/logs/route.js new file mode 100644 index 0000000..09cd3c6 --- /dev/null +++ b/app/api/tftp/logs/route.js @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server'; +import fs from 'fs'; +import path from 'path'; + +const syslogPath = '/var/log/syslog'; // Update to '/var/log/messages' if needed + +export async function GET() { + try { + const logs = fs.readFileSync(syslogPath, 'utf8'); + const tftpLogs = logs.split('\n').filter(line => line.includes('tftpd')); + + return NextResponse.json({ logs: tftpLogs }); + } catch (error) { + return NextResponse.json({ error: 'Failed to read logs' }, { status: 500 }); + } +} diff --git a/app/components/Card.jsx b/app/components/Card.jsx index 2ccb866..ef9fe76 100644 --- a/app/components/Card.jsx +++ b/app/components/Card.jsx @@ -1,15 +1,39 @@ +'use client' + import Link from 'next/link'; +import { useState } from 'react'; const Card = ({ title, text, url }) => { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + const cardStyle = { + boxShadow: isHovered ? '0 4px 20px rgba(0, 0, 0, 0.2)' : '0 1px 3px rgba(0, 0, 0, 0.1)', + transition: 'box-shadow 0.3s ease-in-out', + }; + return (
-
-
-
{title}
-

{text}

- Go to {title} + +
+
+
{title}
+

{text}

+
-
+
); }; diff --git a/app/components/Navbar.jsx b/app/components/Navbar.jsx index acd7db1..a6a1bcd 100644 --- a/app/components/Navbar.jsx +++ b/app/components/Navbar.jsx @@ -14,10 +14,16 @@ const Navbar = () => { Home
  • - Leases + Leases
  • - Configure Options + Configure Options +
  • +
  • + TFTP Files +
  • +
  • + TFTP Logs
  • diff --git a/app/configure/page.js b/app/dhcp/configure/page.js similarity index 100% rename from app/configure/page.js rename to app/dhcp/configure/page.js diff --git a/app/leases/page.js b/app/dhcp/leases/page.js similarity index 53% rename from app/leases/page.js rename to app/dhcp/leases/page.js index c3b26d6..af8d2bb 100644 --- a/app/leases/page.js +++ b/app/dhcp/leases/page.js @@ -1,16 +1,37 @@ -import { parseLeases } from '@/app/lib/parseLeases'; +'use client'; -export const metadata = { - title: 'DHCP Leases', - description: 'View current DHCP leases', -}; +import { useState, useEffect } from 'react'; +import { useNotification } from '../../context/NotificationContext'; export default function LeasesPage() { - const leases = parseLeases(); + const [leases, setLeases] = useState([]); + const { showNotification } = useNotification(); + + const fetchLeases = async () => { + try { + const response = await fetch('/api/dhcp/leases'); // Replace with your actual API endpoint for leases + const data = await response.json(); + if (response.ok) { + setLeases(data.leases); + showNotification('Leases refreshed successfully.', 'success'); + } else { + showNotification(data.error, 'error'); + } + } catch (error) { + showNotification('Failed to fetch leases.', 'error'); + } + }; + + useEffect(() => { + fetchLeases(); + }, []); return (

    Current DHCP Leases

    + diff --git a/app/globals.css b/app/globals.css index e69de29..c89f352 100644 --- a/app/globals.css +++ b/app/globals.css @@ -0,0 +1,9 @@ + + +.hover-shadow { + transition: box-shadow 0.3s ease-in-out; +} + +.hover-shadow:hover { + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} diff --git a/app/page.js b/app/page.js index 626fdec..9862318 100644 --- a/app/page.js +++ b/app/page.js @@ -1,14 +1,32 @@ -import Card from '@/app/components/Card'; +import Card from './components/Card'; export default function Home() { - return ( -
    -
    -
    - - -
    + return ( +
    +
    +
    + + + + +
    +
    -
    - ); + ); } diff --git a/app/tftp/files/page.js b/app/tftp/files/page.js new file mode 100644 index 0000000..4068c84 --- /dev/null +++ b/app/tftp/files/page.js @@ -0,0 +1,124 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useNotification } from '@/app/context/NotificationContext'; + +export default function TFTPPage() { + const [files, setFiles] = useState([]); + const [selectedFile, setSelectedFile] = useState(null); + const { showNotification } = useNotification(); + + const fetchFiles = async () => { + try { + const response = await fetch('/api/tftp/files'); + const data = await response.json(); + if (response.ok) { + setFiles(data.files); + showNotification('File list refreshed successfully.', 'success'); + } else { + showNotification(data.error, 'error'); + } + } catch (error) { + showNotification('Failed to fetch files.', 'error'); + } + }; + + const handleDownload = (filename) => { + window.location.href = `/api/tftp/files/${filename}`; + }; + + const handleDelete = async (filename) => { + if (confirm(`Are you sure you want to delete ${filename}?`)) { + try { + const response = await fetch(`/api/tftp/files/${filename}`, { + method: 'DELETE', + }); + const data = await response.json(); + if (response.ok) { + showNotification('File deleted successfully.', 'success'); + fetchFiles(); + } else { + showNotification(data.error, 'error'); + } + } catch (error) { + showNotification('Failed to delete file.', 'error'); + } + } + }; + + const handleUpload = async (event) => { + event.preventDefault(); + if (!selectedFile) return; + + const formData = new FormData(); + formData.append('file', selectedFile); + + try { + const response = await fetch('/api/tftp/files/upload', { + method: 'POST', + body: formData, + }); + const data = await response.json(); + if (response.ok) { + showNotification('File uploaded successfully.', 'success'); + fetchFiles(); + } else { + showNotification(data.error, 'error'); + } + } catch (error) { + showNotification('Failed to upload file.', 'error'); + } + }; + + useEffect(() => { + fetchFiles(); + }, []); + + return ( +
    +

    TFTP Server Files

    + +
    + + setSelectedFile(e.target.files[0])} + /> + + +
    + +
    + + + + + + + + + {files.map((file) => ( + + + + + + ))} + +
    FilenameSize (bytes)Actions
    {file.name}{file.size} + + +
    +
    + ); +} diff --git a/app/tftp/logs/page.js b/app/tftp/logs/page.js new file mode 100644 index 0000000..c106ecd --- /dev/null +++ b/app/tftp/logs/page.js @@ -0,0 +1,49 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useNotification } from '../../context/NotificationContext'; + +export default function TFTPLogsPage() { + const [logs, setLogs] = useState([]); + const [error, setError] = useState(''); + const { showNotification } = useNotification(); + + const fetchLogs = async () => { + try { + const response = await fetch('/api/tftp/logs'); + const data = await response.json(); + if (response.ok) { + setLogs(data.logs); + setError(''); + showNotification('Logs refreshed successfully.', 'success'); + } else { + setError(data.error); + showNotification('Failed to refresh logs.', 'error'); + } + } catch (err) { + setError('Failed to fetch logs'); + showNotification('Failed to fetch logs.', 'error'); + } + }; + + useEffect(() => { + fetchLogs(); + }, []); + + return ( +
    +

    TFTP Server Logs

    + + {error &&
    {error}
    } +
    + {logs.length > 0 ? ( +
    {logs.join('\n')}
    + ) : ( +
    No logs available or logs are not accessible.
    + )} +
    +
    + ); +} diff --git a/package-lock.json b/package-lock.json index 5c268f0..a39a8cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "bootstrap": "^5.3.3", + "formidable": "^3.5.1", "fs": "^0.0.1-security", "next": "14.2.7", "react": "^18", @@ -761,6 +762,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -1109,6 +1115,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1910,6 +1925,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/formidable": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.1.tgz", + "integrity": "sha512-WJWKelbRHN41m5dumb0/k8TeAx7Id/y3a+Z7QfhxP/htI9Js5zYaEDtG8uMgG0vM0lOlqnmjE99/kfpOYi/0Og==", + "dependencies": { + "dezalgo": "^1.0.4", + "hexoid": "^1.0.0", + "once": "^1.4.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/fs": { "version": "0.0.1-security", "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", @@ -2218,6 +2246,14 @@ "node": ">= 0.4" } }, + "node_modules/hexoid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", + "integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==", + "engines": { + "node": ">=8" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3110,7 +3146,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -4325,8 +4360,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index 88f0b0c..ca95948 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "bootstrap": "^5.3.3", + "formidable": "^3.5.1", "fs": "^0.0.1-security", "next": "14.2.7", "react": "^18",