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 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
|
||||
|
||||
@ -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
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
|
||||
*/
|
||||
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
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
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user