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: `
A checkout request has been made. Please review and approve.
| Book Title | ISBN | Requestor Email | Requestor Name |
|---|---|---|---|
| ${book.title} | ${isbn} | ${email} | ${name} |
Your checkout request for the book with ISBN: ${isbn} has been approved.
Checkout Date: ${checkoutDate.toDateString()}
Return Date: ${returnDate.toDateString()}
` }; 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' }); } });