feat: add password protection and S3 backup functionality with authentication middleware
This commit is contained in:
parent
047c9baf64
commit
7a789e7060
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
# Node.js dependencies
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Build directories
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
|
||||
# IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Docker
|
||||
.docker-volumes/
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
@ -25,9 +25,12 @@ RUN apk add --no-cache nodejs npm supervisor
|
||||
# Copy the static website files to the Nginx serving directory
|
||||
COPY --from=web-builder /app /usr/share/nginx/html
|
||||
|
||||
# Copy the Node.js dependencies and data API
|
||||
# Copy the Node.js dependencies and API scripts
|
||||
COPY --from=base /app/node_modules /usr/share/nginx/api/node_modules
|
||||
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 a custom Nginx configuration that includes the data API proxy
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
55
README.md
55
README.md
@ -9,6 +9,8 @@ A minimalist, privacy-first web application for tracking your weight and daily m
|
||||
- **Data Visualization**: View your weight progress on an interactive chart
|
||||
- **Data Export/Import**: Export and import your data as JSON or CSV
|
||||
- **Privacy-First**: All data stays on your device using browser's localStorage
|
||||
- **Password Protection**: Secure access to your health data
|
||||
- **Automated Backups**: Optional S3-compatible storage backups
|
||||
- **Mobile-Friendly**: Responsive design works on all devices
|
||||
|
||||
## Getting Started
|
||||
@ -78,6 +80,59 @@ After deployment:
|
||||
|
||||
Even with persistent storage configured, it's recommended to occasionally use the "Export Data" feature in the Settings tab as an additional backup.
|
||||
|
||||
### S3 Backup Configuration
|
||||
|
||||
The application includes automated backups to S3-compatible storage (like Minio):
|
||||
|
||||
1. **Configure S3 Credentials in Coolify**:
|
||||
- In your Coolify dashboard, go to your Weight Tracker service
|
||||
- Under the "Environment Variables" section, set the following:
|
||||
- `S3_ENDPOINT`: Your Minio server URL (e.g., `https://minio.example.com`)
|
||||
- `S3_REGION`: Region name (e.g., `us-east-1`, can be any value for Minio)
|
||||
- `S3_BUCKET`: Bucket name for backups (e.g., `weight-tracker-backups`)
|
||||
- `S3_ACCESS_KEY`: Your Minio access key
|
||||
- `S3_SECRET_KEY`: Your Minio secret key
|
||||
- `S3_USE_SSL`: Set to `true` if your Minio server uses HTTPS
|
||||
|
||||
2. **Backup Schedule (Optional)**:
|
||||
- `BACKUP_SCHEDULE`: Cron expression for backup schedule (default: `0 0 * * *` - daily at midnight)
|
||||
- `BACKUP_RETENTION`: Number of backups to keep (default: `7`)
|
||||
|
||||
3. **Verify Backups**:
|
||||
- Check your Minio bucket for backup files named `weight-tracker-backup-[timestamp].json`
|
||||
- Backups are automatically rotated based on the retention setting
|
||||
|
||||
### Password Protection
|
||||
|
||||
The application includes password protection to secure your health data:
|
||||
|
||||
1. **Generate a Password Hash**:
|
||||
```bash
|
||||
# Install dependencies locally
|
||||
npm install bcryptjs
|
||||
|
||||
# Generate a hash for your password
|
||||
node generate-password-hash.js your-secure-password
|
||||
```
|
||||
This will output a bcrypt hash that you can use in your environment variables.
|
||||
|
||||
2. **Configure Password in Coolify**:
|
||||
- In your Coolify dashboard, go to your Weight Tracker service
|
||||
- Under the "Environment Variables" section, set:
|
||||
- `PASSWORD_HASH`: The bcrypt hash generated in step 1
|
||||
- `SESSION_SECRET`: A random string for session encryption
|
||||
- `COOKIE_SECURE`: Set to `true` if using HTTPS
|
||||
|
||||
3. **Alternative: Use Your Caddy Hash**:
|
||||
- If you already have a bcrypt hash from Caddy, you can use it directly
|
||||
- Set the `PASSWORD_HASH` environment variable to your existing bcrypt hash
|
||||
|
||||
4. **Security Notes**:
|
||||
- The password is never stored in plain text
|
||||
- Only the hash is stored in the environment variable
|
||||
- Authentication uses secure HTTP-only cookies
|
||||
- Sessions expire after 24 hours of inactivity
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 (Current)
|
||||
|
||||
103
auth-middleware.js
Normal file
103
auth-middleware.js
Normal file
@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Authentication Middleware for Weight Tracker
|
||||
* Handles password protection and session management
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const session = require('express-session');
|
||||
const cookieParser = require('cookie-parser');
|
||||
|
||||
// Default session configuration
|
||||
const sessionConfig = {
|
||||
secret: process.env.SESSION_SECRET || 'weight-tracker-secret',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.COOKIE_SECURE === 'true',
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize authentication middleware
|
||||
* @param {Object} app - Express app
|
||||
*/
|
||||
function initAuth(app) {
|
||||
// Parse cookies
|
||||
app.use(cookieParser());
|
||||
|
||||
// Session management
|
||||
app.use(session(sessionConfig));
|
||||
|
||||
// Serve login page
|
||||
app.get('/login', (req, res) => {
|
||||
if (req.session.authenticated) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
res.sendFile('login.html', { root: __dirname });
|
||||
});
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Logout endpoint
|
||||
app.get('/auth/logout', (req, res) => {
|
||||
req.session.destroy();
|
||||
res.redirect('/login');
|
||||
});
|
||||
|
||||
// Authentication middleware for all other routes
|
||||
app.use((req, res, next) => {
|
||||
// Skip auth for login-related routes
|
||||
if (req.path === '/login' || req.path === '/auth/login') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
if (req.session.authenticated) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Redirect to login page
|
||||
res.redirect('/login');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a password hash (utility function)
|
||||
* @param {string} password - Plain text password
|
||||
* @returns {Promise<string>} - Hashed password
|
||||
*/
|
||||
function generateHash(password) {
|
||||
return bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initAuth,
|
||||
generateHash
|
||||
};
|
||||
149
backup-s3.js
Normal file
149
backup-s3.js
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* S3 Backup Script for Weight Tracker
|
||||
*
|
||||
* This script creates automated backups of the Weight Tracker data
|
||||
* and uploads them to an S3-compatible storage (like Minio)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { S3Client, PutObjectCommand, ListObjectsV2Command, DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const cron = require('node-cron');
|
||||
|
||||
// Configuration (can be overridden by environment variables)
|
||||
const config = {
|
||||
// S3 Configuration
|
||||
s3: {
|
||||
endpoint: process.env.S3_ENDPOINT || 'https://your-minio-server.example.com',
|
||||
region: process.env.S3_REGION || 'us-east-1', // Default region, can be any value for Minio
|
||||
bucket: process.env.S3_BUCKET || 'weight-tracker-backups',
|
||||
accessKey: process.env.S3_ACCESS_KEY || 'your-access-key',
|
||||
secretKey: process.env.S3_SECRET_KEY || 'your-secret-key',
|
||||
useSSL: process.env.S3_USE_SSL !== 'false' // Default to true unless explicitly set to 'false'
|
||||
},
|
||||
|
||||
// Backup Configuration
|
||||
backup: {
|
||||
dataPath: process.env.DATA_PATH || '/data/weight-tracker-data.json',
|
||||
schedule: process.env.BACKUP_SCHEDULE || '0 0 * * *', // Default: daily at midnight
|
||||
retention: parseInt(process.env.BACKUP_RETENTION || '7') // Default: keep 7 backups
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize S3 client
|
||||
const s3Client = new S3Client({
|
||||
endpoint: config.s3.endpoint,
|
||||
region: config.s3.region,
|
||||
credentials: {
|
||||
accessKeyId: config.s3.accessKey,
|
||||
secretAccessKey: config.s3.secretKey
|
||||
},
|
||||
forcePathStyle: true, // Required for Minio
|
||||
tls: config.s3.useSSL
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a backup and upload to S3
|
||||
*/
|
||||
async function createBackup() {
|
||||
try {
|
||||
// Check if data file exists
|
||||
if (!fs.existsSync(config.backup.dataPath)) {
|
||||
console.error(`Data file not found: ${config.backup.dataPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read data file
|
||||
const data = fs.readFileSync(config.backup.dataPath, 'utf8');
|
||||
|
||||
// Generate backup filename with timestamp
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupFilename = `weight-tracker-backup-${timestamp}.json`;
|
||||
|
||||
// Upload to S3
|
||||
const uploadParams = {
|
||||
Bucket: config.s3.bucket,
|
||||
Key: backupFilename,
|
||||
Body: data,
|
||||
ContentType: 'application/json'
|
||||
};
|
||||
|
||||
const uploadCommand = new PutObjectCommand(uploadParams);
|
||||
await s3Client.send(uploadCommand);
|
||||
|
||||
console.log(`Backup created successfully: ${backupFilename}`);
|
||||
|
||||
// Cleanup old backups
|
||||
await cleanupOldBackups();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating backup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old backups based on retention policy
|
||||
*/
|
||||
async function cleanupOldBackups() {
|
||||
try {
|
||||
// List all backups
|
||||
const listParams = {
|
||||
Bucket: config.s3.bucket,
|
||||
Prefix: 'weight-tracker-backup-'
|
||||
};
|
||||
|
||||
const listCommand = new ListObjectsV2Command(listParams);
|
||||
const response = await s3Client.send(listCommand);
|
||||
|
||||
if (!response.Contents || response.Contents.length <= config.backup.retention) {
|
||||
return; // No cleanup needed
|
||||
}
|
||||
|
||||
// Sort by date (oldest first)
|
||||
const backups = response.Contents.sort((a, b) =>
|
||||
new Date(a.LastModified) - new Date(b.LastModified)
|
||||
);
|
||||
|
||||
// Delete oldest backups that exceed retention count
|
||||
const backupsToDelete = backups.slice(0, backups.length - config.backup.retention);
|
||||
|
||||
for (const backup of backupsToDelete) {
|
||||
const deleteParams = {
|
||||
Bucket: config.s3.bucket,
|
||||
Key: backup.Key
|
||||
};
|
||||
|
||||
const deleteCommand = new DeleteObjectCommand(deleteParams);
|
||||
await s3Client.send(deleteCommand);
|
||||
|
||||
console.log(`Deleted old backup: ${backup.Key}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up old backups:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the backup system
|
||||
*/
|
||||
function init() {
|
||||
console.log('Starting Weight Tracker S3 backup system');
|
||||
console.log(`Backup schedule: ${config.backup.schedule}`);
|
||||
console.log(`Backup retention: ${config.backup.retention} backups`);
|
||||
console.log(`S3 endpoint: ${config.s3.endpoint}`);
|
||||
console.log(`S3 bucket: ${config.s3.bucket}`);
|
||||
|
||||
// Schedule regular backups
|
||||
cron.schedule(config.backup.schedule, () => {
|
||||
console.log('Running scheduled backup...');
|
||||
createBackup();
|
||||
});
|
||||
|
||||
// Create initial backup
|
||||
console.log('Creating initial backup...');
|
||||
createBackup();
|
||||
}
|
||||
|
||||
// Start the backup system
|
||||
init();
|
||||
@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 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');
|
||||
@ -8,6 +9,7 @@ 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();
|
||||
@ -20,8 +22,12 @@ const DATA_FILE = path.join(DATA_DIR, 'weight-tracker-data.json');
|
||||
// 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
|
||||
|
||||
// Initialize authentication middleware
|
||||
initAuth(app);
|
||||
|
||||
// Ensure data directory exists
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
@ -13,8 +13,35 @@ services:
|
||||
- weight-tracker-data:/data
|
||||
networks:
|
||||
- weight-tracker-network
|
||||
environment:
|
||||
# Authentication Configuration
|
||||
- PASSWORD_HASH=${PASSWORD_HASH:-$2a$10$EgxHKjDDFcZKtQY9hl/N4.QvEQHCXVnQXw9dzFYlUDVKOcLMGp9eq}
|
||||
- SESSION_SECRET=${SESSION_SECRET:-change-this-to-a-random-string}
|
||||
- COOKIE_SECURE=${COOKIE_SECURE:-false}
|
||||
|
||||
# S3 Backup Configuration
|
||||
- S3_ENDPOINT=${S3_ENDPOINT:-https://your-minio-server.example.com}
|
||||
- S3_REGION=${S3_REGION:-us-east-1}
|
||||
- S3_BUCKET=${S3_BUCKET:-weight-tracker-backups}
|
||||
- S3_ACCESS_KEY=${S3_ACCESS_KEY:-your-access-key}
|
||||
- S3_SECRET_KEY=${S3_SECRET_KEY:-your-secret-key}
|
||||
- S3_USE_SSL=${S3_USE_SSL:-true}
|
||||
- BACKUP_SCHEDULE=${BACKUP_SCHEDULE:-0 0 * * *}
|
||||
- BACKUP_RETENTION=${BACKUP_RETENTION:-7}
|
||||
labels:
|
||||
- "coolify.volume.weight-tracker-data=/data"
|
||||
# Coolify environment variable labels for authentication
|
||||
- "coolify.env.PASSWORD_HASH=Bcrypt hash of your password"
|
||||
- "coolify.env.SESSION_SECRET=Secret for session encryption (random string)"
|
||||
- "coolify.env.COOKIE_SECURE=Set to true if using HTTPS (default: false)"
|
||||
# Coolify environment variable labels for S3 backup
|
||||
- "coolify.env.S3_ENDPOINT=S3 endpoint URL (e.g., https://minio.example.com)"
|
||||
- "coolify.env.S3_REGION=S3 region (e.g., us-east-1)"
|
||||
- "coolify.env.S3_BUCKET=S3 bucket name for backups"
|
||||
- "coolify.env.S3_ACCESS_KEY=S3 access key"
|
||||
- "coolify.env.S3_SECRET_KEY=S3 secret key"
|
||||
- "coolify.env.BACKUP_SCHEDULE=Cron schedule for backups (default: 0 0 * * *)"
|
||||
- "coolify.env.BACKUP_RETENTION=Number of backups to retain (default: 7)"
|
||||
|
||||
networks:
|
||||
weight-tracker-network:
|
||||
|
||||
44
generate-password-hash.js
Normal file
44
generate-password-hash.js
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 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();
|
||||
168
login.html
Normal file
168
login.html
Normal file
@ -0,0 +1,168 @@
|
||||
<!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>
|
||||
13
package.json
13
package.json
@ -5,12 +5,19 @@
|
||||
"main": "data-api.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node data-api.js"
|
||||
"start": "node data-api.js",
|
||||
"backup": "node backup-s3.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"@aws-sdk/client-s3": "^3.425.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"cors": "^2.8.5"
|
||||
"connect-redis": "^7.1.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"node-cron": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
||||
@ -24,3 +24,14 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:s3-backup]
|
||||
command=node /usr/share/nginx/api/backup-s3.js
|
||||
directory=/usr/share/nginx/api
|
||||
environment=DATA_PATH="/data/weight-tracker-data.json",S3_ENDPOINT="%(ENV_S3_ENDPOINT)s",S3_BUCKET="%(ENV_S3_BUCKET)s",S3_ACCESS_KEY="%(ENV_S3_ACCESS_KEY)s",S3_SECRET_KEY="%(ENV_S3_SECRET_KEY)s",BACKUP_SCHEDULE="%(ENV_BACKUP_SCHEDULE)s",BACKUP_RETENTION="%(ENV_BACKUP_RETENTION)s"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user