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 the static website files to the Nginx serving directory
|
||||||
COPY --from=web-builder /app /usr/share/nginx/html
|
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 --from=base /app/node_modules /usr/share/nginx/api/node_modules
|
||||||
COPY data-api.js /usr/share/nginx/api/
|
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 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
|
||||||
|
|||||||
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 Visualization**: View your weight progress on an interactive chart
|
||||||
- **Data Export/Import**: Export and import your data as JSON or CSV
|
- **Data Export/Import**: Export and import your data as JSON or CSV
|
||||||
- **Privacy-First**: All data stays on your device using browser's localStorage
|
- **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
|
- **Mobile-Friendly**: Responsive design works on all devices
|
||||||
|
|
||||||
## Getting Started
|
## 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.
|
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
|
## Roadmap
|
||||||
|
|
||||||
### Phase 1 (Current)
|
### 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
|
* Simple data API for Weight Tracker
|
||||||
* This script handles data storage operations when deployed in Docker
|
* This script handles data storage operations when deployed in Docker
|
||||||
|
* Includes authentication for password protection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@ -8,6 +9,7 @@ const path = require('path');
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
|
const { initAuth, generateHash } = require('./auth-middleware');
|
||||||
|
|
||||||
// Create Express app
|
// Create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -20,8 +22,12 @@ const DATA_FILE = path.join(DATA_DIR, 'weight-tracker-data.json');
|
|||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(bodyParser.json({ limit: '5mb' }));
|
app.use(bodyParser.json({ limit: '5mb' }));
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true })); // For parsing form data
|
||||||
app.use(express.static('public')); // Serve static files
|
app.use(express.static('public')); // Serve static files
|
||||||
|
|
||||||
|
// Initialize authentication middleware
|
||||||
|
initAuth(app);
|
||||||
|
|
||||||
// Ensure data directory exists
|
// Ensure data directory exists
|
||||||
if (!fs.existsSync(DATA_DIR)) {
|
if (!fs.existsSync(DATA_DIR)) {
|
||||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
|||||||
@ -13,8 +13,35 @@ services:
|
|||||||
- weight-tracker-data:/data
|
- weight-tracker-data:/data
|
||||||
networks:
|
networks:
|
||||||
- weight-tracker-network
|
- 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:
|
labels:
|
||||||
- "coolify.volume.weight-tracker-data=/data"
|
- "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:
|
networks:
|
||||||
weight-tracker-network:
|
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",
|
"main": "data-api.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node data-api.js"
|
"start": "node data-api.js",
|
||||||
|
"backup": "node backup-s3.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"@aws-sdk/client-s3": "^3.425.0",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
"body-parser": "^1.20.2",
|
"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": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
|
|||||||
@ -24,3 +24,14 @@ stdout_logfile=/dev/stdout
|
|||||||
stdout_logfile_maxbytes=0
|
stdout_logfile_maxbytes=0
|
||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
stderr_logfile_maxbytes=0
|
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