const express = require("express"); const router = express.Router(); const { query } = require("../db"); const multer = require("multer"); // Configure multer for file uploads const storage = multer.memoryStorage(); const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max fileFilter: (req, file, cb) => { if ( file.mimetype === "application/json" || file.originalname.endsWith(".json") ) { cb(null, true); } else { cb(new Error("Only JSON files are allowed"), false); } }, }); // ===================== // EXPORT DATA // ===================== // Export all songs as JSON router.get("/export/songs", async (req, res) => { try { const result = await query("SELECT * FROM songs ORDER BY title"); res.setHeader("Content-Type", "application/json"); res.setHeader( "Content-Disposition", "attachment; filename=songs-export.json", ); res.json({ success: true, exportedAt: new Date().toISOString(), count: result.rows.length, songs: result.rows, }); } catch (err) { console.error("Export songs error:", err); res.status(500).json({ success: false, message: "Failed to export songs" }); } }); // Export all profiles as JSON router.get("/export/profiles", async (req, res) => { try { const result = await query("SELECT * FROM profiles ORDER BY name"); res.setHeader("Content-Type", "application/json"); res.setHeader( "Content-Disposition", "attachment; filename=profiles-export.json", ); res.json({ success: true, exportedAt: new Date().toISOString(), count: result.rows.length, profiles: result.rows, }); } catch (err) { console.error("Export profiles error:", err); res .status(500) .json({ success: false, message: "Failed to export profiles" }); } }); // Export all worship lists as JSON router.get("/export/lists", async (req, res) => { try { const result = await query(` SELECT p.*, COALESCE(json_agg( json_build_object( 'song_id', ps.song_id, 'order_index', ps.order_index, 'song_title', s.title ) ORDER BY ps.order_index ) FILTER (WHERE ps.song_id IS NOT NULL), '[]') as songs FROM plans p LEFT JOIN plan_songs ps ON p.id = ps.plan_id LEFT JOIN songs s ON ps.song_id = s.id GROUP BY p.id ORDER BY p.date DESC `); res.setHeader("Content-Type", "application/json"); res.setHeader( "Content-Disposition", "attachment; filename=worship-lists-export.json", ); res.json({ success: true, exportedAt: new Date().toISOString(), count: result.rows.length, lists: result.rows, }); } catch (err) { console.error("Export lists error:", err); res .status(500) .json({ success: false, message: "Failed to export worship lists" }); } }); // Export everything (full database backup) router.get("/export/all", async (req, res) => { try { const [songs, profiles, lists, users] = await Promise.all([ query("SELECT * FROM songs ORDER BY title"), query("SELECT * FROM profiles ORDER BY name"), query(` SELECT p.*, COALESCE(json_agg( json_build_object( 'song_id', ps.song_id, 'order_index', ps.order_index ) ORDER BY ps.order_index ) FILTER (WHERE ps.song_id IS NOT NULL), '[]') as songs FROM plans p LEFT JOIN plan_songs ps ON p.id = ps.plan_id GROUP BY p.id ORDER BY p.date DESC `), query( "SELECT id, username, role, created_at FROM users ORDER BY username", ), ]); res.setHeader("Content-Type", "application/json"); res.setHeader( "Content-Disposition", "attachment; filename=full-backup.json", ); res.json({ success: true, exportedAt: new Date().toISOString(), data: { songs: { count: songs.rows.length, items: songs.rows }, profiles: { count: profiles.rows.length, items: profiles.rows }, worshipLists: { count: lists.rows.length, items: lists.rows }, users: { count: users.rows.length, items: users.rows }, }, }); } catch (err) { console.error("Full export error:", err); res .status(500) .json({ success: false, message: "Failed to export database" }); } }); // ===================== // IMPORT DATA // ===================== // Import songs from JSON router.post("/import/songs", upload.single("file"), async (req, res) => { try { if (!req.file) { return res .status(400) .json({ success: false, message: "No file uploaded" }); } const data = JSON.parse(req.file.buffer.toString()); const songs = data.songs || data; if (!Array.isArray(songs)) { return res .status(400) .json({ success: false, message: "Invalid format: expected array of songs", }); } let imported = 0; let skipped = 0; const errors = []; for (const song of songs) { try { // Check if song exists by title const existing = await query( "SELECT id FROM songs WHERE LOWER(title) = LOWER($1)", [song.title], ); if (existing.rows.length > 0) { skipped++; continue; } await query( `INSERT INTO songs (title, artist, lyrics, chords, tempo, time_signature, category, notes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [ song.title, song.artist || null, song.lyrics || "", song.chords || song.key_chord || null, song.tempo || null, song.time_signature || null, song.category || null, song.notes || null, ], ); imported++; } catch (err) { errors.push({ title: song.title, error: err.message }); } } res.json({ success: true, message: `Imported ${imported} songs, skipped ${skipped} duplicates`, imported, skipped, errors: errors.length > 0 ? errors : undefined, }); } catch (err) { console.error("Import songs error:", err); res .status(500) .json({ success: false, message: "Failed to import songs: " + err.message, }); } }); // ===================== // USER MANAGEMENT // ===================== // Get all users router.get("/users", async (req, res) => { try { const result = await query( "SELECT id, username, role, created_at FROM users ORDER BY username", ); // Add biometric_enabled as false since column may not exist const users = result.rows.map((user) => ({ ...user, biometric_enabled: false, })); res.json({ success: true, users }); } catch (err) { console.error("Get users error:", err); res.status(500).json({ success: false, message: "Failed to fetch users" }); } }); // Create new user router.post("/users", async (req, res) => { const { username, password, role = "user" } = req.body; if (!username || !password) { return res .status(400) .json({ success: false, message: "Username and password are required" }); } try { // Check if username exists const existing = await query( "SELECT id FROM users WHERE LOWER(username) = LOWER($1)", [username], ); if (existing.rows.length > 0) { return res .status(400) .json({ success: false, message: "Username already exists" }); } // Hash password (simple for now - should use bcrypt in production) const bcrypt = require("bcrypt"); const hashedPassword = await bcrypt.hash(password, 10); const result = await query( `INSERT INTO users (username, password, role) VALUES ($1, $2, $3) RETURNING id, username, role, created_at`, [username, hashedPassword, role], ); res.json({ success: true, user: result.rows[0] }); } catch (err) { console.error("Create user error:", err); res.status(500).json({ success: false, message: "Failed to create user" }); } }); // Update user router.put("/users/:id", async (req, res) => { const { id } = req.params; const { username, password, role } = req.body; try { const updates = []; const values = []; let paramCount = 1; if (username) { updates.push(`username = $${paramCount++}`); values.push(username); } if (password) { const bcrypt = require("bcrypt"); const hashedPassword = await bcrypt.hash(password, 10); updates.push(`password = $${paramCount++}`); values.push(hashedPassword); } if (role) { updates.push(`role = $${paramCount++}`); values.push(role); } if (updates.length === 0) { return res .status(400) .json({ success: false, message: "No updates provided" }); } values.push(id); const result = await query( `UPDATE users SET ${updates.join(", ")} WHERE id = $${paramCount} RETURNING id, username, role, created_at`, values, ); if (result.rows.length === 0) { return res .status(404) .json({ success: false, message: "User not found" }); } res.json({ success: true, user: result.rows[0] }); } catch (err) { console.error("Update user error:", err); res.status(500).json({ success: false, message: "Failed to update user" }); } }); // Delete user router.delete("/users/:id", async (req, res) => { const { id } = req.params; try { const result = await query( "DELETE FROM users WHERE id = $1 RETURNING id, username", [id], ); if (result.rows.length === 0) { return res .status(404) .json({ success: false, message: "User not found" }); } res.json({ success: true, message: "User deleted", user: result.rows[0] }); } catch (err) { console.error("Delete user error:", err); res.status(500).json({ success: false, message: "Failed to delete user" }); } }); // Enable biometric authentication for user router.post("/users/:id/biometric", async (req, res) => { const { id } = req.params; const { enable = true } = req.body; try { // Check if user exists first const userCheck = await query( "SELECT id, username FROM users WHERE id = $1", [id], ); if (userCheck.rows.length === 0) { return res .status(404) .json({ success: false, message: "User not found" }); } // Note: biometric_enabled column may not exist yet - this is a placeholder // In production, you would add the column to the database first res.json({ success: true, message: `Biometric authentication ${enable ? "enabled" : "disabled"} (feature pending database migration)`, user: { ...userCheck.rows[0], biometric_enabled: enable }, }); } catch (err) { console.error("Biometric update error:", err); res .status(500) .json({ success: false, message: "Failed to update biometric settings" }); } }); // ===================== // SYSTEM SETTINGS // ===================== // Get system settings router.get("/settings", async (req, res) => { try { const result = await query("SELECT * FROM settings ORDER BY key"); const settings = {}; result.rows.forEach((row) => { settings[row.key] = row.value; }); res.json({ success: true, settings }); } catch (err) { // If settings table doesn't exist, return defaults res.json({ success: true, settings: { church_name: "House of Prayer", default_tempo: "120", default_time_signature: "4/4", auto_transpose: "false", show_chord_diagrams: "true", }, }); } }); // Update system setting router.put("/settings/:key", async (req, res) => { const { key } = req.params; const { value } = req.body; try { // Try upsert await query( `INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`, [key, value], ); res.json({ success: true, message: "Setting updated" }); } catch (err) { console.error("Update setting error:", err); res .status(500) .json({ success: false, message: "Failed to update setting" }); } }); module.exports = router;