3
.gitignore
vendido
3
.gitignore
vendido
@@ -39,3 +39,6 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
*.lock
|
||||||
|
*-lock.json
|
||||||
@@ -1,7 +1,33 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
}
|
"@/components/*": ["./src/components/*"],
|
||||||
}
|
"@/lib/*": ["./src/lib/*"],
|
||||||
|
"@/hooks/*": ["./src/hooks/*"],
|
||||||
|
"@/store/*": ["./src/store/*"],
|
||||||
|
"@/types/*": ["./src/types/*"]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.js",
|
||||||
|
".next/types/**/*.js",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.jsx",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,49 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
// Configuración para Docker
|
||||||
|
output: 'standalone',
|
||||||
|
|
||||||
|
// Configuración de imágenes
|
||||||
|
images: {
|
||||||
|
unoptimized: true
|
||||||
|
},
|
||||||
|
|
||||||
|
// Variables de entorno públicas
|
||||||
|
env: {
|
||||||
|
CUSTOM_KEY: process.env.CUSTOM_KEY,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuración de headers de seguridad
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/(.*)',
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: 'X-Frame-Options',
|
||||||
|
value: 'DENY'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-Content-Type-Options',
|
||||||
|
value: 'nosniff'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'X-XSS-Protection',
|
||||||
|
value: '1; mode=block'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'Referrer-Policy',
|
||||||
|
value: 'strict-origin-when-cross-origin'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// Configuración para desarrollo
|
||||||
|
experimental: {
|
||||||
|
serverComponentsExternalPackages: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
6060
package-lock.json
generado
6060
package-lock.json
generado
La diferencia del archivo ha sido suprimido porque es demasiado grande
Cargar Diff
32
package.json
32
package.json
@@ -6,18 +6,44 @@
|
|||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build --turbopack",
|
"build": "next build --turbopack",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"setup": "npm install && npm run type-check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"next": "15.5.3"
|
"next": "15.5.3",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.1",
|
||||||
|
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
|
"@radix-ui/react-switch": "^1.1.1",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"lucide-react": "^0.468.0",
|
||||||
|
"next-auth": "5.0.0-beta.25",
|
||||||
|
"recharts": "^2.13.3",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
|
"tailwind-merge": "^2.5.4",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.3",
|
"eslint-config-next": "15.5.3",
|
||||||
"@eslint/eslintrc": "^3"
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@types/node": "^20.8.0",
|
||||||
|
"@types/react": "^18.2.25",
|
||||||
|
"@types/react-dom": "^18.2.11",
|
||||||
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
140
src/app/api/auth/route.ts
Archivo normal
140
src/app/api/auth/route.ts
Archivo normal
@@ -0,0 +1,140 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { sign, verify } from 'jsonwebtoken'
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'csf-admin-secret-key-change-this-in-production'
|
||||||
|
|
||||||
|
// Simple user store - in production, use a database
|
||||||
|
const ADMIN_USERS = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin123', // In production, use hashed passwords
|
||||||
|
role: 'admin',
|
||||||
|
permissions: ['all']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { username, password } = await request.json()
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Usuario y contraseña son requeridos'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
const user = ADMIN_USERS.find(u => u.username === username && u.password === password)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Credenciales inválidas'
|
||||||
|
}, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
const token = sign(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
permissions: user.permissions
|
||||||
|
},
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: '24h' }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create response with token in httpOnly cookie
|
||||||
|
const response = NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
permissions: user.permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
response.cookies.set('auth-token', token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 24 * 60 * 60 // 24 hours
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Error interno del servidor'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const token = request.cookies.get('auth-token')?.value
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'No autenticado'
|
||||||
|
}, { status: 401 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
const decoded = verify(token, JWT_SECRET) as any
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: decoded.userId,
|
||||||
|
username: decoded.username,
|
||||||
|
role: decoded.role,
|
||||||
|
permissions: decoded.permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Auth verification error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Token inválido'
|
||||||
|
}, { status: 401 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const response = NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Sesión cerrada correctamente'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear auth cookie
|
||||||
|
response.cookies.set('auth-token', '', {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Logout error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Error al cerrar sesión'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
185
src/app/api/config/route.ts
Archivo normal
185
src/app/api/config/route.ts
Archivo normal
@@ -0,0 +1,185 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const CSF_CONFIG_PATH = '/etc/csf'
|
||||||
|
const CSF_CONF_FILE = path.join(CSF_CONFIG_PATH, 'csf.conf')
|
||||||
|
|
||||||
|
interface CSFConfigValue {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
comment?: string
|
||||||
|
section?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const configContent = await fs.readFile(CSF_CONF_FILE, 'utf-8')
|
||||||
|
const config = parseCSFConfig(configContent)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: config
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Config API error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to read configuration: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { updates } = body
|
||||||
|
|
||||||
|
if (!updates || typeof updates !== 'object') {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Updates object is required'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current config
|
||||||
|
const configContent = await fs.readFile(CSF_CONF_FILE, 'utf-8')
|
||||||
|
let updatedContent = configContent
|
||||||
|
|
||||||
|
// Apply updates
|
||||||
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
|
updatedContent = updateConfigValue(updatedContent, key, value as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup
|
||||||
|
const backupFile = `${CSF_CONF_FILE}.backup.${Date.now()}`
|
||||||
|
await fs.writeFile(backupFile, configContent)
|
||||||
|
|
||||||
|
// Write updated config
|
||||||
|
await fs.writeFile(CSF_CONF_FILE, updatedContent)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: 'Configuration updated successfully',
|
||||||
|
backup: backupFile,
|
||||||
|
updated_keys: Object.keys(updates)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Config update error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to update configuration: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCSFConfig(content: string): Record<string, any> {
|
||||||
|
const lines = content.split('\n')
|
||||||
|
const config: Record<string, any> = {}
|
||||||
|
let currentSection = 'General'
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
|
||||||
|
// Skip empty lines and comments
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) {
|
||||||
|
// Check for section headers in comments
|
||||||
|
const sectionMatch = trimmed.match(/^# SECTION:(.+)/)
|
||||||
|
if (sectionMatch) {
|
||||||
|
currentSection = sectionMatch[1].trim()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse configuration line: KEY = "VALUE"
|
||||||
|
const configMatch = trimmed.match(/^([A-Z_]+)\\s*=\\s*"([^"]*)"/)
|
||||||
|
if (configMatch) {
|
||||||
|
const [, key, value] = configMatch
|
||||||
|
|
||||||
|
// Try to parse as appropriate type
|
||||||
|
let parsedValue: any = value
|
||||||
|
|
||||||
|
// Boolean values
|
||||||
|
if (value === '1' || value.toLowerCase() === 'true') {
|
||||||
|
parsedValue = true
|
||||||
|
} else if (value === '0' || value.toLowerCase() === 'false') {
|
||||||
|
parsedValue = false
|
||||||
|
}
|
||||||
|
// Numeric values
|
||||||
|
else if (/^\\d+$/.test(value)) {
|
||||||
|
parsedValue = parseInt(value)
|
||||||
|
}
|
||||||
|
// Array values (comma-separated)
|
||||||
|
else if (value.includes(',')) {
|
||||||
|
parsedValue = value.split(',').map(v => v.trim()).filter(v => v)
|
||||||
|
}
|
||||||
|
|
||||||
|
config[key] = {
|
||||||
|
value: parsedValue,
|
||||||
|
raw_value: value,
|
||||||
|
section: currentSection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConfigValue(content: string, key: string, newValue: string): string {
|
||||||
|
// Escape special regex characters in the key
|
||||||
|
const escapedKey = key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')
|
||||||
|
|
||||||
|
// Pattern to match the configuration line
|
||||||
|
const pattern = new RegExp(`^(${escapedKey}\\\\s*=\\\\s*)"([^"]*)"`, 'm')
|
||||||
|
|
||||||
|
// Replace the value
|
||||||
|
const replacement = `$1"${newValue}"`
|
||||||
|
|
||||||
|
if (pattern.test(content)) {
|
||||||
|
return content.replace(pattern, replacement)
|
||||||
|
} else {
|
||||||
|
console.warn(`Configuration key ${key} not found`)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get specific configuration values
|
||||||
|
export async function PATCH(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const keys = searchParams.get('keys')?.split(',') || []
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'No configuration keys specified'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configContent = await fs.readFile(CSF_CONF_FILE, 'utf-8')
|
||||||
|
const config = parseCSFConfig(configContent)
|
||||||
|
|
||||||
|
const requestedConfig: Record<string, any> = {}
|
||||||
|
for (const key of keys) {
|
||||||
|
if (config[key]) {
|
||||||
|
requestedConfig[key] = config[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: requestedConfig
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Config retrieval error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to retrieve configuration: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
204
src/app/api/csf/route.ts
Archivo normal
204
src/app/api/csf/route.ts
Archivo normal
@@ -0,0 +1,204 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
// CSF command paths - adjust based on actual CSF installation
|
||||||
|
const CSF_PATH = '/usr/local/csf/bin/csf'
|
||||||
|
const CSF_CONFIG_PATH = '/etc/csf'
|
||||||
|
|
||||||
|
interface CSFCommandResult {
|
||||||
|
success: boolean
|
||||||
|
output?: string
|
||||||
|
error?: string
|
||||||
|
command?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeCsfCommand(command: string, args: string[] = []): Promise<CSFCommandResult> {
|
||||||
|
try {
|
||||||
|
const fullCommand = `${CSF_PATH} ${command} ${args.join(' ')}`
|
||||||
|
console.log(`Executing CSF command: ${fullCommand}`)
|
||||||
|
|
||||||
|
const { stdout, stderr } = await execAsync(fullCommand, {
|
||||||
|
timeout: 30000, // 30 second timeout
|
||||||
|
cwd: CSF_CONFIG_PATH
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: stdout.trim(),
|
||||||
|
command: fullCommand
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`CSF command failed:`, error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Unknown error',
|
||||||
|
command: `${CSF_PATH} ${command} ${args.join(' ')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const action = searchParams.get('action')
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (action) {
|
||||||
|
case 'status':
|
||||||
|
const statusResult = await executeCsfCommand('--status')
|
||||||
|
return NextResponse.json({
|
||||||
|
success: statusResult.success,
|
||||||
|
data: {
|
||||||
|
output: statusResult.output,
|
||||||
|
running: statusResult.success && !statusResult.output?.includes('not running')
|
||||||
|
},
|
||||||
|
error: statusResult.error
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'version':
|
||||||
|
const versionResult = await executeCsfCommand('--version')
|
||||||
|
return NextResponse.json({
|
||||||
|
success: versionResult.success,
|
||||||
|
data: {
|
||||||
|
version: versionResult.output?.replace('csf: ', '').trim()
|
||||||
|
},
|
||||||
|
error: versionResult.error
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'check':
|
||||||
|
const checkResult = await executeCsfCommand('--check')
|
||||||
|
return NextResponse.json({
|
||||||
|
success: checkResult.success,
|
||||||
|
data: {
|
||||||
|
output: checkResult.output,
|
||||||
|
errors: checkResult.output?.includes('RESULT: csf should function correctly') ? [] : checkResult.output?.split('\n').filter(line => line.includes('*'))
|
||||||
|
},
|
||||||
|
error: checkResult.error
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid action specified'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('API error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Internal server error'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { action, args = [] } = body
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Action is required'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: CSFCommandResult
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'start':
|
||||||
|
result = await executeCsfCommand('--start')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'stop':
|
||||||
|
result = await executeCsfCommand('--stop')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'restart':
|
||||||
|
result = await executeCsfCommand('--restart')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'enable':
|
||||||
|
result = await executeCsfCommand('--enable')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'disable':
|
||||||
|
result = await executeCsfCommand('--disable')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'allow':
|
||||||
|
if (!args[0]) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP address is required for allow action'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
result = await executeCsfCommand('--allow', args)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'deny':
|
||||||
|
if (!args[0]) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP address is required for deny action'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
result = await executeCsfCommand('--deny', args)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'tempdeny':
|
||||||
|
if (!args[0] || !args[1]) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP address and duration are required for tempdeny action'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
result = await executeCsfCommand('--tempdeny', args)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'denyrm':
|
||||||
|
if (!args[0]) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP address is required for denyrm action'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
result = await executeCsfCommand('--denyrm', args)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'allowrm':
|
||||||
|
if (!args[0]) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP address is required for allowrm action'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
result = await executeCsfCommand('--allowrm', args)
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Unknown action: ${action}`
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: result.success,
|
||||||
|
data: {
|
||||||
|
output: result.output,
|
||||||
|
command: result.command
|
||||||
|
},
|
||||||
|
error: result.error
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('API error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid request body or internal server error'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/app/api/health/route.ts
Archivo normal
25
src/app/api/health/route.ts
Archivo normal
@@ -0,0 +1,25 @@
|
|||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Verificaciones básicas de salud del sistema
|
||||||
|
const health = {
|
||||||
|
status: 'healthy',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
version: process.env.npm_package_version || '1.0.0',
|
||||||
|
node_env: process.env.NODE_ENV,
|
||||||
|
memory: {
|
||||||
|
used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
||||||
|
total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024)
|
||||||
|
},
|
||||||
|
uptime: Math.round(process.uptime())
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(health)
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({
|
||||||
|
status: 'unhealthy',
|
||||||
|
error: 'Health check failed'
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
328
src/app/api/logs/route.ts
Archivo normal
328
src/app/api/logs/route.ts
Archivo normal
@@ -0,0 +1,328 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const type = searchParams.get('type')
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '100')
|
||||||
|
const since = searchParams.get('since') // timestamp
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'firewall':
|
||||||
|
return await getFirewallLogs(limit, since)
|
||||||
|
|
||||||
|
case 'lfd':
|
||||||
|
return await getLfdLogs(limit, since)
|
||||||
|
|
||||||
|
case 'system':
|
||||||
|
return await getSystemLogs(limit, since)
|
||||||
|
|
||||||
|
case 'blocked':
|
||||||
|
return await getBlockedIPs(limit)
|
||||||
|
|
||||||
|
case 'connections':
|
||||||
|
return await getActiveConnections()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return await getAllLogs(limit, since)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Logs API error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to retrieve logs: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFirewallLogs(limit: number, since?: string) {
|
||||||
|
try {
|
||||||
|
// Get iptables logs from syslog
|
||||||
|
let command = `grep "CSF\\|iptables" /var/log/syslog | tail -${limit}`
|
||||||
|
if (since) {
|
||||||
|
const sinceDate = new Date(since).toISOString().split('T')[0]
|
||||||
|
command = `grep "CSF\\|iptables" /var/log/syslog | grep "${sinceDate}" | tail -${limit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout } = await execAsync(command)
|
||||||
|
const logs = parseFirewallLogs(stdout)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
logs,
|
||||||
|
total: logs.length,
|
||||||
|
type: 'firewall'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to get firewall logs: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLfdLogs(limit: number, since?: string) {
|
||||||
|
try {
|
||||||
|
let command = `grep "lfd" /var/log/lfd.log 2>/dev/null | tail -${limit} || echo ""`
|
||||||
|
if (since) {
|
||||||
|
const sinceDate = new Date(since).toISOString().split('T')[0]
|
||||||
|
command = `grep "lfd" /var/log/lfd.log 2>/dev/null | grep "${sinceDate}" | tail -${limit} || echo ""`
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout } = await execAsync(command)
|
||||||
|
const logs = parseLfdLogs(stdout)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
logs,
|
||||||
|
total: logs.length,
|
||||||
|
type: 'lfd'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to get LFD logs: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSystemLogs(limit: number, since?: string) {
|
||||||
|
try {
|
||||||
|
let command = `grep "kernel\\|iptables" /var/log/syslog | tail -${limit}`
|
||||||
|
if (since) {
|
||||||
|
const sinceDate = new Date(since).toISOString().split('T')[0]
|
||||||
|
command = `grep "kernel\\|iptables" /var/log/syslog | grep "${sinceDate}" | tail -${limit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout } = await execAsync(command)
|
||||||
|
const logs = parseSystemLogs(stdout)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
logs,
|
||||||
|
total: logs.length,
|
||||||
|
type: 'system'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to get system logs: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBlockedIPs(limit: number) {
|
||||||
|
try {
|
||||||
|
// Get current blocked IPs from iptables
|
||||||
|
const { stdout } = await execAsync('/usr/local/csf/bin/csf --status | grep DROP | head -' + limit)
|
||||||
|
const blockedIPs = parseBlockedIPs(stdout)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
blocked_ips: blockedIPs,
|
||||||
|
total: blockedIPs.length,
|
||||||
|
type: 'blocked'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to get blocked IPs: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getActiveConnections() {
|
||||||
|
try {
|
||||||
|
// Get active network connections
|
||||||
|
const { stdout } = await execAsync('netstat -tn | grep ESTABLISHED | wc -l')
|
||||||
|
const activeConnections = parseInt(stdout.trim()) || 0
|
||||||
|
|
||||||
|
// Get connection details
|
||||||
|
const { stdout: connections } = await execAsync('netstat -tn | grep ESTABLISHED | head -20')
|
||||||
|
const connectionList = parseConnections(connections)
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
active_connections: activeConnections,
|
||||||
|
connections: connectionList,
|
||||||
|
type: 'connections'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to get active connections: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllLogs(limit: number, since?: string) {
|
||||||
|
try {
|
||||||
|
const [firewallResult, lfdResult, systemResult] = await Promise.allSettled([
|
||||||
|
getFirewallLogs(Math.floor(limit / 3), since),
|
||||||
|
getLfdLogs(Math.floor(limit / 3), since),
|
||||||
|
getSystemLogs(Math.floor(limit / 3), since)
|
||||||
|
])
|
||||||
|
|
||||||
|
const allLogs = []
|
||||||
|
|
||||||
|
if (firewallResult.status === 'fulfilled') {
|
||||||
|
const response = await firewallResult.value.json()
|
||||||
|
if (response.success) allLogs.push(...response.data.logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lfdResult.status === 'fulfilled') {
|
||||||
|
const response = await lfdResult.value.json()
|
||||||
|
if (response.success) allLogs.push(...response.data.logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemResult.status === 'fulfilled') {
|
||||||
|
const response = await systemResult.value.json()
|
||||||
|
if (response.success) allLogs.push(...response.data.logs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by timestamp
|
||||||
|
allLogs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
logs: allLogs.slice(0, limit),
|
||||||
|
total: allLogs.length,
|
||||||
|
type: 'all'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to get all logs: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsing functions
|
||||||
|
function parseFirewallLogs(output: string) {
|
||||||
|
const logs = []
|
||||||
|
const lines = output.split('\\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const logEntry = parseLogLine(line, 'firewall')
|
||||||
|
if (logEntry) logs.push(logEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLfdLogs(output: string) {
|
||||||
|
const logs = []
|
||||||
|
const lines = output.split('\\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const logEntry = parseLogLine(line, 'lfd')
|
||||||
|
if (logEntry) logs.push(logEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSystemLogs(output: string) {
|
||||||
|
const logs = []
|
||||||
|
const lines = output.split('\\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const logEntry = parseLogLine(line, 'system')
|
||||||
|
if (logEntry) logs.push(logEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLogLine(line: string, type: string) {
|
||||||
|
// Basic log parsing - adjust regex patterns based on actual log format
|
||||||
|
const patterns = {
|
||||||
|
timestamp: /^(\\w{3}\\s+\\d{1,2}\\s+\\d{2}:\\d{2}:\\d{2})/,
|
||||||
|
ip: /(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})/,
|
||||||
|
port: /DPT=(\\d+)/,
|
||||||
|
protocol: /(TCP|UDP|ICMP)/i,
|
||||||
|
action: /(BLOCK|DROP|ACCEPT|ALLOW|DENY)/i
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestampMatch = line.match(patterns.timestamp)
|
||||||
|
const ipMatch = line.match(patterns.ip)
|
||||||
|
const portMatch = line.match(patterns.port)
|
||||||
|
const protocolMatch = line.match(patterns.protocol)
|
||||||
|
const actionMatch = line.match(patterns.action)
|
||||||
|
|
||||||
|
if (!timestampMatch) return null
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const timestamp = new Date(`${currentYear} ${timestampMatch[1]}`).toISOString()
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `${type}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
timestamp,
|
||||||
|
level: actionMatch && (actionMatch[1].toLowerCase() === 'block' || actionMatch[1].toLowerCase() === 'drop') ? 'block' : 'info',
|
||||||
|
message: line,
|
||||||
|
ip: ipMatch ? ipMatch[1] : undefined,
|
||||||
|
port: portMatch ? portMatch[1] : undefined,
|
||||||
|
protocol: protocolMatch ? protocolMatch[1].toUpperCase() : undefined,
|
||||||
|
action: actionMatch ? actionMatch[1].toUpperCase() : undefined,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBlockedIPs(output: string) {
|
||||||
|
const blockedIPs = []
|
||||||
|
const lines = output.split('\\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const ipMatch = line.match(/(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})/)
|
||||||
|
if (ipMatch) {
|
||||||
|
blockedIPs.push({
|
||||||
|
ip: ipMatch[1],
|
||||||
|
reason: 'Firewall rule',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockedIPs
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseConnections(output: string) {
|
||||||
|
const connections = []
|
||||||
|
const lines = output.split('\\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.trim().split(/\\s+/)
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
const [protocol, , , localAddress, foreignAddress] = parts
|
||||||
|
connections.push({
|
||||||
|
protocol,
|
||||||
|
local_address: localAddress,
|
||||||
|
foreign_address: foreignAddress,
|
||||||
|
state: 'ESTABLISHED'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connections
|
||||||
|
}
|
||||||
273
src/app/api/rules/route.ts
Archivo normal
273
src/app/api/rules/route.ts
Archivo normal
@@ -0,0 +1,273 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
// CSF configuration paths
|
||||||
|
const CSF_CONFIG_PATH = '/etc/csf'
|
||||||
|
const CSF_CONF_FILE = path.join(CSF_CONFIG_PATH, 'csf.conf')
|
||||||
|
const CSF_ALLOW_FILE = path.join(CSF_CONFIG_PATH, 'csf.allow')
|
||||||
|
const CSF_DENY_FILE = path.join(CSF_CONFIG_PATH, 'csf.deny')
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const type = searchParams.get('type')
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case 'allow':
|
||||||
|
const allowContent = await fs.readFile(CSF_ALLOW_FILE, 'utf-8')
|
||||||
|
const allowRules = parseRulesFile(allowContent, 'allow')
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: allowRules
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'deny':
|
||||||
|
const denyContent = await fs.readFile(CSF_DENY_FILE, 'utf-8')
|
||||||
|
const denyRules = parseRulesFile(denyContent, 'deny')
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: denyRules
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'temp':
|
||||||
|
// Get temporary bans from CSF
|
||||||
|
const { stdout } = await execAsync('/usr/local/csf/bin/csf --temp')
|
||||||
|
const tempRules = parseTempRules(stdout)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: tempRules
|
||||||
|
})
|
||||||
|
|
||||||
|
case 'all':
|
||||||
|
const [allowData, denyData] = await Promise.all([
|
||||||
|
fs.readFile(CSF_ALLOW_FILE, 'utf-8'),
|
||||||
|
fs.readFile(CSF_DENY_FILE, 'utf-8')
|
||||||
|
])
|
||||||
|
|
||||||
|
const allRules = [
|
||||||
|
...parseRulesFile(allowData, 'allow'),
|
||||||
|
...parseRulesFile(denyData, 'deny')
|
||||||
|
]
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: allRules
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid rule type specified'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Rules API error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to read rules: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { action, ip, comment = '', port = '', direction = 'in' } = body
|
||||||
|
|
||||||
|
if (!action || !ip) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Action and IP address are required'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate IP address format
|
||||||
|
if (!isValidIP(ip)) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Invalid IP address format'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
let command: string
|
||||||
|
let args: string[] = []
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'add_allow':
|
||||||
|
command = '--allow'
|
||||||
|
args = [ip]
|
||||||
|
if (comment) {
|
||||||
|
args.push(`# ${comment}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'add_deny':
|
||||||
|
command = '--deny'
|
||||||
|
args = [ip]
|
||||||
|
if (comment) {
|
||||||
|
args.push(`# ${comment}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'remove_allow':
|
||||||
|
command = '--allowrm'
|
||||||
|
args = [ip]
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'remove_deny':
|
||||||
|
command = '--denyrm'
|
||||||
|
args = [ip]
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'add_temp_deny':
|
||||||
|
const { duration = '3600' } = body // Default 1 hour
|
||||||
|
command = '--tempdeny'
|
||||||
|
args = [ip, duration]
|
||||||
|
if (comment) {
|
||||||
|
args.push(`# ${comment}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Unknown action: ${action}`
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the CSF command
|
||||||
|
const fullCommand = `/usr/local/csf/bin/csf ${command} ${args.join(' ')}`
|
||||||
|
const { stdout, stderr } = await execAsync(fullCommand, { timeout: 15000 })
|
||||||
|
|
||||||
|
// Restart CSF to apply changes
|
||||||
|
await execAsync('/usr/local/csf/bin/csf --restart', { timeout: 30000 })
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: `Rule ${action} applied successfully`,
|
||||||
|
output: stdout.trim(),
|
||||||
|
command: fullCommand
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Rules modification error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to modify rule: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const ip = searchParams.get('ip')
|
||||||
|
const type = searchParams.get('type') // 'allow' or 'deny'
|
||||||
|
|
||||||
|
if (!ip || !type) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP address and type are required'
|
||||||
|
}, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const command = type === 'allow' ? '--allowrm' : '--denyrm'
|
||||||
|
const fullCommand = `/usr/local/csf/bin/csf ${command} ${ip}`
|
||||||
|
|
||||||
|
const { stdout } = await execAsync(fullCommand, { timeout: 15000 })
|
||||||
|
|
||||||
|
// Restart CSF to apply changes
|
||||||
|
await execAsync('/usr/local/csf/bin/csf --restart', { timeout: 30000 })
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: `Rule removed successfully`,
|
||||||
|
output: stdout.trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Rule deletion error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to remove rule: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
function parseRulesFile(content: string, type: 'allow' | 'deny') {
|
||||||
|
const lines = content.split('\n')
|
||||||
|
const rules = []
|
||||||
|
let id = 1
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) continue
|
||||||
|
|
||||||
|
// Parse line format: IP_ADDRESS # Comment
|
||||||
|
const commentIndex = trimmed.indexOf('#')
|
||||||
|
const ip = commentIndex > -1 ? trimmed.substring(0, commentIndex).trim() : trimmed
|
||||||
|
const comment = commentIndex > -1 ? trimmed.substring(commentIndex + 1).trim() : ''
|
||||||
|
|
||||||
|
if (ip) {
|
||||||
|
rules.push({
|
||||||
|
id: `${type}-${id++}`,
|
||||||
|
ip,
|
||||||
|
comment,
|
||||||
|
type,
|
||||||
|
direction: 'both',
|
||||||
|
created: new Date().toISOString()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTempRules(output: string) {
|
||||||
|
const rules = []
|
||||||
|
const lines = output.split('\n')
|
||||||
|
let id = 1
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes('Temporary')) {
|
||||||
|
const match = line.match(/(\d+\.\d+\.\d+\.\d+).*?(\d+)\s+seconds?/)
|
||||||
|
if (match) {
|
||||||
|
const [, ip, seconds] = match
|
||||||
|
const expires = new Date(Date.now() + parseInt(seconds) * 1000).toISOString()
|
||||||
|
|
||||||
|
rules.push({
|
||||||
|
id: `temp-${id++}`,
|
||||||
|
ip,
|
||||||
|
comment: 'Temporary ban',
|
||||||
|
type: 'temp',
|
||||||
|
direction: 'both',
|
||||||
|
created: new Date().toISOString(),
|
||||||
|
expires
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidIP(ip: string): boolean {
|
||||||
|
// Basic IPv4 validation
|
||||||
|
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||||
|
// Basic IPv6 validation (simplified)
|
||||||
|
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/
|
||||||
|
// CIDR notation
|
||||||
|
const cidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2])$/
|
||||||
|
|
||||||
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip) || cidrRegex.test(ip)
|
||||||
|
}
|
||||||
221
src/app/api/stats/route.ts
Archivo normal
221
src/app/api/stats/route.ts
Archivo normal
@@ -0,0 +1,221 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// Get server statistics
|
||||||
|
const stats = await Promise.all([
|
||||||
|
getCPUUsage(),
|
||||||
|
getMemoryUsage(),
|
||||||
|
getDiskUsage(),
|
||||||
|
getNetworkStats(),
|
||||||
|
getCSFStats(),
|
||||||
|
getSystemUptime()
|
||||||
|
])
|
||||||
|
|
||||||
|
const [cpu, memory, disk, network, csf, uptime] = stats
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
cpu_usage: cpu,
|
||||||
|
memory_usage: memory,
|
||||||
|
disk_usage: disk,
|
||||||
|
network: network,
|
||||||
|
csf_stats: csf,
|
||||||
|
uptime: uptime,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Stats API error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to retrieve statistics: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCPUUsage(): Promise<number> {
|
||||||
|
try {
|
||||||
|
// Get CPU usage from /proc/loadavg
|
||||||
|
const { stdout } = await execAsync('cat /proc/loadavg')
|
||||||
|
const loadAvg = parseFloat(stdout.split(' ')[0])
|
||||||
|
|
||||||
|
// Convert to percentage (assuming single core, adjust for multi-core)
|
||||||
|
const { stdout: cpuInfo } = await execAsync('nproc')
|
||||||
|
const cores = parseInt(cpuInfo.trim())
|
||||||
|
|
||||||
|
return Math.min((loadAvg / cores) * 100, 100)
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMemoryUsage(): Promise<{ used: number; total: number; percentage: number }> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('free -b')
|
||||||
|
const lines = stdout.split('\\n')
|
||||||
|
const memLine = lines[1].split(/\\s+/)
|
||||||
|
|
||||||
|
const total = parseInt(memLine[1])
|
||||||
|
const used = parseInt(memLine[2])
|
||||||
|
const percentage = (used / total) * 100
|
||||||
|
|
||||||
|
return {
|
||||||
|
used,
|
||||||
|
total,
|
||||||
|
percentage: Math.round(percentage * 100) / 100
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { used: 0, total: 0, percentage: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDiskUsage(): Promise<{ used: number; total: number; percentage: number }> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('df -B1 / | tail -1')
|
||||||
|
const parts = stdout.split(/\\s+/)
|
||||||
|
|
||||||
|
const total = parseInt(parts[1])
|
||||||
|
const used = parseInt(parts[2])
|
||||||
|
const percentage = (used / total) * 100
|
||||||
|
|
||||||
|
return {
|
||||||
|
used,
|
||||||
|
total,
|
||||||
|
percentage: Math.round(percentage * 100) / 100
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { used: 0, total: 0, percentage: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNetworkStats(): Promise<{ in_bytes: number; out_bytes: number; connections: number }> {
|
||||||
|
try {
|
||||||
|
// Get network interface stats
|
||||||
|
const { stdout } = await execAsync('cat /proc/net/dev | grep -E "(eth0|ens|enp)" | head -1')
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
const parts = stdout.split(/\\s+/)
|
||||||
|
const inBytes = parseInt(parts[1]) || 0
|
||||||
|
const outBytes = parseInt(parts[9]) || 0
|
||||||
|
|
||||||
|
// Get active connections
|
||||||
|
const { stdout: connStdout } = await execAsync('netstat -tn | grep ESTABLISHED | wc -l')
|
||||||
|
const connections = parseInt(connStdout.trim()) || 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
in_bytes: inBytes,
|
||||||
|
out_bytes: outBytes,
|
||||||
|
connections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { in_bytes: 0, out_bytes: 0, connections: 0 }
|
||||||
|
} catch {
|
||||||
|
return { in_bytes: 0, out_bytes: 0, connections: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCSFStats(): Promise<{
|
||||||
|
status: string;
|
||||||
|
blocked_ips: number;
|
||||||
|
allowed_ips: number;
|
||||||
|
temp_blocks: number;
|
||||||
|
rules_count: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Check CSF status
|
||||||
|
let status = 'unknown'
|
||||||
|
try {
|
||||||
|
await execAsync('/usr/local/csf/bin/csf --status >/dev/null 2>&1')
|
||||||
|
status = 'active'
|
||||||
|
} catch {
|
||||||
|
status = 'inactive'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count rules in files
|
||||||
|
let blockedIps = 0
|
||||||
|
let allowedIps = 0
|
||||||
|
let tempBlocks = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const denyContent = await fs.readFile('/etc/csf/csf.deny', 'utf-8')
|
||||||
|
blockedIps = denyContent.split('\\n').filter(line =>
|
||||||
|
line.trim() && !line.trim().startsWith('#')
|
||||||
|
).length
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allowContent = await fs.readFile('/etc/csf/csf.allow', 'utf-8')
|
||||||
|
allowedIps = allowContent.split('\\n').filter(line =>
|
||||||
|
line.trim() && !line.trim().startsWith('#')
|
||||||
|
).length
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('/usr/local/csf/bin/csf --temp 2>/dev/null | grep -c "Temporary" || echo "0"')
|
||||||
|
tempBlocks = parseInt(stdout.trim()) || 0
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
blocked_ips: blockedIps,
|
||||||
|
allowed_ips: allowedIps,
|
||||||
|
temp_blocks: tempBlocks,
|
||||||
|
rules_count: blockedIps + allowedIps
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
status: 'unknown',
|
||||||
|
blocked_ips: 0,
|
||||||
|
allowed_ips: 0,
|
||||||
|
temp_blocks: 0,
|
||||||
|
rules_count: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSystemUptime(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('cat /proc/uptime')
|
||||||
|
const uptime = parseFloat(stdout.split(' ')[0])
|
||||||
|
return Math.floor(uptime)
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real-time stats endpoint
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { interval = 5000 } = body // Default 5 seconds
|
||||||
|
|
||||||
|
// This would typically be handled by WebSocket
|
||||||
|
// For now, return current stats
|
||||||
|
const stats = await GET()
|
||||||
|
const response = await stats.json()
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
...response.data,
|
||||||
|
interval,
|
||||||
|
next_update: Date.now() + interval
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to get real-time stats: ${error.message}`
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/app/page.tsx
Archivo normal
7
src/app/page.tsx
Archivo normal
@@ -0,0 +1,7 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { AuthWrapper } from '@/components/AuthWrapper'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return <AuthWrapper />
|
||||||
|
}
|
||||||
85
src/components/AuthWrapper.tsx
Archivo normal
85
src/components/AuthWrapper.tsx
Archivo normal
@@ -0,0 +1,85 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useAuth } from '@/hooks/use-auth'
|
||||||
|
import { LoginForm } from './LoginForm'
|
||||||
|
import { Dashboard } from './Dashboard'
|
||||||
|
|
||||||
|
export function AuthWrapper() {
|
||||||
|
const { user, loading, error, login, logout, clearError } = useAuth()
|
||||||
|
|
||||||
|
// Show loading spinner while checking authentication
|
||||||
|
if (loading && !user) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<svg className="animate-spin h-12 w-12 text-blue-600 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p className="text-gray-600">Verificando autenticación...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show login form if not authenticated
|
||||||
|
if (!user) {
|
||||||
|
return (
|
||||||
|
<LoginForm
|
||||||
|
onLogin={login}
|
||||||
|
loading={loading}
|
||||||
|
error={error || undefined}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dashboard if authenticated
|
||||||
|
return <Dashboard />
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced Dashboard component with auth-aware header
|
||||||
|
function AuthenticatedDashboard() {
|
||||||
|
const { user, logout } = useAuth()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Header with user info and logout */}
|
||||||
|
<header className="bg-white shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between items-center py-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">CSF Admin Panel</h1>
|
||||||
|
<p className="text-sm text-gray-500">ConfigServer Security & Firewall</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
<span className="font-medium">{user?.username}</span>
|
||||||
|
<span className="text-gray-400 ml-2">({user?.role})</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{new Date().toLocaleString('es-ES')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="text-sm text-gray-600 hover:text-gray-900 border border-gray-300 px-3 py-1 rounded-md hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Cerrar Sesión
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Dashboard content */}
|
||||||
|
<Dashboard />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
188
src/components/Dashboard.tsx
Archivo normal
188
src/components/Dashboard.tsx
Archivo normal
@@ -0,0 +1,188 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { FirewallStatusCard } from './FirewallStatusCard'
|
||||||
|
import { ServerStats } from './ServerStats'
|
||||||
|
import { FirewallRules } from './FirewallRules'
|
||||||
|
import { LogViewer } from './LogViewer'
|
||||||
|
import { RealtimeIndicator } from './RealtimeIndicator'
|
||||||
|
import { useRealtimeData } from '@/hooks/use-realtime-data'
|
||||||
|
|
||||||
|
type TabType = 'dashboard' | 'rules' | 'logs' | 'config'
|
||||||
|
|
||||||
|
interface TabButtonProps {
|
||||||
|
active: boolean
|
||||||
|
onClick: () => void
|
||||||
|
children: React.ReactNode
|
||||||
|
icon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabButton({ active, onClick, children, icon }: TabButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`flex items-center px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
|
||||||
|
active
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{icon && <span className="mr-2">{icon}</span>}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dashboard() {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('dashboard')
|
||||||
|
const { connected, stats: realtimeStats, logs: realtimeLogs } = useRealtimeData()
|
||||||
|
|
||||||
|
const dashboardIcon = (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const rulesIcon = (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const logsIcon = (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const configIcon = (
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between items-center py-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">CSF Admin Panel</h1>
|
||||||
|
<p className="text-sm text-gray-500">ConfigServer Security & Firewall</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<RealtimeIndicator />
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{new Date().toLocaleString('es-ES')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Navigation Tabs */}
|
||||||
|
<div className="bg-white border-b border-gray-200">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<nav className="flex space-x-1 py-4">
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'dashboard'}
|
||||||
|
onClick={() => setActiveTab('dashboard')}
|
||||||
|
icon={dashboardIcon}
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</TabButton>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'rules'}
|
||||||
|
onClick={() => setActiveTab('rules')}
|
||||||
|
icon={rulesIcon}
|
||||||
|
>
|
||||||
|
Reglas
|
||||||
|
</TabButton>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'logs'}
|
||||||
|
onClick={() => setActiveTab('logs')}
|
||||||
|
icon={logsIcon}
|
||||||
|
>
|
||||||
|
Logs
|
||||||
|
</TabButton>
|
||||||
|
<TabButton
|
||||||
|
active={activeTab === 'config'}
|
||||||
|
onClick={() => setActiveTab('config')}
|
||||||
|
icon={configIcon}
|
||||||
|
>
|
||||||
|
Configuración
|
||||||
|
</TabButton>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{activeTab === 'dashboard' && (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<FirewallStatusCard />
|
||||||
|
</div>
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<ServerStats />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'rules' && (
|
||||||
|
<FirewallRules />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'logs' && (
|
||||||
|
<LogViewer />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'config' && (
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 mb-4">Configuración del Firewall</h2>
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||||
|
<p className="text-blue-700">
|
||||||
|
La configuración avanzada del firewall CSF estará disponible próximamente.
|
||||||
|
</p>
|
||||||
|
<p className="text-blue-600 text-sm mt-2">
|
||||||
|
Por ahora puedes gestionar las reglas básicas desde la pestaña "Reglas".
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-white border-t border-gray-200 mt-16">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
© 2025 CSF Web Admin Panel. Sistema de administración para ConfigServer Security & Firewall.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<a href="#" className="text-sm text-gray-500 hover:text-gray-700">
|
||||||
|
Documentación
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-sm text-gray-500 hover:text-gray-700">
|
||||||
|
Soporte
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
375
src/components/FirewallRules.tsx
Archivo normal
375
src/components/FirewallRules.tsx
Archivo normal
@@ -0,0 +1,375 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useCSFApi } from '@/hooks/use-csf-api'
|
||||||
|
import { FirewallRule } from '@/types/csf'
|
||||||
|
|
||||||
|
function formatTimestamp(timestamp: string): string {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toLocaleString('es-ES', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidIP(ip: string): boolean {
|
||||||
|
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||||
|
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/
|
||||||
|
const cidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/([0-9]|[1-2][0-9]|3[0-2])$/
|
||||||
|
|
||||||
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip) || cidrRegex.test(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddRuleModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onAdd: (ip: string, type: 'allow' | 'deny', comment?: string) => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddRuleModal({ isOpen, onClose, onAdd }: AddRuleModalProps) {
|
||||||
|
const [ip, setIp] = useState('')
|
||||||
|
const [type, setType] = useState<'allow' | 'deny'>('deny')
|
||||||
|
const [comment, setComment] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
if (!ip.trim()) {
|
||||||
|
setError('Dirección IP es requerida')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidIP(ip.trim())) {
|
||||||
|
setError('Formato de IP inválido')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const success = await onAdd(ip.trim(), type, comment.trim() || undefined)
|
||||||
|
if (success) {
|
||||||
|
setIp('')
|
||||||
|
setComment('')
|
||||||
|
onClose()
|
||||||
|
} else {
|
||||||
|
setError('Error al agregar la regla')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Error al agregar la regla')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Agregar Nueva Regla</h3>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Dirección IP
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={ip}
|
||||||
|
onChange={(e) => setIp(e.target.value)}
|
||||||
|
placeholder="192.168.1.1 o 192.168.1.0/24"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tipo de Regla
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={type}
|
||||||
|
onChange={(e) => setType(e.target.value as 'allow' | 'deny')}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<option value="deny">Bloquear (Deny)</option>
|
||||||
|
<option value="allow">Permitir (Allow)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Comentario (Opcional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
placeholder="Descripción de la regla"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-md p-3">
|
||||||
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Agregando...' : 'Agregar Regla'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleRowProps {
|
||||||
|
rule: FirewallRule
|
||||||
|
onRemove: (ip: string, type: 'allow' | 'deny') => Promise<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
function RuleRow({ rule, onRemove }: RuleRowProps) {
|
||||||
|
const [removing, setRemoving] = useState(false)
|
||||||
|
|
||||||
|
const handleRemove = async () => {
|
||||||
|
if (confirm(`¿Está seguro de que desea eliminar la regla para ${rule.ip}?`)) {
|
||||||
|
setRemoving(true)
|
||||||
|
try {
|
||||||
|
await onRemove(rule.ip, rule.type as 'allow' | 'deny')
|
||||||
|
} finally {
|
||||||
|
setRemoving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeColor = () => {
|
||||||
|
switch (rule.type) {
|
||||||
|
case 'allow':
|
||||||
|
return 'bg-green-100 text-green-800'
|
||||||
|
case 'deny':
|
||||||
|
return 'bg-red-100 text-red-800'
|
||||||
|
case 'temp':
|
||||||
|
return 'bg-yellow-100 text-yellow-800'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100 text-gray-800'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeLabel = () => {
|
||||||
|
switch (rule.type) {
|
||||||
|
case 'allow':
|
||||||
|
return 'Permitir'
|
||||||
|
case 'deny':
|
||||||
|
return 'Bloquear'
|
||||||
|
case 'temp':
|
||||||
|
return 'Temporal'
|
||||||
|
default:
|
||||||
|
return rule.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{rule.ip}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex px-2 py-1 text-xs font-medium rounded-full ${getTypeColor()}`}>
|
||||||
|
{getTypeLabel()}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-500">
|
||||||
|
{rule.comment || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{formatTimestamp(rule.created)}
|
||||||
|
</td>
|
||||||
|
{rule.expires && (
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{formatTimestamp(rule.expires)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
{rule.type !== 'temp' && (
|
||||||
|
<button
|
||||||
|
onClick={handleRemove}
|
||||||
|
disabled={removing}
|
||||||
|
className="text-red-600 hover:text-red-900 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{removing ? 'Eliminando...' : 'Eliminar'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FirewallRulesProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FirewallRules({ className = '' }: FirewallRulesProps) {
|
||||||
|
const { rules, getRules, addRule, removeRule, loading, error } = useCSFApi()
|
||||||
|
const [filter, setFilter] = useState<'all' | 'allow' | 'deny' | 'temp'>('all')
|
||||||
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getRules()
|
||||||
|
}, [getRules])
|
||||||
|
|
||||||
|
const filteredRules = rules.filter(rule => {
|
||||||
|
const matchesFilter = filter === 'all' || rule.type === filter
|
||||||
|
const matchesSearch = searchTerm === '' ||
|
||||||
|
rule.ip.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
rule.comment?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
|
||||||
|
return matchesFilter && matchesSearch
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleAddRule = async (ip: string, type: 'allow' | 'deny', comment?: string) => {
|
||||||
|
return await addRule(ip, type, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveRule = async (ip: string, type: 'allow' | 'deny') => {
|
||||||
|
return await removeRule(ip, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-white rounded-lg shadow-md ${className}`}>
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">Reglas del Firewall</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModal(true)}
|
||||||
|
className="mt-3 sm:mt-0 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Agregar Regla
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-col sm:flex-row gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar por IP o comentario..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value as 'all' | 'allow' | 'deny' | 'temp')}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="all">Todas las reglas</option>
|
||||||
|
<option value="allow">Solo Permitir</option>
|
||||||
|
<option value="deny">Solo Bloquear</option>
|
||||||
|
<option value="temp">Solo Temporales</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-red-50 border-b border-red-200">
|
||||||
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Dirección IP
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Tipo
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Comentario
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Creado
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Expira
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Acciones
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{loading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="px-6 py-4 text-center">
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<svg className="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="ml-2">Cargando reglas...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : filteredRules.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="px-6 py-4 text-center text-gray-500">
|
||||||
|
{rules.length === 0 ? 'No hay reglas configuradas' : 'No se encontraron reglas que coincidan con los filtros'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
filteredRules.map((rule) => (
|
||||||
|
<RuleRow
|
||||||
|
key={rule.id}
|
||||||
|
rule={rule}
|
||||||
|
onRemove={handleRemoveRule}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-3 bg-gray-50 border-t border-gray-200">
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
Mostrando {filteredRules.length} de {rules.length} reglas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddRuleModal
|
||||||
|
isOpen={showModal}
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
onAdd={handleAddRule}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
126
src/components/FirewallStatusCard.tsx
Archivo normal
126
src/components/FirewallStatusCard.tsx
Archivo normal
@@ -0,0 +1,126 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useCSFApi } from '@/hooks/use-csf-api'
|
||||||
|
|
||||||
|
interface FirewallStatusCardProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FirewallStatusCard({ className = '' }: FirewallStatusCardProps) {
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
getStatus,
|
||||||
|
startFirewall,
|
||||||
|
stopFirewall,
|
||||||
|
restartFirewall,
|
||||||
|
loading,
|
||||||
|
error
|
||||||
|
} = useCSFApi()
|
||||||
|
|
||||||
|
const [actionLoading, setActionLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getStatus()
|
||||||
|
}, [getStatus])
|
||||||
|
|
||||||
|
const handleAction = async (action: () => Promise<boolean>) => {
|
||||||
|
setActionLoading(true)
|
||||||
|
try {
|
||||||
|
await action()
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = () => {
|
||||||
|
if (!status) return 'bg-gray-500'
|
||||||
|
return status.running ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = () => {
|
||||||
|
if (!status) return 'Desconocido'
|
||||||
|
return status.running ? 'Activo' : 'Inactivo'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Estado del Firewall</h3>
|
||||||
|
<div className={`w-3 h-3 rounded-full ${getStatusColor()}`}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-md p-3 mb-4">
|
||||||
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Estado:</span>
|
||||||
|
<span className={`font-medium ${status?.running ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{getStatusText()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status && (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Versión:</span>
|
||||||
|
<span className="font-medium">{status.version}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">IPv4:</span>
|
||||||
|
<span className={`font-medium ${status.ipv4_active ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{status.ipv4_active ? 'Activo' : 'Inactivo'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">IPv6:</span>
|
||||||
|
<span className={`font-medium ${status.ipv6_active ? 'text-green-600' : 'text-red-600'}`}>
|
||||||
|
{status.ipv6_active ? 'Activo' : 'Inactivo'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status.testing_mode && (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-3">
|
||||||
|
<p className="text-yellow-700 text-sm font-medium">
|
||||||
|
⚠️ Modo de prueba activado
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 space-y-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleAction(startFirewall)}
|
||||||
|
disabled={loading || actionLoading || status?.running}
|
||||||
|
className="flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Procesando...' : 'Iniciar'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleAction(stopFirewall)}
|
||||||
|
disabled={loading || actionLoading || !status?.running}
|
||||||
|
className="flex-1 bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Procesando...' : 'Detener'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleAction(restartFirewall)}
|
||||||
|
disabled={loading || actionLoading}
|
||||||
|
className="w-full bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{actionLoading ? 'Reiniciando...' : 'Reiniciar'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
317
src/components/LogViewer.tsx
Archivo normal
317
src/components/LogViewer.tsx
Archivo normal
@@ -0,0 +1,317 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useCSFApi } from '@/hooks/use-csf-api'
|
||||||
|
import { LogEntry } from '@/types/csf'
|
||||||
|
|
||||||
|
function formatTimestamp(timestamp: string): string {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toLocaleString('es-ES', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogLevelColor(level: string): string {
|
||||||
|
switch (level.toLowerCase()) {
|
||||||
|
case 'error':
|
||||||
|
return 'text-red-500 bg-red-50'
|
||||||
|
case 'warning':
|
||||||
|
return 'text-yellow-600 bg-yellow-50'
|
||||||
|
case 'info':
|
||||||
|
return 'text-blue-500 bg-blue-50'
|
||||||
|
case 'block':
|
||||||
|
return 'text-purple-600 bg-purple-50'
|
||||||
|
default:
|
||||||
|
return 'text-gray-500 bg-gray-50'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogLevelIcon(level: string) {
|
||||||
|
switch (level.toLowerCase()) {
|
||||||
|
case 'error':
|
||||||
|
return (
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'warning':
|
||||||
|
return (
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'block':
|
||||||
|
return (
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogEntryRowProps {
|
||||||
|
log: LogEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogEntryRow({ log }: LogEntryRowProps) {
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<tr
|
||||||
|
className={`hover:bg-gray-50 cursor-pointer ${getLogLevelColor(log.level).includes('bg-') ? getLogLevelColor(log.level) : ''}`}
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{formatTimestamp(log.timestamp)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className={`flex items-center ${getLogLevelColor(log.level)}`}>
|
||||||
|
<span className="mr-2">{getLogLevelIcon(log.level)}</span>
|
||||||
|
<span className="text-xs font-medium uppercase">{log.level}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-900 max-w-md truncate">
|
||||||
|
{log.message}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{log.ip || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{log.port || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{log.protocol || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm text-gray-400">
|
||||||
|
<svg className={`w-4 h-4 transition-transform ${expanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{expanded && (
|
||||||
|
<tr className="bg-gray-50">
|
||||||
|
<td colSpan={7} className="px-6 py-4">
|
||||||
|
<div className="bg-white rounded-md p-4 border">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 mb-2">Detalles del Log</h4>
|
||||||
|
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-500">ID:</dt>
|
||||||
|
<dd className="text-gray-900">{log.id}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-500">Tipo:</dt>
|
||||||
|
<dd className="text-gray-900">Sistema</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-500">Acción:</dt>
|
||||||
|
<dd className="text-gray-900">{log.action || '-'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="font-medium text-gray-500">Timestamp:</dt>
|
||||||
|
<dd className="text-gray-900">{log.timestamp}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="sm:col-span-2">
|
||||||
|
<dt className="font-medium text-gray-500">Mensaje Completo:</dt>
|
||||||
|
<dd className="text-gray-900 mt-1 font-mono text-xs bg-gray-100 p-2 rounded overflow-x-auto">
|
||||||
|
{log.message}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogViewerProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogViewer({ className = '' }: LogViewerProps) {
|
||||||
|
const { logs, getLogs, loading, error } = useCSFApi()
|
||||||
|
const [logType, setLogType] = useState<string>('all')
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(true)
|
||||||
|
const [limit, setLimit] = useState(100)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getLogs(logType, limit)
|
||||||
|
}, [getLogs, logType, limit])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!autoRefresh) return
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
getLogs(logType, limit)
|
||||||
|
}, 10000) // Refresh every 10 seconds
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [autoRefresh, getLogs, logType, limit])
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
getLogs(logType, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const logTypeCounts = logs.reduce((acc, log) => {
|
||||||
|
acc[log.level] = (acc[log.level] || 0) + 1
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, number>)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-white rounded-lg shadow-md ${className}`}>
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">Logs del Sistema</h2>
|
||||||
|
<div className="mt-3 sm:mt-0 flex items-center space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{loading ? 'Actualizando...' : 'Actualizar'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-col sm:flex-row gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<select
|
||||||
|
value={logType}
|
||||||
|
onChange={(e) => setLogType(e.target.value)}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="all">Todos los logs</option>
|
||||||
|
<option value="firewall">Firewall</option>
|
||||||
|
<option value="lfd">LFD</option>
|
||||||
|
<option value="system">Sistema</option>
|
||||||
|
<option value="blocked">IPs Bloqueadas</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
value={limit}
|
||||||
|
onChange={(e) => setLimit(parseInt(e.target.value))}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value={50}>50 entradas</option>
|
||||||
|
<option value={100}>100 entradas</option>
|
||||||
|
<option value={200}>200 entradas</option>
|
||||||
|
<option value={500}>500 entradas</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoRefresh}
|
||||||
|
onChange={(e) => setAutoRefresh(e.target.checked)}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="ml-2 text-sm text-gray-700">Auto-actualizar</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Log Level Summary */}
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{Object.entries(logTypeCounts).map(([level, count]) => (
|
||||||
|
<span
|
||||||
|
key={level}
|
||||||
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getLogLevelColor(level)}`}
|
||||||
|
>
|
||||||
|
<span className="mr-1">{getLogLevelIcon(level)}</span>
|
||||||
|
{level}: {count}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="p-4 bg-red-50 border-b border-red-200">
|
||||||
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Timestamp
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Nivel
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Mensaje
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
IP
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Puerto
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Protocolo
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Detalle
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{loading && logs.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7} className="px-6 py-4 text-center">
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<svg className="animate-spin h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="ml-2">Cargando logs...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : logs.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7} className="px-6 py-4 text-center text-gray-500">
|
||||||
|
No hay logs disponibles
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
logs.map((log) => (
|
||||||
|
<LogEntryRow key={log.id} log={log} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-3 bg-gray-50 border-t border-gray-200">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<p className="text-sm text-gray-700">
|
||||||
|
Mostrando {logs.length} entradas
|
||||||
|
</p>
|
||||||
|
{autoRefresh && (
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Actualización automática activa
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
163
src/components/LoginForm.tsx
Archivo normal
163
src/components/LoginForm.tsx
Archivo normal
@@ -0,0 +1,163 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
interface LoginFormProps {
|
||||||
|
onLogin: (username: string, password: string) => Promise<boolean>
|
||||||
|
loading?: boolean
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginForm({ onLogin, loading = false, error }: LoginFormProps) {
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (username.trim() && password.trim()) {
|
||||||
|
await onLogin(username.trim(), password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<svg className="w-12 h-12 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||||
|
CSF Admin Panel
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-center text-sm text-gray-600">
|
||||||
|
Acceso al panel de administración del firewall
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||||
|
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
|
||||||
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
|
||||||
|
Usuario
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
autoComplete="username"
|
||||||
|
required
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed sm:text-sm"
|
||||||
|
placeholder="Ingrese su usuario"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
|
||||||
|
Contraseña
|
||||||
|
</label>
|
||||||
|
<div className="mt-1 relative">
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
className="appearance-none block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50 disabled:cursor-not-allowed sm:text-sm"
|
||||||
|
placeholder="Ingrese su contraseña"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{showPassword ? (
|
||||||
|
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-md p-3">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<h3 className="text-sm font-medium text-red-800">
|
||||||
|
Error de autenticación
|
||||||
|
</h3>
|
||||||
|
<div className="mt-2 text-sm text-red-700">
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading || !username.trim() || !password.trim()}
|
||||||
|
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Iniciando sesión...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Iniciar Sesión'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<div className="w-full border-t border-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-sm">
|
||||||
|
<span className="px-2 bg-white text-gray-500">Información</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 text-center">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
<p>Panel de administración seguro para CSF</p>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
Acceso restringido solo para administradores autorizados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
29
src/components/RealtimeIndicator.tsx
Archivo normal
29
src/components/RealtimeIndicator.tsx
Archivo normal
@@ -0,0 +1,29 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useRealtimeData } from '@/hooks/use-realtime-data'
|
||||||
|
|
||||||
|
interface RealtimeIndicatorProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RealtimeIndicator({ className = '' }: RealtimeIndicatorProps) {
|
||||||
|
const { connected, error } = useRealtimeData()
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center space-x-2 ${className}`}>
|
||||||
|
<div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>
|
||||||
|
<span className="text-sm text-red-600">Error de conexión</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center space-x-2 ${className}`}>
|
||||||
|
<div className={`w-2 h-2 rounded-full ${connected ? 'bg-green-500' : 'bg-gray-400'} ${connected ? 'animate-pulse' : ''}`}></div>
|
||||||
|
<span className={`text-sm ${connected ? 'text-green-600' : 'text-gray-500'}`}>
|
||||||
|
{connected ? 'En tiempo real' : 'Desconectado'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
250
src/components/ServerStats.tsx
Archivo normal
250
src/components/ServerStats.tsx
Archivo normal
@@ -0,0 +1,250 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useCSFApi } from '@/hooks/use-csf-api'
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
||||||
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(seconds: number): string {
|
||||||
|
const days = Math.floor(seconds / 86400)
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}d ${hours}h ${minutes}m`
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m`
|
||||||
|
} else {
|
||||||
|
return `${minutes}m`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatsCardProps {
|
||||||
|
title: string
|
||||||
|
value: string | number
|
||||||
|
subtitle?: string
|
||||||
|
color?: 'blue' | 'green' | 'red' | 'yellow' | 'purple'
|
||||||
|
icon?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatsCard({ title, value, subtitle, color = 'blue', icon }: StatsCardProps) {
|
||||||
|
const colorClasses = {
|
||||||
|
blue: 'bg-blue-500',
|
||||||
|
green: 'bg-green-500',
|
||||||
|
red: 'bg-red-500',
|
||||||
|
yellow: 'bg-yellow-500',
|
||||||
|
purple: 'bg-purple-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{icon && (
|
||||||
|
<div className={`w-12 h-12 ${colorClasses[color]} rounded-lg flex items-center justify-center mr-4`}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-sm font-medium text-gray-500 uppercase">{title}</h3>
|
||||||
|
<p className="text-2xl font-bold text-gray-900">{value}</p>
|
||||||
|
{subtitle && <p className="text-sm text-gray-600">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProgressBarProps {
|
||||||
|
percentage: number
|
||||||
|
color?: 'blue' | 'green' | 'red' | 'yellow'
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProgressBar({ percentage, color = 'blue', label }: ProgressBarProps) {
|
||||||
|
const colorClasses = {
|
||||||
|
blue: 'bg-blue-500',
|
||||||
|
green: 'bg-green-500',
|
||||||
|
red: 'bg-red-500',
|
||||||
|
yellow: 'bg-yellow-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getColor = () => {
|
||||||
|
if (percentage >= 90) return 'red'
|
||||||
|
if (percentage >= 70) return 'yellow'
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentColor = getColor()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<div className="flex justify-between items-center mb-2">
|
||||||
|
<span className="text-sm font-medium text-gray-700">{label}</span>
|
||||||
|
<span className="text-sm text-gray-500">{percentage.toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full transition-all duration-300 ${colorClasses[currentColor]}`}
|
||||||
|
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServerStatsProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ServerStats({ className = '' }: ServerStatsProps) {
|
||||||
|
const { stats, getStats, loading } = useCSFApi()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getStats()
|
||||||
|
const interval = setInterval(getStats, 5000) // Update every 5 seconds
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [getStats])
|
||||||
|
|
||||||
|
if (!stats) {
|
||||||
|
return (
|
||||||
|
<div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="h-4 bg-gray-200 rounded mb-4"></div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="h-8 bg-gray-200 rounded"></div>
|
||||||
|
<div className="h-8 bg-gray-200 rounded"></div>
|
||||||
|
<div className="h-8 bg-gray-200 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpuIcon = (
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const memoryIcon = (
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const diskIcon = (
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const networkIcon = (
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-6 ${className}`}>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900">Estadísticas del Servidor</h2>
|
||||||
|
|
||||||
|
{/* Resource Usage */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<ProgressBar
|
||||||
|
percentage={stats.cpu_usage}
|
||||||
|
label="Uso de CPU"
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<ProgressBar
|
||||||
|
percentage={stats.memory_usage}
|
||||||
|
label="Uso de Memoria"
|
||||||
|
color="green"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<ProgressBar
|
||||||
|
percentage={stats.disk_usage}
|
||||||
|
label="Uso de Disco"
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
title="Conexiones Activas"
|
||||||
|
value={stats.active_connections}
|
||||||
|
color="yellow"
|
||||||
|
icon={networkIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Network Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<StatsCard
|
||||||
|
title="Tráfico de Entrada"
|
||||||
|
value={formatBytes(stats.network_in)}
|
||||||
|
subtitle="Total recibido"
|
||||||
|
color="green"
|
||||||
|
icon={networkIcon}
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
title="Tráfico de Salida"
|
||||||
|
value={formatBytes(stats.network_out)}
|
||||||
|
subtitle="Total enviado"
|
||||||
|
color="blue"
|
||||||
|
icon={networkIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CSF Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<StatsCard
|
||||||
|
title="IPs Bloqueadas"
|
||||||
|
value={stats.blocked_ips}
|
||||||
|
color="red"
|
||||||
|
icon={
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L5.636 5.636" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
title="IPs Permitidas"
|
||||||
|
value={stats.allowed_ips}
|
||||||
|
color="green"
|
||||||
|
icon={
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatsCard
|
||||||
|
title="Tiempo Activo"
|
||||||
|
value={formatUptime(stats.uptime)}
|
||||||
|
subtitle="Sistema en línea"
|
||||||
|
color="blue"
|
||||||
|
icon={
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<div className="text-center py-4">
|
||||||
|
<div className="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md text-gray-500 bg-white">
|
||||||
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Actualizando estadísticas...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
111
src/hooks/use-auth.ts
Archivo normal
111
src/hooks/use-auth.ts
Archivo normal
@@ -0,0 +1,111 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { AuthUser, ApiResponse } from '@/types/csf'
|
||||||
|
|
||||||
|
interface UseAuthResult {
|
||||||
|
user: AuthUser | null
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
login: (username: string, password: string) => Promise<boolean>
|
||||||
|
logout: () => Promise<void>
|
||||||
|
checkAuth: () => Promise<void>
|
||||||
|
clearError: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth(): UseAuthResult {
|
||||||
|
const [user, setUser] = useState<AuthUser | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const clearError = useCallback(() => setError(null), [])
|
||||||
|
|
||||||
|
const checkAuth = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
clearError()
|
||||||
|
|
||||||
|
const response = await fetch('/api/auth', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
|
||||||
|
const data: ApiResponse<{ user: AuthUser }> = await response.json()
|
||||||
|
|
||||||
|
if (data.success && data.data) {
|
||||||
|
setUser(data.data.user)
|
||||||
|
} else {
|
||||||
|
setUser(null)
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Auth check failed:', err)
|
||||||
|
setUser(null)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [clearError])
|
||||||
|
|
||||||
|
const login = useCallback(async (username: string, password: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
clearError()
|
||||||
|
|
||||||
|
const response = await fetch('/api/auth', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
})
|
||||||
|
|
||||||
|
const data: ApiResponse<{ user: AuthUser }> = await response.json()
|
||||||
|
|
||||||
|
if (data.success && data.data) {
|
||||||
|
setUser(data.data.user)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Error de autenticación')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Login failed:', err)
|
||||||
|
setError('Error de conexión')
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [clearError])
|
||||||
|
|
||||||
|
const logout = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
await fetch('/api/auth', {
|
||||||
|
method: 'DELETE',
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
|
||||||
|
setUser(null)
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Logout failed:', err)
|
||||||
|
// Still clear user even if logout request fails
|
||||||
|
setUser(null)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Check authentication on mount
|
||||||
|
useEffect(() => {
|
||||||
|
checkAuth()
|
||||||
|
}, [checkAuth])
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
checkAuth,
|
||||||
|
clearError
|
||||||
|
}
|
||||||
|
}
|
||||||
330
src/hooks/use-csf-api.ts
Archivo normal
330
src/hooks/use-csf-api.ts
Archivo normal
@@ -0,0 +1,330 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { ApiResponse, CSFStatus, FirewallRule, LogEntry, ServerStats, CSFConfig } from '@/types/csf'
|
||||||
|
|
||||||
|
interface UseCSFApiResult {
|
||||||
|
// Status
|
||||||
|
status: CSFStatus | null
|
||||||
|
getStatus: () => Promise<void>
|
||||||
|
|
||||||
|
// Control
|
||||||
|
startFirewall: () => Promise<boolean>
|
||||||
|
stopFirewall: () => Promise<boolean>
|
||||||
|
restartFirewall: () => Promise<boolean>
|
||||||
|
enableFirewall: () => Promise<boolean>
|
||||||
|
disableFirewall: () => Promise<boolean>
|
||||||
|
|
||||||
|
// Rules
|
||||||
|
rules: FirewallRule[]
|
||||||
|
getRules: (type?: 'allow' | 'deny' | 'temp' | 'all') => Promise<void>
|
||||||
|
addRule: (ip: string, type: 'allow' | 'deny', comment?: string) => Promise<boolean>
|
||||||
|
removeRule: (ip: string, type: 'allow' | 'deny') => Promise<boolean>
|
||||||
|
addTempBlock: (ip: string, duration: string, comment?: string) => Promise<boolean>
|
||||||
|
|
||||||
|
// Config
|
||||||
|
config: CSFConfig | null
|
||||||
|
getConfig: () => Promise<void>
|
||||||
|
updateConfig: (updates: Record<string, any>) => Promise<boolean>
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
logs: LogEntry[]
|
||||||
|
getLogs: (type?: string, limit?: number) => Promise<void>
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
stats: ServerStats | null
|
||||||
|
getStats: () => Promise<void>
|
||||||
|
|
||||||
|
// State
|
||||||
|
loading: boolean
|
||||||
|
error: string | null
|
||||||
|
clearError: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCSFApi(): UseCSFApiResult {
|
||||||
|
const [status, setStatus] = useState<CSFStatus | null>(null)
|
||||||
|
const [rules, setRules] = useState<FirewallRule[]>([])
|
||||||
|
const [config, setConfig] = useState<CSFConfig | null>(null)
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||||
|
const [stats, setStats] = useState<ServerStats | null>(null)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const clearError = useCallback(() => setError(null), [])
|
||||||
|
|
||||||
|
const apiRequest = useCallback(async <T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<ApiResponse<T>> => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
clearError()
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data: ApiResponse<T> = await response.json()
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'API request failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
} catch (err: any) {
|
||||||
|
const errorMessage = err.message || 'Network error'
|
||||||
|
setError(errorMessage)
|
||||||
|
throw new Error(errorMessage)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [clearError])
|
||||||
|
|
||||||
|
// Status operations
|
||||||
|
const getStatus = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<{ output: string; running: boolean }>('/api/csf?action=status')
|
||||||
|
if (response.data) {
|
||||||
|
// Get version info as well
|
||||||
|
const versionResponse = await apiRequest<{ version: string }>('/api/csf?action=version')
|
||||||
|
|
||||||
|
setStatus({
|
||||||
|
running: response.data.running,
|
||||||
|
version: versionResponse.data?.version || 'Unknown',
|
||||||
|
ipv4_active: response.data.running,
|
||||||
|
ipv6_active: response.data.running,
|
||||||
|
testing_mode: false // This would need to be parsed from config
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to get status:', err)
|
||||||
|
}
|
||||||
|
}, [apiRequest])
|
||||||
|
|
||||||
|
// Control operations
|
||||||
|
const startFirewall = useCallback(async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest('/api/csf', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ action: 'start' })
|
||||||
|
})
|
||||||
|
await getStatus() // Refresh status
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to start firewall:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getStatus])
|
||||||
|
|
||||||
|
const stopFirewall = useCallback(async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest('/api/csf', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ action: 'stop' })
|
||||||
|
})
|
||||||
|
await getStatus() // Refresh status
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to stop firewall:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getStatus])
|
||||||
|
|
||||||
|
const restartFirewall = useCallback(async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest('/api/csf', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ action: 'restart' })
|
||||||
|
})
|
||||||
|
await getStatus() // Refresh status
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to restart firewall:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getStatus])
|
||||||
|
|
||||||
|
const enableFirewall = useCallback(async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest('/api/csf', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ action: 'enable' })
|
||||||
|
})
|
||||||
|
await getStatus() // Refresh status
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to enable firewall:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getStatus])
|
||||||
|
|
||||||
|
const disableFirewall = useCallback(async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest('/api/csf', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ action: 'disable' })
|
||||||
|
})
|
||||||
|
await getStatus() // Refresh status
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to disable firewall:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getStatus])
|
||||||
|
|
||||||
|
// Rules operations
|
||||||
|
const getRules = useCallback(async (type: 'allow' | 'deny' | 'temp' | 'all' = 'all') => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<FirewallRule[]>(`/api/rules?type=${type}`)
|
||||||
|
if (response.data) {
|
||||||
|
setRules(response.data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to get rules:', err)
|
||||||
|
}
|
||||||
|
}, [apiRequest])
|
||||||
|
|
||||||
|
const addRule = useCallback(async (ip: string, type: 'allow' | 'deny', comment?: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const action = type === 'allow' ? 'add_allow' : 'add_deny'
|
||||||
|
const response = await apiRequest('/api/rules', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ action, ip, comment })
|
||||||
|
})
|
||||||
|
await getRules() // Refresh rules
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to add rule:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getRules])
|
||||||
|
|
||||||
|
const removeRule = useCallback(async (ip: string, type: 'allow' | 'deny'): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest(`/api/rules?ip=${encodeURIComponent(ip)}&type=${type}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
await getRules() // Refresh rules
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to remove rule:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getRules])
|
||||||
|
|
||||||
|
const addTempBlock = useCallback(async (ip: string, duration: string, comment?: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest('/api/rules', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ action: 'add_temp_deny', ip, duration, comment })
|
||||||
|
})
|
||||||
|
await getRules() // Refresh rules
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to add temporary block:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getRules])
|
||||||
|
|
||||||
|
// Config operations
|
||||||
|
const getConfig = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<CSFConfig>('/api/config')
|
||||||
|
if (response.data) {
|
||||||
|
setConfig(response.data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to get config:', err)
|
||||||
|
}
|
||||||
|
}, [apiRequest])
|
||||||
|
|
||||||
|
const updateConfig = useCallback(async (updates: Record<string, any>): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ updates })
|
||||||
|
})
|
||||||
|
await getConfig() // Refresh config
|
||||||
|
return response.success
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update config:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [apiRequest, getConfig])
|
||||||
|
|
||||||
|
// Logs operations
|
||||||
|
const getLogs = useCallback(async (type: string = 'all', limit: number = 100) => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<{ logs: LogEntry[]; total: number; type: string }>(`/api/logs?type=${type}&limit=${limit}`)
|
||||||
|
if (response.data) {
|
||||||
|
setLogs(response.data.logs)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to get logs:', err)
|
||||||
|
}
|
||||||
|
}, [apiRequest])
|
||||||
|
|
||||||
|
// Stats operations
|
||||||
|
const getStats = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiRequest<ServerStats>('/api/stats')
|
||||||
|
if (response.data) {
|
||||||
|
setStats(response.data)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to get stats:', err)
|
||||||
|
}
|
||||||
|
}, [apiRequest])
|
||||||
|
|
||||||
|
// Auto-refresh data periodically
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!loading) {
|
||||||
|
getStatus()
|
||||||
|
getStats()
|
||||||
|
}
|
||||||
|
}, 10000) // Refresh every 10 seconds
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [loading, getStatus, getStats])
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Status
|
||||||
|
status,
|
||||||
|
getStatus,
|
||||||
|
|
||||||
|
// Control
|
||||||
|
startFirewall,
|
||||||
|
stopFirewall,
|
||||||
|
restartFirewall,
|
||||||
|
enableFirewall,
|
||||||
|
disableFirewall,
|
||||||
|
|
||||||
|
// Rules
|
||||||
|
rules,
|
||||||
|
getRules,
|
||||||
|
addRule,
|
||||||
|
removeRule,
|
||||||
|
addTempBlock,
|
||||||
|
|
||||||
|
// Config
|
||||||
|
config,
|
||||||
|
getConfig,
|
||||||
|
updateConfig,
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
logs,
|
||||||
|
getLogs,
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
stats,
|
||||||
|
getStats,
|
||||||
|
|
||||||
|
// State
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
clearError
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/hooks/use-realtime-data.ts
Archivo normal
137
src/hooks/use-realtime-data.ts
Archivo normal
@@ -0,0 +1,137 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import io, { Socket } from 'socket.io-client'
|
||||||
|
import { LogEntry, ServerStats } from '@/types/csf'
|
||||||
|
|
||||||
|
interface UseRealtimeDataResult {
|
||||||
|
connected: boolean
|
||||||
|
stats: ServerStats | null
|
||||||
|
logs: LogEntry[]
|
||||||
|
connect: () => void
|
||||||
|
disconnect: () => void
|
||||||
|
requestStats: () => void
|
||||||
|
requestLogs: (limit?: number) => void
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRealtimeData(): UseRealtimeDataResult {
|
||||||
|
const socketRef = useRef<Socket | null>(null)
|
||||||
|
const [connected, setConnected] = useState(false)
|
||||||
|
const [stats, setStats] = useState<ServerStats | null>(null)
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const connect = () => {
|
||||||
|
if (socketRef.current?.connected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
socketRef.current = io({
|
||||||
|
path: '/api/socket',
|
||||||
|
transports: ['websocket', 'polling']
|
||||||
|
})
|
||||||
|
|
||||||
|
const socket = socketRef.current
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log('Conectado al servidor WebSocket')
|
||||||
|
setConnected(true)
|
||||||
|
setError(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('Desconectado del servidor WebSocket')
|
||||||
|
setConnected(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('connected', (data) => {
|
||||||
|
console.log('Mensaje de bienvenida:', data.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('stats', (newStats: ServerStats) => {
|
||||||
|
setStats(newStats)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('logs', (newLogs: LogEntry[]) => {
|
||||||
|
setLogs(prevLogs => {
|
||||||
|
// Combinar logs existentes con nuevos, evitando duplicados
|
||||||
|
const combined = [...newLogs, ...prevLogs]
|
||||||
|
const unique = combined.filter((log, index, array) =>
|
||||||
|
array.findIndex(l => l.id === log.id) === index
|
||||||
|
)
|
||||||
|
// Mantener solo los últimos 500 logs
|
||||||
|
return unique.slice(0, 500).sort((a, b) =>
|
||||||
|
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('connect_error', (err) => {
|
||||||
|
console.error('Error de conexión WebSocket:', err)
|
||||||
|
setError('Error de conexión WebSocket')
|
||||||
|
setConnected(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('error', (err) => {
|
||||||
|
console.error('Error WebSocket:', err)
|
||||||
|
setError('Error en WebSocket')
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error inicializando WebSocket:', err)
|
||||||
|
setError('Error inicializando WebSocket')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const disconnect = () => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.disconnect()
|
||||||
|
socketRef.current = null
|
||||||
|
setConnected(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestStats = () => {
|
||||||
|
if (socketRef.current?.connected) {
|
||||||
|
socketRef.current.emit('request-stats')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestLogs = (limit: number = 50) => {
|
||||||
|
if (socketRef.current?.connected) {
|
||||||
|
socketRef.current.emit('request-logs', { limit })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-conectar al montar el componente
|
||||||
|
useEffect(() => {
|
||||||
|
connect()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
disconnect()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Reconectar automáticamente si se pierde la conexión
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connected && socketRef.current) {
|
||||||
|
const reconnectTimer = setTimeout(() => {
|
||||||
|
console.log('Intentando reconectar...')
|
||||||
|
connect()
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
return () => clearTimeout(reconnectTimer)
|
||||||
|
}
|
||||||
|
}, [connected])
|
||||||
|
|
||||||
|
return {
|
||||||
|
connected,
|
||||||
|
stats,
|
||||||
|
logs,
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
requestStats,
|
||||||
|
requestLogs,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/lib/utils.ts
Archivo normal
102
src/lib/utils.ts
Archivo normal
@@ -0,0 +1,102 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytes(bytes: number): string {
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024))
|
||||||
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatUptime(seconds: number): string {
|
||||||
|
const days = Math.floor(seconds / 86400)
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600)
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
return `${days}d ${hours}h ${minutes}m`
|
||||||
|
} else if (hours > 0) {
|
||||||
|
return `${hours}h ${minutes}m`
|
||||||
|
} else {
|
||||||
|
return `${minutes}m`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidIP(ip: string): boolean {
|
||||||
|
// IPv4 regex
|
||||||
|
const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
||||||
|
// IPv6 regex (simplified)
|
||||||
|
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/
|
||||||
|
|
||||||
|
return ipv4Regex.test(ip) || ipv6Regex.test(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidPort(port: string): boolean {
|
||||||
|
const portNum = parseInt(port)
|
||||||
|
return !isNaN(portNum) && portNum >= 1 && portNum <= 65535
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePortRange(ports: string): number[] {
|
||||||
|
const result: number[] = []
|
||||||
|
const ranges = ports.split(',')
|
||||||
|
|
||||||
|
for (const range of ranges) {
|
||||||
|
if (range.includes('-')) {
|
||||||
|
const [start, end] = range.split('-').map(p => parseInt(p.trim()))
|
||||||
|
if (!isNaN(start) && !isNaN(end) && start <= end) {
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
result.push(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const port = parseInt(range.trim())
|
||||||
|
if (!isNaN(port)) {
|
||||||
|
result.push(port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTimestamp(timestamp: string | Date): string {
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toLocaleString('es-ES', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogLevelColor(level: string): string {
|
||||||
|
switch (level.toLowerCase()) {
|
||||||
|
case 'error':
|
||||||
|
return 'text-red-500'
|
||||||
|
case 'warning':
|
||||||
|
return 'text-yellow-500'
|
||||||
|
case 'info':
|
||||||
|
return 'text-blue-500'
|
||||||
|
case 'block':
|
||||||
|
return 'text-purple-500'
|
||||||
|
default:
|
||||||
|
return 'text-gray-500'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
|
func: T,
|
||||||
|
wait: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
let timeout: NodeJS.Timeout
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => func(...args), wait)
|
||||||
|
}
|
||||||
|
}
|
||||||
248
src/pages/api/socket.ts
Archivo normal
248
src/pages/api/socket.ts
Archivo normal
@@ -0,0 +1,248 @@
|
|||||||
|
import { Server as HTTPServer } from 'http'
|
||||||
|
import { Server as SocketIOServer } from 'socket.io'
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
// Configuración del servidor Socket.IO
|
||||||
|
let io: SocketIOServer | null = null
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CSFStats {
|
||||||
|
cpu_usage: number
|
||||||
|
memory_usage: number
|
||||||
|
disk_usage: number
|
||||||
|
network_in: number
|
||||||
|
network_out: number
|
||||||
|
active_connections: number
|
||||||
|
blocked_ips: number
|
||||||
|
allowed_ips: number
|
||||||
|
uptime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
id: string
|
||||||
|
timestamp: string
|
||||||
|
level: string
|
||||||
|
message: string
|
||||||
|
ip?: string
|
||||||
|
port?: string
|
||||||
|
protocol?: string
|
||||||
|
action?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para obtener estadísticas del sistema
|
||||||
|
async function getSystemStats(): Promise<CSFStats> {
|
||||||
|
try {
|
||||||
|
// CPU Usage
|
||||||
|
const { stdout: loadAvg } = await execAsync('cat /proc/loadavg')
|
||||||
|
const cpuUsage = parseFloat(loadAvg.split(' ')[0]) * 100
|
||||||
|
|
||||||
|
// Memory Usage
|
||||||
|
const { stdout: memInfo } = await execAsync('free -b')
|
||||||
|
const memLines = memInfo.split('\n')
|
||||||
|
const memLine = memLines[1].split(/\s+/)
|
||||||
|
const totalMem = parseInt(memLine[1])
|
||||||
|
const usedMem = parseInt(memLine[2])
|
||||||
|
const memoryUsage = (usedMem / totalMem) * 100
|
||||||
|
|
||||||
|
// Disk Usage
|
||||||
|
const { stdout: diskInfo } = await execAsync('df -B1 / | tail -1')
|
||||||
|
const diskParts = diskInfo.split(/\s+/)
|
||||||
|
const totalDisk = parseInt(diskParts[1])
|
||||||
|
const usedDisk = parseInt(diskParts[2])
|
||||||
|
const diskUsage = (usedDisk / totalDisk) * 100
|
||||||
|
|
||||||
|
// Network stats (placeholder)
|
||||||
|
const networkIn = Math.random() * 1000000
|
||||||
|
const networkOut = Math.random() * 1000000
|
||||||
|
|
||||||
|
// Active connections
|
||||||
|
const { stdout: connStdout } = await execAsync('netstat -tn | grep ESTABLISHED | wc -l')
|
||||||
|
const activeConnections = parseInt(connStdout.trim()) || 0
|
||||||
|
|
||||||
|
// CSF specific stats
|
||||||
|
let blockedIps = 0
|
||||||
|
let allowedIps = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const denyContent = await fs.readFile('/etc/csf/csf.deny', 'utf-8')
|
||||||
|
blockedIps = denyContent.split('\n').filter(line =>
|
||||||
|
line.trim() && !line.trim().startsWith('#')
|
||||||
|
).length
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allowContent = await fs.readFile('/etc/csf/csf.allow', 'utf-8')
|
||||||
|
allowedIps = allowContent.split('\n').filter(line =>
|
||||||
|
line.trim() && !line.trim().startsWith('#')
|
||||||
|
).length
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// System uptime
|
||||||
|
const { stdout: uptimeInfo } = await execAsync('cat /proc/uptime')
|
||||||
|
const uptime = parseFloat(uptimeInfo.split(' ')[0])
|
||||||
|
|
||||||
|
return {
|
||||||
|
cpu_usage: Math.min(cpuUsage, 100),
|
||||||
|
memory_usage: Math.round(memoryUsage * 100) / 100,
|
||||||
|
disk_usage: Math.round(diskUsage * 100) / 100,
|
||||||
|
network_in: networkIn,
|
||||||
|
network_out: networkOut,
|
||||||
|
active_connections: activeConnections,
|
||||||
|
blocked_ips: blockedIps,
|
||||||
|
allowed_ips: allowedIps,
|
||||||
|
uptime: Math.floor(uptime)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting system stats:', error)
|
||||||
|
return {
|
||||||
|
cpu_usage: 0,
|
||||||
|
memory_usage: 0,
|
||||||
|
disk_usage: 0,
|
||||||
|
network_in: 0,
|
||||||
|
network_out: 0,
|
||||||
|
active_connections: 0,
|
||||||
|
blocked_ips: 0,
|
||||||
|
allowed_ips: 0,
|
||||||
|
uptime: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función para obtener logs recientes
|
||||||
|
async function getRecentLogs(limit: number = 50): Promise<LogEntry[]> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync(`tail -${limit} /var/log/syslog | grep -E "(CSF|iptables|lfd)" || echo ""`)
|
||||||
|
|
||||||
|
const logs: LogEntry[] = []
|
||||||
|
const lines = stdout.split('\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i]
|
||||||
|
const logEntry = parseLogLine(line, i)
|
||||||
|
if (logEntry) logs.push(logEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs.reverse() // Más recientes primero
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting recent logs:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLogLine(line: string, index: number): LogEntry | null {
|
||||||
|
try {
|
||||||
|
// Patrón básico para logs de syslog
|
||||||
|
const timestampMatch = line.match(/^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})/)
|
||||||
|
const ipMatch = line.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/)
|
||||||
|
const portMatch = line.match(/DPT=(\d+)/)
|
||||||
|
const protocolMatch = line.match(/(TCP|UDP|ICMP)/i)
|
||||||
|
const actionMatch = line.match(/(BLOCK|DROP|ACCEPT|ALLOW|DENY)/i)
|
||||||
|
|
||||||
|
if (!timestampMatch) return null
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const timestamp = new Date(`${currentYear} ${timestampMatch[1]}`).toISOString()
|
||||||
|
|
||||||
|
let level = 'info'
|
||||||
|
if (line.toLowerCase().includes('error')) level = 'error'
|
||||||
|
else if (line.toLowerCase().includes('warning')) level = 'warning'
|
||||||
|
else if (actionMatch && (actionMatch[1].toLowerCase() === 'block' || actionMatch[1].toLowerCase() === 'drop')) level = 'block'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `log-${Date.now()}-${index}`,
|
||||||
|
timestamp,
|
||||||
|
level,
|
||||||
|
message: line,
|
||||||
|
ip: ipMatch ? ipMatch[1] : undefined,
|
||||||
|
port: portMatch ? portMatch[1] : undefined,
|
||||||
|
protocol: protocolMatch ? protocolMatch[1].toUpperCase() : undefined,
|
||||||
|
action: actionMatch ? actionMatch[1].toUpperCase() : undefined
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler principal
|
||||||
|
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (req.method !== 'GET') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializar Socket.IO si no está inicializado
|
||||||
|
if (!io) {
|
||||||
|
const httpServer = (res.socket as any)?.server as HTTPServer
|
||||||
|
io = new SocketIOServer(httpServer, {
|
||||||
|
path: '/api/socket',
|
||||||
|
cors: {
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
console.log('Cliente conectado:', socket.id)
|
||||||
|
|
||||||
|
// Enviar datos iniciales
|
||||||
|
socket.emit('connected', { message: 'Conectado al servidor de monitoreo' })
|
||||||
|
|
||||||
|
// Configurar intervals para envío de datos
|
||||||
|
const statsInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const stats = await getSystemStats()
|
||||||
|
socket.emit('stats', stats)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error enviando stats:', error)
|
||||||
|
}
|
||||||
|
}, 5000) // Cada 5 segundos
|
||||||
|
|
||||||
|
const logsInterval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const logs = await getRecentLogs(20)
|
||||||
|
socket.emit('logs', logs)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error enviando logs:', error)
|
||||||
|
}
|
||||||
|
}, 10000) // Cada 10 segundos
|
||||||
|
|
||||||
|
// Limpiar intervals cuando el cliente se desconecta
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
console.log('Cliente desconectado:', socket.id)
|
||||||
|
clearInterval(statsInterval)
|
||||||
|
clearInterval(logsInterval)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Manejar solicitudes específicas
|
||||||
|
socket.on('request-stats', async () => {
|
||||||
|
try {
|
||||||
|
const stats = await getSystemStats()
|
||||||
|
socket.emit('stats', stats)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error en request-stats:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('request-logs', async (data) => {
|
||||||
|
try {
|
||||||
|
const limit = data?.limit || 50
|
||||||
|
const logs = await getRecentLogs(limit)
|
||||||
|
socket.emit('logs', logs)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error en request-logs:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({ message: 'Socket.IO server running' })
|
||||||
|
}
|
||||||
98
src/store/csf-store.ts
Archivo normal
98
src/store/csf-store.ts
Archivo normal
@@ -0,0 +1,98 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
import { CSFStatus, FirewallRule, LogEntry, ServerStats, DashboardData, CSFConfig } from '@/types/csf'
|
||||||
|
|
||||||
|
interface CSFStore {
|
||||||
|
// Status
|
||||||
|
status: CSFStatus | null
|
||||||
|
setStatus: (status: CSFStatus) => void
|
||||||
|
|
||||||
|
// Rules
|
||||||
|
rules: FirewallRule[]
|
||||||
|
setRules: (rules: FirewallRule[]) => void
|
||||||
|
addRule: (rule: FirewallRule) => void
|
||||||
|
removeRule: (ruleId: string) => void
|
||||||
|
updateRule: (ruleId: string, updates: Partial<FirewallRule>) => void
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
logs: LogEntry[]
|
||||||
|
setLogs: (logs: LogEntry[]) => void
|
||||||
|
addLog: (log: LogEntry) => void
|
||||||
|
clearLogs: () => void
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
stats: ServerStats | null
|
||||||
|
setStats: (stats: ServerStats) => void
|
||||||
|
|
||||||
|
// Config
|
||||||
|
config: CSFConfig | null
|
||||||
|
setConfig: (config: CSFConfig) => void
|
||||||
|
updateConfig: (updates: Partial<CSFConfig>) => void
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
isLoading: boolean
|
||||||
|
setLoading: (loading: boolean) => void
|
||||||
|
|
||||||
|
error: string | null
|
||||||
|
setError: (error: string | null) => void
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
dashboardData: DashboardData | null
|
||||||
|
setDashboardData: (data: DashboardData) => void
|
||||||
|
|
||||||
|
// Real-time updates
|
||||||
|
isConnected: boolean
|
||||||
|
setConnected: (connected: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useCSFStore = create<CSFStore>((set, get) => ({
|
||||||
|
// Status
|
||||||
|
status: null,
|
||||||
|
setStatus: (status) => set({ status }),
|
||||||
|
|
||||||
|
// Rules
|
||||||
|
rules: [],
|
||||||
|
setRules: (rules) => set({ rules }),
|
||||||
|
addRule: (rule) => set(state => ({ rules: [...state.rules, rule] })),
|
||||||
|
removeRule: (ruleId) => set(state => ({
|
||||||
|
rules: state.rules.filter(rule => rule.id !== ruleId)
|
||||||
|
})),
|
||||||
|
updateRule: (ruleId, updates) => set(state => ({
|
||||||
|
rules: state.rules.map(rule =>
|
||||||
|
rule.id === ruleId ? { ...rule, ...updates } : rule
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
logs: [],
|
||||||
|
setLogs: (logs) => set({ logs }),
|
||||||
|
addLog: (log) => set(state => ({
|
||||||
|
logs: [log, ...state.logs].slice(0, 1000) // Keep last 1000 logs
|
||||||
|
})),
|
||||||
|
clearLogs: () => set({ logs: [] }),
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
stats: null,
|
||||||
|
setStats: (stats) => set({ stats }),
|
||||||
|
|
||||||
|
// Config
|
||||||
|
config: null,
|
||||||
|
setConfig: (config) => set({ config }),
|
||||||
|
updateConfig: (updates) => set(state => ({
|
||||||
|
config: state.config ? { ...state.config, ...updates } : null
|
||||||
|
})),
|
||||||
|
|
||||||
|
// UI State
|
||||||
|
isLoading: false,
|
||||||
|
setLoading: (isLoading) => set({ isLoading }),
|
||||||
|
|
||||||
|
error: null,
|
||||||
|
setError: (error) => set({ error }),
|
||||||
|
|
||||||
|
// Dashboard
|
||||||
|
dashboardData: null,
|
||||||
|
setDashboardData: (dashboardData) => set({ dashboardData }),
|
||||||
|
|
||||||
|
// Real-time updates
|
||||||
|
isConnected: false,
|
||||||
|
setConnected: (isConnected) => set({ isConnected }),
|
||||||
|
}))
|
||||||
91
src/types/csf.ts
Archivo normal
91
src/types/csf.ts
Archivo normal
@@ -0,0 +1,91 @@
|
|||||||
|
// CSF Administration Panel Types
|
||||||
|
|
||||||
|
export interface CSFStatus {
|
||||||
|
running: boolean;
|
||||||
|
version: string;
|
||||||
|
ipv4_active: boolean;
|
||||||
|
ipv6_active: boolean;
|
||||||
|
testing_mode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirewallRule {
|
||||||
|
id: string;
|
||||||
|
ip: string;
|
||||||
|
port?: string;
|
||||||
|
comment?: string;
|
||||||
|
type: 'allow' | 'deny' | 'temp';
|
||||||
|
direction: 'in' | 'out' | 'both';
|
||||||
|
created: string;
|
||||||
|
expires?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CSFConfig {
|
||||||
|
testing: boolean;
|
||||||
|
testing_interval: number;
|
||||||
|
ipv6: boolean;
|
||||||
|
tcp_in: string;
|
||||||
|
tcp_out: string;
|
||||||
|
udp_in: string;
|
||||||
|
udp_out: string;
|
||||||
|
tcp6_in: string;
|
||||||
|
tcp6_out: string;
|
||||||
|
udp6_in: string;
|
||||||
|
udp6_out: string;
|
||||||
|
deny_ip_limit: number;
|
||||||
|
deny_temp_ip_limit: number;
|
||||||
|
lf_dshield: boolean;
|
||||||
|
lf_spamhaus: boolean;
|
||||||
|
auto_updates: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogEntry {
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
level: 'info' | 'warning' | 'error' | 'block';
|
||||||
|
message: string;
|
||||||
|
ip?: string;
|
||||||
|
port?: string;
|
||||||
|
protocol?: string;
|
||||||
|
action?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerStats {
|
||||||
|
cpu_usage: number;
|
||||||
|
memory_usage: number;
|
||||||
|
disk_usage: number;
|
||||||
|
network_in: number;
|
||||||
|
network_out: number;
|
||||||
|
active_connections: number;
|
||||||
|
blocked_ips: number;
|
||||||
|
allowed_ips: number;
|
||||||
|
uptime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CSFCommand {
|
||||||
|
command: string;
|
||||||
|
args?: string[];
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthUser {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
role: 'admin' | 'operator';
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DashboardData {
|
||||||
|
status: CSFStatus;
|
||||||
|
stats: ServerStats;
|
||||||
|
recent_logs: LogEntry[];
|
||||||
|
active_rules: number;
|
||||||
|
blocked_countries: string[];
|
||||||
|
}
|
||||||
33
tsconfig.json
Archivo normal
33
tsconfig.json
Archivo normal
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "es6"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"@/components/*": ["./src/components/*"],
|
||||||
|
"@/lib/*": ["./src/lib/*"],
|
||||||
|
"@/hooks/*": ["./src/hooks/*"],
|
||||||
|
"@/store/*": ["./src/store/*"],
|
||||||
|
"@/types/*": ["./src/types/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Referencia en una nueva incidencia
Block a user