feat: implement password protection with bcrypt authentication and session management
This commit is contained in:
parent
7ed0bca7c2
commit
bbd9c44259
@ -31,6 +31,7 @@ COPY data-api.js /usr/share/nginx/api/
|
|||||||
COPY backup-s3.js /usr/share/nginx/api/
|
COPY backup-s3.js /usr/share/nginx/api/
|
||||||
COPY auth-middleware.js /usr/share/nginx/api/
|
COPY auth-middleware.js /usr/share/nginx/api/
|
||||||
COPY login.html /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 a custom Nginx configuration that includes the data API proxy
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|||||||
@ -6,6 +6,10 @@
|
|||||||
<title>Keep My Weight - Personal Weight & Meal Tracker</title>
|
<title>Keep My Weight - Personal Weight & Meal Tracker</title>
|
||||||
<link rel="stylesheet" href="css/styles.css">
|
<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>">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
@ -203,6 +207,7 @@
|
|||||||
<script src="js/dataManager.js"></script>
|
<script src="js/dataManager.js"></script>
|
||||||
<script src="js/ui.js"></script>
|
<script src="js/ui.js"></script>
|
||||||
<script src="js/charts.js"></script>
|
<script src="js/charts.js"></script>
|
||||||
|
<script src="js/auth.js"></script>
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
45
inject-password-hash.js
Normal file
45
inject-password-hash.js
Normal 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');
|
||||||
23
js/app.js
23
js/app.js
@ -3,7 +3,10 @@
|
|||||||
* Entry point for the application, initializes all components
|
* Entry point for the application, initializes all components
|
||||||
*/
|
*/
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Initialize all modules
|
// Initialize authentication first
|
||||||
|
Auth.init();
|
||||||
|
|
||||||
|
// Initialize all other modules
|
||||||
DataManager.init();
|
DataManager.init();
|
||||||
UI.init();
|
UI.init();
|
||||||
Charts.init();
|
Charts.init();
|
||||||
@ -12,5 +15,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
UI.renderWeightTable();
|
UI.renderWeightTable();
|
||||||
UI.renderMealTable();
|
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');
|
console.log('Weight Tracker app initialized successfully');
|
||||||
});
|
});
|
||||||
|
|||||||
147
js/auth.js
Normal file
147
js/auth.js
Normal 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
|
||||||
|
};
|
||||||
|
})();
|
||||||
@ -5,6 +5,19 @@ logfile=/dev/stdout
|
|||||||
logfile_maxbytes=0
|
logfile_maxbytes=0
|
||||||
pidfile=/var/run/supervisord.pid
|
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]
|
[program:nginx]
|
||||||
command=nginx -g 'daemon off;'
|
command=nginx -g 'daemon off;'
|
||||||
autostart=true
|
autostart=true
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user