Initial commit - Church Music Database
This commit is contained in:
453
new-site/backend/routes/admin.js
Normal file
453
new-site/backend/routes/admin.js
Normal file
@@ -0,0 +1,453 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user