refactor: remove server-side auth and migrate to client-side data storage
This commit is contained in:
parent
7d1bb92799
commit
5a800d684c
@ -40,29 +40,10 @@ function initAuth(app) {
|
||||
|
||||
// Handle login form submission
|
||||
app.post('/auth/login', (req, res) => {
|
||||
const { password } = req.body;
|
||||
const storedHash = process.env.PASSWORD_HASH;
|
||||
|
||||
if (!storedHash) {
|
||||
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');
|
||||
}
|
||||
});
|
||||
// This server-side authentication middleware (auth-middleware.js) has been deprecated.
|
||||
// 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.
|
||||
res.redirect('/login?error=deprecated');
|
||||
});
|
||||
|
||||
// Logout endpoint
|
||||
@ -73,9 +54,9 @@ function initAuth(app) {
|
||||
|
||||
// Authentication check endpoint for Nginx auth_request
|
||||
app.get('/auth/check', (req, res) => {
|
||||
if (req.session.authenticated) {
|
||||
return res.status(200).send('OK');
|
||||
}
|
||||
// This server-side authentication middleware (auth-middleware.js) has been deprecated.
|
||||
// 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');
|
||||
});
|
||||
|
||||
|
||||
171
data-api.js
171
data-api.js
@ -1,168 +1,3 @@
|
||||
/**
|
||||
* Simple data API for Weight Tracker
|
||||
* This script handles data storage operations when deployed in Docker
|
||||
* 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}`);
|
||||
});
|
||||
// This server-side data API (data-api.js) has been deprecated.
|
||||
// Data handling is now managed client-side using browser file APIs
|
||||
// in accordance with the project vision for a static site.
|
||||
|
||||
@ -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');
|
||||
@ -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();
|
||||
@ -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}`);
|
||||
});
|
||||
});
|
||||
@ -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');
|
||||
@ -14,63 +14,19 @@ const DataManager = (() => {
|
||||
// Current application data
|
||||
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 () => {
|
||||
try {
|
||||
console.log('Initializing data manager...');
|
||||
console.log('Docker environment detected:', isDockerEnvironment());
|
||||
|
||||
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();
|
||||
}
|
||||
loadFromLocalStorage(); // Load from localStorage or set defaults
|
||||
} catch (error) {
|
||||
console.error('Error initializing data:', error);
|
||||
appData = {...defaultData};
|
||||
console.log('Saving default data due to initialization error');
|
||||
saveData(); // Save default data structure on error
|
||||
appData = {...defaultData}; // Fallback to default data
|
||||
// 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); }
|
||||
}
|
||||
};
|
||||
|
||||
@ -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 () => {
|
||||
if (isDockerEnvironment()) {
|
||||
return saveDataToServer();
|
||||
} else {
|
||||
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
|
||||
@ -294,32 +205,23 @@ const DataManager = (() => {
|
||||
*/
|
||||
const exportData = () => {
|
||||
try {
|
||||
const dataStr = JSON.stringify(appData, null, 2);
|
||||
|
||||
// Create a blob instead of using data URI
|
||||
const blob = new Blob([dataStr], { type: 'application/json' });
|
||||
const jsonData = JSON.stringify(appData, null, 2); // Pretty print JSON
|
||||
const blob = new Blob([jsonData], { type: 'application/json;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
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');
|
||||
linkElement.setAttribute('href', url);
|
||||
linkElement.setAttribute('download', exportFileDefaultName);
|
||||
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);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
console.log('Data exported as JSON file.');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error exporting data:', error);
|
||||
console.error('Error exporting data as JSON:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
168
login.html
168
login.html
@ -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>
|
||||
@ -10,7 +10,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.425.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"connect-redis": "^7.1.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user