Enhance library management system with new features and improvements
- Updated `docker-compose.yml` to include `ADMIN_PASSWORD` environment variable for enhanced security. - Modified `index.js` to allow checkout requests to capture user names and improved email notifications with detailed information. - Added a new endpoint to fetch books currently on loan, providing better tracking of borrowed items. - Implemented a `list_books_on_loan` function in `libraryManager.py` to display books on loan in a formatted table. - Updated `models.js` to include an `approved` column in the `Checkout` model for tracking approval status. - Enhanced the user interface in `public/index.html` with a dark mode toggle and improved checkout request handling. - Updated styles in `public/styles.css` to support dark mode and improve overall aesthetics.
This commit is contained in:
parent
a2a485dd8e
commit
370ad7c107
@ -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
|
||||
|
||||
129
index.js
129
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: `
|
||||
<p>A checkout request has been made for the book titled "${book.title}" with ISBN: ${isbn} by ${email}. Please review and approve.</p>
|
||||
<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>
|
||||
`
|
||||
@ -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: `
|
||||
<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' });
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
97
models.js
97
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!');
|
||||
});
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<button id="dark-mode-toggle">🌙</button>
|
||||
<h1>Ramsey Library</h1>
|
||||
<input type="text" id="search-bar" placeholder="Search by title, author, publisher, etc.">
|
||||
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -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 */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user