Add basic authentication to non-GET requests and update library management features

- Integrated express-basic-auth middleware in index.js to secure non-GET routes with basic authentication.
- Updated libraryManager.py to use HTTPBasicAuth for API requests, enhancing security for book management operations.
- Modified public/index.html to improve the user interface with a new search feature and dynamic book table.
- Removed obsolete public/library.html file to streamline the project structure.
- Updated package.json and package-lock.json to include express-basic-auth as a new dependency.
This commit is contained in:
knight 2024-12-11 10:01:51 -05:00
parent 3de9f3d8ee
commit a2a485dd8e
10 changed files with 283 additions and 176 deletions

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
# Use an official Node.js runtime as a parent image
FROM node:14
# Set the working directory in the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install any needed packages specified in package.json
RUN npm install
# Copy the rest of the application code to the working directory
COPY . .
# Expose the port the app runs on
EXPOSE 3000
# Define environment variable
ENV NODE_ENV=production
# Run the application
CMD ["node", "index.js"]

BIN
books.db

Binary file not shown.

20
docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
version: '3.8'
services:
library-manager:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- GOOGLE_BOOKS_API_KEY=${GOOGLE_BOOKS_API_KEY}
- ADMIN_EMAIL=${ADMIN_EMAIL}
- EMAIL_PASSWORD=${EMAIL_PASSWORD}
- DOMAIN=${DOMAIN}
volumes:
- .:/usr/src/app
- ./data:/usr/src/app/data

View File

@ -7,6 +7,7 @@ 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 {
@ -38,6 +39,21 @@ 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
app.use((req, res, next) => {
if (req.method !== 'GET') {
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}`);

View File

@ -3,10 +3,15 @@ from rich.console import Console
from rich.prompt import Prompt
from rich.table import Table
from fuzzywuzzy import process
from requests.auth import HTTPBasicAuth
API_BASE_URL = "https://localhost:3000" # Replace with your actual API base URL
console = Console()
# Use environment variables or a secure method to store credentials
USERNAME = 'admin'
PASSWORD = 'library@123' # Replace with your actual password
def list_books():
response = requests.get(f"{API_BASE_URL}/api/books-with-images", verify=False)
if response.status_code == 200:
@ -30,7 +35,12 @@ def add_book():
"title": title,
"authors": authors
}
response = requests.post(f"{API_BASE_URL}/store-book", json=data, verify=False)
response = requests.post(
f"{API_BASE_URL}/store-book",
json=data,
verify=False,
auth=HTTPBasicAuth(USERNAME, PASSWORD)
)
if response.status_code == 200:
console.print("Book added successfully.", style="bold green")
else:
@ -38,7 +48,11 @@ def add_book():
def remove_book():
isbn = Prompt.ask("Enter ISBN of the book to remove")
response = requests.delete(f"{API_BASE_URL}/book/{isbn}", verify=False)
response = requests.delete(
f"{API_BASE_URL}/book/{isbn}",
verify=False,
auth=HTTPBasicAuth(USERNAME, PASSWORD)
)
if response.status_code == 200:
console.print("Book removed successfully.", style="bold green")
else:
@ -48,7 +62,12 @@ def change_book_status():
isbn = Prompt.ask("Enter ISBN of the book to change status")
status = Prompt.ask("Enter new status (e.g., Available, Checked Out)")
data = {"status": status}
response = requests.put(f"{API_BASE_URL}/book/{isbn}", json=data, verify=False)
response = requests.put(
f"{API_BASE_URL}/book/{isbn}",
json=data,
verify=False,
auth=HTTPBasicAuth(USERNAME, PASSWORD)
)
if response.status_code == 200:
console.print("Book status updated successfully.", style="bold green")
else:

28
package-lock.json generated
View File

@ -12,6 +12,7 @@
"axios": "^1.7.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"express-rate-limit": "^7.4.1",
"nodemailer": "^6.9.16",
"sequelize": "^6.37.3",
@ -275,6 +276,24 @@
],
"license": "MIT"
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/basic-auth/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -851,6 +870,15 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/express-basic-auth": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz",
"integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==",
"license": "MIT",
"dependencies": {
"basic-auth": "^2.0.1"
}
},
"node_modules/express-rate-limit": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz",

View File

@ -14,6 +14,7 @@
"axios": "^1.7.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"express-rate-limit": "^7.4.1",
"nodemailer": "^6.9.16",
"sequelize": "^6.37.3",

View File

@ -3,34 +3,145 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ISBN Scanner and Title Lookup</title>
<title>Ramsey Library</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Scan a Book ISBN or Search by Title</h1>
<div>
<label for="camera-select">Select Camera:</label>
<select id="camera-select"></select>
<!-- Confirm mode toggle -->
<input type="checkbox" id="confirm-mode">
<label for="confirm-mode">Confirm mode</label>s
</div>
<div id="interactive" class="viewport"></div>
<div>
<h2>Or search for a book by title</h2>
<input type="text" id="title-input" placeholder="Enter book title">
<button id="search-title">Search by Title</button>
</div>
<div id="book-info"></div>
<div id="prompt">
<p id="prompt-message"></p>
<p id="book-title"></p>
<p id="book-author"></p>
<p id="book-desc"></p>
<button id="confirm">Add to Database</button>
<button id="edit-title">No, I'll add a title</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script src="script.js"></script>
<h1>Ramsey Library</h1>
<input type="text" id="search-bar" placeholder="Search by title, author, publisher, etc.">
<table id="book-table">
<thead>
<tr>
<th onclick="sortTable(0)">Title</th>
<th onclick="sortTable(1)">Authors</th>
<th onclick="sortTable(2)">Publisher</th>
<th onclick="sortTable(3)">Published Date</th>
<th onclick="sortTable(4)">ISBN</th>
<th onclick="sortTable(5)">Status</th>
<th>Checkout</th>
</tr>
</thead>
<tbody id="book-table-body">
<!-- Book rows will be inserted here -->
</tbody>
</table>
<script>
// Fetch all books when the page loads
window.onload = fetchBooks('');
document.getElementById('search-bar').addEventListener('input', function() {
fetchBooks(this.value);
});
function fetchBooks(query = '') {
let url = '/search-title';
if (query) {
url += `?title=${encodeURIComponent(query)}&internalOnly=true`;
}
fetch(url)
.then(response => response.json())
.then(data => {
if (data.results) {
displayBooks(data.results);
} else {
document.getElementById('book-table-body').innerHTML = '<tr><td colspan="5">No books found</td></tr>';
}
})
.catch(error => console.error('Error fetching books:', error));
}
function displayBooks(books) {
console.debug('Books:', books);
const tbody = document.getElementById('book-table-body');
tbody.innerHTML = '';
books.forEach(book => {
const row = document.createElement('tr');
const isDisabled = book.status === 'Checked Out' ? 'disabled' : '';
row.innerHTML = `
<td><a href="/book/${book.isbn}" target="_blank">${book.title || ''}</a></td>
<td>${book.authors ? book.authors : ''}</td>
<td>${book.publishers || ''}</td>
<td>${book.publishedDate || ''}</td>
<td>${book.isbn || ''}</td>
<td>${book.status || ''}</td>
<td><button id="checkout-btn-${book.isbn}" onclick="requestCheckout('${book.isbn}')" ${isDisabled}>Checkout</button></td>
`;
tbody.appendChild(row);
});
}
// 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) {
let email = getCookie('lastEmail') || prompt('Please enter your email:');
let name = getCookie('lastName') || prompt('Please enter your name:');
if (!email || !name) {
alert('Email and name are required to proceed with checkout.');
return;
}
// Store the email and name in cookies for future use
setCookie('lastEmail', email, 7); // Store for 7 days
setCookie('lastName', name, 7); // Store for 7 days
fetch(`/api/checkout/${isbn}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, name })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Checkout request sent successfully.');
} else {
alert(data.error || 'Failed to send checkout request.');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while sending the checkout request.');
});
}
function sortTable(columnIndex) {
const table = document.getElementById('book-table');
const rows = Array.from(table.rows).slice(1);
const isAscending = table.getAttribute('data-sort-order') === 'asc';
const direction = isAscending ? 1 : -1;
rows.sort((a, b) => {
const aText = a.cells[columnIndex].innerText.toLowerCase();
const bText = b.cells[columnIndex].innerText.toLowerCase();
if (aText < bText) return -1 * direction;
if (aText > bText) return 1 * direction;
return 0;
});
rows.forEach(row => table.tBodies[0].appendChild(row));
table.setAttribute('data-sort-order', isAscending ? 'desc' : 'asc');
}
</script>
</body>
</html>

View File

@ -1,147 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ramsey Library</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Ramsey Library</h1>
<input type="text" id="search-bar" placeholder="Search by title, author, publisher, etc.">
<table id="book-table">
<thead>
<tr>
<th onclick="sortTable(0)">Title</th>
<th onclick="sortTable(1)">Authors</th>
<th onclick="sortTable(2)">Publisher</th>
<th onclick="sortTable(3)">Published Date</th>
<th onclick="sortTable(4)">ISBN</th>
<th onclick="sortTable(5)">Status</th>
<th>Checkout</th>
</tr>
</thead>
<tbody id="book-table-body">
<!-- Book rows will be inserted here -->
</tbody>
</table>
<script>
// Fetch all books when the page loads
window.onload = fetchBooks('');
document.getElementById('search-bar').addEventListener('input', function() {
fetchBooks(this.value);
});
function fetchBooks(query = '') {
let url = '/search-title';
if (query) {
url += `?title=${encodeURIComponent(query)}&internalOnly=true`;
}
fetch(url)
.then(response => response.json())
.then(data => {
if (data.results) {
displayBooks(data.results);
} else {
document.getElementById('book-table-body').innerHTML = '<tr><td colspan="5">No books found</td></tr>';
}
})
.catch(error => console.error('Error fetching books:', error));
}
function displayBooks(books) {
console.debug('Books:', books);
const tbody = document.getElementById('book-table-body');
tbody.innerHTML = '';
books.forEach(book => {
const row = document.createElement('tr');
const isDisabled = book.status === 'Checked Out' ? 'disabled' : '';
row.innerHTML = `
<td><a href="/book/${book.isbn}" target="_blank">${book.title || ''}</a></td>
<td>${book.authors ? book.authors : ''}</td>
<td>${book.publishers || ''}</td>
<td>${book.publishedDate || ''}</td>
<td>${book.isbn || ''}</td>
<td>${book.status || ''}</td>
<td><button id="checkout-btn-${book.isbn}" onclick="requestCheckout('${book.isbn}')" ${isDisabled}>Checkout</button></td>
`;
tbody.appendChild(row);
});
}
// 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) {
let email = getCookie('lastEmail') || prompt('Please enter your email:');
let name = getCookie('lastName') || prompt('Please enter your name:');
if (!email || !name) {
alert('Email and name are required to proceed with checkout.');
return;
}
// Store the email and name in cookies for future use
setCookie('lastEmail', email, 7); // Store for 7 days
setCookie('lastName', name, 7); // Store for 7 days
fetch(`/api/checkout/${isbn}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ email, name })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Checkout request sent successfully.');
} else {
alert(data.error || 'Failed to send checkout request.');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while sending the checkout request.');
});
}
function sortTable(columnIndex) {
const table = document.getElementById('book-table');
const rows = Array.from(table.rows).slice(1);
const isAscending = table.getAttribute('data-sort-order') === 'asc';
const direction = isAscending ? 1 : -1;
rows.sort((a, b) => {
const aText = a.cells[columnIndex].innerText.toLowerCase();
const bText = b.cells[columnIndex].innerText.toLowerCase();
if (aText < bText) return -1 * direction;
if (aText > bText) return 1 * direction;
return 0;
});
rows.forEach(row => table.tBodies[0].appendChild(row));
table.setAttribute('data-sort-order', isAscending ? 'desc' : 'asc');
}
</script>
</body>
</html>

36
public/scanner.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ISBN Scanner and Title Lookup</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Scan a Book ISBN or Search by Title</h1>
<div>
<label for="camera-select">Select Camera:</label>
<select id="camera-select"></select>
<!-- Confirm mode toggle -->
<input type="checkbox" id="confirm-mode">
<label for="confirm-mode">Confirm mode</label>s
</div>
<div id="interactive" class="viewport"></div>
<div>
<h2>Or search for a book by title</h2>
<input type="text" id="title-input" placeholder="Enter book title">
<button id="search-title">Search by Title</button>
</div>
<div id="book-info"></div>
<div id="prompt">
<p id="prompt-message"></p>
<p id="book-title"></p>
<p id="book-author"></p>
<p id="book-desc"></p>
<button id="confirm">Add to Database</button>
<button id="edit-title">No, I'll add a title</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script src="script.js"></script>
</body>
</html>