Initial commit - Church Music Database
This commit is contained in:
309
new-site/backend/api/admin.js
Normal file
309
new-site/backend/api/admin.js
Normal 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;
|
||||
380
new-site/backend/api/auth.js
Normal file
380
new-site/backend/api/auth.js
Normal 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;
|
||||
241
new-site/backend/api/lists.js
Normal file
241
new-site/backend/api/lists.js
Normal 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;
|
||||
128
new-site/backend/api/profiles.js
Normal file
128
new-site/backend/api/profiles.js
Normal 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;
|
||||
250
new-site/backend/api/songs.js
Normal file
250
new-site/backend/api/songs.js
Normal 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;
|
||||
Reference in New Issue
Block a user