added file management

master
bjarne 4 weeks ago
parent 2280abae90
commit 863560921e

@ -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 });
}
}

@ -2,6 +2,7 @@ import { NextResponse } from 'next/server';
import { exec } from 'child_process'; import { exec } from 'child_process';
const dhcpConfigPath = '/etc/dhcp/dhcpd.conf'; const dhcpConfigPath = '/etc/dhcp/dhcpd.conf';
const tftpDir = '/var/lib/tftpboot/';
export async function POST(request) { export async function POST(request) {
const { username, password } = await request.json(); const { username, password } = await request.json();
@ -11,6 +12,7 @@ export async function POST(request) {
echo '${password}' | sudo -S visudo -c && echo '${password}' | sudo -S visudo -c &&
echo '${username} ALL=(ALL) NOPASSWD: /bin/systemctl restart isc-dhcp-server' | sudo tee -a /etc/sudoers && 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 chown ${username}:${username} ${dhcpConfigPath}
echo '${password}' | sudo -S chmod -R 777 ${tftpDir}
`; `;
exec(command, (error, stdout, stderr) => { exec(command, (error, stdout, stderr) => {

@ -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 });
}
}

@ -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 });
}
}

@ -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 });
}
}

@ -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 });
}
}

@ -1,15 +1,39 @@
'use client'
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react';
const Card = ({ title, text, url }) => { 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 ( return (
<div className="col-md-6 mb-4"> <div className="col-md-6 mb-4">
<div className="card"> <Link href={url} className="text-decoration-none">
<div
className="card h-100"
style={cardStyle}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="card-body text-center"> <div className="card-body text-center">
<h5 className="card-title">{title}</h5> <h5 className="card-title">{title}</h5>
<p className="card-text">{text}</p> <p className="card-text">{text}</p>
<Link href={url} className="btn btn-primary">Go to {title}</Link>
</div> </div>
</div> </div>
</Link>
</div> </div>
); );
}; };

@ -14,10 +14,16 @@ const Navbar = () => {
<Link href="/" className="nav-link">Home</Link> <Link href="/" className="nav-link">Home</Link>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<Link href="/leases" className="nav-link">Leases</Link> <Link href="/dhcp/leases" className="nav-link">Leases</Link>
</li> </li>
<li className="nav-item"> <li className="nav-item">
<Link href="/configure" className="nav-link">Configure Options</Link> <Link href="/dhcp/configure" className="nav-link">Configure Options</Link>
</li>
<li className="nav-item">
<Link href="/tftp/files" className="nav-link">TFTP Files</Link>
</li>
<li className="nav-item">
<Link href="/tftp/logs" className="nav-link">TFTP Logs</Link>
</li> </li>
</ul> </ul>
</div> </div>

@ -1,16 +1,37 @@
import { parseLeases } from '@/app/lib/parseLeases'; 'use client';
export const metadata = { import { useState, useEffect } from 'react';
title: 'DHCP Leases', import { useNotification } from '../../context/NotificationContext';
description: 'View current DHCP leases',
};
export default function LeasesPage() { 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 ( return (
<div className="container mt-5"> <div className="container mt-5">
<h1 className="mb-4">Current DHCP Leases</h1> <h1 className="mb-4">Current DHCP Leases</h1>
<button className="btn btn-secondary mb-3" onClick={fetchLeases}>
Refresh Leases
</button>
<table className="table table-striped"> <table className="table table-striped">
<thead> <thead>
<tr> <tr>

@ -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);
}

@ -1,12 +1,30 @@
import Card from '@/app/components/Card'; import Card from './components/Card';
export default function Home() { export default function Home() {
return ( return (
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '80vh' }}> <div className="container mt-5">
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<Card title="Leases" text="View current DHCP leases." url="/leases" /> <Card
<Card title="Configure Options" text="Configure new DHCP options." url="/configure" /> title="Leases"
text="View current DHCP leases."
url="/dhcp/leases"
/>
<Card
title="Configure Options"
text="Edit DHCP server options and restart the server."
url="/dhcp/configure"
/>
<Card
title="TFTP Files"
text="Manage files in the TFTP server directory."
url="/tftp/files"
/>
<Card
title="TFTP Logs"
text="See the TFTP Logs."
url="/tftp/logs"
/>
</div> </div>
</div> </div>
</div> </div>

@ -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 (
<div className="container mt-5">
<h1 className="mb-4">TFTP Server Files</h1>
<div className="mb-3">
<label htmlFor="fileUpload" className="form-label">Upload New File</label>
<input
type="file"
className="form-control"
id="fileUpload"
onChange={(e) => setSelectedFile(e.target.files[0])}
/>
<button className="btn btn-primary mt-2" onClick={handleUpload}>
Upload File
</button>
<button className="btn btn-secondary mt-2 ml-2" onClick={fetchFiles}>
Refresh
</button>
</div>
<table className="table table-striped">
<thead>
<tr>
<th scope="col">Filename</th>
<th scope="col">Size (bytes)</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{files.map((file) => (
<tr key={file.name}>
<td>{file.name}</td>
<td>{file.size}</td>
<td>
<button className="btn btn-success btn-sm mr-2" onClick={() => handleDownload(file.name)}>
Download
</button>
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(file.name)}>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

@ -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 (
<div className="container mt-5">
<h1 className="mb-4">TFTP Server Logs</h1>
<button className="btn btn-secondary mb-3" onClick={fetchLogs}>
Refresh Logs
</button>
{error && <div className="alert alert-danger">{error}</div>}
<div className="log-container" style={{ maxHeight: '500px', overflowY: 'auto' }}>
{logs.length > 0 ? (
<pre>{logs.join('\n')}</pre>
) : (
<div className="alert alert-info">No logs available or logs are not accessible.</div>
)}
</div>
</div>
);
}

40
package-lock.json generated

@ -9,6 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"formidable": "^3.5.1",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"next": "14.2.7", "next": "14.2.7",
"react": "^18", "react": "^18",
@ -761,6 +762,11 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/ast-types-flow": {
"version": "0.0.8", "version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", "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" "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": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -1910,6 +1925,19 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/fs": {
"version": "0.0.1-security", "version": "0.0.1-security",
"resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz",
@ -2218,6 +2246,14 @@
"node": ">= 0.4" "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": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -3110,7 +3146,6 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true,
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
} }
@ -4325,8 +4360,7 @@
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
"dev": true
}, },
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"formidable": "^3.5.1",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"next": "14.2.7", "next": "14.2.7",
"react": "^18", "react": "^18",

Loading…
Cancel
Save

Powered by TurnKey Linux.