bookManagement/index.js
knight 3de9f3d8ee Refactor book management system:
- Updated .gitignore to exclude node_modules directory.
- Enhanced index.js with new endpoints for book fetching and checkout requests, integrating nodemailer and express-rate-limit for email notifications and request management.
- Added functionality to confirm book presence in the library and improved error handling for external book sources.
- Updated package.json and package-lock.json to include new dependencies (nodemailer, express-rate-limit) and their respective versions.
- Modified public HTML and JavaScript files to support new features, including a confirm mode for book scanning and improved UI elements.
- Updated styles for better user experience in the library interface.
2024-12-11 09:41:22 -05:00

594 lines
22 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 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')));
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'],
});
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 or exit the loop if necessary
break;
} else {
console.error(`Error fetching data for ISBN: ${isbn}`, error.message);
}
}
// Optional: Delay to handle 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(`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 });
}
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Failed to fetch book data' });
}
});
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('/locations', async (req, res) => {
try {
const locations = await Location.findAll();
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 } = req.body;
const ip = req.ip;
try {
const book = await Book.findOne({ where: { isbn } });
if (book) {
// Update book status to Pending
await book.update({ status: 'Pending' });
// 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 for the book titled "${book.title}" with ISBN: ${isbn} by ${email}. Please review and approve.</p>
<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') {
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}`,
text: `Your checkout request for the book with ISBN: ${isbn} has been approved.`
};
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: '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.');
});
}
const httpsServer = https.createServer(credentials, app);
httpsServer.listen(PORT, () => {
console.log(`HTTPS Server running on https://localhost:${PORT}`);
});