Files
Church-Music/new-site/backend/routes/admin.js

454 lines
12 KiB
JavaScript

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;