refactor: remove server-side auth and migrate to client-side data storage

This commit is contained in:
Greg 2025-05-29 15:31:40 +02:00
parent 7d1bb92799
commit 5a800d684c
9 changed files with 30 additions and 649 deletions

View File

@ -40,29 +40,10 @@ function initAuth(app) {
// Handle login form submission // Handle login form submission
app.post('/auth/login', (req, res) => { app.post('/auth/login', (req, res) => {
const { password } = req.body; // This server-side authentication middleware (auth-middleware.js) has been deprecated.
const storedHash = process.env.PASSWORD_HASH; // Authentication for data access was tied to the server-side data API, which is now removed.
// Client-side data encryption could be an alternative if data protection is required.
if (!storedHash) { res.redirect('/login?error=deprecated');
console.error('PASSWORD_HASH environment variable not set');
return res.redirect('/login?error=config');
}
// Verify password
bcrypt.compare(password, storedHash, (err, isMatch) => {
if (err) {
console.error('Error verifying password:', err);
return res.redirect('/login?error=server');
}
if (isMatch) {
// Set session as authenticated
req.session.authenticated = true;
res.redirect('/');
} else {
res.redirect('/login?error=invalid');
}
});
}); });
// Logout endpoint // Logout endpoint
@ -73,9 +54,9 @@ function initAuth(app) {
// Authentication check endpoint for Nginx auth_request // Authentication check endpoint for Nginx auth_request
app.get('/auth/check', (req, res) => { app.get('/auth/check', (req, res) => {
if (req.session.authenticated) { // This server-side authentication middleware (auth-middleware.js) has been deprecated.
return res.status(200).send('OK'); // Authentication for data access was tied to the server-side data API, which is now removed.
} // Client-side data encryption could be an alternative if data protection is required.
return res.status(401).send('Unauthorized'); return res.status(401).send('Unauthorized');
}); });

View File

@ -1,168 +1,3 @@
/** // This server-side data API (data-api.js) has been deprecated.
* Simple data API for Weight Tracker // Data handling is now managed client-side using browser file APIs
* This script handles data storage operations when deployed in Docker // in accordance with the project vision for a static site.
* Includes authentication for password protection
*/
const fs = require('fs');
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const { initAuth, generateHash } = require('./auth-middleware');
// Create Express app
const app = express();
const port = process.env.PORT || 3000;
// Data file path in the Docker volume
const DATA_DIR = process.env.DATA_DIR || '/data';
const DATA_FILE = path.join(DATA_DIR, 'weight-tracker-data.json');
// Print data file path for debugging
console.log(`[STARTUP] Data directory: ${DATA_DIR}`);
console.log(`[STARTUP] Data file path: ${DATA_FILE}`);
// List files in data directory if it exists
if (fs.existsSync(DATA_DIR)) {
try {
const files = fs.readdirSync(DATA_DIR);
console.log(`[STARTUP] Files in ${DATA_DIR}:`, files);
// If data file exists, log its size and content preview
if (fs.existsSync(DATA_FILE)) {
const stats = fs.statSync(DATA_FILE);
console.log(`[STARTUP] Data file exists: ${DATA_FILE}, size: ${stats.size} bytes`);
if (stats.size > 0) {
const preview = fs.readFileSync(DATA_FILE, 'utf8').substring(0, 200);
console.log(`[STARTUP] Data file content preview: ${preview}...`);
} else {
console.log(`[STARTUP] Data file is empty`);
}
} else {
console.log(`[STARTUP] Data file does not exist: ${DATA_FILE}`);
}
} catch (error) {
console.error(`[STARTUP] Error reading data directory: ${error.message}`);
}
} else {
console.log(`[STARTUP] Data directory does not exist: ${DATA_DIR}`);
}
// Middleware
app.use(cors());
app.use(bodyParser.json({ limit: '5mb' }));
app.use(bodyParser.urlencoded({ extended: true })); // For parsing form data
app.use(express.static('public')); // Serve static files
// Copy login.html to the correct location for serving
const loginHtmlPath = path.join(__dirname, 'login.html');
if (fs.existsSync(loginHtmlPath)) {
// Ensure public directory exists
const publicDir = path.join(__dirname, 'public');
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
// Copy login.html to public directory
fs.copyFileSync(loginHtmlPath, path.join(publicDir, 'login.html'));
console.log('Login page copied to public directory');
}
// Initialize authentication middleware
initAuth(app);
// Ensure data directory exists
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
console.log(`Created data directory: ${DATA_DIR}`);
}
// Initialize data file if it doesn't exist
if (!fs.existsSync(DATA_FILE)) {
const defaultData = {
weights: [],
meals: [],
version: '1.0.0'
};
fs.writeFileSync(DATA_FILE, JSON.stringify(defaultData, null, 2));
console.log(`Created initial data file: ${DATA_FILE}`);
}
// GET endpoint to retrieve data
app.get('/data/weight-tracker-data.json', (req, res) => {
try {
console.log(`[DEBUG] GET request received for ${DATA_FILE}`);
console.log(`[DEBUG] File exists: ${fs.existsSync(DATA_FILE)}`);
if (fs.existsSync(DATA_FILE)) {
const data = fs.readFileSync(DATA_FILE, 'utf8');
console.log(`[DEBUG] Data read from file: ${data.substring(0, 100)}...`);
res.setHeader('Content-Type', 'application/json');
res.send(data);
console.log(`[DEBUG] Data sent to client`);
} else {
console.log(`[DEBUG] Data file not found at ${DATA_FILE}`);
res.status(404).send({ error: 'Data file not found' });
}
} catch (error) {
console.error('[DEBUG] Error reading data file:', error);
res.status(500).send({ error: 'Failed to read data file' });
}
});
// PUT endpoint to update data
app.put('/data/weight-tracker-data.json', (req, res) => {
try {
console.log(`[DEBUG] PUT request received for ${DATA_FILE}`);
const data = req.body;
// Log request body summary
console.log(`[DEBUG] Request body received:`, {
hasData: !!data,
hasWeights: data && !!data.weights,
weightCount: data && data.weights ? data.weights.length : 0,
hasMeals: data && !!data.meals,
mealCount: data && data.meals ? data.meals.length : 0
});
// Validate data structure
if (!data || !data.weights || !data.meals) {
console.log(`[DEBUG] Invalid data structure received`);
return res.status(400).send({ error: 'Invalid data structure' });
}
// Ensure data directory exists
if (!fs.existsSync(DATA_DIR)) {
console.log(`[DEBUG] Creating data directory: ${DATA_DIR}`);
fs.mkdirSync(DATA_DIR, { recursive: true });
}
// Write to file
console.log(`[DEBUG] Writing data to file: ${DATA_FILE}`);
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
// Verify file was written
const fileExists = fs.existsSync(DATA_FILE);
console.log(`[DEBUG] File exists after write: ${fileExists}`);
if (fileExists) {
const stats = fs.statSync(DATA_FILE);
console.log(`[DEBUG] File size after write: ${stats.size} bytes`);
}
res.send({ success: true, message: 'Data saved successfully' });
console.log(`[DEBUG] Data updated: ${new Date().toISOString()}`);
} catch (error) {
console.error('[DEBUG] Error writing data file:', error);
res.status(500).send({ error: 'Failed to write data file' });
}
});
// Start server
app.listen(port, () => {
console.log(`Data API server running on port ${port}`);
console.log(`Data directory: ${DATA_DIR}`);
console.log(`Data file: ${DATA_FILE}`);
});

View File

@ -1,37 +0,0 @@
/**
* Generate .htpasswd file for Nginx basic authentication
*
* This script creates a .htpasswd file from the bcrypt hash provided in the
* PASSWORD_HASH environment variable.
*/
const fs = require('fs');
const path = require('path');
// Get username from environment variable or use default
const USERNAME = process.env.AUTH_USERNAME || 'user';
// Get password hash from environment variable
const passwordHash = process.env.PASSWORD_HASH || '$2a$10$EgxHKjDDFcZKtQY9hl/N4.QvEQHCXVnQXw9dzFYlUDVKOcLMGp9eq';
// For Nginx basic auth, we need to use the format: username:{PLAIN}password
// This is simpler and more reliable than trying to use bcrypt hashes with Nginx
// Extract the original password from environment variable if available
const plainPassword = process.env.AUTH_PASSWORD || 'password';
// Format for .htpasswd with plaintext password
const htpasswdContent = `${USERNAME}:{PLAIN}${plainPassword}`;
// Path to the .htpasswd file
const htpasswdPath = '/etc/nginx/.htpasswd';
// Write the .htpasswd file
try {
fs.writeFileSync(htpasswdPath, htpasswdContent);
console.log(`Generated .htpasswd file at ${htpasswdPath}`);
} catch (error) {
console.error(`Error generating .htpasswd file: ${error.message}`);
process.exit(1);
}
console.log('Basic authentication setup complete');

View File

@ -1,44 +0,0 @@
/**
* Password Hash Generator for Weight Tracker
*
* This utility script generates a bcrypt hash for your password
* that can be used in the PASSWORD_HASH environment variable.
*
* Usage: node generate-password-hash.js <your-password>
*/
const bcrypt = require('bcryptjs');
async function generateHash() {
// Get password from command line argument
const password = process.argv[2];
if (!password) {
console.error('Error: No password provided');
console.log('Usage: node generate-password-hash.js <your-password>');
process.exit(1);
}
try {
// Generate hash with bcrypt (cost factor 10)
const hash = await bcrypt.hash(password, 10);
console.log('\nPassword Hash Generated Successfully\n');
console.log('Copy this hash to your PASSWORD_HASH environment variable in Coolify:');
console.log('----------------------------------------------------------------');
console.log(hash);
console.log('----------------------------------------------------------------\n');
console.log('For docker-compose.yml, use:');
console.log(`PASSWORD_HASH=${hash}\n`);
console.log('For .env file, use:');
console.log(`PASSWORD_HASH=${hash}\n`);
} catch (error) {
console.error('Error generating hash:', error);
}
}
// Run the function
generateHash();

View File

@ -1,42 +0,0 @@
/**
* Password Hash Generator for Weight Tracker
*
* This script generates a bcrypt hash for a given password that can be used
* with the Weight Tracker application's authentication system.
*
* Usage:
* node generate-password.js <your-password>
*/
const bcrypt = require('bcryptjs');
// Get password from command line arguments
const password = process.argv[2];
if (!password) {
console.error('Error: Password is required');
console.log('Usage: node generate-password.js <your-password>');
process.exit(1);
}
// Generate salt and hash
const saltRounds = 10;
bcrypt.genSalt(saltRounds, (err, salt) => {
if (err) {
console.error('Error generating salt:', err);
process.exit(1);
}
bcrypt.hash(password, salt, (err, hash) => {
if (err) {
console.error('Error generating hash:', err);
process.exit(1);
}
console.log('\nPassword Hash for Nginx Basic Authentication:');
console.log(hash);
console.log('\nUse this hash in your PASSWORD_HASH environment variable in Coolify.');
console.log('Example:');
console.log(`PASSWORD_HASH=${hash}`);
});
});

View File

@ -1,45 +0,0 @@
/**
* Password Hash Injector for Weight Tracker
*
* This script injects the password hash from environment variables
* into the HTML file before it's served to the client.
*/
const fs = require('fs');
const path = require('path');
// Default password hash (can be overridden via environment variables)
const DEFAULT_HASH = '$2a$10$EgxHKjDDFcZKtQY9hl/N4.QvEQHCXVnQXw9dzFYlUDVKOcLMGp9eq'; // hash for "password"
/**
* Inject password hash into HTML file
* @param {string} htmlPath - Path to the HTML file
* @param {string} passwordHash - Bcrypt password hash
*/
function injectPasswordHash(htmlPath, passwordHash) {
try {
// Read the HTML file
let html = fs.readFileSync(htmlPath, 'utf8');
// Replace the placeholder with the actual password hash
html = html.replace('$PASSWORD_HASH$', passwordHash);
// Write the modified HTML back to the file
fs.writeFileSync(htmlPath, html);
console.log(`Password hash injected into ${htmlPath}`);
} catch (error) {
console.error(`Error injecting password hash: ${error.message}`);
}
}
// Get password hash from environment variable
const passwordHash = process.env.PASSWORD_HASH || DEFAULT_HASH;
// Path to the HTML file
const htmlPath = path.join(__dirname, 'public', 'index.html');
// Inject password hash
injectPasswordHash(htmlPath, passwordHash);
console.log('Password hash injection complete');

View File

@ -14,63 +14,19 @@ const DataManager = (() => {
// Current application data // Current application data
let appData = {...defaultData}; let appData = {...defaultData};
// Determine if we're running in Docker (has /data endpoint)
const isDockerEnvironment = () => {
// Always return true for now since we're using Docker with Coolify
return true;
};
// API endpoint path for data operations - this must match the API endpoint in data-api.js
// Note: This is NOT a file system path, but an API endpoint URL path
const serverDataPath = '/data/weight-tracker-data.json';
/** /**
* Initialize data - load from server if in Docker, otherwise use localStorage * Initialize data - load from localStorage or use defaults.
* Actual file loading will be a separate user-initiated action.
*/ */
const init = async () => { const init = async () => {
try { try {
console.log('Initializing data manager...'); console.log('Initializing data manager...');
console.log('Docker environment detected:', isDockerEnvironment()); loadFromLocalStorage(); // Load from localStorage or set defaults
if (isDockerEnvironment()) {
// Try to load from server-side storage
try {
console.log('Attempting to load data from server at:', serverDataPath);
const response = await fetch(serverDataPath);
console.log('Server response status:', response.status, response.statusText);
if (response.ok) {
const data = await response.json();
console.log('Data received from server:', {
hasData: !!data,
hasWeights: data && !!data.weights,
weightCount: data && data.weights ? data.weights.length : 0,
hasMeals: data && !!data.meals,
mealCount: data && data.meals ? data.meals.length : 0
});
appData = data;
console.log('Data loaded from server storage successfully');
} else {
console.log('No server data found or error response. Starting with empty data.');
appData = {...defaultData};
console.log('Saving default data to server...');
await saveData(); // Save default data to server
}
} catch (serverError) {
console.warn('Exception loading from server:', serverError);
console.log('Falling back to localStorage');
loadFromLocalStorage();
}
} else {
// Use localStorage in development environment
console.log('Using localStorage in development environment');
loadFromLocalStorage();
}
} catch (error) { } catch (error) {
console.error('Error initializing data:', error); console.error('Error initializing data:', error);
appData = {...defaultData}; appData = {...defaultData}; // Fallback to default data
console.log('Saving default data due to initialization error'); // Attempt to save default data to localStorage if init failed badly
saveData(); // Save default data structure on error try { saveDataToLocalStorage(); } catch (e) { console.error('Failed to save default data to LS during init error handling', e); }
} }
}; };
@ -90,14 +46,11 @@ const DataManager = (() => {
}; };
/** /**
* Save data to either server (in Docker) or localStorage (in development) * Save data to localStorage.
* Actual file saving will be a separate user-initiated action (handled by exportData).
*/ */
const saveData = async () => { const saveData = async () => {
if (isDockerEnvironment()) {
return saveDataToServer();
} else {
return saveDataToLocalStorage(); return saveDataToLocalStorage();
}
}; };
/** /**
@ -114,48 +67,6 @@ const DataManager = (() => {
} }
}; };
/**
* Save data to server (Docker environment)
*/
const saveDataToServer = async () => {
try {
console.log('Attempting to save data to server at:', serverDataPath);
console.log('Data to save:', {
weights: appData.weights.length,
meals: appData.meals.length,
version: appData.version
});
const response = await fetch(serverDataPath, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(appData)
});
if (response.ok) {
const result = await response.json();
console.log('Server response:', result);
console.log('Data saved to server storage successfully');
return true;
} else {
console.error('Error saving data to server. Status:', response.status, response.statusText);
try {
const errorData = await response.text();
console.error('Error details:', errorData);
} catch (e) {
console.error('Could not parse error response');
}
console.log('Falling back to localStorage');
return saveDataToLocalStorage(); // Fallback to localStorage
}
} catch (error) {
console.error('Exception while saving data to server:', error);
console.log('Falling back to localStorage');
return saveDataToLocalStorage(); // Fallback to localStorage
}
};
/** /**
* Add a weight entry * Add a weight entry
@ -294,32 +205,23 @@ const DataManager = (() => {
*/ */
const exportData = () => { const exportData = () => {
try { try {
const dataStr = JSON.stringify(appData, null, 2); const jsonData = JSON.stringify(appData, null, 2); // Pretty print JSON
const blob = new Blob([jsonData], { type: 'application/json;charset=utf-8;' });
// Create a blob instead of using data URI const link = document.createElement('a');
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const exportFileDefaultName = `weight-tracker-backup-${formatDateForFilename(new Date())}.json`; link.setAttribute('href', url);
link.setAttribute('download', `weight-tracker-data-${formatDateForFilename(new Date())}.json`);
link.style.visibility = 'hidden';
const linkElement = document.createElement('a'); document.body.appendChild(link);
linkElement.setAttribute('href', url); link.click();
linkElement.setAttribute('download', exportFileDefaultName); document.body.removeChild(link);
linkElement.style.display = 'none';
// Add to DOM, trigger click, and clean up
document.body.appendChild(linkElement);
linkElement.click();
// Clean up
setTimeout(() => {
document.body.removeChild(linkElement);
URL.revokeObjectURL(url);
}, 100);
console.log('Data exported as JSON file.');
return true; return true;
} catch (error) { } catch (error) {
console.error('Error exporting data:', error); console.error('Error exporting data as JSON:', error);
return false; return false;
} }
}; };

View File

@ -1,168 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Keep My Weight Tracker</title>
<style>
:root {
--primary-color: #4a6fa5;
--primary-dark: #3a5a8c;
--background-color: #f8f9fa;
--card-bg: #ffffff;
--text-color: #333;
--border-color: #e0e0e0;
--error-color: #d9534f;
--border-radius: 8px;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
--font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Roboto', Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
background-color: var(--background-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.login-container {
background-color: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
width: 100%;
max-width: 400px;
padding: 30px;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
h1 {
color: var(--primary-color);
font-size: 2rem;
margin-bottom: 5px;
}
.tagline {
color: #666;
font-size: 1rem;
margin-bottom: 30px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
input[type="password"] {
width: 100%;
padding: 12px;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
font-family: var(--font-family);
font-size: 1rem;
}
input[type="password"]:focus {
outline: none;
border-color: var(--primary-color);
}
.btn {
width: 100%;
padding: 12px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
font-weight: 600;
font-size: 1rem;
transition: background-color 0.3s;
}
.btn:hover {
background-color: var(--primary-dark);
}
.error-message {
color: var(--error-color);
margin-top: 20px;
text-align: center;
font-size: 0.9rem;
}
.privacy-note {
margin-top: 30px;
text-align: center;
font-size: 0.8rem;
color: #666;
}
@media (max-width: 480px) {
.login-container {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="logo">
<h1>Keep My Weight</h1>
</div>
<p class="tagline">Simple, private weight & meal tracking</p>
<form id="login-form" action="/auth/login" method="POST">
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn">Login</button>
<div id="error-message" class="error-message">
<!-- Error messages will be displayed here -->
<!-- If there was an error in the URL, it will be displayed via JavaScript -->
</div>
</form>
<p class="privacy-note">Your data stays private, always. This password protects your personal health information.</p>
</div>
<script>
// Check for error parameter in URL
document.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
if (error) {
const errorElement = document.getElementById('error-message');
if (error === 'invalid') {
errorElement.textContent = 'Invalid password. Please try again.';
} else if (error === 'session') {
errorElement.textContent = 'Your session has expired. Please login again.';
}
}
});
</script>
</body>
</html>

View File

@ -10,7 +10,6 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.425.0", "@aws-sdk/client-s3": "^3.425.0",
"bcryptjs": "^2.4.3",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"connect-redis": "^7.1.0", "connect-redis": "^7.1.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",