freesched/server.js

419 lines
13 KiB
JavaScript
Raw Permalink Normal View History

2025-02-21 20:20:12 +00:00
const express = require('express');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const app = express();
const port = 3000;
// Serve static files from the 'public' directory
app.use(express.static(path.join(__dirname, 'public')));
// Serve HTML files from the 'views' directory
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'index.html'));
});
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'admin.html'));
});
// Middleware to parse JSON bodies
app.use(express.json());
// Helper function to create database tables
const createDatabaseTables = (callback) => {
db.serialize(() => {
// Enable foreign key support
db.run('PRAGMA foreign_keys = ON', (err) => {
if (err) {
callback(new Error(`Failed to enable foreign keys: ${err.message}`));
return;
}
// Create uids table
db.run(`CREATE TABLE IF NOT EXISTS uids (
uid TEXT PRIMARY KEY,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err) {
callback(new Error(`Failed to create uids table: ${err.message}`));
return;
}
// Create availability table
db.run(`CREATE TABLE IF NOT EXISTS availability (
uid TEXT,
date TEXT,
times TEXT,
PRIMARY KEY (uid, date),
FOREIGN KEY (uid) REFERENCES uids(uid) ON DELETE CASCADE
)`, (err) => {
if (err) {
callback(new Error(`Failed to create availability table: ${err.message}`));
return;
}
callback(null);
});
});
});
});
};
// Helper function to initialize database
const initializeDatabase = () => {
// Ensure db directory exists
const fs = require('fs');
const dbDir = 'db';
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir);
}
// Connect to SQLite database
db = new sqlite3.Database('db/freesched.db', (err) => {
if (err) {
console.error('Could not connect to database:', err.message);
return;
}
console.log('Connected to SQLite database');
// Create tables
createDatabaseTables((err) => {
if (err) {
console.error('Error creating database tables:', err.message);
return;
}
console.log('Database tables created successfully');
});
});
};
// Initialize database
let db;
initializeDatabase();
// Admin view - must be defined before the catch-all UID route
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'admin.html'));
});
// Public view - handle both root and UID-based routes
app.get(['/', '/:uid'], (req, res) => {
// Only validate UID if it's provided (not root path)
if (req.params.uid) {
// Validate UID format
if (!/^[a-z0-9-]+$/.test(req.params.uid)) {
res.sendFile(path.join(__dirname, 'views', 'index.html'));
return;
}
}
res.sendFile(path.join(__dirname, 'views', 'index.html'));
});
// Helper function to convert time string to minutes for sorting
function timeToMinutes(timeStr) {
const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i);
if (!match) return 0;
let [_, hours, minutes, period] = match;
hours = parseInt(hours);
minutes = minutes ? parseInt(minutes) : 0;
period = period.toLowerCase();
if (period === 'pm' && hours !== 12) hours += 12;
if (period === 'am' && hours === 12) hours = 0;
return hours * 60 + minutes;
}
// API endpoint to get available dates and times for a specific UID
app.get('/api/availability/:uid', (req, res) => {
const { uid } = req.params;
// Verify UID exists
db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (!row) {
res.status(404).json({ error: 'UID not found' });
return;
}
// Get availability for this UID
db.all('SELECT date, times FROM availability WHERE uid = ?', [uid], (err, rows) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const availability = {};
rows.forEach(row => {
// Handle case where times might be null or undefined
if (row.times) {
// Split, trim, and sort times chronologically
const times = row.times.split(',').map(t => t.trim());
times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
availability[row.date] = times;
} else {
availability[row.date] = [];
}
});
res.json(availability);
});
});
});
// API endpoint to add/update availability for a specific UID
app.post('/api/availability/:uid', (req, res) => {
const { uid } = req.params;
const { date, times } = req.body;
if (!date) {
res.status(400).json({ error: 'Date is required' });
return;
}
// Verify UID exists
db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (!row) {
res.status(404).json({ error: 'UID not found' });
return;
}
if (times.length === 0) {
// Delete the entry if no times remain
db.run('DELETE FROM availability WHERE uid = ? AND date = ?', [uid, date], (err) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json({ message: 'Availability removed successfully' });
});
} else {
// Sort times before storing
const sortedTimes = [...times].sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
// Insert or replace with the sorted times
db.run('INSERT OR REPLACE INTO availability (uid, date, times) VALUES (?, ?, ?)',
[uid, date, sortedTimes.join(',')],
(err) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json({ message: 'Availability updated successfully' });
}
);
}
});
});
// API endpoint to get all UIDs
app.get('/api/uids', (req, res) => {
createDatabaseTables((err) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
// Get all UIDs
db.all('SELECT uid, created_at FROM uids ORDER BY created_at DESC', (err, rows) => {
if (err) {
res.status(500).json({ error: `Failed to fetch UIDs: ${err.message}` });
return;
}
res.json(rows || []);
});
});
});
// API endpoint to create a new UID
app.post('/api/uids', (req, res) => {
const { uid } = req.body;
// Validate UID format (lowercase letters, numbers, and hyphens only)
if (!uid || !/^[a-z0-9-]+$/.test(uid)) {
res.status(400).json({ error: 'Invalid UID format. Use only lowercase letters, numbers, and hyphens.' });
return;
}
createDatabaseTables((err) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
db.run('INSERT INTO uids (uid) VALUES (?)', [uid], function(err) {
if (err) {
if (err.code === 'SQLITE_CONSTRAINT') {
res.status(409).json({ error: 'UID already exists' });
} else {
res.status(500).json({ error: `Failed to insert UID: ${err.message}` });
}
return;
}
res.status(201).json({ uid, created_at: new Date().toISOString() });
});
});
});
// API endpoint to delete a UID and its availability
app.delete('/api/uids/:uid', (req, res) => {
const { uid } = req.params;
// Begin transaction
db.serialize(() => {
db.run('BEGIN TRANSACTION');
// Delete from availability first due to foreign key constraint
db.run('DELETE FROM availability WHERE uid = ?', [uid], (err) => {
if (err) {
db.run('ROLLBACK');
res.status(500).json({ error: err.message });
return;
}
// Then delete from uids
db.run('DELETE FROM uids WHERE uid = ?', [uid], function(err) {
if (err) {
db.run('ROLLBACK');
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
db.run('ROLLBACK');
res.status(404).json({ error: 'UID not found' });
return;
}
// Commit transaction
db.run('COMMIT', (err) => {
if (err) {
db.run('ROLLBACK');
res.status(500).json({ error: err.message });
return;
}
res.json({ message: 'UID deleted successfully' });
});
});
});
});
});
// API endpoint to flush (delete) all availability entries for a specific UID
app.delete('/api/availability/:uid/flush', (req, res) => {
const { uid } = req.params;
// Verify UID exists
db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (!row) {
res.status(404).json({ error: 'UID not found' });
return;
}
// Delete all availability for this UID
db.run('DELETE FROM availability WHERE uid = ?', [uid], (err) => {
if (err) {
console.error('Error deleting availability:', err);
res.status(500).json({ error: err.message });
return;
}
console.log(`All availability entries for UID ${uid} deleted successfully`);
res.json({ message: `All availability entries for UID ${uid} deleted successfully` });
});
});
});
// API endpoint to reset the entire database
app.post('/api/reset', async (req, res) => {
const fs = require('fs').promises;
const path = require('path');
const dbDir = 'db';
const dbPath = path.join(dbDir, 'freesched.db');
try {
// Close current database connection
await new Promise((resolve, reject) => {
if (!db) {
resolve();
return;
}
db.close((err) => {
if (err) reject(err);
else resolve();
});
});
// Ensure db directory exists
try {
await fs.access(dbDir);
} catch {
await fs.mkdir(dbDir);
}
// Delete existing database if it exists
try {
await fs.unlink(dbPath);
} catch (err) {
if (err.code !== 'ENOENT') throw err; // Ignore if file doesn't exist
}
// Initialize fresh database
initializeDatabase();
// Wait for database to be ready
await new Promise((resolve, reject) => {
const checkDb = () => {
if (!db) {
setTimeout(checkDb, 100); // Check every 100ms
return;
}
// Test database connection
db.get('SELECT 1', (err) => {
if (err) {
setTimeout(checkDb, 100);
} else {
resolve();
}
});
};
checkDb();
// Set a timeout of 5 seconds
setTimeout(() => {
reject(new Error('Database initialization timeout'));
}, 5000);
});
res.json({ message: 'Database completely wiped and recreated successfully' });
} catch (error) {
console.error('Error in flush-global:', error);
// Try to reinitialize database
try {
initializeDatabase();
} catch (reinitError) {
console.error('Failed to reinitialize database:', reinitError);
}
// Ensure we haven't already sent a response
if (!res.headersSent) {
res.status(500).json({ error: error.message });
}
}
});
app.listen(port, () => {
console.log(`FreeSched app listening at http://localhost:${port}`);
});