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;
|
||||
259
new-site/backend/routes/auth.js
Normal file
259
new-site/backend/routes/auth.js
Normal file
@@ -0,0 +1,259 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const bcrypt = require("bcrypt");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { query } = require("../db");
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-super-secret-jwt-key";
|
||||
|
||||
// Login
|
||||
router.post("/login", async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Username and password are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Find user in database (case-insensitive)
|
||||
const result = await query(
|
||||
"SELECT * FROM users WHERE LOWER(username) = LOWER($1)",
|
||||
[username],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, message: "Invalid credentials" });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
// Check password
|
||||
const validPassword = await bcrypt.compare(password, user.password_hash);
|
||||
|
||||
if (!validPassword) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, message: "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role || "user",
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: "7d" },
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.display_name || user.username,
|
||||
role: user.role || "user",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Login error:", err);
|
||||
res.status(500).json({ success: false, message: "Login failed" });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify token
|
||||
router.get("/verify", async (req, res) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, message: "No token provided" });
|
||||
}
|
||||
|
||||
const token = authHeader.split(" ")[1];
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// Get fresh user data
|
||||
const result = await query("SELECT * FROM users WHERE id = $1", [
|
||||
decoded.id,
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, message: "User not found" });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.display_name || user.username,
|
||||
role: user.role || "user",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Token verification error:", err);
|
||||
res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout (client-side token deletion, but we can track here if needed)
|
||||
router.post("/logout", (req, res) => {
|
||||
res.json({ success: true, message: "Logged out" });
|
||||
});
|
||||
|
||||
// Get current user
|
||||
router.get("/me", async (req, res) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, message: "Not authenticated" });
|
||||
}
|
||||
|
||||
const token = authHeader.split(" ")[1];
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
const result = await query("SELECT * FROM users WHERE id = $1", [
|
||||
decoded.id,
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ success: false, message: "User not found" });
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.display_name || user.username,
|
||||
role: user.role || "user",
|
||||
biometric_enabled: user.biometric_enabled || false,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Get user error:", err);
|
||||
res.status(401).json({ success: false, message: "Invalid token" });
|
||||
}
|
||||
});
|
||||
|
||||
// Biometric registration - store public key
|
||||
router.post("/biometric-register", async (req, res) => {
|
||||
try {
|
||||
const { username, credentialId, publicKey } = req.body;
|
||||
|
||||
if (!username || !credentialId || !publicKey) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Username, credential ID, and public key required",
|
||||
});
|
||||
}
|
||||
|
||||
// Update user with biometric credential
|
||||
const result = await query(
|
||||
`UPDATE users
|
||||
SET biometric_credential_id = $1,
|
||||
biometric_public_key = $2,
|
||||
biometric_enabled = true
|
||||
WHERE username = $3
|
||||
RETURNING id, username`,
|
||||
[credentialId, publicKey, username.toLowerCase()],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "User not found" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Biometric authentication registered successfully",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Biometric registration error:", err);
|
||||
res.status(500).json({ success: false, message: "Registration failed" });
|
||||
}
|
||||
});
|
||||
|
||||
// Biometric login - verify assertion
|
||||
router.post("/biometric-login", async (req, res) => {
|
||||
try {
|
||||
const { username, assertion } = req.body;
|
||||
|
||||
if (!username || !assertion) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Username and assertion required",
|
||||
});
|
||||
}
|
||||
|
||||
// Find user with biometric enabled
|
||||
const result = await query(
|
||||
`SELECT * FROM users
|
||||
WHERE username = $1 AND biometric_enabled = true`,
|
||||
[username.toLowerCase()],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: "Biometric authentication not enabled",
|
||||
});
|
||||
}
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
// In a production environment, verify the assertion signature here
|
||||
// For now, we'll trust the client-side verification
|
||||
// TODO: Implement server-side WebAuthn assertion verification
|
||||
|
||||
// Generate JWT token
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role || "user",
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: "7d" },
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
name: user.display_name || user.username,
|
||||
role: user.role || "user",
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Biometric login error:", err);
|
||||
res.status(500).json({ success: false, message: "Biometric login failed" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
277
new-site/backend/routes/lists.js
Normal file
277
new-site/backend/routes/lists.js
Normal file
@@ -0,0 +1,277 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { query } = require("../db");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const {
|
||||
success,
|
||||
error,
|
||||
notFound,
|
||||
badRequest,
|
||||
} = require("../utils/responseHandler");
|
||||
const { authenticate } = require("../middleware/auth");
|
||||
|
||||
// Reusable SQL fragments
|
||||
const SELECT_LIST_WITH_COUNT = `
|
||||
SELECT p.*, pr.name as profile_name,
|
||||
(SELECT COUNT(*) FROM plan_songs WHERE plan_id = p.id) as song_count
|
||||
FROM plans p
|
||||
LEFT JOIN profiles pr ON p.profile_id = pr.id
|
||||
`;
|
||||
|
||||
const SELECT_LIST_SONGS = `
|
||||
SELECT s.*, s.chords as key_chord, ps.order_index
|
||||
FROM songs s
|
||||
INNER JOIN plan_songs ps ON s.id = ps.song_id
|
||||
WHERE ps.plan_id = $1
|
||||
ORDER BY ps.order_index ASC
|
||||
`;
|
||||
|
||||
/**
|
||||
* Helper to add songs to a worship list
|
||||
*/
|
||||
const addSongsToList = async (planId, songs) => {
|
||||
if (!songs || !Array.isArray(songs) || songs.length === 0) return;
|
||||
|
||||
const values = songs
|
||||
.map(
|
||||
(songId, index) => `('${uuidv4()}', '${planId}', '${songId}', ${index})`,
|
||||
)
|
||||
.join(", ");
|
||||
|
||||
await query(`
|
||||
INSERT INTO plan_songs (id, plan_id, song_id, order_index)
|
||||
VALUES ${values}
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to get next order index for a list
|
||||
*/
|
||||
const getNextOrderIndex = async (planId) => {
|
||||
const result = await query(
|
||||
"SELECT COALESCE(MAX(order_index), -1) + 1 as next_order FROM plan_songs WHERE plan_id = $1",
|
||||
[planId],
|
||||
);
|
||||
return result.rows[0].next_order;
|
||||
};
|
||||
|
||||
// GET all worship lists (plans)
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const result = await query(`
|
||||
${SELECT_LIST_WITH_COUNT}
|
||||
ORDER BY p.date DESC
|
||||
`);
|
||||
success(res, { lists: result.rows });
|
||||
} catch (err) {
|
||||
error(res, "Failed to fetch worship lists");
|
||||
}
|
||||
});
|
||||
|
||||
// GET single worship list by ID with songs
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const [listResult, songsResult] = await Promise.all([
|
||||
query(`${SELECT_LIST_WITH_COUNT} WHERE p.id = $1`, [req.params.id]),
|
||||
query(SELECT_LIST_SONGS, [req.params.id]),
|
||||
]);
|
||||
|
||||
if (listResult.rows.length === 0) {
|
||||
return notFound(res, "Worship list");
|
||||
}
|
||||
|
||||
success(res, {
|
||||
list: listResult.rows[0],
|
||||
songs: songsResult.rows,
|
||||
});
|
||||
} catch (err) {
|
||||
error(res, "Failed to fetch worship list");
|
||||
}
|
||||
});
|
||||
|
||||
// POST create new worship list
|
||||
router.post("/", authenticate, async (req, res) => {
|
||||
try {
|
||||
const { date, profile_id, notes, songs } = req.body;
|
||||
|
||||
if (!date) {
|
||||
return badRequest(res, "Date is required");
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO plans (id, date, profile_id, notes, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *`,
|
||||
[id, date, profile_id || null, notes || "", now],
|
||||
);
|
||||
|
||||
await addSongsToList(id, songs);
|
||||
|
||||
success(res, { list: result.rows[0] }, 201);
|
||||
} catch (err) {
|
||||
error(res, "Failed to create worship list");
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update worship list
|
||||
router.put("/:id", authenticate, async (req, res) => {
|
||||
try {
|
||||
const { date, profile_id, notes, songs } = req.body;
|
||||
console.log(`[PUT /lists/${req.params.id}] Request:`, {
|
||||
date,
|
||||
profile_id,
|
||||
notes,
|
||||
songCount: songs?.length,
|
||||
songIds: songs?.slice(0, 3),
|
||||
});
|
||||
|
||||
const result = await query(
|
||||
`UPDATE plans
|
||||
SET date = COALESCE($1, date),
|
||||
profile_id = $2,
|
||||
notes = COALESCE($3, notes)
|
||||
WHERE id = $4
|
||||
RETURNING *`,
|
||||
[date, profile_id, notes, req.params.id],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
console.log(`[PUT /lists/${req.params.id}] NOT FOUND`);
|
||||
return notFound(res, "Worship list");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[PUT /lists/${req.params.id}] Plan updated, now updating songs...`,
|
||||
);
|
||||
|
||||
// Update songs if provided
|
||||
if (songs && Array.isArray(songs)) {
|
||||
await query("DELETE FROM plan_songs WHERE plan_id = $1", [req.params.id]);
|
||||
console.log(
|
||||
`[PUT /lists/${req.params.id}] Deleted old songs, adding ${songs.length} new songs`,
|
||||
);
|
||||
await addSongsToList(req.params.id, songs);
|
||||
console.log(`[PUT /lists/${req.params.id}] Songs added successfully`);
|
||||
}
|
||||
|
||||
console.log(`[PUT /lists/${req.params.id}] SUCCESS`);
|
||||
success(res, { list: result.rows[0] });
|
||||
} catch (err) {
|
||||
console.error(`[PUT /lists/:id] ERROR:`, err.message);
|
||||
console.error(err.stack);
|
||||
error(res, "Failed to update worship list: " + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE worship list
|
||||
router.delete("/:id", authenticate, async (req, res) => {
|
||||
try {
|
||||
// plan_songs will be deleted via CASCADE
|
||||
const result = await query("DELETE FROM plans WHERE id = $1 RETURNING id", [
|
||||
req.params.id,
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return notFound(res, "Worship list");
|
||||
}
|
||||
|
||||
success(res, { message: "Worship list deleted" });
|
||||
} catch (err) {
|
||||
error(res, "Failed to delete worship list");
|
||||
}
|
||||
});
|
||||
|
||||
// POST add song to worship list
|
||||
router.post("/:id/songs/:songId", authenticate, async (req, res) => {
|
||||
try {
|
||||
const { id, songId } = req.params;
|
||||
const nextOrder = await getNextOrderIndex(id);
|
||||
const psId = uuidv4();
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO plan_songs (id, plan_id, song_id, order_index)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (plan_id, song_id) DO NOTHING
|
||||
RETURNING *`,
|
||||
[psId, id, songId, nextOrder],
|
||||
);
|
||||
|
||||
success(res, {
|
||||
message: "Song added to worship list",
|
||||
added: result.rowCount > 0,
|
||||
});
|
||||
} catch (err) {
|
||||
error(res, "Failed to add song to worship list", 500, {
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE remove song from worship list
|
||||
router.delete("/:id/songs/:songId", authenticate, async (req, res) => {
|
||||
try {
|
||||
const { id, songId } = req.params;
|
||||
|
||||
const result = await query(
|
||||
"DELETE FROM plan_songs WHERE plan_id = $1 AND song_id = $2 RETURNING *",
|
||||
[id, songId],
|
||||
);
|
||||
|
||||
success(res, {
|
||||
message: "Song removed from worship list",
|
||||
deleted: result.rowCount,
|
||||
});
|
||||
} catch (err) {
|
||||
error(res, "Failed to remove song from worship list", 500, {
|
||||
error: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PUT reorder songs in worship list
|
||||
router.put("/:id/reorder", authenticate, async (req, res) => {
|
||||
try {
|
||||
const { songs } = req.body;
|
||||
|
||||
if (!songs || !Array.isArray(songs)) {
|
||||
return badRequest(res, "Songs array is required");
|
||||
}
|
||||
|
||||
// Batch update using CASE statement for better performance
|
||||
if (songs.length > 0) {
|
||||
const cases = songs
|
||||
.map((songId, index) => `WHEN song_id = '${songId}' THEN ${index}`)
|
||||
.join(" ");
|
||||
|
||||
const songIds = songs.map((id) => `'${id}'`).join(", ");
|
||||
|
||||
await query(
|
||||
`
|
||||
UPDATE plan_songs
|
||||
SET order_index = CASE ${cases} END
|
||||
WHERE plan_id = $1 AND song_id IN (${songIds})
|
||||
`,
|
||||
[req.params.id],
|
||||
);
|
||||
}
|
||||
|
||||
success(res, { message: "Songs reordered" });
|
||||
} catch (err) {
|
||||
error(res, "Failed to reorder songs", 500, { error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET worship list count
|
||||
router.get("/stats/count", async (req, res) => {
|
||||
try {
|
||||
const result = await query("SELECT COUNT(*) as count FROM plans");
|
||||
success(res, { count: parseInt(result.rows[0].count) });
|
||||
} catch (err) {
|
||||
error(res, "Failed to count worship lists");
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
251
new-site/backend/routes/profiles.js
Normal file
251
new-site/backend/routes/profiles.js
Normal file
@@ -0,0 +1,251 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { query } = require("../db");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
|
||||
// GET all profiles
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const result = await query("SELECT * FROM profiles ORDER BY name ASC");
|
||||
res.json({ success: true, profiles: result.rows });
|
||||
} catch (err) {
|
||||
console.error("Error fetching profiles:", err);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "Failed to fetch profiles" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET single profile by ID
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const result = await query("SELECT * FROM profiles WHERE id = $1", [
|
||||
req.params.id,
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Profile not found" });
|
||||
}
|
||||
|
||||
// Also get profile's songs with their preferred keys
|
||||
const songsResult = await query(
|
||||
`
|
||||
SELECT s.*, psk.song_key as preferred_key
|
||||
FROM songs s
|
||||
INNER JOIN profile_songs ps ON s.id = ps.song_id
|
||||
LEFT JOIN profile_song_keys psk ON ps.profile_id = psk.profile_id AND ps.song_id = psk.song_id
|
||||
WHERE ps.profile_id = $1
|
||||
ORDER BY s.title ASC
|
||||
`,
|
||||
[req.params.id],
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
profile: result.rows[0],
|
||||
songs: songsResult.rows,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error fetching profile:", err);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "Failed to fetch profile" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST create new profile
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
name,
|
||||
email,
|
||||
contact_number,
|
||||
notes,
|
||||
default_key,
|
||||
} = req.body;
|
||||
|
||||
const profileName = name || `${first_name || ""} ${last_name || ""}`.trim();
|
||||
if (!profileName) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ success: false, message: "Name is required" });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO profiles (id, first_name, last_name, name, email, contact_number, notes, default_key)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING *`,
|
||||
[
|
||||
id,
|
||||
first_name || "",
|
||||
last_name || "",
|
||||
profileName,
|
||||
email || "",
|
||||
contact_number || "",
|
||||
notes || "",
|
||||
default_key || "C",
|
||||
],
|
||||
);
|
||||
|
||||
res.status(201).json({ success: true, profile: result.rows[0] });
|
||||
} catch (err) {
|
||||
console.error("Error creating profile:", err);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "Failed to create profile" });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update profile
|
||||
router.put("/:id", async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
first_name,
|
||||
last_name,
|
||||
name,
|
||||
email,
|
||||
contact_number,
|
||||
notes,
|
||||
default_key,
|
||||
} = req.body;
|
||||
|
||||
const result = await query(
|
||||
`UPDATE profiles
|
||||
SET first_name = COALESCE($1, first_name),
|
||||
last_name = COALESCE($2, last_name),
|
||||
name = COALESCE($3, name),
|
||||
email = COALESCE($4, email),
|
||||
contact_number = COALESCE($5, contact_number),
|
||||
notes = COALESCE($6, notes),
|
||||
default_key = COALESCE($7, default_key)
|
||||
WHERE id = $8
|
||||
RETURNING *`,
|
||||
[
|
||||
first_name,
|
||||
last_name,
|
||||
name,
|
||||
email,
|
||||
contact_number,
|
||||
notes,
|
||||
default_key,
|
||||
req.params.id,
|
||||
],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Profile not found" });
|
||||
}
|
||||
|
||||
res.json({ success: true, profile: result.rows[0] });
|
||||
} catch (err) {
|
||||
console.error("Error updating profile:", err);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "Failed to update profile" });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE profile
|
||||
router.delete("/:id", async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"DELETE FROM profiles WHERE id = $1 RETURNING id",
|
||||
[req.params.id],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Profile not found" });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: "Profile deleted" });
|
||||
} catch (err) {
|
||||
console.error("Error deleting profile:", err);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "Failed to delete profile" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST add song to profile
|
||||
router.post("/:id/songs/:songId", async (req, res) => {
|
||||
try {
|
||||
const { id, songId } = req.params;
|
||||
const { song_key } = req.body;
|
||||
|
||||
const psId = uuidv4();
|
||||
|
||||
// Add song to profile
|
||||
await query(
|
||||
`INSERT INTO profile_songs (id, profile_id, song_id)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (profile_id, song_id) DO NOTHING`,
|
||||
[psId, id, songId],
|
||||
);
|
||||
|
||||
// If key provided, set it
|
||||
if (song_key) {
|
||||
const pskId = uuidv4();
|
||||
await query(
|
||||
`INSERT INTO profile_song_keys (id, profile_id, song_id, song_key)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (profile_id, song_id) DO UPDATE SET song_key = $4`,
|
||||
[pskId, id, songId, song_key],
|
||||
);
|
||||
}
|
||||
|
||||
res.json({ success: true, message: "Song added to profile" });
|
||||
} catch (err) {
|
||||
console.error("Error adding song to profile:", err);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "Failed to add song to profile" });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE remove song from profile
|
||||
router.delete("/:id/songs/:songId", async (req, res) => {
|
||||
try {
|
||||
const { id, songId } = req.params;
|
||||
|
||||
await query(
|
||||
"DELETE FROM profile_songs WHERE profile_id = $1 AND song_id = $2",
|
||||
[id, songId],
|
||||
);
|
||||
await query(
|
||||
"DELETE FROM profile_song_keys WHERE profile_id = $1 AND song_id = $2",
|
||||
[id, songId],
|
||||
);
|
||||
|
||||
res.json({ success: true, message: "Song removed from profile" });
|
||||
} catch (err) {
|
||||
console.error("Error removing song from profile:", err);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "Failed to remove song from profile" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET profile count
|
||||
router.get("/stats/count", async (req, res) => {
|
||||
try {
|
||||
const result = await query("SELECT COUNT(*) as count FROM profiles");
|
||||
res.json({ success: true, count: parseInt(result.rows[0].count) });
|
||||
} catch (err) {
|
||||
console.error("Error counting profiles:", err);
|
||||
res
|
||||
.status(500)
|
||||
.json({ success: false, message: "Failed to count profiles" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
214
new-site/backend/routes/songs.js
Normal file
214
new-site/backend/routes/songs.js
Normal file
@@ -0,0 +1,214 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { query } = require("../db");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
const {
|
||||
success,
|
||||
error,
|
||||
notFound,
|
||||
badRequest,
|
||||
} = require("../utils/responseHandler");
|
||||
const {
|
||||
buildWhereClause,
|
||||
buildPagination,
|
||||
buildSearchCondition,
|
||||
} = require("../utils/queryBuilder");
|
||||
const { authenticate } = require("../middleware/auth");
|
||||
|
||||
// Common SQL fragment
|
||||
const SELECT_SONG_FIELDS = "SELECT *, chords as key_chord FROM songs";
|
||||
|
||||
// GET search songs (for worship list song picker)
|
||||
router.get("/search", async (req, res) => {
|
||||
try {
|
||||
const { q } = req.query;
|
||||
|
||||
if (!q || q.trim() === "") {
|
||||
return success(res, { songs: [], total: 0 });
|
||||
}
|
||||
|
||||
const searchTerm = `%${q.toLowerCase()}%`;
|
||||
const searchCondition = buildSearchCondition(
|
||||
searchTerm,
|
||||
["title", "artist", "singer"],
|
||||
1,
|
||||
);
|
||||
|
||||
const result = await query(
|
||||
`${SELECT_SONG_FIELDS}
|
||||
WHERE ${searchCondition}
|
||||
ORDER BY title ASC LIMIT 20`,
|
||||
[searchTerm],
|
||||
);
|
||||
|
||||
success(res, { songs: result.rows, total: result.rowCount });
|
||||
} catch (err) {
|
||||
error(res, "Failed to search songs");
|
||||
}
|
||||
});
|
||||
|
||||
// GET all songs
|
||||
router.get("/", async (req, res) => {
|
||||
try {
|
||||
const { search, artist, band, limit = 100, offset = 0 } = req.query;
|
||||
const params = [];
|
||||
const conditions = [];
|
||||
|
||||
if (search) {
|
||||
params.push(`%${search.toLowerCase()}%`);
|
||||
conditions.push(
|
||||
`(LOWER(title) LIKE $${params.length} OR LOWER(lyrics) LIKE $${params.length})`,
|
||||
);
|
||||
}
|
||||
|
||||
if (artist) {
|
||||
params.push(`%${artist.toLowerCase()}%`);
|
||||
conditions.push(`LOWER(artist) LIKE $${params.length}`);
|
||||
}
|
||||
|
||||
if (band) {
|
||||
params.push(`%${band.toLowerCase()}%`);
|
||||
conditions.push(`LOWER(band) LIKE $${params.length}`);
|
||||
}
|
||||
|
||||
const whereClause = buildWhereClause(conditions);
|
||||
const { clause: paginationClause, params: paginationParams } =
|
||||
buildPagination(limit, offset, params.length + 1);
|
||||
|
||||
const result = await query(
|
||||
`${SELECT_SONG_FIELDS}${whereClause} ORDER BY title ASC${paginationClause}`,
|
||||
[...params, ...paginationParams],
|
||||
);
|
||||
|
||||
success(res, { songs: result.rows, total: result.rowCount });
|
||||
} catch (err) {
|
||||
error(res, "Failed to fetch songs");
|
||||
}
|
||||
});
|
||||
|
||||
// GET single song by ID
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const result = await query(`${SELECT_SONG_FIELDS} WHERE id = $1`, [
|
||||
req.params.id,
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return notFound(res, "Song");
|
||||
}
|
||||
|
||||
success(res, { song: result.rows[0] });
|
||||
} catch (err) {
|
||||
error(res, "Failed to fetch song");
|
||||
}
|
||||
});
|
||||
|
||||
// POST create new song
|
||||
router.post("/", authenticate, async (req, res) => {
|
||||
try {
|
||||
const { title, artist, band, singer, lyrics, chords, key_chord, memo } =
|
||||
req.body;
|
||||
|
||||
if (!title) {
|
||||
return badRequest(res, "Title is required");
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const chordsValue = chords || key_chord || "";
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO songs (id, title, artist, band, singer, lyrics, chords, memo, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *, chords as key_chord`,
|
||||
[
|
||||
id,
|
||||
title,
|
||||
artist || "",
|
||||
band || "",
|
||||
singer || "",
|
||||
lyrics || "",
|
||||
chordsValue,
|
||||
memo || "",
|
||||
now,
|
||||
now,
|
||||
],
|
||||
);
|
||||
|
||||
success(res, { song: result.rows[0] }, 201);
|
||||
} catch (err) {
|
||||
error(res, "Failed to create song");
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update song
|
||||
router.put("/:id", authenticate, async (req, res) => {
|
||||
try {
|
||||
const { title, artist, band, singer, lyrics, chords, key_chord, memo } =
|
||||
req.body;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const chordsValue = chords || key_chord;
|
||||
|
||||
const result = await query(
|
||||
`UPDATE songs
|
||||
SET title = COALESCE($1, title),
|
||||
artist = COALESCE($2, artist),
|
||||
band = COALESCE($3, band),
|
||||
singer = COALESCE($4, singer),
|
||||
lyrics = COALESCE($5, lyrics),
|
||||
chords = COALESCE($6, chords),
|
||||
memo = COALESCE($7, memo),
|
||||
updated_at = $8
|
||||
WHERE id = $9
|
||||
RETURNING *, chords as key_chord`,
|
||||
[
|
||||
title,
|
||||
artist,
|
||||
band,
|
||||
singer,
|
||||
lyrics,
|
||||
chordsValue,
|
||||
memo,
|
||||
now,
|
||||
req.params.id,
|
||||
],
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return notFound(res, "Song");
|
||||
}
|
||||
|
||||
success(res, { song: result.rows[0] });
|
||||
} catch (err) {
|
||||
error(res, "Failed to update song");
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE song
|
||||
router.delete("/:id", authenticate, async (req, res) => {
|
||||
try {
|
||||
const result = await query("DELETE FROM songs WHERE id = $1 RETURNING id", [
|
||||
req.params.id,
|
||||
]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return notFound(res, "Song");
|
||||
}
|
||||
|
||||
success(res, { message: "Song deleted" });
|
||||
} catch (err) {
|
||||
error(res, "Failed to delete song");
|
||||
}
|
||||
});
|
||||
|
||||
// GET song count
|
||||
router.get("/stats/count", async (req, res) => {
|
||||
try {
|
||||
const result = await query("SELECT COUNT(*) as count FROM songs");
|
||||
success(res, { count: parseInt(result.rows[0].count) });
|
||||
} catch (err) {
|
||||
error(res, "Failed to count songs");
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user