454 lines
12 KiB
JavaScript
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;
|