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 bodyParser = require('body-parser');
const nodemailer = require('nodemailer'); // Add nodemailer for sending emails const nodemailer = require('nodemailer'); // Add nodemailer for sending emails
const rateLimit = require('express-rate-limit'); // Ensure this line is present 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 library = require('./library');
const { const {
@ -38,6 +39,21 @@ app.use(express.json()); // Use built-in body-parser for JSON
// Serve static files from the 'public' directory // Serve static files from the 'public' directory
app.use(express.static(path.join(__dirname, 'public'))); 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) => { app.get('/book/:isbn', async (req, res) => {
const { isbn } = req.params; const { isbn } = req.params;
console.log(`Fetching book data for ISBN: ${isbn}`); 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.prompt import Prompt
from rich.table import Table from rich.table import Table
from fuzzywuzzy import process from fuzzywuzzy import process
from requests.auth import HTTPBasicAuth
API_BASE_URL = "https://localhost:3000" # Replace with your actual API base URL API_BASE_URL = "https://localhost:3000" # Replace with your actual API base URL
console = Console() 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(): def list_books():
response = requests.get(f"{API_BASE_URL}/api/books-with-images", verify=False) response = requests.get(f"{API_BASE_URL}/api/books-with-images", verify=False)
if response.status_code == 200: if response.status_code == 200:
@ -30,7 +35,12 @@ def add_book():
"title": title, "title": title,
"authors": authors "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: if response.status_code == 200:
console.print("Book added successfully.", style="bold green") console.print("Book added successfully.", style="bold green")
else: else:
@ -38,7 +48,11 @@ def add_book():
def remove_book(): def remove_book():
isbn = Prompt.ask("Enter ISBN of the book to remove") 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: if response.status_code == 200:
console.print("Book removed successfully.", style="bold green") console.print("Book removed successfully.", style="bold green")
else: else:
@ -48,7 +62,12 @@ def change_book_status():
isbn = Prompt.ask("Enter ISBN of the book to change status") isbn = Prompt.ask("Enter ISBN of the book to change status")
status = Prompt.ask("Enter new status (e.g., Available, Checked Out)") status = Prompt.ask("Enter new status (e.g., Available, Checked Out)")
data = {"status": status} 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: if response.status_code == 200:
console.print("Book status updated successfully.", style="bold green") console.print("Book status updated successfully.", style="bold green")
else: else:

28
package-lock.json generated
View File

@ -12,6 +12,7 @@
"axios": "^1.7.5", "axios": "^1.7.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"express-rate-limit": "^7.4.1", "express-rate-limit": "^7.4.1",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",
@ -275,6 +276,24 @@
], ],
"license": "MIT" "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": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -851,6 +870,15 @@
"url": "https://opencollective.com/express" "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": { "node_modules/express-rate-limit": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", "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", "axios": "^1.7.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"express-rate-limit": "^7.4.1", "express-rate-limit": "^7.4.1",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"sequelize": "^6.37.3", "sequelize": "^6.37.3",

View File

@ -3,34 +3,145 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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"> <link rel="stylesheet" href="styles.css">
</head> </head>
<body> <body>
<h1>Scan a Book ISBN or Search by Title</h1> <h1>Ramsey Library</h1>
<div> <input type="text" id="search-bar" placeholder="Search by title, author, publisher, etc.">
<label for="camera-select">Select Camera:</label>
<select id="camera-select"></select> <table id="book-table">
<!-- Confirm mode toggle --> <thead>
<input type="checkbox" id="confirm-mode"> <tr>
<label for="confirm-mode">Confirm mode</label>s <th onclick="sortTable(0)">Title</th>
</div> <th onclick="sortTable(1)">Authors</th>
<div id="interactive" class="viewport"></div> <th onclick="sortTable(2)">Publisher</th>
<div> <th onclick="sortTable(3)">Published Date</th>
<h2>Or search for a book by title</h2> <th onclick="sortTable(4)">ISBN</th>
<input type="text" id="title-input" placeholder="Enter book title"> <th onclick="sortTable(5)">Status</th>
<button id="search-title">Search by Title</button> <th>Checkout</th>
</div> </tr>
<div id="book-info"></div> </thead>
<div id="prompt"> <tbody id="book-table-body">
<p id="prompt-message"></p> <!-- Book rows will be inserted here -->
<p id="book-title"></p> </tbody>
<p id="book-author"></p> </table>
<p id="book-desc"></p>
<button id="confirm">Add to Database</button> <script>
<button id="edit-title">No, I'll add a title</button> // Fetch all books when the page loads
</div> window.onload = fetchBooks('');
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script src="script.js"></script> 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> </body>
</html> </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>