Skip to content

Commit

Permalink
updating web app for api key
Browse files Browse the repository at this point in the history
  • Loading branch information
mfreeman451 committed Feb 26, 2025
1 parent fc0ce96 commit ae6f57a
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 107 deletions.
6 changes: 6 additions & 0 deletions web/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# .env.example
# API key for backend authentication
VITE_API_KEY=your-api-key-here

# The actual .env file would look similar but with real values
# Remember to add .env to your .gitignore file
78 changes: 78 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# API Key Authentication for ServiceRadar Web Interface

This document explains how the API key authentication works in the ServiceRadar web interface.

## Overview

The ServiceRadar web application has been updated to support API key authentication for all API requests to the backend. This follows the 12-factor application methodology by loading the API key from environment variables.

## Setup Instructions

### 1. Environment Configuration

Create a `.env` file in the root directory of the project with the following content:

```
VITE_API_KEY=your-api-key-here
```

Make sure this matches the API key configured on your server.

> **Note:** The `.env` file should never be committed to version control. A `.env.example` file is provided as a template.
### 2. For Production Deployment

In a production environment, you should set the environment variable according to your deployment platform:

- **Docker:** Add it to your Docker Compose file or Docker run command:
```
docker run -e VITE_API_KEY=your-api-key-here ...
```

- **Kubernetes:** Add it to your deployment configuration:
```yaml
env:
- name: VITE_API_KEY
value: your-api-key-here
# Or better, use a secret:
env:
- name: VITE_API_KEY
valueFrom:
secretKeyRef:
name: serviceradar-secrets
key: api-key
```
- **Traditional hosting:** Set the environment variable in your hosting environment or through your CI/CD pipeline.
### 3. Development Environment
For local development, simply create a `.env` file as described above. The Vite development server will automatically load the environment variables.

## How It Works

1. The web interface reads the API key from the environment variable `VITE_API_KEY`.
2. All API requests are processed through a centralized API service that automatically appends the API key as an `X-API-Key` header.
3. The backend validates this header against its configured API key.
4. Static assets (JavaScript, CSS, images) are exempt from API key validation.

## API Service

The API service is implemented in `src/services/api.js` and provides methods for making authenticated API requests:

```javascript
// Example usage:
import { get, post } from '../services/api';
// GET request
const data = await get('/api/nodes');
// POST request
const response = await post('/api/some-endpoint', { key: 'value' });
```

## Security Considerations

- The API key is included in the compiled JavaScript bundle and can be viewed by users with access to your application. This provides a basic level of authentication but should not be considered fully secure.
- For more sensitive operations, consider implementing a more robust authentication system (OAuth, JWT, etc.).
- Always use HTTPS in production to prevent the API key from being intercepted.
5 changes: 3 additions & 2 deletions web/src/components/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
// src/components/Dashboard.jsx
import React, { useState, useEffect } from 'react';
import { get } from '../services/api';


function Dashboard() {
const [systemStatus, setSystemStatus] = useState(null);

useEffect(() => {
const fetchStatus = async () => {
try {
const response = await fetch('/api/status');
const data = await response.json();
const data = await get('/api/status');
setSystemStatus(data);
} catch (error) {
console.error('Error fetching status:', error);
Expand Down
8 changes: 4 additions & 4 deletions web/src/components/DuskDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
Legend,
ResponsiveContainer,
} from 'recharts';
import { get } from '../services/api';


const DuskDashboard = () => {
const [nodeStatus, setNodeStatus] = useState(null);
Expand All @@ -19,10 +21,8 @@ const DuskDashboard = () => {
useEffect(() => {
const fetchData = async () => {
try {
// Fetch nodes list
const nodesResponse = await fetch('/api/nodes');
if (!nodesResponse.ok) throw new Error('Failed to fetch nodes');
const nodes = await nodesResponse.json();
// Fetch nodes list using the API service
const nodes = await get('/api/nodes');

console.log('Fetched nodes:', nodes);

Expand Down
87 changes: 1 addition & 86 deletions web/src/components/NetworkStatus.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,89 +58,4 @@ const PingStatus = ({ details }) => {
);
};

// Summary component for multiple hosts
const ICMPSummary = ({ hosts }) => {
if (!Array.isArray(hosts) || hosts.length === 0) {
return (
<div className="text-gray-500 dark:text-gray-400 transition-colors">
No ICMP data available
</div>
);
}

const respondingHosts = hosts.filter((h) => h.available).length;
const totalResponseTime = hosts.reduce((sum, host) => {
if (host.available && host.response_time) {
return sum + host.response_time;
}
return sum;
}, 0);
const avgResponseTime = respondingHosts > 0 ? totalResponseTime / respondingHosts : 0;

return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 transition-colors">
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="font-medium text-gray-600 dark:text-gray-400">ICMP Responding:</div>
<div className="text-gray-800 dark:text-gray-100">{respondingHosts} hosts</div>

<div className="font-medium text-gray-600 dark:text-gray-400">
Average Response Time:
</div>
<div className="text-gray-800 dark:text-gray-100">
{formatResponseTime(avgResponseTime)}
</div>
</div>
</div>
);
};

// Network sweep ICMP summary
const NetworkSweepICMP = ({ sweepData }) => {
if (!sweepData || !sweepData.hosts) {
return (
<div className="text-gray-500 dark:text-gray-400 transition-colors">
No sweep data available
</div>
);
}

const hosts = sweepData.hosts.filter((host) => host.icmp_status);
const respondingHosts = hosts.filter((host) => host.icmp_status.available).length;

let avgResponseTime = 0;
const respondingHostsWithTime = hosts.filter((host) => {
return host.icmp_status.available && host.icmp_status.round_trip;
});

if (respondingHostsWithTime.length > 0) {
const totalTime = respondingHostsWithTime.reduce((sum, host) => {
return sum + (host.icmp_status.round_trip || 0);
}, 0);
avgResponseTime = totalTime / respondingHostsWithTime.length;
}

return (
<div className="space-y-4 transition-colors">
<h3 className="text-lg font-medium text-gray-800 dark:text-gray-100">
ICMP Status Summary
</h3>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 transition-colors">
<div className="grid grid-cols-2 gap-4 text-sm">
<div className="font-medium text-gray-600 dark:text-gray-400">
ICMP Responding:
</div>
<div className="text-gray-800 dark:text-gray-100">{respondingHosts} hosts</div>

<div className="font-medium text-gray-600 dark:text-gray-400">
Average Response Time:
</div>
<div className="text-gray-800 dark:text-gray-100">
{formatResponseTime(avgResponseTime)}
</div>
</div>
</div>
</div>
);
};

export { PingStatus, ICMPSummary, NetworkSweepICMP };
export { PingStatus };
1 change: 1 addition & 0 deletions web/src/components/NetworkSweepView.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import ExportButton from './ExportButton';


const compareIPAddresses = (ip1, ip2) => {
// Split IPs into their octets and convert to numbers
const ip1Parts = ip1.split('.').map(Number);
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/NodeList.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { LineChart, Line } from 'recharts';
import NodeTimeline from './NodeTimeline';
import _ from 'lodash';
import ServiceSparkline from "./ServiceSparkline.jsx";
import { useNavigate } from 'react-router-dom';
import { get } from '../services/api';


function NodeList() {
const [nodes, setNodes] = useState([]);
Expand Down Expand Up @@ -93,8 +94,7 @@ function NodeList() {
useEffect(() => {
const fetchNodes = async () => {
try {
const response = await fetch('/api/nodes');
const newData = await response.json();
const newData = await get('/api/nodes');
const sortedData = newData.sort(sortNodesByName);

setNodes((prevNodes) => {
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/NodeTimeline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
Tooltip,
ResponsiveContainer,
} from 'recharts';
import { get } from '../services/api';


const NodeTimeline = ({ nodeId }) => {
const [availabilityData, setAvailabilityData] = useState([]);
Expand All @@ -17,9 +19,7 @@ const NodeTimeline = ({ nodeId }) => {
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`/api/nodes/${nodeId}/history`);
if (!response.ok) throw new Error('Failed to fetch node history');
const data = await response.json();
const data = await get(`/api/nodes/${nodeId}/history`);

// Transform the history data for the chart
const timelineData = data.map((point) => ({
Expand All @@ -37,6 +37,7 @@ const NodeTimeline = ({ nodeId }) => {
}
};


fetchData();
const interval = setInterval(fetchData, 10000);
return () => clearInterval(interval);
Expand Down
1 change: 1 addition & 0 deletions web/src/components/SNMPDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
LineChart, Line, XAxis, YAxis, CartesianGrid,
Tooltip, Legend, ResponsiveContainer
} from 'recharts';
import { get } from '../services/api';

const SNMPDashboard = ({ nodeId, serviceName }) => {
const [snmpData, setSNMPData] = useState([]);
Expand Down
14 changes: 8 additions & 6 deletions web/src/components/ServiceDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import NetworkSweepView from './NetworkSweepView';
import { PingStatus } from './NetworkStatus';
import SNMPDashboard from "./SNMPDashboard.jsx";
import { get } from '../services/api';


const ServiceDashboard = () => {
const { nodeId, serviceName } = useParams();
Expand All @@ -27,9 +29,7 @@ const ServiceDashboard = () => {
const fetchData = async () => {
try {
// Fetch nodes list
const nodesResponse = await fetch('/api/nodes');
if (!nodesResponse.ok) throw new Error('Failed to fetch nodes');
const nodes = await nodesResponse.json();
const nodes = await get('/api/nodes');

// Find the specific node
const node = nodes.find((n) => n.node_id === nodeId);
Expand All @@ -46,13 +46,15 @@ const ServiceDashboard = () => {
setServiceData(service);

// Fetch metrics data
const metricsResponse = await fetch(`/api/nodes/${nodeId}/metrics`);
if (metricsResponse.ok) {
const metrics = await metricsResponse.json();
try {
const metrics = await get(`/api/nodes/${nodeId}/metrics`);
const serviceMetrics = metrics.filter(
(m) => m.service_name === serviceName
);
setMetricsData(serviceMetrics);
} catch (metricsError) {
console.error('Error fetching metrics data:', metricsError);
// Don't fail the whole request if metrics fail
}

setLoading(false);
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/ServiceSparkline.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { useState, useEffect, useMemo } from 'react';
import { LineChart, Line, YAxis, ResponsiveContainer } from 'recharts';
import { TrendingUp, TrendingDown, Minus } from 'lucide-react';
import _ from 'lodash';
import { get } from '../services/api';


const MAX_POINTS = 100;
const POLLING_INTERVAL = 10;
Expand Down Expand Up @@ -58,9 +60,7 @@ const ServiceSparkline = ({ nodeId, serviceName }) => {
useEffect(() => {
const fetchMetrics = async () => {
try {
const response = await fetch(`/api/nodes/${nodeId}/metrics`);
if (!response.ok) throw new Error('Failed to fetch metrics');
const data = await response.json();
const data = await get(`/api/nodes/${nodeId}/metrics`);

const serviceMetrics = data
.filter((m) => m.service_name === serviceName)
Expand Down
Loading

0 comments on commit ae6f57a

Please sign in to comment.