initial commit
Some checks failed
deploy / deploy (push) Failing after -22s

This commit is contained in:
Ari Yeger
2025-07-17 15:20:56 -04:00
commit f0b66e6335
53 changed files with 34571 additions and 0 deletions

319
server/app.js Normal file
View File

@ -0,0 +1,319 @@
// server/app.js
import express from 'express';
import cors from 'cors';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs";
import db from './db.js';
import AuthHandler from './middleware/AuthHandler.js';
/**@typedef {import("../client/src/DBRecord.ts").DBRecord} DBRecord */
console.log("Starting Server...");
// test db connection
try{
await db.query("SELECT 1")
console.log("DB connection successful")
const scan = await db.query("SELECT * FROM public.scans WHERE id = 1");
console.debug(scan[0]);
console.log("Scans table exists");
}
catch(err){
console.error(err);
process.exit(1);
}
const JWT_SECRET = process.env.JWT_SECRET || "super-secret-key";
const app = express();
app.use(cors());
app.use(express.json());
app.use("/", express.static('../client/dist'));
app.use("/search", express.static('../client/dist'));
app.use("/add", express.static('../client/dist'));
app.use("/edit/:id", express.static('../client/dist'));
app.use("/login", express.static('../client/dist'));
app.use("/register", express.static('../client/dist'));
const UPLOAD_FOLDER = path.join(process.cwd(), 'Scans');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, UPLOAD_FOLDER);
},
filename: (req, file, cb) => {
cb(null, file.originalname);
}
});
const upload = multer({ storage });
// Add a new music scan
app.post('/api/music-scans/add', upload.single('file'), async (req, res) => {
try {
const { name, composer, arranger, words, year, genre, jewish, choral, liturgical, language, instrument } = req.body;
const safeName = name.replace(/[^a-zA-Z0-9_\- ]/g, '').toLowerCase() + '.pdf';
if (!safeName) res.status(400).json({ message: 'Invalid file name!' });
else {
const locs = [
jewish === 'true' ? 'Jewish' : 'Non-Jewish',
liturgical === 'true' ? 'Liturgical' : 'Non-Liturgical',
choral === 'true' ? 'Choral' : 'Non-Choral',
(genre || 'unknown').toLowerCase(),
(composer || 'unknown').toLowerCase()
];
const link = path.join(...locs, safeName);
const filePath = path.join(UPLOAD_FOLDER, link);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.renameSync(req.file.path, filePath);
/** @type DBRecord[] */
const newScan = await db.query(
`INSERT INTO public.scans
(name, composer, arranger, words, link, year, genre, jewish, choral, liturgical, language, instrument)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
RETURNING *`,
[
name.toLowerCase(),
(composer || 'unknown').toLowerCase(),
(arranger || 'unknown').toLowerCase(),
(words || 'unknown').toLowerCase(),
link,
year,
genre.toLowerCase(),
jewish === 'true',
choral === 'true',
liturgical === 'true',
language.toLowerCase(),
instrument.toLowerCase()
]
);
if (!newScan || newScan.length === 0) res.status(500).json({ message: 'Failed to save music scan!' });
else res.json({ message: 'Music scan added successfully!', saved_at: filePath });
}
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Get a music scan by id
app.get('/api/music-scans/scan/:id', async (req, res) => {
/** @type DBRecord[] */
const queryResult = await db.query('SELECT * FROM public.scans WHERE id = $1', [req.params.id]);
if (!queryResult || queryResult.length === 0) res.status(404).json({ message: 'Not found' });
else if (queryResult.length > 1) res.status(500).json({ message: 'Multiple scans found with the same ID' });
else res.json(queryResult[0]);
});
// Download a music scan file
app.get('/api/music-scans/download/:id', AuthHandler, async (req, res) => {
/** @type DBRecord[] */
const queryResult = await db.query('SELECT * FROM public.scans WHERE id = $1', [req.params.id]);
if (!queryResult || queryResult.length === 0) res.status(404).json({ message: 'Not found' });
else if (queryResult.length > 1) res.status(500).json({ message: 'Multiple scans found with the same ID' });
else {
const scan = queryResult[0];
const filePath = path.join(UPLOAD_FOLDER, scan.link);
res.download(filePath, scan.name + '.pdf');
}
});
// Update a music scan
app.patch('/api/music-scans/scan/:id', AuthHandler, async (req, res) => {
const queryResult = await db.query('SELECT * FROM public.scans WHERE id = $1', [req.params.id]);
if (!queryResult || queryResult.length === 0) res.status(404).json({ message: 'Not found' });
else if (queryResult.length > 1) res.status(500).json({ message: 'Multiple scans found with the same ID' });
else {
const data = req.body;
if (!data.name || !data.link) res.status(400).json({ message: 'Name and link are required!' });
else if (data.link && !data.link.endsWith('.pdf')) res.status(400).json({ message: 'Link must end with .pdf' });
else {
await db.query(
`UPDATE public.scans
SET name = $1, composer = $2, arranger = $3, words = $4, link = $5, year = $6, genre = $7, jewish = $8, choral = $9, liturgical = $10, language = $11, instrument = $12
WHERE id = $13`,
[
data.name,
data.composer || 'Unknown',
data.arranger || 'Unknown',
data.words || 'Unknown',
data.link,
data.year,
data.genre,
data.jewish === 'true',
data.choral === 'true',
data.liturgical === 'true',
data.language,
data.instrument,
req.params.id
]
);
res.json({ message: 'Music scan updated successfully!' });
}
}
});
// Delete a music scan
app.delete('/api/music-scans/delete/:id', AuthHandler, async (req, res) => {
/** @type DBRecord[] */
const queryResult = await db.query('SELECT * FROM public.scans WHERE id = $1', [req.params.id]);
if (!queryResult || queryResult.length === 0) res.status(404).json({ message: 'Not found' });
else if (queryResult.length > 1) res.status(500).json({ message: 'Multiple scans found with the same ID' });
else {
const scan = queryResult[0];
const filePath = path.join(UPLOAD_FOLDER, scan.link);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
await db.query('DELETE FROM public.scans WHERE id = $1', [req.params.id]);
res.json({ message: 'Music scan deleted successfully!' });
}
});
// Search music scans
app.get('/api/music-scans/search', async (req, res) => {
const whereClauses = [];
const values = [];
let valIndex = 1;
for (const key in req.query) {
if (req.query[key] !== 'undefined' && req.query[key] !== '') {
if (['jewish', 'choral', 'liturgical'].includes(key)) {
whereClauses.push(`${key} = $${valIndex++}`);
values.push(req.query[key] === 'true');
} else {
whereClauses.push(`${key} ILIKE $${valIndex++}`);
values.push(`%${req.query[key]}%`);
}
}
}
const whereSQL = whereClauses.length ? `WHERE ${whereClauses.join(' AND ')}` : '';
const results = await db.query(`SELECT * FROM public.scans ${whereSQL}`, values);
res.json(results);
});
// Autocomplete all
app.get('/api/music-scans/all-autocomplete', async (req, res) => {
const results = await db.query(
`SELECT name,
composer,
arranger,
words,
genre,
language,
instrument,
year
FROM public.scans`
);
res.json(results);
});
// Autocomplete with search
app.get('/api/music-scans/autocomplete', async (req, res) => {
// Similar to search, but return unique values for each field
const whereClauses = [];
const values = [];
let valIndex = 1;
/** @type string[] */
const validKeys = []
for (const key in req.query) {
if (req.query[key] !== 'undefined' && req.query[key] !== '') {
validKeys.push(key);
if (['jewish', 'choral', 'liturgical'].includes(key)) {
whereClauses.push(`${key} = $${valIndex++}`);
values.push(req.query[key] === 'true');
} else {
whereClauses.push(`${key} ILIKE $${valIndex++}`);
values.push(`%${req.query[key]}%`);
}
}
}
if (validKeys.length === 0) {
return;
}
const whereSQL = whereClauses.length ? `WHERE ${whereClauses.join(' AND ')}` : '';
const text = `SELECT DISTINCT (${validKeys.join(', ')}) FROM public.scans ${whereSQL ? whereSQL : ''}`
/** @type {string[][] | string[]} */
const results = await db.query(text, values).then( res => {
console.debug(res);
const sectionArray = [];
if(validKeys.length > 1) {
for (let row in res) sectionArray.push(res[row]['row'].replaceAll(/[()"]/g, '').split(',').map(item => item.trim()));
} else {
for (let row in res) sectionArray.push(res[row][validKeys[0]].replaceAll(/[()"]/g,'').trim());
}
return sectionArray;
});
const data = {};
if (validKeys.length === 1) data[validKeys[0]] = results
else {
for (let i = 0; i < validKeys.length; i++) {
const column = []
for (const row of results) {
column.push(row[i])
}
console.debug(column);
data[validKeys[i]] = [...new Set(column)].filter(item => item !== null && item !== undefined && item !== '');
}
}
res.json(data);
});
app.post("/api/music-scans/register", async (req, res) => {
const { username, password } = req.body
const users = await db.query('SELECT * FROM public.users')
let user = users.find(u => u.username === username)
if (user) {
res.status(401).json({
data: null,
error: 'User already exists'
})
return
}
const hashedPassword = await bcrypt.hash(password, 10)
const insertedUsers = await db.query('INSERT INTO public.users (username, password, role) VALUES ($1, $2, $3) RETURNING *', [username, hashedPassword, 'user'])
if (insertedUsers.length === 0) {
res.status(500).json({ data: null, error: 'Failed to create user: User already exists' })
return
}
user = insertedUsers[0]
const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: '1d' })
res.json({
data: {
id: user.id,
username: user.username,
password: null, // Don't send the password back
token: token
}
})
})
app.post("/api/music-scans/login", async (req, res) => {
const { username, password } = req.body
const users = await db.query('SELECT * FROM public.users')
const user = users.find(u => u.username === username)
if (!user || !bcrypt.compareSync(password, user.password)) {
res.status(401).json({ data: null, error: 'Invalid credentials' })
return
}
const token = jwt.sign({ id: user.id, role: user.role }, JWT_SECRET, { expiresIn: '1d' })
res.json({
data: {
id: user.id,
username: user.username,
role: user.role,
token: token
}
})
})
// 404 handler
app.use((req, res) => {
res.status(404).json({ message: 'Route not found' });
});
const PORT = process.env.PORT || 8000;
app.listen(PORT, () => {
console.log(`Express server running on port ${PORT}`);
});