feat: implement secure data API with basic auth and Docker support
This commit is contained in:
parent
abfd0bf636
commit
b9d5e29207
52
Dockerfile
52
Dockerfile
@ -1,21 +1,45 @@
|
|||||||
# Use Nginx as the base image for serving static content
|
# Stage 1: Build Node.js application
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package.json and package-lock.json (if available)
|
||||||
|
COPY package.json ./
|
||||||
|
# COPY package-lock.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
# Copy the rest of the application files (API code)
|
||||||
|
COPY data-api.js ./
|
||||||
|
# Add any other necessary files for the API here
|
||||||
|
|
||||||
|
# Stage 2: Setup Nginx and Supervisor
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
# Copy the custom Nginx configuration
|
# Install Supervisor, Node.js runtime, and apache2-utils (for htpasswd)
|
||||||
# This replaces the default Nginx configuration
|
RUN apk add --no-cache supervisor nodejs npm apache2-utils
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# Copy all static website files to the Nginx serving directory
|
# Remove default Nginx configuration
|
||||||
# This includes HTML, CSS, JavaScript, images, and the js/dataManager.js for client-side logic
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
COPY . /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Remove unnecessary files and directories from the Nginx serving directory
|
# Declare build arguments for credentials
|
||||||
# This helps to keep the final image size small and clean
|
ARG AUTH_USERNAME
|
||||||
RUN rm -rf /usr/share/nginx/html/Dockerfile \
|
ARG AUTH_PASSWORD
|
||||||
/usr/share/nginx/html/docker-compose.yml \
|
|
||||||
/usr/share/nginx/html/nginx.conf \
|
# Copy custom Nginx configuration
|
||||||
/usr/share/nginx/html/nginx-auth.conf \
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
/usr/share/nginx/html/supervisord.conf \
|
|
||||||
|
# Copy Basic Auth config (nginx-auth.conf)
|
||||||
|
COPY nginx-auth.conf /etc/nginx/nginx-auth.conf
|
||||||
|
|
||||||
|
# Generate .htpasswd file using build arguments
|
||||||
|
RUN htpasswd -cb /etc/nginx/.htpasswd ${AUTH_USERNAME} ${AUTH_PASSWORD} \
|
||||||
|
&& chown nginx:nginx /etc/nginx/.htpasswd \
|
||||||
|
&& chmod 600 /etc/nginx/.htpasswd
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY --from=builder /app/data-api.js \
|
||||||
/usr/share/nginx/html/data-api.js \
|
/usr/share/nginx/html/data-api.js \
|
||||||
/usr/share/nginx/html/auth-middleware.js \
|
/usr/share/nginx/html/auth-middleware.js \
|
||||||
/usr/share/nginx/html/backup-s3.js \
|
/usr/share/nginx/html/backup-s3.js \
|
||||||
|
|||||||
84
data-api.js
84
data-api.js
@ -1,3 +1,81 @@
|
|||||||
// This server-side data API (data-api.js) has been deprecated.
|
const express = require('express');
|
||||||
// Data handling is now managed client-side using browser file APIs
|
const bodyParser = require('body-parser');
|
||||||
// in accordance with the project vision for a static site.
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3000;
|
||||||
|
const DATA_DIR = process.env.DATA_DIR || '/data'; // Fallback for local dev, Docker will set via supervisord
|
||||||
|
const DATA_FILE_NAME = 'weight-tracker-data.json';
|
||||||
|
const DATA_FILE_PATH = path.join(DATA_DIR, DATA_FILE_NAME);
|
||||||
|
|
||||||
|
const DEFAULT_DATA = { weights: [], meals: [] };
|
||||||
|
|
||||||
|
app.use(bodyParser.json({ limit: '2mb' })); // Use body-parser to handle JSON POST requests, limit size
|
||||||
|
|
||||||
|
// Middleware for basic logging
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET endpoint to retrieve data
|
||||||
|
app.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
await fs.access(DATA_FILE_PATH);
|
||||||
|
const fileContent = await fs.readFile(DATA_FILE_PATH, 'utf8');
|
||||||
|
res.json(JSON.parse(fileContent));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
// File doesn't exist, return default data
|
||||||
|
console.log(`Data file not found at ${DATA_FILE_PATH}, returning default data.`);
|
||||||
|
res.json(DEFAULT_DATA);
|
||||||
|
} else {
|
||||||
|
console.error('Error reading data file:', error);
|
||||||
|
res.status(500).json({ message: 'Error reading data file', error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT endpoint to save data
|
||||||
|
app.put('/', async (req, res) => {
|
||||||
|
const newData = req.body;
|
||||||
|
|
||||||
|
// Basic Input Validation
|
||||||
|
if (typeof newData !== 'object' || newData === null) {
|
||||||
|
return res.status(400).json({ message: 'Invalid data format: body must be an object.' });
|
||||||
|
}
|
||||||
|
if (!Array.isArray(newData.weights)) {
|
||||||
|
return res.status(400).json({ message: 'Invalid data format: weights must be an array.' });
|
||||||
|
}
|
||||||
|
if (!Array.isArray(newData.meals)) {
|
||||||
|
return res.status(400).json({ message: 'Invalid data format: meals must be an array.' });
|
||||||
|
}
|
||||||
|
// Add more specific validation for array items if needed
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(DATA_FILE_PATH, JSON.stringify(newData, null, 2), 'utf8');
|
||||||
|
res.status(200).json({ message: 'Data saved successfully.' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error writing data file:', error);
|
||||||
|
res.status(500).json({ message: 'Error writing data file', error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`Data API server listening on port ${PORT}`);
|
||||||
|
console.log(`Expecting data file at: ${DATA_FILE_PATH}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Basic error handling for uncaught exceptions
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('Uncaught Exception:', error);
|
||||||
|
// Optionally, exit the process or perform other cleanup
|
||||||
|
// process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
// Optionally, exit the process or perform other cleanup
|
||||||
|
// process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
@ -5,6 +5,9 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
- AUTH_USERNAME=${AUTH_USERNAME}
|
||||||
|
- AUTH_PASSWORD=${AUTH_PASSWORD}
|
||||||
container_name: weight-tracker
|
container_name: weight-tracker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@ -20,13 +20,26 @@ const DataManager = (() => {
|
|||||||
*/
|
*/
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing data manager...');
|
console.log('Initializing data manager by fetching from API...');
|
||||||
loadFromLocalStorage(); // Load from localStorage or set defaults
|
const response = await fetch('/app-data/'); // Nginx proxies this to Node API
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
appData = data;
|
||||||
|
console.log('Successfully loaded data from API.');
|
||||||
|
// Optionally, update localStorage for offline or quick display, but API is truth
|
||||||
|
// saveDataToLocalStorage();
|
||||||
|
} else {
|
||||||
|
console.warn('Data from API was not valid JSON or empty. Using default data.');
|
||||||
|
appData = { ...defaultData };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to fetch data from API: ${response.status} ${response.statusText}. Using default data.`);
|
||||||
|
appData = { ...defaultData };
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing data:', error);
|
console.error('Error initializing data via API:', error);
|
||||||
appData = {...defaultData}; // Fallback to default data
|
appData = { ...defaultData }; // Fallback to default data on critical error
|
||||||
// Attempt to save default data to localStorage if init failed badly
|
|
||||||
try { saveDataToLocalStorage(); } catch (e) { console.error('Failed to save default data to LS during init error handling', e); }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -46,11 +59,37 @@ const DataManager = (() => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save data to localStorage.
|
* Save data to API.
|
||||||
* Actual file saving will be a separate user-initiated action (handled by exportData).
|
|
||||||
*/
|
*/
|
||||||
const saveData = async () => {
|
const saveData = async () => {
|
||||||
return saveDataToLocalStorage();
|
console.log('Attempting to save data via API...');
|
||||||
|
try {
|
||||||
|
const response = await fetch('/app-data/', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(appData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('Data saved successfully to API:', result.message);
|
||||||
|
// Optionally, update localStorage after successful API save
|
||||||
|
// saveDataToLocalStorage();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const errorResult = await response.json();
|
||||||
|
console.error(`Failed to save data to API: ${response.status} ${response.statusText}`, errorResult.message);
|
||||||
|
// Consider how to inform the user: alert, UI message, etc.
|
||||||
|
alert(`Failed to save data: ${errorResult.message || response.statusText}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving data via API:', error);
|
||||||
|
alert(`Error saving data: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
17
nginx.conf
17
nginx.conf
@ -9,12 +9,29 @@ server {
|
|||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
# Main application location
|
# Main application location
|
||||||
|
# API proxy: All requests to /app-data/* will be forwarded to the Node.js API
|
||||||
|
location /app-data/ {
|
||||||
|
include /etc/nginx/nginx-auth.conf; # Apply Basic Auth to API
|
||||||
|
|
||||||
|
proxy_pass http://localhost:3000/; # Assuming Node API runs on port 3000
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
include /etc/nginx/nginx-auth.conf; # Apply Basic Auth to the whole site
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Enable browser caching for static assets
|
# Enable browser caching for static assets
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
|
include /etc/nginx/nginx-auth.conf; # Apply Basic Auth to static assets too
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, no-transform";
|
add_header Cache-Control "public, no-transform";
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user