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 });
|
||||||
|
}
|
||||||
|
}
|
@ -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,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,14 +1,32 @@
|
|||||||
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"
|
||||||
</div>
|
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>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in new issue