319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
// 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}`);
|
|
}); |