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 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' }); } @@ -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 @@ +