This commit is contained in:
319
server/app.js
Normal file
319
server/app.js
Normal 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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user