Initial commit - Church Music Database

This commit is contained in:
2026-01-27 18:04:50 -06:00
commit d367261867
336 changed files with 103545 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
import express from "express";
import { isAdmin } from "../middleware/auth.js";
const router = express.Router();
// Apply admin check to all routes
router.use(isAdmin);
// In-memory stores (replace with database in production)
const activityLogs = [
{
id: "1",
action: "User login",
user: "Pastor John",
timestamp: new Date().toISOString(),
ip: "192.168.1.100",
status: "success",
},
{
id: "2",
action: "Song created",
user: "Sarah Miller",
timestamp: new Date().toISOString(),
details: "New song: Blessed Be Your Name",
status: "success",
},
{
id: "3",
action: "Failed login attempt",
user: "unknown@test.com",
timestamp: new Date().toISOString(),
ip: "203.45.67.89",
status: "failed",
},
];
const users = new Map([
[
"1",
{
id: "1",
name: "Pastor John",
email: "john@church.org",
role: "admin",
status: "active",
lastLogin: new Date().toISOString(),
},
],
[
"2",
{
id: "2",
name: "Sarah Miller",
email: "sarah@church.org",
role: "leader",
status: "active",
lastLogin: new Date().toISOString(),
},
],
]);
const devices = new Map([
[
"1",
{
id: "1",
name: "iPad Pro - Worship",
userId: "2",
type: "tablet",
lastActive: new Date().toISOString(),
status: "online",
},
],
[
"2",
{
id: "2",
name: "MacBook Pro",
userId: "1",
type: "desktop",
lastActive: new Date().toISOString(),
status: "online",
},
],
]);
// Dashboard stats
router.get("/stats", (req, res) => {
res.json({
totalUsers: users.size,
activeDevices: Array.from(devices.values()).filter(
(d) => d.status === "online",
).length,
actionsToday: activityLogs.filter((log) => {
const logDate = new Date(log.timestamp).toDateString();
return logDate === new Date().toDateString();
}).length,
securityAlerts: activityLogs.filter((log) => log.status === "failed")
.length,
});
});
// Get all users
router.get("/users", (req, res) => {
const result = Array.from(users.values());
res.json({ users: result, total: result.length });
});
// Get single user
router.get("/users/:id", (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json({ user });
});
// Create user
router.post("/users", (req, res) => {
const { name, email, role } = req.body;
if (!name || !email) {
return res.status(400).json({ error: "Name and email are required" });
}
const user = {
id: Date.now().toString(),
name,
email,
role: role || "volunteer",
status: "active",
lastLogin: null,
createdAt: new Date().toISOString(),
};
users.set(user.id, user);
// Log activity
activityLogs.push({
id: Date.now().toString(),
action: "User created",
user: req.user.name,
timestamp: new Date().toISOString(),
details: `Created user: ${name}`,
status: "success",
});
res.status(201).json({ message: "User created", user });
});
// Update user
router.put("/users/:id", (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const { name, email, role, status } = req.body;
const updatedUser = {
...user,
name: name || user.name,
email: email || user.email,
role: role || user.role,
status: status || user.status,
};
users.set(user.id, updatedUser);
res.json({ message: "User updated", user: updatedUser });
});
// Delete user
router.delete("/users/:id", (req, res) => {
const user = users.get(req.params.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
if (user.role === "admin") {
return res.status(403).json({ error: "Cannot delete admin user" });
}
users.delete(req.params.id);
res.json({ message: "User deleted" });
});
// Get devices
router.get("/devices", (req, res) => {
const result = Array.from(devices.values()).map((device) => ({
...device,
user: users.get(device.userId)?.name || "Unknown",
}));
res.json({ devices: result, total: result.length });
});
// Revoke device
router.delete("/devices/:id", (req, res) => {
const device = devices.get(req.params.id);
if (!device) {
return res.status(404).json({ error: "Device not found" });
}
devices.delete(req.params.id);
res.json({ message: "Device revoked" });
});
// Get activity logs
router.get("/logs", (req, res) => {
const { action, status, limit = 50 } = req.query;
let result = [...activityLogs];
if (action) {
result = result.filter((log) =>
log.action.toLowerCase().includes(action.toLowerCase()),
);
}
if (status) {
result = result.filter((log) => log.status === status);
}
// Sort by timestamp (newest first)
result.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
// Limit results
result = result.slice(0, parseInt(limit));
res.json({ logs: result, total: result.length });
});
// Get roles
router.get("/roles", (req, res) => {
const roles = [
{
id: "admin",
name: "Administrator",
description: "Full access to all features",
permissions: ["all"],
userCount: Array.from(users.values()).filter((u) => u.role === "admin")
.length,
},
{
id: "leader",
name: "Worship Leader",
description: "Manage songs and worship lists",
permissions: [
"songs.read",
"songs.write",
"lists.read",
"lists.write",
"profiles.read",
],
userCount: Array.from(users.values()).filter((u) => u.role === "leader")
.length,
},
{
id: "tech",
name: "Tech Team",
description: "View and present songs",
permissions: ["songs.read", "lists.read"],
userCount: Array.from(users.values()).filter((u) => u.role === "tech")
.length,
},
{
id: "volunteer",
name: "Volunteer",
description: "Basic access",
permissions: ["songs.read"],
userCount: Array.from(users.values()).filter(
(u) => u.role === "volunteer",
).length,
},
];
res.json({ roles });
});
// Security settings
router.get("/security", (req, res) => {
res.json({
twoFactorRequired: false,
sessionTimeout: 3600, // 1 hour
passwordMinLength: 8,
passwordRequireNumber: true,
ipWhitelist: [],
failedLoginAlerts: 3,
});
});
router.put("/security", (req, res) => {
// Update security settings
const {
twoFactorRequired,
sessionTimeout,
passwordMinLength,
passwordRequireNumber,
ipWhitelist,
} = req.body;
// In a real app, save to database
res.json({ message: "Security settings updated" });
});
export default router;

View File

@@ -0,0 +1,380 @@
import express from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { v4 as uuidv4 } from "uuid";
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from "@simplewebauthn/server";
import {
validate,
loginValidation,
registerValidation,
} from "../middleware/validate.js";
const router = express.Router();
// In-memory store (replace with database in production)
const users = new Map();
const webAuthnCredentials = new Map();
const sessions = new Map();
// Helper to generate JWT
const generateToken = (user) => {
return jwt.sign(
{ id: user.id, email: user.email, role: user.role, name: user.name },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || "7d" },
);
};
// Register
router.post(
"/register",
validate(registerValidation),
async (req, res, next) => {
try {
const { name, email, password } = req.body;
// Check if user exists
const existingUser = Array.from(users.values()).find(
(u) => u.email === email,
);
if (existingUser) {
return res.status(400).json({ error: "Email already registered" });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create user
const user = {
id: uuidv4(),
name,
email,
password: hashedPassword,
role: users.size === 0 ? "admin" : "volunteer", // First user is admin
createdAt: new Date().toISOString(),
};
users.set(user.id, user);
// Generate token
const token = generateToken(user);
res.status(201).json({
message: "User registered successfully",
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
},
});
} catch (error) {
next(error);
}
},
);
// Login
router.post("/login", validate(loginValidation), async (req, res, next) => {
try {
const { email, password } = req.body;
// Find user
const user = Array.from(users.values()).find((u) => u.email === email);
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ error: "Invalid credentials" });
}
// Generate token
const token = generateToken(user);
// Create session
const sessionId = uuidv4();
sessions.set(sessionId, {
userId: user.id,
createdAt: new Date().toISOString(),
lastActive: new Date().toISOString(),
});
res.json({
message: "Login successful",
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
},
});
} catch (error) {
next(error);
}
});
// Get current user
router.get("/me", (req, res) => {
// This route should be protected - req.user comes from auth middleware
if (!req.user) {
return res.status(401).json({ error: "Not authenticated" });
}
const user = users.get(req.user.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.json({
user: { id: user.id, name: user.name, email: user.email, role: user.role },
});
});
// Logout
router.post("/logout", (req, res) => {
// Invalidate session on client side
res.json({ message: "Logged out successfully" });
});
// Google OAuth
router.post("/google", async (req, res, next) => {
try {
const { token } = req.body;
// Verify Google token (simplified - use passport-google-oauth20 in production)
// For now, return mock response
res.json({
message: "Google OAuth not yet configured",
token: null,
user: null,
});
} catch (error) {
next(error);
}
});
// WebAuthn Registration Options
router.post("/webauthn/register-options", async (req, res, next) => {
try {
if (!req.user) {
return res.status(401).json({ error: "Authentication required" });
}
const user = users.get(req.user.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
const options = await generateRegistrationOptions({
rpName: process.env.RP_NAME || "Worship Platform",
rpID: process.env.RP_ID || "localhost",
userID: user.id,
userName: user.email,
userDisplayName: user.name,
attestationType: "none",
authenticatorSelection: {
residentKey: "preferred",
userVerification: "preferred",
},
});
// Store challenge for verification
sessions.set(`webauthn-${user.id}`, {
challenge: options.challenge,
createdAt: new Date().toISOString(),
});
res.json(options);
} catch (error) {
next(error);
}
});
// WebAuthn Registration Verification
router.post("/webauthn/register", async (req, res, next) => {
try {
if (!req.user) {
return res.status(401).json({ error: "Authentication required" });
}
const session = sessions.get(`webauthn-${req.user.id}`);
if (!session) {
return res.status(400).json({ error: "Registration session expired" });
}
const verification = await verifyRegistrationResponse({
response: req.body,
expectedChallenge: session.challenge,
expectedOrigin: process.env.RP_ORIGIN || "http://localhost:5100",
expectedRPID: process.env.RP_ID || "localhost",
});
if (!verification.verified) {
return res
.status(400)
.json({ error: "Registration verification failed" });
}
// Store credential
const { registrationInfo } = verification;
webAuthnCredentials.set(req.user.id, {
credentialID: registrationInfo.credentialID,
credentialPublicKey: registrationInfo.credentialPublicKey,
counter: registrationInfo.counter,
transports: req.body.response.transports,
});
// Clean up session
sessions.delete(`webauthn-${req.user.id}`);
res.json({ message: "Biometric registration successful" });
} catch (error) {
next(error);
}
});
// WebAuthn Authentication Options
router.post("/webauthn/authenticate-options", async (req, res, next) => {
try {
const { email } = req.body;
// Find user by email or use session
const user = email
? Array.from(users.values()).find((u) => u.email === email)
: null;
const options = await generateAuthenticationOptions({
rpID: process.env.RP_ID || "localhost",
userVerification: "preferred",
allowCredentials:
user && webAuthnCredentials.has(user.id)
? [
{
id: webAuthnCredentials.get(user.id).credentialID,
type: "public-key",
transports: webAuthnCredentials.get(user.id).transports,
},
]
: [],
});
// Store challenge
const sessionKey = user ? `webauthn-auth-${user.id}` : `webauthn-auth-temp`;
sessions.set(sessionKey, {
challenge: options.challenge,
userId: user?.id,
createdAt: new Date().toISOString(),
});
res.json(options);
} catch (error) {
next(error);
}
});
// WebAuthn Authentication Verification
router.post("/webauthn/authenticate", async (req, res, next) => {
try {
// Find credential and user
let userId = null;
let credential = null;
for (const [id, cred] of webAuthnCredentials.entries()) {
if (
Buffer.compare(
cred.credentialID,
Buffer.from(req.body.id, "base64url"),
) === 0
) {
userId = id;
credential = cred;
break;
}
}
if (!userId || !credential) {
return res.status(400).json({ error: "Credential not found" });
}
const session = sessions.get(`webauthn-auth-${userId}`);
if (!session) {
return res.status(400).json({ error: "Authentication session expired" });
}
const verification = await verifyAuthenticationResponse({
response: req.body,
expectedChallenge: session.challenge,
expectedOrigin: process.env.RP_ORIGIN || "http://localhost:5100",
expectedRPID: process.env.RP_ID || "localhost",
authenticator: credential,
});
if (!verification.verified) {
return res
.status(400)
.json({ error: "Authentication verification failed" });
}
// Update counter
credential.counter = verification.authenticationInfo.newCounter;
const user = users.get(userId);
const token = generateToken(user);
// Clean up session
sessions.delete(`webauthn-auth-${userId}`);
res.json({
message: "Biometric authentication successful",
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
},
});
} catch (error) {
next(error);
}
});
// Switch profile
router.post("/switch-profile", (req, res, next) => {
try {
const { profileId } = req.body;
// In a real app, this would switch to a different profile/user context
const user = users.get(profileId);
if (!user) {
return res.status(404).json({ error: "Profile not found" });
}
const token = generateToken(user);
res.json({
message: "Profile switched successfully",
token,
user: {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
},
});
} catch (error) {
next(error);
}
});
export default router;

View File

@@ -0,0 +1,241 @@
import express from "express";
import { v4 as uuidv4 } from "uuid";
import { validate, listValidation } from "../middleware/validate.js";
const router = express.Router();
// In-memory store (replace with database in production)
const lists = new Map([
[
"1",
{
id: "1",
name: "Sunday Morning",
date: "2026-01-26",
songs: ["1", "2"],
createdBy: "system",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
[
"2",
{
id: "2",
name: "Wednesday Night",
date: "2026-01-22",
songs: ["2"],
createdBy: "system",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
]);
// Get all lists
router.get("/", (req, res) => {
const { search, upcoming } = req.query;
let result = Array.from(lists.values());
// Search filter
if (search) {
const searchLower = search.toLowerCase();
result = result.filter((list) =>
list.name.toLowerCase().includes(searchLower),
);
}
// Upcoming filter
if (upcoming === "true") {
const today = new Date().toISOString().split("T")[0];
result = result.filter((list) => list.date >= today);
}
// Sort by date (newest first)
result.sort((a, b) => new Date(b.date) - new Date(a.date));
res.json({
lists: result,
total: result.length,
});
});
// Get single list with populated songs
router.get("/:id", (req, res) => {
const list = lists.get(req.params.id);
if (!list) {
return res.status(404).json({ error: "List not found" });
}
res.json({ list });
});
// Create list
router.post("/", validate(listValidation), (req, res) => {
const { name, date, songs: songIds } = req.body;
const list = {
id: uuidv4(),
name,
date: date || new Date().toISOString().split("T")[0],
songs: songIds || [],
createdBy: req.user.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
lists.set(list.id, list);
res.status(201).json({
message: "List created successfully",
list,
});
});
// Update list
router.put("/:id", validate(listValidation), (req, res) => {
const list = lists.get(req.params.id);
if (!list) {
return res.status(404).json({ error: "List not found" });
}
const { name, date, songs: songIds } = req.body;
const updatedList = {
...list,
name: name || list.name,
date: date || list.date,
songs: songIds !== undefined ? songIds : list.songs,
updatedAt: new Date().toISOString(),
};
lists.set(list.id, updatedList);
res.json({
message: "List updated successfully",
list: updatedList,
});
});
// Delete list
router.delete("/:id", (req, res) => {
const list = lists.get(req.params.id);
if (!list) {
return res.status(404).json({ error: "List not found" });
}
lists.delete(req.params.id);
res.json({ message: "List deleted successfully" });
});
// Add song to list
router.post("/:id/songs", (req, res) => {
const list = lists.get(req.params.id);
if (!list) {
return res.status(404).json({ error: "List not found" });
}
const { songId, position } = req.body;
if (!songId) {
return res.status(400).json({ error: "Song ID required" });
}
const songs = [...list.songs];
if (position !== undefined && position >= 0 && position <= songs.length) {
songs.splice(position, 0, songId);
} else {
songs.push(songId);
}
list.songs = songs;
list.updatedAt = new Date().toISOString();
lists.set(list.id, list);
res.json({
message: "Song added to list",
list,
});
});
// Remove song from list
router.delete("/:id/songs/:songId", (req, res) => {
const list = lists.get(req.params.id);
if (!list) {
return res.status(404).json({ error: "List not found" });
}
const songIndex = list.songs.indexOf(req.params.songId);
if (songIndex === -1) {
return res.status(404).json({ error: "Song not in list" });
}
list.songs.splice(songIndex, 1);
list.updatedAt = new Date().toISOString();
lists.set(list.id, list);
res.json({
message: "Song removed from list",
list,
});
});
// Reorder songs in list
router.put("/:id/reorder", (req, res) => {
const list = lists.get(req.params.id);
if (!list) {
return res.status(404).json({ error: "List not found" });
}
const { songs: newOrder } = req.body;
if (!Array.isArray(newOrder)) {
return res.status(400).json({ error: "Songs array required" });
}
list.songs = newOrder;
list.updatedAt = new Date().toISOString();
lists.set(list.id, list);
res.json({
message: "List reordered successfully",
list,
});
});
// Duplicate list
router.post("/:id/duplicate", (req, res) => {
const list = lists.get(req.params.id);
if (!list) {
return res.status(404).json({ error: "List not found" });
}
const duplicateList = {
...list,
id: uuidv4(),
name: `${list.name} (Copy)`,
songs: [...list.songs],
createdBy: req.user.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
lists.set(duplicateList.id, duplicateList);
res.status(201).json({
message: "List duplicated successfully",
list: duplicateList,
});
});
export default router;

View File

@@ -0,0 +1,128 @@
import express from "express";
import { v4 as uuidv4 } from "uuid";
const router = express.Router();
// In-memory store (replace with database in production)
const profiles = new Map([
[
"1",
{
id: "1",
name: "Pastor John",
role: "admin",
avatar: "👨‍💼",
createdAt: new Date().toISOString(),
},
],
[
"2",
{
id: "2",
name: "Sarah Miller",
role: "leader",
avatar: "👩‍🎤",
createdAt: new Date().toISOString(),
},
],
[
"3",
{
id: "3",
name: "Mike Johnson",
role: "tech",
avatar: "🎛️",
createdAt: new Date().toISOString(),
},
],
]);
// Get all profiles
router.get("/", (req, res) => {
const result = Array.from(profiles.values());
res.json({
profiles: result,
total: result.length,
});
});
// Get single profile
router.get("/:id", (req, res) => {
const profile = profiles.get(req.params.id);
if (!profile) {
return res.status(404).json({ error: "Profile not found" });
}
res.json({ profile });
});
// Create profile
router.post("/", (req, res) => {
const { name, role, avatar } = req.body;
if (!name) {
return res.status(400).json({ error: "Name is required" });
}
const profile = {
id: uuidv4(),
name,
role: role || "volunteer",
avatar: avatar || "👤",
createdAt: new Date().toISOString(),
};
profiles.set(profile.id, profile);
res.status(201).json({
message: "Profile created successfully",
profile,
});
});
// Update profile
router.put("/:id", (req, res) => {
const profile = profiles.get(req.params.id);
if (!profile) {
return res.status(404).json({ error: "Profile not found" });
}
const { name, role, avatar } = req.body;
const updatedProfile = {
...profile,
name: name || profile.name,
role: role || profile.role,
avatar: avatar || profile.avatar,
};
profiles.set(profile.id, updatedProfile);
res.json({
message: "Profile updated successfully",
profile: updatedProfile,
});
});
// Delete profile
router.delete("/:id", (req, res) => {
const profile = profiles.get(req.params.id);
if (!profile) {
return res.status(404).json({ error: "Profile not found" });
}
// Don't allow deleting admin profiles
if (profile.role === "admin") {
return res.status(403).json({ error: "Cannot delete admin profile" });
}
profiles.delete(req.params.id);
res.json({ message: "Profile deleted successfully" });
});
export default router;

View File

@@ -0,0 +1,250 @@
import express from "express";
import { v4 as uuidv4 } from "uuid";
import { validate, songValidation } from "../middleware/validate.js";
const router = express.Router();
// In-memory store (replace with database in production)
const songs = new Map([
[
"1",
{
id: "1",
title: "Amazing Grace",
artist: "John Newton",
key: "G",
originalKey: "G",
tempo: 72,
category: "Hymn",
lyrics: `[Verse 1]
[G]Amazing [D]grace, how [G]sweet the [G7]sound
That [C]saved a [G]wretch like [Em]me
I [G]once was [D]lost, but [G]now am [Em]found
Was [G]blind but [D]now I [G]see`,
createdBy: "system",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
[
"2",
{
id: "2",
title: "How Great Is Our God",
artist: "Chris Tomlin",
key: "C",
originalKey: "C",
tempo: 78,
category: "Contemporary",
lyrics: `[Verse 1]
The [C]splendor of the [Am]King
[F]Clothed in majesty [C]
Let all the earth re[Am]joice
[F]All the earth re[G]joice`,
createdBy: "system",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
],
]);
// Search songs (for worship list)
router.get("/search", (req, res) => {
const { q } = req.query;
if (!q) {
return res.json({
success: true,
songs: [],
total: 0,
});
}
const searchLower = q.toLowerCase();
const result = Array.from(songs.values()).filter(
(song) =>
song.title.toLowerCase().includes(searchLower) ||
song.artist.toLowerCase().includes(searchLower),
);
res.json({
success: true,
songs: result,
total: result.length,
});
});
// Get all songs
router.get("/", (req, res) => {
const { search, key, category, sort } = req.query;
let result = Array.from(songs.values());
// Search filter
if (search) {
const searchLower = search.toLowerCase();
result = result.filter(
(song) =>
song.title.toLowerCase().includes(searchLower) ||
song.artist.toLowerCase().includes(searchLower),
);
}
// Key filter
if (key) {
result = result.filter((song) => song.key === key);
}
// Category filter
if (category) {
result = result.filter((song) => song.category === category);
}
// Sort
if (sort) {
const [field, order] = sort.split(":");
result.sort((a, b) => {
const aVal = a[field] || "";
const bVal = b[field] || "";
const comparison =
typeof aVal === "string" ? aVal.localeCompare(bVal) : aVal - bVal;
return order === "desc" ? -comparison : comparison;
});
}
res.json({
songs: result,
total: result.length,
});
});
// Get single song
router.get("/:id", (req, res) => {
const song = songs.get(req.params.id);
if (!song) {
return res.status(404).json({ error: "Song not found" });
}
res.json({ song });
});
// Create song
router.post("/", validate(songValidation), (req, res) => {
const { title, artist, key, tempo, category, lyrics } = req.body;
const song = {
id: uuidv4(),
title,
artist: artist || "Unknown",
key: key || "C",
originalKey: key || "C",
tempo: tempo || 72,
category: category || "Contemporary",
lyrics: lyrics || "",
createdBy: req.user.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
songs.set(song.id, song);
res.status(201).json({
message: "Song created successfully",
song,
});
});
// Update song
router.put("/:id", validate(songValidation), (req, res) => {
const song = songs.get(req.params.id);
if (!song) {
return res.status(404).json({ error: "Song not found" });
}
const { title, artist, key, tempo, category, lyrics } = req.body;
const updatedSong = {
...song,
title: title || song.title,
artist: artist || song.artist,
key: key || song.key,
tempo: tempo || song.tempo,
category: category || song.category,
lyrics: lyrics !== undefined ? lyrics : song.lyrics,
updatedAt: new Date().toISOString(),
};
songs.set(song.id, updatedSong);
res.json({
message: "Song updated successfully",
song: updatedSong,
});
});
// Delete song
router.delete("/:id", (req, res) => {
const song = songs.get(req.params.id);
if (!song) {
return res.status(404).json({ error: "Song not found" });
}
songs.delete(req.params.id);
res.json({ message: "Song deleted successfully" });
});
// Duplicate song
router.post("/:id/duplicate", (req, res) => {
const song = songs.get(req.params.id);
if (!song) {
return res.status(404).json({ error: "Song not found" });
}
const duplicateSong = {
...song,
id: uuidv4(),
title: `${song.title} (Copy)`,
createdBy: req.user.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
songs.set(duplicateSong.id, duplicateSong);
res.status(201).json({
message: "Song duplicated successfully",
song: duplicateSong,
});
});
// Transpose song
router.post("/:id/transpose", (req, res) => {
const song = songs.get(req.params.id);
if (!song) {
return res.status(404).json({ error: "Song not found" });
}
const { targetKey, useFlats } = req.body;
// Transposition logic would go here
// For now, just return the song with updated key
const transposedSong = {
...song,
key: targetKey || song.key,
// In a real implementation, lyrics would be transposed
};
res.json({
message: "Song transposed",
song: transposedSong,
});
});
export default router;