feat: implement password protection with bcrypt authentication and session management

This commit is contained in:
Greg 2025-05-27 00:22:36 +02:00
parent 7ed0bca7c2
commit bbd9c44259
6 changed files with 233 additions and 1 deletions

View File

@ -31,6 +31,7 @@ COPY data-api.js /usr/share/nginx/api/
COPY backup-s3.js /usr/share/nginx/api/
COPY auth-middleware.js /usr/share/nginx/api/
COPY login.html /usr/share/nginx/api/
COPY inject-password-hash.js /usr/share/nginx/api/
# Copy a custom Nginx configuration that includes the data API proxy
COPY nginx.conf /etc/nginx/conf.d/default.conf

View File

@ -6,6 +6,10 @@
<title>Keep My Weight - Personal Weight & Meal Tracker</title>
<link rel="stylesheet" href="css/styles.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚖️</text></svg>">
<!-- Password hash for authentication (will be replaced by server) -->
<meta name="password-hash" content="$PASSWORD_HASH$">
<!-- bcrypt.js for password verification -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/bcryptjs/2.4.3/bcrypt.min.js"></script>
</head>
<body>
<div class="app-container">
@ -203,6 +207,7 @@
<script src="js/dataManager.js"></script>
<script src="js/ui.js"></script>
<script src="js/charts.js"></script>
<script src="js/auth.js"></script>
<script src="js/app.js"></script>
</body>
</html>

45
inject-password-hash.js Normal file
View File

@ -0,0 +1,45 @@
/**
* 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

@ -3,7 +3,10 @@
* Entry point for the application, initializes all components
*/
document.addEventListener('DOMContentLoaded', () => {
// Initialize all modules
// Initialize authentication first
Auth.init();
// Initialize all other modules
DataManager.init();
UI.init();
Charts.init();
@ -12,5 +15,23 @@ document.addEventListener('DOMContentLoaded', () => {
UI.renderWeightTable();
UI.renderMealTable();
// Add logout button to settings tab
const settingsCard = document.querySelector('#settings-content .card:last-child');
if (settingsCard) {
const logoutSection = document.createElement('div');
logoutSection.className = 'form-group';
logoutSection.innerHTML = `
<h3>Security</h3>
<p>Log out of your weight tracker application</p>
<button id="logout-button" class="btn secondary-btn">Logout</button>
`;
settingsCard.appendChild(logoutSection);
// Add logout functionality
document.getElementById('logout-button').addEventListener('click', () => {
Auth.logout();
});
}
console.log('Weight Tracker app initialized successfully');
});

147
js/auth.js Normal file
View File

@ -0,0 +1,147 @@
/**
* Authentication Module for Weight Tracker
* Provides password protection for the application
*/
const Auth = (() => {
// Session storage key
const AUTH_KEY = 'weight_tracker_auth';
// Default password hash (can be overridden via environment variables)
// This is a bcrypt hash of "password" - should be replaced in production
let passwordHash = '$2a$10$EgxHKjDDFcZKtQY9hl/N4.QvEQHCXVnQXw9dzFYlUDVKOcLMGp9eq';
// Login state
let isAuthenticated = false;
/**
* Initialize the authentication module
*/
const init = () => {
// Check for password hash in environment variables (passed via meta tag)
const envPasswordHash = document.querySelector('meta[name="password-hash"]')?.getAttribute('content');
if (envPasswordHash) {
passwordHash = envPasswordHash;
}
// Check if already authenticated
checkAuthStatus();
// If not authenticated, show login screen
if (!isAuthenticated) {
showLoginScreen();
}
};
/**
* Check if the user is authenticated
*/
const checkAuthStatus = () => {
const authData = sessionStorage.getItem(AUTH_KEY);
isAuthenticated = authData === 'true';
return isAuthenticated;
};
/**
* Show the login screen
*/
const showLoginScreen = () => {
// Create login overlay
const overlay = document.createElement('div');
overlay.className = 'login-overlay';
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'var(--background-color, #f8f9fa)';
overlay.style.zIndex = '9999';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
// Create login form
overlay.innerHTML = `
<div class="login-container" style="background-color: var(--card-bg, #ffffff); border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); width: 100%; max-width: 400px; padding: 30px;">
<div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: var(--primary-color, #4a6fa5); font-size: 2rem; margin-bottom: 5px;">Keep My Weight</h1>
</div>
<p style="color: #666; font-size: 1rem; margin-bottom: 30px; text-align: center;">Simple, private weight & meal tracking</p>
<form id="login-form">
<div style="margin-bottom: 20px;">
<label for="password" style="display: block; margin-bottom: 8px; font-weight: 500;">Password</label>
<input type="password" id="password" required style="width: 100%; padding: 12px; border-radius: 8px; border: 1px solid #e0e0e0; font-family: inherit; font-size: 1rem;">
</div>
<button type="submit" style="width: 100%; padding: 12px; background-color: var(--primary-color, #4a6fa5); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 1rem;">Login</button>
<div id="error-message" style="color: #d9534f; margin-top: 20px; text-align: center; font-size: 0.9rem;"></div>
</form>
<p style="margin-top: 30px; text-align: center; font-size: 0.8rem; color: #666;">Your data stays private, always. This password protects your personal health information.</p>
</div>
`;
// Add to DOM
document.body.appendChild(overlay);
// Handle form submission
const form = document.getElementById('login-form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('password').value;
const errorMessage = document.getElementById('error-message');
try {
// Use bcrypt.js for password verification
const bcrypt = window.dcodeIO?.bcrypt;
if (!bcrypt) {
throw new Error('bcrypt.js not loaded');
}
// Verify password
const isMatch = await new Promise((resolve) => {
bcrypt.compare(password, passwordHash, (err, result) => {
if (err) {
console.error('Error verifying password:', err);
resolve(false);
} else {
resolve(result);
}
});
});
if (isMatch) {
// Set as authenticated
sessionStorage.setItem(AUTH_KEY, 'true');
isAuthenticated = true;
// Remove login overlay
document.body.removeChild(overlay);
} else {
errorMessage.textContent = 'Invalid password. Please try again.';
}
} catch (error) {
console.error('Authentication error:', error);
errorMessage.textContent = 'An error occurred during authentication. Please try again.';
}
});
};
/**
* Log out the user
*/
const logout = () => {
sessionStorage.removeItem(AUTH_KEY);
isAuthenticated = false;
showLoginScreen();
};
// Return public API
return {
init,
checkAuthStatus,
logout
};
})();

View File

@ -5,6 +5,19 @@ logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
[program:inject-password-hash]
command=node /usr/share/nginx/api/inject-password-hash.js
directory=/usr/share/nginx/api
environment=PASSWORD_HASH="%(ENV_PASSWORD_HASH)s"
autostart=true
autorestart=false
startsecs=0
startretries=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=nginx -g 'daemon off;'
autostart=true