913 lines
35 KiB
JavaScript
913 lines
35 KiB
JavaScript
const axios = require('axios');
|
|
const express = require('express');
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const sqlite3 = require('sqlite3').verbose();
|
|
const bodyParser = require('body-parser');
|
|
const nodemailer = require('nodemailer'); // Add nodemailer for sending emails
|
|
const rateLimit = require('express-rate-limit'); // Ensure this line is present
|
|
const basicAuth = require('express-basic-auth'); // Add express-basic-auth
|
|
|
|
const library = require('./library');
|
|
const {
|
|
fetchBookFromLocalDatabase,
|
|
fetchBookFromGoogleBooks,
|
|
fetchBookFromOpenLibrary,
|
|
fetchBookFromInternetArchive,
|
|
searchBooksInLocalDatabase,
|
|
searchBooksInOpenLibrary
|
|
} = require('./bookHelpers'); // Import the helper functions from the new file
|
|
|
|
require('dotenv').config();
|
|
|
|
const { Book, Location, Checkout, User } = require('./models');
|
|
const { Op } = require('sequelize'); // Import Sequelize Operators
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
// SSL certificate paths
|
|
const privateKey = fs.readFileSync(path.join(__dirname, 'server.key'), 'utf8');
|
|
const certificate = fs.readFileSync(path.join(__dirname, 'server.cert'), 'utf8');
|
|
|
|
const credentials = { key: privateKey, cert: certificate };
|
|
|
|
// Middleware to parse JSON bodies
|
|
app.use(express.json()); // Use built-in body-parser for JSON
|
|
|
|
|
|
// Serve static files from the 'public' directory
|
|
app.use(express.static(path.join(__dirname, 'public')));
|
|
|
|
// Basic Auth middleware
|
|
const authMiddleware = basicAuth({
|
|
users: { 'admin': process.env.ADMIN_PASSWORD }, // Use environment variable for password
|
|
challenge: true,
|
|
unauthorizedResponse: (req) => 'Unauthorized'
|
|
});
|
|
|
|
// Apply auth middleware to all non-GET requests except for checkout
|
|
app.use((req, res, next) => {
|
|
if (req.method !== 'GET' && !req.path.startsWith('/api/checkout')) {
|
|
return authMiddleware(req, res, next);
|
|
}
|
|
next();
|
|
});
|
|
|
|
app.get('/book/:isbn', async (req, res) => {
|
|
const { isbn } = req.params;
|
|
console.log(`Fetching book data for ISBN: ${isbn}`);
|
|
|
|
try {
|
|
// Check if the book is in the local database
|
|
const localBook = await fetchBookFromLocalDatabase(isbn);
|
|
|
|
if (localBook) {
|
|
console.log('Book found in the local database');
|
|
return res.json({ source: 'local', data: localBook });
|
|
}
|
|
|
|
// If not found locally, try external sources
|
|
const googleBooksApiKey = 'AIzaSyCQikthZ5TlkFTcKTG8n171dRafosK2Mg8';
|
|
|
|
try {
|
|
const googleBookData = await fetchBookFromGoogleBooks(isbn, googleBooksApiKey);
|
|
if (googleBookData) {
|
|
console.log('Book data found in Google Books');
|
|
return res.json({ source: 'external', data: googleBookData });
|
|
}
|
|
} catch (errorStatus) {
|
|
if (errorStatus === 429) {
|
|
console.log('Rate limit exceeded for Google Books');
|
|
return res.status(429).json({ error: 'Rate limit exceeded' });
|
|
} else if (errorStatus === 408) {
|
|
console.log('Request Timeout for Google Books');
|
|
return res.status(408).json({ error: 'Request Timeout' });
|
|
}
|
|
}
|
|
|
|
try {
|
|
const openLibraryData = await fetchBookFromOpenLibrary(isbn);
|
|
if (openLibraryData) {
|
|
console.log('Book data found in Open Library');
|
|
return res.json({ source: 'external', data: openLibraryData });
|
|
}
|
|
} catch (errorStatus) {
|
|
if (errorStatus === 429) {
|
|
console.log('Rate limit exceeded for Open Library');
|
|
return res.status(429).json({ error: 'Rate limit exceeded' });
|
|
} else if (errorStatus === 408) {
|
|
console.log('Request Timeout for Open Library');
|
|
return res.status(408).json({ error: 'Request Timeout' });
|
|
}
|
|
}
|
|
|
|
const archiveData = await fetchBookFromInternetArchive(isbn);
|
|
if (archiveData) {
|
|
console.log('Book data found in the Internet Archive');
|
|
return res.json({ source: 'external', data: archiveData });
|
|
}
|
|
|
|
console.log('Book not found in any source');
|
|
return res.status(404).json({ error: 'Book not found' });
|
|
|
|
} catch (error) {
|
|
console.error(error);
|
|
return res.status(500).json({ error: 'Failed to fetch book data' });
|
|
}
|
|
});
|
|
|
|
// New API endpoint to fetch books with images
|
|
app.get('/api/books-with-images', async (req, res) => {
|
|
try {
|
|
const books = await Book.findAll({
|
|
where: {
|
|
[Op.or]: [
|
|
{ cover_small: { [Op.ne]: null } },
|
|
{ cover_medium: { [Op.ne]: null } },
|
|
{ cover_large: { [Op.ne]: null } },
|
|
],
|
|
},
|
|
attributes: ['isbn', 'title', 'cover_small', 'cover_medium', 'cover_large', 'authors'],
|
|
});
|
|
// CORRECTED DEBUG LOGGING ON SERVER
|
|
if (books && books.length > 0) {
|
|
// Log a specific book if its ID is known, e.g., ID 207 for Artemis Fowl
|
|
const specificBookForDebug = books.find(b => b.id === 207);
|
|
if (specificBookForDebug) {
|
|
console.log("Server-side specific book from /api/books (ID 207):", JSON.stringify(specificBookForDebug, null, 2));
|
|
} else {
|
|
console.log("Server-side book ID 207 not found in /api/books results, logging first book instead:", JSON.stringify(books[0], null, 2));
|
|
}
|
|
}
|
|
res.json(books);
|
|
} catch (error) {
|
|
console.error('Failed to fetch books with images:', error);
|
|
res.status(500).json({ error: 'Error fetching books with images' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/refetch-cover-images', async (req, res) => {
|
|
try {
|
|
await refetchCoverImages();
|
|
res.json({ success: true, message: 'Cover images re-fetching initiated.' });
|
|
} catch (error) {
|
|
console.error('Failed to re-fetch cover images:', error.message);
|
|
res.status(500).json({ error: 'Failed to re-fetch cover images' });
|
|
}
|
|
});
|
|
|
|
|
|
async function refetchCoverImages() {
|
|
const googleBooksApiKey = process.env.GOOGLE_BOOKS_API_KEY;
|
|
console.log(googleBooksApiKey)
|
|
|
|
try {
|
|
// Fetch all books from the database
|
|
const books = await Book.findAll();
|
|
|
|
for (const book of books) {
|
|
const isbn = book.isbn;
|
|
|
|
// Fetch data from Google Books API
|
|
|
|
const url = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}&key=${googleBooksApiKey}`;
|
|
|
|
console.log(url)
|
|
try {
|
|
// Wait a second before making the next request
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
const response = await axios.get(url);
|
|
|
|
if (response.data.totalItems > 0) {
|
|
const volumeInfo = response.data.items[0].volumeInfo;
|
|
|
|
// Extract new cover image URLs
|
|
const imageLinks = volumeInfo.imageLinks || {};
|
|
const cover_small = imageLinks.smallThumbnail || null;
|
|
const cover_medium = imageLinks.thumbnail || null;
|
|
const cover_large = imageLinks.large || null;
|
|
|
|
// Update the book entry if any new image URLs are found
|
|
if (cover_small || cover_medium || cover_large) {
|
|
await book.update({
|
|
cover_small: cover_small || book.cover_small,
|
|
cover_medium: cover_medium || book.cover_medium,
|
|
cover_large: cover_large || book.cover_large,
|
|
});
|
|
|
|
console.log(`Updated cover images for ISBN: ${isbn}`);
|
|
} else {
|
|
console.log(`No new cover images found for ISBN: ${isbn}`);
|
|
}
|
|
} else {
|
|
console.log(`No data found in Google Books for ISBN: ${isbn}`);
|
|
}
|
|
} catch (error) {
|
|
if (error.response && error.response.status === 429) {
|
|
console.error('Rate limit exceeded. Pausing requests.');
|
|
// Implement a delay or exit the loop if necessary
|
|
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
|
|
} else {
|
|
console.error(`Error fetching data for ISBN: ${isbn}`, error.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Cover image re-fetching completed.');
|
|
} catch (error) {
|
|
console.error('Failed to re-fetch cover images:', error.message);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
app.post('/api/refetch-book-data', async (req, res) => {
|
|
// Handle start index for pagination, start index is in json body
|
|
const { startIndex = 0 } = req.body;
|
|
try {
|
|
await refetchBookData(startIndex);
|
|
res.json({ success: true, message: 'Book data re-fetching initiated.' });
|
|
} catch (error) {
|
|
console.error('Failed to re-fetch book data:', error.message);
|
|
res.status(500).json({ error: 'Failed to re-fetch book data' });
|
|
}
|
|
});
|
|
|
|
async function refetchBookData(startIndex = 0) {
|
|
const googleBooksApiKey = process.env.GOOGLE_BOOKS_API_KEY;
|
|
|
|
try {
|
|
// Fetch all books from the database
|
|
const books = await Book.findAll();
|
|
|
|
console.log(`Found ${books.length} books to update.`);
|
|
console.log(`Starting from index: ${startIndex}`);
|
|
|
|
for (const [index, book] of books.entries()) {
|
|
if (index < startIndex) {
|
|
continue;
|
|
}
|
|
const isbn = book.isbn;
|
|
|
|
// Fetch data from Google Books API
|
|
const url = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}&key=${googleBooksApiKey}`;
|
|
|
|
try {
|
|
const response = await axios.get(url);
|
|
|
|
if (response.data.totalItems > 0) {
|
|
const volumeInfo = response.data.items[0].volumeInfo;
|
|
|
|
// Extract data from the API response
|
|
const updatedData = {
|
|
title: volumeInfo.title || book.title,
|
|
authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : book.authors,
|
|
publishedDate: volumeInfo.publishedDate || book.publishedDate,
|
|
description: volumeInfo.description || book.description,
|
|
number_of_pages: volumeInfo.pageCount || book.number_of_pages,
|
|
publishers: volumeInfo.publisher || book.publishers,
|
|
subjects: volumeInfo.categories ? volumeInfo.categories.join(', ') : book.subjects,
|
|
cover_small: volumeInfo.imageLinks ? volumeInfo.imageLinks.smallThumbnail : book.cover_small,
|
|
cover_medium: volumeInfo.imageLinks ? volumeInfo.imageLinks.thumbnail : book.cover_medium,
|
|
cover_large: volumeInfo.imageLinks ? volumeInfo.imageLinks.large : book.cover_large,
|
|
// Add more fields as necessary
|
|
};
|
|
|
|
// Update the book entry in the database
|
|
await book.update(updatedData);
|
|
|
|
console.log(`[${index + 1}/${books.length}] Updated data for ISBN: ${isbn}`);
|
|
} else {
|
|
console.log(`[${index + 1}/${books.length}] No data found in Google Books for ISBN: ${isbn}`);
|
|
}
|
|
} catch (error) {
|
|
if (error.response && error.response.status === 429) {
|
|
console.error('Rate limit exceeded. Pausing requests.');
|
|
// Implement a delay
|
|
await delay(10000); // Delay for 10 seconds
|
|
index--; // Retry the current book
|
|
} else {
|
|
console.error(`Error fetching data for ISBN: ${isbn}`, error.message);
|
|
}
|
|
}
|
|
|
|
// Delay between requests to respect rate limits
|
|
await delay(1000); // Adjust the delay as needed
|
|
}
|
|
|
|
console.log('Book data re-fetching completed.');
|
|
} catch (error) {
|
|
console.error('Failed to re-fetch book data:', error.message);
|
|
}
|
|
}
|
|
|
|
// Utility function to add delay
|
|
function delay(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
// Confirm book is in the library by ISBN
|
|
app.get('/book/confirm/:isbn', async (req, res) => {
|
|
const { isbn } = req.params;
|
|
console.log(`Cascade: /book/confirm/:isbn - Confirming ISBN: ${isbn}`);
|
|
|
|
try {
|
|
// Check if the book is in the local database
|
|
const localBook = await fetchBookFromLocalDatabase(isbn);
|
|
|
|
if (localBook) {
|
|
console.log('Cascade: /book/confirm/:isbn - Book found in local database.');
|
|
return res.json({ source: 'local', data: localBook });
|
|
} else {
|
|
console.log('Cascade: /book/confirm/:isbn - Book NOT found in local database.');
|
|
return res.status(404).json({ error: 'Book not found in local library' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Cascade: /book/confirm/:isbn - Error during lookup:', error);
|
|
return res.status(500).json({ error: 'Failed to confirm book in local library' });
|
|
}
|
|
});
|
|
|
|
app.get('/search-title', async (req, res) => {
|
|
const { title, internalOnly = false } = req.query;
|
|
console.log(`Searching for books by title or related fields: ${title}`);
|
|
|
|
try {
|
|
const localBooks = await searchBooksInLocalDatabase(title);
|
|
|
|
if (localBooks.length > 0) {
|
|
console.log('Books found in the local database');
|
|
return res.json({ source: 'local', results: localBooks });
|
|
}
|
|
|
|
if (internalOnly) {
|
|
return res.status(404).json({ error: 'No books found with that title or related fields.' });
|
|
}
|
|
|
|
const externalBooks = await searchBooksInOpenLibrary(title);
|
|
|
|
if (externalBooks.length > 0) {
|
|
console.log('Books found by title in external sources');
|
|
return res.json({ source: 'external', results: externalBooks });
|
|
} else {
|
|
return res.status(404).json({ error: 'No books found with that title or related fields.' });
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(error);
|
|
return res.status(500).json({ error: 'Failed to search for book by title or related fields' });
|
|
}
|
|
});
|
|
|
|
|
|
// Endpoint to store book in the database
|
|
app.post('/store-book', async (req, res) => {
|
|
try {
|
|
const book = await Book.create(req.body);
|
|
res.json({ success: true, book });
|
|
} catch (error) {
|
|
console.error('Failed to store book:', error);
|
|
res.status(500).json({ error: 'Failed to store book in database' });
|
|
}
|
|
});
|
|
|
|
app.put('/book/:isbn', async (req, res) => {
|
|
try {
|
|
const { isbn } = req.params;
|
|
const book = await Book.findOne({ where: { isbn } });
|
|
if (book) {
|
|
await book.update(req.body);
|
|
res.json({ success: true, message: 'Book updated successfully' });
|
|
} else {
|
|
res.status(404).json({ error: 'Book not found' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update book:', error);
|
|
res.status(500).json({ error: 'Failed to update book in database' });
|
|
}
|
|
});
|
|
|
|
app.delete('/book/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const book = await Book.findByPk(id);
|
|
if (book) {
|
|
await book.destroy();
|
|
res.json({ success: true, message: 'Book deleted successfully' });
|
|
} else {
|
|
res.status(404).json({ error: 'Book not found' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to delete book:', error);
|
|
res.status(500).json({ error: 'Failed to delete book from database' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/locations', async (req, res) => {
|
|
try {
|
|
const locations = await Location.findAll({
|
|
order: [
|
|
['name', 'ASC'],
|
|
['shelf', 'ASC']
|
|
]
|
|
});
|
|
res.json(locations);
|
|
} catch (error) {
|
|
console.error('Failed to fetch locations:', error);
|
|
res.status(500).json({ error: 'Failed to fetch locations' });
|
|
}
|
|
});
|
|
|
|
app.post('/location', async (req, res) => {
|
|
try {
|
|
console.debug('Creating location:', req.body);
|
|
const location = await Location.create(req.body);
|
|
res.json({ success: true, location });
|
|
} catch (error) {
|
|
console.error('Failed to create location:', error);
|
|
res.status(500).json({ error: 'Failed to create location' });
|
|
}
|
|
});
|
|
|
|
app.get('/location/:id', async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const location = await Location.findByPk(id);
|
|
if (location) {
|
|
res.json(location);
|
|
} else {
|
|
res.status(404).json({ error: 'Location not found' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch location:', error);
|
|
res.status(500).json({ error: 'Failed to fetch location' });
|
|
}
|
|
});
|
|
|
|
// Configure nodemailer with your own server
|
|
const transporter = nodemailer.createTransport({
|
|
host: 'mail.uplink.tel', // Replace with your SMTP server
|
|
port: 465, // Replace with your SMTP port (587 is common for TLS)
|
|
secure: true, // Set to true if using port 465
|
|
auth: {
|
|
user: process.env.ADMIN_EMAIL, // Admin email
|
|
pass: process.env.EMAIL_PASSWORD // Email password
|
|
},
|
|
tls: {
|
|
rejectUnauthorized: false // Use this if you encounter certificate issues
|
|
}
|
|
});
|
|
|
|
// Rate limiter middleware
|
|
const checkoutLimiter = rateLimit({
|
|
windowMs: 24 * 60 * 60 * 1000, // 24 hours
|
|
max: 5, // Limit each email to 5 requests per windowMs
|
|
keyGenerator: (req) => req.body.email, // Use email as the key
|
|
handler: (req, res) => {
|
|
res.status(429).json({ error: 'Too many requests, please try again later.' });
|
|
}
|
|
});
|
|
|
|
// New endpoint to handle checkout requests
|
|
app.post('/api/checkout/:isbn', checkoutLimiter, async (req, res) => {
|
|
const { isbn } = req.params;
|
|
const { email, name } = req.body; // Ensure name is also captured
|
|
const ip = req.ip;
|
|
|
|
try {
|
|
const book = await Book.findOne({ where: { isbn } });
|
|
if (book) {
|
|
// Update book status to Pending
|
|
await book.update({ status: 'Pending' });
|
|
|
|
// Find or create the user in the User table
|
|
const [user, created] = await User.findOrCreate({
|
|
where: { email },
|
|
defaults: { name }
|
|
});
|
|
|
|
// Create a new checkout record
|
|
await Checkout.create({
|
|
book_id: book.id,
|
|
user_id: user.id,
|
|
checkout_date: new Date()
|
|
});
|
|
|
|
// Log the requestor's email and IP
|
|
console.log(`Checkout requested by ${email} from IP: ${ip}`);
|
|
|
|
// Send email to admin
|
|
const mailOptions = {
|
|
from: process.env.ADMIN_EMAIL,
|
|
to: process.env.ADMIN_EMAIL,
|
|
subject: `Checkout Request for ISBN: ${isbn}`,
|
|
html: `
|
|
<p>A checkout request has been made. Please review and approve.</p>
|
|
<table style="border: 1px solid black; border-collapse: collapse; width: 100%;">
|
|
<tr style="background-color: black; color: white;">
|
|
<th style="border: 1px solid black; padding: 8px;">Book Title</th>
|
|
<th style="border: 1px solid black; padding: 8px;">ISBN</th>
|
|
<th style="border: 1px solid black; padding: 8px;">Requestor Email</th>
|
|
<th style="border: 1px solid black; padding: 8px;">Requestor Name</th>
|
|
</tr>
|
|
<tr>
|
|
<td style="border: 1px solid black; padding: 8px;">${book.title}</td>
|
|
<td style="border: 1px solid black; padding: 8px;">${isbn}</td>
|
|
<td style="border: 1px solid black; padding: 8px;">${email}</td>
|
|
<td style="border: 1px solid black; padding: 8px;">${name}</td>
|
|
</tr>
|
|
</table>
|
|
<a href="${process.env.DOMAIN}/api/approve-checkout/${isbn}/${email}" style="margin-right: 10px; padding: 10px; background-color: green; color: white; text-decoration: none;">Approve</a>
|
|
<a href="${process.env.DOMAIN}/api/deny-checkout/${isbn}/${email}" style="padding: 10px; background-color: red; color: white; text-decoration: none;">Deny</a>
|
|
`
|
|
};
|
|
|
|
transporter.sendMail(mailOptions, (error, info) => {
|
|
if (error) {
|
|
console.error('Error sending email:', error);
|
|
return res.status(500).json({ error: 'Failed to send email' });
|
|
}
|
|
console.log('Email sent:', info.response);
|
|
res.json({ success: true, message: 'Checkout request sent and status updated to Pending.' });
|
|
});
|
|
} else {
|
|
res.status(404).json({ error: 'Book not found' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to process checkout request:', error);
|
|
res.status(500).json({ error: 'Failed to process checkout request' });
|
|
}
|
|
});
|
|
|
|
// Endpoint to approve checkout request
|
|
app.get('/api/approve-checkout/:isbn/:email', async (req, res) => {
|
|
const { isbn, email } = req.params;
|
|
|
|
try {
|
|
const book = await Book.findOne({ where: { isbn } });
|
|
if (book && book.status === 'Pending') {
|
|
// Find the checkout record
|
|
const checkout = await Checkout.findOne({
|
|
where: {
|
|
book_id: book.id,
|
|
user_id: (await User.findOne({ where: { email } })).id,
|
|
approved: false
|
|
}
|
|
});
|
|
|
|
if (checkout) {
|
|
// Set the checkout date to now and return date to one month from now
|
|
const checkoutDate = new Date();
|
|
const returnDate = new Date();
|
|
returnDate.setMonth(returnDate.getMonth() + 1);
|
|
|
|
await checkout.update({
|
|
approved: true,
|
|
checkout_date: checkoutDate,
|
|
return_date: returnDate
|
|
});
|
|
|
|
await book.update({ status: 'Checked Out' });
|
|
|
|
// Send email to the requesting user
|
|
const mailOptions = {
|
|
from: process.env.ADMIN_EMAIL,
|
|
to: email,
|
|
subject: `Checkout Approved for ISBN: ${isbn}`,
|
|
html: `
|
|
<p>Your checkout request for the book with ISBN: ${isbn} has been approved.</p>
|
|
<p>Checkout Date: ${checkoutDate.toDateString()}</p>
|
|
<p>Return Date: ${returnDate.toDateString()}</p>
|
|
`
|
|
};
|
|
|
|
transporter.sendMail(mailOptions, (error, info) => {
|
|
if (error) {
|
|
console.error('Error sending email:', error);
|
|
return res.status(500).json({ error: 'Failed to send approval email' });
|
|
}
|
|
console.log('Approval email sent:', info.response);
|
|
res.json({ success: true, message: 'Checkout approved and user notified.' });
|
|
});
|
|
} else {
|
|
res.status(404).json({ error: 'Checkout record not found or already approved' });
|
|
}
|
|
} else {
|
|
res.status(404).json({ error: 'Book not found or not Pending' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to approve checkout request:', error);
|
|
res.status(500).json({ error: 'Failed to approve checkout request' });
|
|
}
|
|
});
|
|
|
|
// Endpoint to deny checkout request
|
|
app.get('/api/deny-checkout/:isbn/:email', async (req, res) => {
|
|
const { isbn, email } = req.params;
|
|
|
|
try {
|
|
const book = await Book.findOne({ where: { isbn } });
|
|
if (book && book.status === 'Pending') {
|
|
await book.update({ status: null });
|
|
|
|
// Send email to the requesting user
|
|
const mailOptions = {
|
|
from: process.env.ADMIN_EMAIL,
|
|
to: email,
|
|
subject: `Checkout Denied for ISBN: ${isbn}`,
|
|
text: `Your checkout request for the book with ISBN: ${isbn} has been denied.`
|
|
};
|
|
|
|
transporter.sendMail(mailOptions, (error, info) => {
|
|
if (error) {
|
|
console.error('Error sending email:', error);
|
|
return res.status(500).json({ error: 'Failed to send denial email' });
|
|
}
|
|
console.log('Denial email sent:', info.response);
|
|
res.json({ success: true, message: 'Checkout denied and user notified.' });
|
|
});
|
|
} else {
|
|
res.status(404).json({ error: 'Book not found or not Pending' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to deny checkout request:', error);
|
|
res.status(500).json({ error: 'Failed to deny checkout request' });
|
|
}
|
|
});
|
|
|
|
// Utility function to get a cookie by name
|
|
function getCookie(name) {
|
|
const value = `; ${document.cookie}`;
|
|
const parts = value.split(`; ${name}=`);
|
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
|
}
|
|
|
|
// Utility function to set a cookie
|
|
function setCookie(name, value, days) {
|
|
const date = new Date();
|
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
const expires = `expires=${date.toUTCString()}`;
|
|
document.cookie = `${name}=${value}; ${expires}; path=/`;
|
|
}
|
|
|
|
// Client-side function to request checkout
|
|
function requestCheckout(isbn) {
|
|
fetch(`/api/checkout/${isbn}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert('Checkout request sent successfully.');
|
|
} else {
|
|
alert('Failed to send checkout request.');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('An error occurred while sending the checkout request.');
|
|
});
|
|
}
|
|
|
|
// API endpoint to get all books
|
|
app.get('/api/books', async (req, res) => {
|
|
try {
|
|
const books = await Book.findAll({
|
|
include: [{
|
|
model: Location,
|
|
as: 'Location' // Corrected alias based on model definition
|
|
}],
|
|
order: [['title', 'ASC']]
|
|
});
|
|
res.json(books.map(book => ({
|
|
id: book.id,
|
|
title: book.title,
|
|
authors: book.authors,
|
|
isbn: book.isbn,
|
|
publishedDate: book.publishedDate,
|
|
description: book.description,
|
|
cover_small: book.cover_small,
|
|
number_of_pages: book.number_of_pages,
|
|
publishers: book.publishers,
|
|
subjects: book.subjects,
|
|
location_id: book.location_id,
|
|
Location: book.Location ? { id: book.Location.id, name: book.Location.name, room: book.Location.room, shelf: book.Location.shelf } : null
|
|
})));
|
|
} catch (error) {
|
|
console.error('Failed to fetch books:', error);
|
|
res.status(500).json({ error: 'Failed to fetch books' });
|
|
}
|
|
});
|
|
|
|
// API endpoint to add a new book
|
|
app.post('/api/books', async (req, res) => {
|
|
try {
|
|
const {
|
|
title,
|
|
authors,
|
|
publishedDate,
|
|
description,
|
|
isbn,
|
|
number_of_pages,
|
|
publishers,
|
|
subjects,
|
|
cover_small,
|
|
cover_medium,
|
|
cover_large,
|
|
location_id
|
|
} = req.body;
|
|
|
|
// Basic validation (e.g., title is required)
|
|
if (!title) {
|
|
return res.status(400).json({ error: 'Title is required' });
|
|
}
|
|
|
|
const newBookData = {
|
|
title,
|
|
authors: Array.isArray(authors) ? authors.join(', ') : authors,
|
|
publishedDate,
|
|
description,
|
|
isbn,
|
|
number_of_pages,
|
|
publishers: Array.isArray(publishers) ? publishers.join(', ') : publishers,
|
|
subjects: Array.isArray(subjects) ? subjects.join(', ') : subjects,
|
|
cover_small,
|
|
cover_medium,
|
|
cover_large,
|
|
location_id: location_id === '' ? null : location_id // Handle empty string as null
|
|
};
|
|
|
|
const book = await Book.create(newBookData);
|
|
|
|
// Fetch the newly created book with its location to send back in the response
|
|
const createdBookWithLocation = await Book.findByPk(book.id, {
|
|
include: [{
|
|
model: Location,
|
|
as: 'Location'
|
|
}]
|
|
});
|
|
|
|
res.status(201).json({ success: true, book: createdBookWithLocation });
|
|
} catch (error) {
|
|
console.error('Failed to add new book:', error);
|
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
|
let errorMessage = 'Failed to add new book due to a conflict.';
|
|
// Assuming 'isbn' is a unique field in your Book model
|
|
if (error.fields && typeof error.fields === 'object' && 'isbn' in error.fields) {
|
|
errorMessage = 'A book with this ISBN already exists. Please use the edit feature if you want to update it.';
|
|
}
|
|
return res.status(409).json({ error: errorMessage });
|
|
}
|
|
res.status(500).json({ error: 'Failed to add new book' });
|
|
}
|
|
});
|
|
|
|
// API endpoint to update a book's metadata by its primary key ID
|
|
app.put('/api/books/:id', async (req, res) => {
|
|
try {
|
|
const bookId = req.params.id;
|
|
const book = await Book.findByPk(bookId);
|
|
|
|
if (!book) {
|
|
return res.status(404).json({ error: 'Book not found' });
|
|
}
|
|
|
|
const {
|
|
title,
|
|
authors,
|
|
publishedDate,
|
|
description,
|
|
isbn,
|
|
number_of_pages,
|
|
publishers,
|
|
subjects,
|
|
cover_small,
|
|
cover_medium,
|
|
cover_large,
|
|
location_id // Added to handle location updates
|
|
} = req.body;
|
|
|
|
const updateData = {};
|
|
if (title !== undefined) updateData.title = title;
|
|
if (authors !== undefined) updateData.authors = Array.isArray(authors) ? authors.join(', ') : authors;
|
|
if (publishedDate !== undefined) updateData.publishedDate = publishedDate;
|
|
if (description !== undefined) updateData.description = description;
|
|
if (isbn !== undefined) updateData.isbn = isbn === '' ? null : isbn;
|
|
if (number_of_pages !== undefined) updateData.number_of_pages = number_of_pages;
|
|
if (publishers !== undefined) updateData.publishers = Array.isArray(publishers) ? publishers.join(', ') : publishers;
|
|
if (subjects !== undefined) updateData.subjects = Array.isArray(subjects) ? subjects.join(', ') : subjects;
|
|
if (cover_small !== undefined) updateData.cover_small = cover_small;
|
|
if (cover_medium !== undefined) updateData.cover_medium = cover_medium;
|
|
if (cover_large !== undefined) updateData.cover_large = cover_large;
|
|
if (location_id !== undefined) updateData.location_id = location_id === '' ? null : location_id; // Handle empty string as null
|
|
|
|
await book.update(updateData);
|
|
console.log(`Book data for ID ${bookId} updated successfully.`);
|
|
|
|
// Fetch the updated book with its location to send back in the response
|
|
const updatedBookWithLocation = await Book.findByPk(bookId, {
|
|
include: [{
|
|
model: Location,
|
|
as: 'Location'
|
|
}]
|
|
});
|
|
res.json({ success: true, book: updatedBookWithLocation });
|
|
|
|
} catch (error) {
|
|
console.error(`Failed to update book with ID ${req.params.id}:`, error);
|
|
res.status(500).json({ error: 'Failed to update book' });
|
|
}
|
|
});
|
|
|
|
const httpsServer = https.createServer(credentials, app);
|
|
|
|
|
|
httpsServer.listen(PORT, () => {
|
|
console.log(`HTTPS Server running on https://localhost:${PORT}`);
|
|
});
|
|
|
|
app.get('/api/books-on-loan', async (req, res) => {
|
|
try {
|
|
const loans = await Checkout.findAll({
|
|
where: {
|
|
returned_date: null
|
|
},
|
|
include: [
|
|
{
|
|
model: Book,
|
|
attributes: ['isbn', 'title']
|
|
},
|
|
{
|
|
model: User,
|
|
attributes: ['name']
|
|
}
|
|
],
|
|
attributes: ['checkout_date', 'return_date']
|
|
});
|
|
|
|
const result = loans.map(loan => ({
|
|
isbn: loan.Book.isbn,
|
|
title: loan.Book.title,
|
|
name: loan.User.name,
|
|
checkout_date: loan.checkout_date,
|
|
return_date: loan.return_date
|
|
}));
|
|
|
|
res.json(result);
|
|
} catch (error) {
|
|
console.error('Error fetching books on loan:', error);
|
|
res.status(500).send('Internal Server Error');
|
|
}
|
|
});
|
|
|
|
app.post('/book/update/:isbn', authMiddleware, async (req, res) => { // Added authMiddleware for consistency
|
|
const { isbn } = req.params;
|
|
const googleBooksApiKey = process.env.GOOGLE_BOOKS_API_KEY;
|
|
|
|
try {
|
|
// Fetch data from Google Books API
|
|
const url = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}&key=${googleBooksApiKey}`;
|
|
const response = await axios.get(url);
|
|
|
|
if (response.data.totalItems > 0) {
|
|
const volumeInfo = response.data.items[0].volumeInfo;
|
|
|
|
// Extract data from the API response
|
|
const updatedData = {
|
|
title: volumeInfo.title || null,
|
|
authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : null,
|
|
publishedDate: volumeInfo.publishedDate || null,
|
|
description: volumeInfo.description || null,
|
|
number_of_pages: volumeInfo.pageCount || null,
|
|
publishers: volumeInfo.publisher || null,
|
|
subjects: volumeInfo.categories ? volumeInfo.categories.join(', ') : null,
|
|
cover_small: volumeInfo.imageLinks ? volumeInfo.imageLinks.smallThumbnail : null,
|
|
cover_medium: volumeInfo.imageLinks ? volumeInfo.imageLinks.thumbnail : null,
|
|
cover_large: volumeInfo.imageLinks ? volumeInfo.imageLinks.large : null,
|
|
// Add more fields as needed
|
|
};
|
|
|
|
// Update the book entry in the database
|
|
const book = await Book.findOne({ where: { isbn } });
|
|
|
|
if (book) {
|
|
await book.update(updatedData);
|
|
console.log(`Book data for ISBN ${isbn} updated.`);
|
|
res.json({ success: true, book: updatedData });
|
|
} else {
|
|
res.status(404).json({ error: 'Book not found in local database' });
|
|
}
|
|
} else {
|
|
res.status(404).json({ error: 'No data found in Google Books API' });
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating book data:', error);
|
|
res.status(500).json({ error: 'Failed to update book data' });
|
|
}
|
|
});
|