diff --git a/books.db b/books.db index e9e4c69..31d02b2 100644 Binary files a/books.db and b/books.db differ diff --git a/docker-compose.yml b/docker-compose.yml index 174347a..f1e5354 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: - ADMIN_EMAIL=${ADMIN_EMAIL} - EMAIL_PASSWORD=${EMAIL_PASSWORD} - DOMAIN=${DOMAIN} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} volumes: - .:/usr/src/app - ./data:/usr/src/app/data diff --git a/index.js b/index.js index 3bad1e9..d81cde3 100644 --- a/index.js +++ b/index.js @@ -46,9 +46,9 @@ const authMiddleware = basicAuth({ unauthorizedResponse: (req) => 'Unauthorized' }); -// Apply auth middleware to all non-GET requests +// Apply auth middleware to all non-GET requests except for checkout app.use((req, res, next) => { - if (req.method !== 'GET') { + if (req.method !== 'GET' && !req.path.startsWith('/api/checkout')) { return authMiddleware(req, res, next); } next(); @@ -455,7 +455,7 @@ const checkoutLimiter = rateLimit({ // New endpoint to handle checkout requests app.post('/api/checkout/:isbn', checkoutLimiter, async (req, res) => { const { isbn } = req.params; - const { email } = req.body; + const { email, name } = req.body; // Ensure name is also captured const ip = req.ip; try { @@ -464,6 +464,19 @@ app.post('/api/checkout/:isbn', checkoutLimiter, async (req, res) => { // 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}`); @@ -473,7 +486,21 @@ app.post('/api/checkout/:isbn', checkoutLimiter, async (req, res) => { to: process.env.ADMIN_EMAIL, subject: `Checkout Request for ISBN: ${isbn}`, html: ` -

A checkout request has been made for the book titled "${book.title}" with ISBN: ${isbn} by ${email}. Please review and approve.

+

A checkout request has been made. Please review and approve.

+ + + + + + + + + + + + + +
Book TitleISBNRequestor EmailRequestor Name
${book.title}${isbn}${email}${name}
Approve Deny ` @@ -503,24 +530,52 @@ app.get('/api/approve-checkout/:isbn/:email', async (req, res) => { 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' }); + // Find the checkout record + const checkout = await Checkout.findOne({ + where: { + book_id: book.id, + user_id: (await User.findOne({ where: { email } })).id, + approved: false } - console.log('Approval email sent:', info.response); - res.json({ success: true, message: 'Checkout approved and user notified.' }); }); + + 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: ` +

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' }); } @@ -607,3 +662,37 @@ 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'); + } +}); diff --git a/libraryManager.py b/libraryManager.py index 205dc0d..e4d52c2 100644 --- a/libraryManager.py +++ b/libraryManager.py @@ -99,6 +99,24 @@ def search_books(): else: console.print("Failed to fetch books.", style="bold red") +def list_books_on_loan(): + response = requests.get(f"{API_BASE_URL}/api/books-on-loan", verify=False) + if response.status_code == 200: + loans = response.json() + table = Table(title="Books on Loan") + table.add_column("ISBN", justify="right", style="cyan", no_wrap=True) + table.add_column("Title", style="magenta") + table.add_column("User Name", style="blue") + table.add_column("Checkout Date", style="green") + table.add_column("Return Date", style="yellow") + + for loan in loans: + table.add_row(loan['isbn'], loan['title'], loan['name'], loan['checkout_date'], loan['return_date']) + + console.print(table) + else: + console.print("Failed to fetch books on loan.", style="bold red") + def main(): while True: console.print("\n[bold]Admin Console[/bold]") @@ -107,8 +125,9 @@ def main(): console.print("3. Remove Book") console.print("4. Change Book Status") console.print("5. Search Books") - console.print("6. Exit") - choice = Prompt.ask("Choose an option", choices=["1", "2", "3", "4", "5", "6"], default="6") + console.print("6. List Books on Loan") + console.print("7. Exit") + choice = Prompt.ask("Choose an option", choices=["1", "2", "3", "4", "5", "6", "7"], default="7") if choice == "1": list_books() @@ -121,6 +140,8 @@ def main(): elif choice == "5": search_books() elif choice == "6": + list_books_on_loan() + elif choice == "7": console.print("Exiting...", style="bold yellow") break diff --git a/models.js b/models.js index bf33e24..4962dec 100644 --- a/models.js +++ b/models.js @@ -105,59 +105,62 @@ const Book = sequelize.define('Book', { // Define a table for checkouts const Checkout = sequelize.define('Checkout', { -id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, -}, -book_id: { - type: DataTypes.INTEGER, - references: { - model: Book, - key: 'id' + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + book_id: { + type: DataTypes.INTEGER, + references: { + model: Book, + key: 'id' + } + }, + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + checkout_date: { + type: DataTypes.DATE, + }, + return_date: { + type: DataTypes.DATE, + }, + returned_date: { + type: DataTypes.DATE, + }, + approved: { // New column to track approval status + type: DataTypes.BOOLEAN, + defaultValue: false } -}, -user_id: { - type: DataTypes.INTEGER, - allowNull: false, -}, -checkout_date: { - type: DataTypes.DATE, - defaultValue: Sequelize.NOW -}, -return_date: { - type: DataTypes.DATE, -}, -returned_date: { - type: DataTypes.DATE, -} }, { -tableName: 'checkouts', -timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` + tableName: 'checkouts', + timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` }); // Define User table const User = sequelize.define('User', { -id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true, -}, -name: { - type: DataTypes.TEXT, - allowNull: false, -}, -email: { - type: DataTypes.TEXT, - allowNull: false, -}, -role: { - type: DataTypes.TEXT, - defaultValue: 'user' -} + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + name: { + type: DataTypes.TEXT, + allowNull: false, + }, + email: { + type: DataTypes.TEXT, + allowNull: false, + }, + role: { + type: DataTypes.TEXT, + defaultValue: 'user' + } }, { -tableName: 'users', -timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` + tableName: 'users', + timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` }); // Define the relationships @@ -168,7 +171,7 @@ Book.hasOne(Checkout, { foreignKey: 'book_id' }); Checkout.belongsTo(User, { foreignKey: 'user_id' }); User.hasMany(Checkout, { foreignKey: 'user_id' }); -sequelize.sync({ alter: true }).then(() => { +sequelize.sync().then(() => { console.log('Database & tables synced!'); }); diff --git a/public/index.html b/public/index.html index 0e7776b..4b4e20b 100644 --- a/public/index.html +++ b/public/index.html @@ -7,6 +7,7 @@ +

Ramsey Library

@@ -90,8 +91,12 @@ function setCookie(name, value, days) { // Client-side function to request checkout function requestCheckout(isbn) { - let email = getCookie('lastEmail') || prompt('Please enter your email:'); - let name = getCookie('lastName') || prompt('Please enter your name:'); + let email = getCookie('lastEmail') || ''; + let name = getCookie('lastName') || ''; + + // Prompt the user for email and name, pre-filling with existing values if available + email = prompt('Please enter your email:', email); + name = prompt('Please enter your name:', name); if (!email || !name) { alert('Email and name are required to proceed with checkout.'); @@ -142,6 +147,28 @@ function requestCheckout(isbn) { rows.forEach(row => table.tBodies[0].appendChild(row)); table.setAttribute('data-sort-order', isAscending ? 'desc' : 'asc'); } + + // Dark mode toggle functionality + const darkModeToggle = document.getElementById('dark-mode-toggle'); + const body = document.body; + + // Check for saved user preference, if any, on load + if (localStorage.getItem('darkMode') === 'enabled') { + body.classList.add('dark-mode'); + darkModeToggle.textContent = '☀️'; // Set to sun icon if dark mode is enabled + } + + darkModeToggle.addEventListener('click', () => { + body.classList.toggle('dark-mode'); + // Toggle icon + if (body.classList.contains('dark-mode')) { + darkModeToggle.textContent = '☀️'; // Sun icon + localStorage.setItem('darkMode', 'enabled'); + } else { + darkModeToggle.textContent = '🌙'; // Moon icon + localStorage.setItem('darkMode', 'disabled'); + } + }); diff --git a/public/styles.css b/public/styles.css index e2811a5..673de36 100644 --- a/public/styles.css +++ b/public/styles.css @@ -67,6 +67,43 @@ h1 { background-color: #f0f0f0; } +body.dark-mode #search-bar { + background-color: #333; + color: #fff; + border-color: #444; +} + +#dark-mode-toggle { + position: fixed; + top: 10px; + right: 10px; + width: 40px; + height: 40px; + background-color: rgba(255, 255, 255, 0.5); + border: none; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + transition: background-color 0.3s; + opacity: 0.7; +} + +#dark-mode-toggle:hover { + background-color: rgba(255, 255, 255, 0.7); + opacity: 1; +} + +body.dark-mode #dark-mode-toggle { + background-color: rgba(0, 0, 0, 0.5); +} + +body.dark-mode #dark-mode-toggle:hover { + background-color: rgba(0, 0, 0, 0.7); +} + table { width: 90%; margin: 20px auto; @@ -121,3 +158,43 @@ a:hover { outline-style: solid; outline-width: 2px; } + +body.dark-mode { + background-color: #121212; + color: #ffffff; +} + +body.dark-mode table { + background-color: #2b2b2b; +} + +body.dark-mode th, body.dark-mode td { + border-color: #444; +} + +body.dark-mode button { + background-color: #333; + color: #fff; +} + +body.dark-mode th { + background-color: #3a3a3a; +} + +body.dark-mode td { + background-color: #2b2b2b; +} + +body.dark-mode #book-table-body tr:nth-child(even) { + background-color: #333; +} + +body.dark-mode #book-table-body tr:nth-child(odd) { + background-color: #2b2b2b; +} + +body.dark-mode #book-table-body tr:hover { + background-color: #444; +} + +/* Add more styles as needed for other elements */