Final
This commit is contained in:
@@ -1,32 +1,21 @@
|
|||||||
# COS498 HW3: PDF Document Management System
|
# COS498 HW3: PDF Document Management System
|
||||||
|
|
||||||
This is Homework 3 for **COS498: Server Side Programming Languages** that demonstrates a PDF document management system with custom routing and validation modules.
|
This is Homework 3 for **COS498: Server Side Programming Languages** that demonstrates a PDF document management system with custom routing.
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
This project implements a PDF document management system with the following features:
|
This project implements a PDF document management system with the following features:
|
||||||
- **Frontend**: Nginx serving static files and Handlebars templates
|
- **Frontend**: Nginx serving static files and Handlebars templates
|
||||||
- **Backend**: Node.js/Express server with custom routing and PDF management
|
- **Backend**: Node.js/Express server with custom routing
|
||||||
- **PDF Management**: PDF validation and secure serving system
|
- **JSON Metadata**: Book and chapter metadata stored as JSON files
|
||||||
- **Security**: Path validation and access control for PDF serving
|
|
||||||
- **Database Integration**: SQLite database for book and chapter metadata
|
|
||||||
- **Containerization**: Docker containers orchestrated with Docker Compose
|
- **Containerization**: Docker containers orchestrated with Docker Compose
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### PDF Document Management
|
### PDF Document Management
|
||||||
- **PDF Validation Module**: Comprehensive validation before serving any PDF
|
|
||||||
- **Custom Routing**: Dedicated routing module for book and chapter navigation
|
- **Custom Routing**: Dedicated routing module for book and chapter navigation
|
||||||
- **Security Controls**: Path validation and access restrictions
|
- **JSON Metadata**: Book and chapter information stored in JSON format
|
||||||
- **Database Integration**: SQLite database with books and chapters metadata
|
- **File Serving**: Direct access to PDF files through designated routes
|
||||||
|
|
||||||
### PDF Validation System
|
|
||||||
- **File Existence Checks**: Validates PDF files exist before serving
|
|
||||||
- **Path Security**: Prevents access outside designated directories
|
|
||||||
- **Input Validation**: Sanitizes book names and filenames
|
|
||||||
- **Extension Validation**: Only allows `.pdf` files
|
|
||||||
- **File Size Limits**: Enforces maximum file size restrictions
|
|
||||||
- **Error Responses**: Appropriate HTTP status codes (400, 403, 404, 413, 500)
|
|
||||||
|
|
||||||
### User Interface
|
### User Interface
|
||||||
- **Book Browser**: Navigate through available books and chapters
|
- **Book Browser**: Navigate through available books and chapters
|
||||||
@@ -52,14 +41,13 @@ COS498-HW3/
|
|||||||
│ ├── Dockerfile # Backend container configuration
|
│ ├── Dockerfile # Backend container configuration
|
||||||
│ ├── package.json # Node.js dependencies and scripts
|
│ ├── package.json # Node.js dependencies and scripts
|
||||||
│ ├── package-lock.json # Locked dependency versions
|
│ ├── package-lock.json # Locked dependency versions
|
||||||
│ ├── server.js # Node.js Express server with PDF management
|
│ ├── server.js # Node.js Express server
|
||||||
│ ├── database/
|
│ ├── database/
|
||||||
│ │ ├── backend.db # SQLite database with book metadata
|
│ │ └── backend.schema # Database schema definition
|
||||||
│ │ ├── backend.schema # Database schema definition
|
|
||||||
│ │ └── backend_initial.db # Initial database backup
|
|
||||||
│ └── modules/
|
│ └── modules/
|
||||||
│ ├── RoutingManager.js # Custom routing module
|
│ ├── RoutingManager.js # Custom routing module
|
||||||
│ └── PDFValidationManager.js # PDF validation and security
|
│ ├── PDFValidationManager.js # PDF validation module
|
||||||
|
│ └── PDFDatabaseManager.js # Database management module
|
||||||
└── frontend/
|
└── frontend/
|
||||||
├── Dockerfile # Frontend container configuration
|
├── Dockerfile # Frontend container configuration
|
||||||
├── default.conf # Nginx configuration
|
├── default.conf # Nginx configuration
|
||||||
@@ -74,7 +62,9 @@ COS498-HW3/
|
|||||||
│ └── WaysOfTheWorld-Strayer/ # Sample textbook (git submodule)
|
│ └── WaysOfTheWorld-Strayer/ # Sample textbook (git submodule)
|
||||||
│ ├── README.md # Book information
|
│ ├── README.md # Book information
|
||||||
│ ├── .git # Submodule repository metadata
|
│ ├── .git # Submodule repository metadata
|
||||||
│ └── chapter1-23.pdf # 23 chapter PDF files
|
│ ├── bookMetadata.json # Book metadata
|
||||||
|
│ ├── chapter1-23.json # Chapter metadata files
|
||||||
|
│ └── chapter1-23.pdf # 23 chapter PDF files
|
||||||
└── styles/
|
└── styles/
|
||||||
└── main.css # Application stylesheet
|
└── main.css # Application stylesheet
|
||||||
```
|
```
|
||||||
|
|||||||
Binary file not shown.
@@ -13,30 +13,4 @@ CREATE TABLE IF NOT EXISTS "chapters" (
|
|||||||
"book_id" INTEGER NOT NULL,
|
"book_id" INTEGER NOT NULL,
|
||||||
PRIMARY KEY("id" AUTOINCREMENT),
|
PRIMARY KEY("id" AUTOINCREMENT),
|
||||||
FOREIGN KEY("book_id") REFERENCES "books"("id")
|
FOREIGN KEY("book_id") REFERENCES "books"("id")
|
||||||
);
|
);
|
||||||
INSERT INTO chapters
|
|
||||||
(chapter_number, display_name, filename, book_id)
|
|
||||||
VALUES
|
|
||||||
(1, "First Peoples; First Farmers", "chapter1.pdf", 1),
|
|
||||||
(2, "First Civilizations", "chapter2.pdf", 1),
|
|
||||||
(3, "State and Empire in Eurasia / North Africa", "chapter3.pdf", 1),
|
|
||||||
(4, "Culture and Religion in Eurasia / North Africa", "chapter4.pdf", 1),
|
|
||||||
(5, "Society and Inequality in Eurasia / North Africa", "chapter5.pdf", 1),
|
|
||||||
(6, "Commonalities and Variations", "chapter6.pdf", 1),
|
|
||||||
(7, "Commerce and Culture", "chapter7.pdf", 1),
|
|
||||||
(8, "China and the World", "chapter8.pdf", 1),
|
|
||||||
(9, "The Worlds Of Island", "chapter9.pdf", 1),
|
|
||||||
(10, "The Worlds of Christendom", "chapter10.pdf", 1),
|
|
||||||
(11, "Pastoral Peoples on the Global Stage", "chapter11.pdf", 1),
|
|
||||||
(12, "The Worlds of the Fifteenth Century", "chapter12.pdf", 1),
|
|
||||||
(13, "Political Transformations", "chapter13.pdf", 1),
|
|
||||||
(14, "Economic Transformations", "chapter14.pdf", 1),
|
|
||||||
(15, "Cultural Transformations", "chapter15.pdf", 1),
|
|
||||||
(16, "Atlantic Revolutions, Global Echos", "chapter16.pdf", 1),
|
|
||||||
(17, "Revolutions of Industrialization", "chapter17.pdf", 1),
|
|
||||||
(18, "Colonial Encounters in Asia and Africa", "chapter18.pdf", 1),
|
|
||||||
(19, "Empires in Collision", "chapter19.pdf", 1),
|
|
||||||
(20, "Collapse at the Center", "chapter20.pdf", 1),
|
|
||||||
(21, "Revolution, Socialism, and Global Conflict", "chapter21.pdf", 1),
|
|
||||||
(22, "The End of Empire", "chapter22.pdf", 1),
|
|
||||||
(23, "Capitalism and Culture", "chapter23.pdf", 1)
|
|
||||||
Binary file not shown.
@@ -0,0 +1,124 @@
|
|||||||
|
// Imports
|
||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Configuration Constants
|
||||||
|
const DB_PATH = "database/backend.db";
|
||||||
|
const SCHEMA_PATH = "database/backend.schema";
|
||||||
|
const BOOK_PATH = "../frontend/public/books";
|
||||||
|
|
||||||
|
// PDF Database Manager - Manages SQLite database for storing PDF metadata
|
||||||
|
// Also handles scanning book directory and building DB
|
||||||
|
class PDFDatabaseManager {
|
||||||
|
constructor() {
|
||||||
|
if (fs.existsSync(DB_PATH)) {
|
||||||
|
fs.unlinkSync(DB_PATH);
|
||||||
|
}
|
||||||
|
this.db = new sqlite3.Database(DB_PATH);
|
||||||
|
this.createDatabase();
|
||||||
|
this.scanAndPopulateDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scans the books folder and adds all books / chapters to the database
|
||||||
|
async scanAndPopulateDatabase() {
|
||||||
|
const bookFolders = fs.readdirSync(BOOK_PATH);
|
||||||
|
bookFolders.forEach(folder => {
|
||||||
|
const bookDir = path.join(BOOK_PATH, folder);
|
||||||
|
const bookMetadataPath = path.join(bookDir, 'bookMetadata.json');
|
||||||
|
if (fs.existsSync(bookMetadataPath)) {
|
||||||
|
const bookMetadata = JSON.parse(fs.readFileSync(bookMetadataPath, 'utf-8'));
|
||||||
|
this.addBook(bookMetadata);
|
||||||
|
|
||||||
|
const chapterFiles = fs.readdirSync(bookDir).filter(file => file.endsWith('.json') && file !== 'bookMetadata.json');
|
||||||
|
|
||||||
|
chapterFiles.forEach(async chapterFile => {
|
||||||
|
const chapterMetadataPath = path.join(bookDir, chapterFile);
|
||||||
|
const chapterMetadata = JSON.parse(fs.readFileSync(chapterMetadataPath, 'utf-8'));
|
||||||
|
chapterMetadata.book_id = await this.getBookIdByFolderName(folder);
|
||||||
|
this.addChapter(chapterMetadata);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds chapter metadata from FS to db
|
||||||
|
addChapter(chapterMetadata) {
|
||||||
|
let chapterQuery = `INSERT INTO chapters (chapter_number, display_name, filename, book_id) VALUES (?, ?, ?, ?)`;
|
||||||
|
this.db.run(chapterQuery, [chapterMetadata.chapter_number, chapterMetadata.display_name, chapterMetadata.filename, chapterMetadata.book_id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds book metadata from FS to db
|
||||||
|
addBook(bookMetadata) {
|
||||||
|
let bookQuery = `INSERT INTO books (folder_name, display_name, author) VALUES (?, ?, ?)`;
|
||||||
|
this.db.run(bookQuery, [bookMetadata.folder_name, bookMetadata.display_name, bookMetadata.author]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves book ID by folder name
|
||||||
|
getBookIdByFolderName(folderName) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let query = `SELECT id FROM books WHERE folder_name = ?`;
|
||||||
|
this.db.get(query, [folderName], (err, row) => {
|
||||||
|
if (err) {
|
||||||
|
resolve(null);
|
||||||
|
} else {
|
||||||
|
resolve(row ? row.id : null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates the database if it doesn't exist
|
||||||
|
createDatabase() {
|
||||||
|
// Load DB schema from file and initialize database
|
||||||
|
const schema = fs.readFileSync(SCHEMA_PATH, 'utf-8');
|
||||||
|
this.db.exec(schema, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error creating database schema:', err.message);
|
||||||
|
} else {
|
||||||
|
console.log('Database schema created successfully.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all books in the database
|
||||||
|
loadBooks() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const query = 'SELECT id, folder_name, display_name, author FROM books ORDER BY display_name';
|
||||||
|
this.db.all(query, [], (err, rows) => {
|
||||||
|
const books = rows.map(row => ({
|
||||||
|
id: row.id,
|
||||||
|
name: row.folder_name,
|
||||||
|
displayName: row.display_name,
|
||||||
|
author: row.author
|
||||||
|
}));
|
||||||
|
resolve(books);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all chapters for a specific book
|
||||||
|
loadChapters(bookName) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const query = `
|
||||||
|
SELECT c.id, c.chapter_number, c.display_name, c.filename, b.folder_name
|
||||||
|
FROM chapters c
|
||||||
|
JOIN books b ON c.book_id = b.id
|
||||||
|
WHERE b.folder_name = ?
|
||||||
|
ORDER BY c.chapter_number
|
||||||
|
`;
|
||||||
|
this.db.all(query, [bookName], (err, rows) => {
|
||||||
|
const chapters = rows.map(row => ({
|
||||||
|
id: row.id,
|
||||||
|
chapterNumber: row.chapter_number,
|
||||||
|
filename: row.filename,
|
||||||
|
displayName: row.display_name,
|
||||||
|
url: `/pdf/${row.folder_name}/${row.filename}`
|
||||||
|
}));
|
||||||
|
resolve(chapters);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PDFDatabaseManager;
|
||||||
@@ -1,52 +1,25 @@
|
|||||||
|
// Imports
|
||||||
const fs = require('fs').promises;
|
const fs = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
/**
|
// Configuration Constants
|
||||||
* PDF Validation Module - Validates PDF existence and access permissions
|
const BOOK_PATH = "../frontend/public/books";
|
||||||
* Ensures only authorized access to PDFs within designated folders
|
|
||||||
*/
|
// PDF Validation Module - Validates PDF existence and access permissions
|
||||||
|
// Ensures only authorized access to PDFs within designated folders
|
||||||
class PDFValidationManager {
|
class PDFValidationManager {
|
||||||
constructor(baseDir = '/app/frontend/public/books') {
|
// Validate if a requested PDF document exists and is accessible
|
||||||
this.baseDir = path.resolve(baseDir);
|
static async validatePDF(bookName, filename) {
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if a requested PDF document exists and is accessible
|
|
||||||
*/
|
|
||||||
async validatePDF(bookName, filename) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Construct file path
|
const filePath = path.resolve(BOOK_PATH, bookName, filename)
|
||||||
const filePath = path.resolve(this.baseDir, bookName, filename)
|
const file = await fs.stat(filePath);
|
||||||
|
if (file.isFile()) {
|
||||||
// Check if file exists and get file stats
|
return filePath;
|
||||||
const fileStats = await fs.stat(filePath);
|
} else {
|
||||||
if (!fileStats.isFile()) {
|
throw new Error('Invalid PDF request');
|
||||||
return {
|
}
|
||||||
valid: false,
|
} catch {
|
||||||
error: 'File not found or is not a regular file',
|
throw new Error('Invalid PDF request');
|
||||||
statusCode: 404
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// All validations passed
|
|
||||||
return {
|
|
||||||
valid: true,
|
|
||||||
filePath: filePath,
|
|
||||||
fileStats: {
|
|
||||||
size: fileStats.size,
|
|
||||||
lastModified: fileStats.mtime,
|
|
||||||
isFile: fileStats.isFile()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`PDF validation error for ${bookName}/${filename}:`, error);
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
error: 'Internal validation error',
|
|
||||||
statusCode: 500
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+147
-141
@@ -1,154 +1,160 @@
|
|||||||
// Helper functions for server-side rendering
|
// Imports
|
||||||
function loadBooks(db) {
|
const hbs = require('hbs');
|
||||||
return new Promise((resolve, reject) => {
|
const express = require('express');
|
||||||
const query = 'SELECT id, folder_name, display_name, author FROM books ORDER BY display_name';
|
|
||||||
db.all(query, [], (err, rows) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error loading books from database:', err.message);
|
|
||||||
resolve([]);
|
|
||||||
} else {
|
|
||||||
const books = rows.map(row => ({
|
|
||||||
id: row.id,
|
|
||||||
name: row.folder_name,
|
|
||||||
displayName: row.display_name || row.folder_name.replace(/-/g, ' '),
|
|
||||||
author: row.author
|
|
||||||
}));
|
|
||||||
resolve(books);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadChapters(db, bookName) {
|
// Local Module Imports
|
||||||
return new Promise((resolve, reject) => {
|
const PDFValidationManager = require('./PDFValidationManager');
|
||||||
const query = `
|
const PDFDatabaseManager = require('./PDFDatabaseManager');
|
||||||
SELECT c.id, c.chapter_number, c.display_name, c.filename, b.folder_name
|
|
||||||
FROM chapters c
|
|
||||||
JOIN books b ON c.book_id = b.id
|
|
||||||
WHERE b.folder_name = ?
|
|
||||||
ORDER BY c.chapter_number
|
|
||||||
`;
|
|
||||||
db.all(query, [bookName], (err, rows) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error loading chapters from database:', err.message);
|
|
||||||
resolve([]);
|
|
||||||
} else {
|
|
||||||
const chapters = rows.map(row => ({
|
|
||||||
id: row.id,
|
|
||||||
chapterNumber: row.chapter_number,
|
|
||||||
filename: row.filename,
|
|
||||||
displayName: row.display_name,
|
|
||||||
url: `/pdf/${row.folder_name}/${encodeURIComponent(row.filename)}`
|
|
||||||
}));
|
|
||||||
resolve(chapters);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page Route handlers
|
class RoutingManager {
|
||||||
async function homePage(db, req, res) {
|
constructor(app) {
|
||||||
try {
|
this.databaseManager = new PDFDatabaseManager();
|
||||||
const books = await loadBooks(db);
|
this.db = this.databaseManager.db;
|
||||||
res.render('index', {
|
this.app = app;
|
||||||
title: 'Book Viewer',
|
}
|
||||||
books: books,
|
|
||||||
selectedBook: null,
|
// Setup Routing Manager
|
||||||
selectedChapter: null,
|
setup() {
|
||||||
chapters: []
|
this.setupHandlebars();
|
||||||
});
|
this.middleware();
|
||||||
} catch (error) {
|
this.setupRoutes();
|
||||||
console.error('Error loading books for home page:', error);
|
}
|
||||||
res.render('index', {
|
|
||||||
title: 'Book Viewer',
|
// Setup Handlebars
|
||||||
books: [],
|
setupHandlebars() {
|
||||||
selectedBook: null,
|
// Handlebars - Views
|
||||||
selectedChapter: null,
|
this.app.set('view engine', 'hbs');
|
||||||
chapters: [],
|
this.app.set('views', '../frontend/views');
|
||||||
error: 'Failed to load books'
|
// Handlebars - Partials
|
||||||
|
hbs.registerPartials('../frontend/partials');
|
||||||
|
// Handlebars Helpers
|
||||||
|
hbs.registerHelper('eq', function(a, b) {
|
||||||
|
return a === b;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Render page with specific book
|
// Middleware for Application
|
||||||
async function bookPage(db, req, res) {
|
middleware() {
|
||||||
const bookName = req.params.bookName;
|
this.app.use(express.static('../frontend/public'));
|
||||||
|
this.app.use(express.json());
|
||||||
try {
|
// this.app.use(cookieParser());
|
||||||
const [books, chapters] = await Promise.all([
|
}
|
||||||
loadBooks(db),
|
|
||||||
loadChapters(db, bookName)
|
// Setup Page Routes
|
||||||
]);
|
setupRoutes() {
|
||||||
const selectedBook = books.find(book => book.name === bookName);
|
this.app.get('/', async (req, res) => {
|
||||||
|
await this.homePage(req, res);
|
||||||
if (!selectedBook) {
|
});
|
||||||
return res.redirect('/');
|
|
||||||
|
this.app.get('/book/:bookName', async (req, res) => {
|
||||||
|
await this.bookPage(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.get('/book/:bookName/chapter/:chapterFile', async (req, res) => {
|
||||||
|
await this.chapterPage(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.get('/pdf/:bookName/:chapterFile', async (req, res) => {
|
||||||
|
await this.servePDF(req, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Home Page Handler
|
||||||
|
async homePage(req, res) {
|
||||||
|
try {
|
||||||
|
const books = await this.databaseManager.loadBooks();
|
||||||
|
res.render('index', {
|
||||||
|
title: 'Book Viewer',
|
||||||
|
books: books,
|
||||||
|
selectedBook: null,
|
||||||
|
selectedChapter: null,
|
||||||
|
chapters: []
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.render('index', {
|
||||||
|
title: 'Book Viewer',
|
||||||
|
books: [],
|
||||||
|
selectedBook: null,
|
||||||
|
selectedChapter: null,
|
||||||
|
chapters: [],
|
||||||
|
error: 'Failed to load books'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('index', {
|
|
||||||
title: 'Book Viewer',
|
|
||||||
books: books,
|
|
||||||
selectedBook: selectedBook,
|
|
||||||
selectedChapter: null,
|
|
||||||
chapters: chapters
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading book page:', error);
|
|
||||||
res.redirect('/');
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Render page with specific chapter
|
// Book Page Handler
|
||||||
async function chapterPage(db, req, res) {
|
async bookPage(req, res) {
|
||||||
const bookName = req.params.bookName;
|
const bookName = req.params.bookName;
|
||||||
const chapterFile = decodeURIComponent(req.params.chapterFile);
|
try {
|
||||||
|
const [books, chapters] = await Promise.all([
|
||||||
try {
|
this.databaseManager.loadBooks(),
|
||||||
const [books, chapters] = await Promise.all([
|
this.databaseManager.loadChapters(bookName)
|
||||||
loadBooks(db),
|
]);
|
||||||
loadChapters(db, bookName)
|
const selectedBook = books.find(book => book.name === bookName);
|
||||||
]);
|
|
||||||
const selectedBook = books.find(book => book.name === bookName);
|
if (!selectedBook) {
|
||||||
const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile);
|
return res.redirect('/');
|
||||||
|
}
|
||||||
if (!selectedBook || !selectedChapter) {
|
|
||||||
return res.redirect('/');
|
res.render('index', {
|
||||||
|
title: 'Book Viewer',
|
||||||
|
books: books,
|
||||||
|
selectedBook: selectedBook,
|
||||||
|
selectedChapter: null,
|
||||||
|
chapters: chapters
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.redirect('/');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapter Page Handler
|
||||||
|
async chapterPage(req, res) {
|
||||||
|
const bookName = req.params.bookName;
|
||||||
|
const chapterFile = req.params.chapterFile;
|
||||||
|
|
||||||
res.render('index', {
|
try {
|
||||||
title: 'Book Viewer',
|
const [books, chapters] = await Promise.all([
|
||||||
books: books,
|
this.databaseManager.loadBooks(),
|
||||||
selectedBook: selectedBook,
|
this.databaseManager.loadChapters(bookName)
|
||||||
selectedChapter: selectedChapter,
|
]);
|
||||||
chapters: chapters
|
const selectedBook = books.find(book => book.name === bookName);
|
||||||
});
|
const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile);
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading chapter page:', error);
|
if (!selectedBook || !selectedChapter) {
|
||||||
res.redirect('/');
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('index', {
|
||||||
|
title: 'Book Viewer',
|
||||||
|
books: books,
|
||||||
|
selectedBook: selectedBook,
|
||||||
|
selectedChapter: selectedChapter,
|
||||||
|
chapters: chapters
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF Serving Handler
|
||||||
|
async servePDF(req, res) {
|
||||||
|
const { bookName, chapterFile } = req.params;
|
||||||
|
try {
|
||||||
|
// Validate PDF using validation module
|
||||||
|
const filePath = await PDFValidationManager.validatePDF(bookName, chapterFile);
|
||||||
|
|
||||||
|
// Serve the PDF file using sendFile
|
||||||
|
res.sendFile(filePath, (sendErr) => {
|
||||||
|
if (sendErr) {
|
||||||
|
res.status(500).json({ error: 'Error serving PDF file' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Invalid PDF request' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup routes function
|
module.exports = RoutingManager;
|
||||||
function setupRoutes(app, db) {
|
|
||||||
// Page Routes
|
|
||||||
app.get('/', async (req, res) => {
|
|
||||||
await homePage(db, req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/book/:bookName', async (req, res) => {
|
|
||||||
await bookPage(db, req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/book/:bookName/chapter/:chapterFile', async (req, res) => {
|
|
||||||
await chapterPage(db, req, res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
setupRoutes,
|
|
||||||
loadBooks,
|
|
||||||
loadChapters,
|
|
||||||
homePage,
|
|
||||||
bookPage,
|
|
||||||
chapterPage
|
|
||||||
};
|
|
||||||
+5
-75
@@ -1,85 +1,15 @@
|
|||||||
|
// Imports
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const hbs = require('hbs');
|
|
||||||
const cookieParser = require('cookie-parser');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const sqlite3 = require('sqlite3').verbose();
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
// Handlebars
|
// Local Module Imports
|
||||||
app.set('view engine', 'hbs');
|
|
||||||
app.set('views', '../frontend/views');
|
|
||||||
|
|
||||||
// Register partials directory
|
|
||||||
hbs.registerPartials('../frontend/partials');
|
|
||||||
|
|
||||||
// Register Handlebars helpers
|
|
||||||
hbs.registerHelper('eq', function(a, b) {
|
|
||||||
return a === b;
|
|
||||||
});
|
|
||||||
|
|
||||||
hbs.registerHelper('encodeURIComponent', function(str) {
|
|
||||||
return encodeURIComponent(str);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Database setup
|
|
||||||
const dbPath = path.join(__dirname, 'database/backend.db');
|
|
||||||
const db = new sqlite3.Database(dbPath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error opening database:', err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Import routing module
|
|
||||||
const RoutingManager = require('./modules/RoutingManager');
|
const RoutingManager = require('./modules/RoutingManager');
|
||||||
const PDFValidationManager = require('./modules/PDFValidationManager');
|
|
||||||
|
|
||||||
// Initialize PDF validation module
|
|
||||||
const pdfValidator = new PDFValidationManager();
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
// Serve only specific static assets, not the entire public directory
|
|
||||||
app.use(express.static('../frontend/public'));
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(cookieParser());
|
|
||||||
|
|
||||||
// Setup routes using RoutingManager
|
// Setup routes using RoutingManager
|
||||||
RoutingManager.setupRoutes(app, db);
|
const routingManager = new RoutingManager(app);
|
||||||
|
routingManager.setup();
|
||||||
// PDF serving route with validation
|
|
||||||
app.get('/pdf/:bookName/:chapterFile', async (req, res) => {
|
|
||||||
const { bookName, chapterFile } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate PDF using validation module
|
|
||||||
const validation = await pdfValidator.validatePDF(bookName, chapterFile);
|
|
||||||
|
|
||||||
if (!validation.valid) {
|
|
||||||
console.warn(`PDF validation failed: ${validation.error}`);
|
|
||||||
return res.status(validation.statusCode).json({
|
|
||||||
error: validation.error,
|
|
||||||
bookName,
|
|
||||||
chapterFile
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve the PDF file using sendFile
|
|
||||||
res.sendFile(validation.filePath, (sendErr) => {
|
|
||||||
if (sendErr) {
|
|
||||||
console.error(`Error serving PDF file: ${sendErr.message}`);
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.status(500).json({ error: 'Error serving PDF file' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error in PDF serving route:', error);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{#if chapters}}
|
{{#if chapters}}
|
||||||
{{#each chapters}}
|
{{#each chapters}}
|
||||||
<div class="chapter-item {{#if (eq this.filename ../selectedChapter.filename)}}selected{{/if}}" data-chapter-url="{{this.url}}">
|
<div class="chapter-item {{#if (eq this.filename ../selectedChapter.filename)}}selected{{/if}}" data-chapter-url="{{this.url}}">
|
||||||
<a href="/book/{{../selectedBook.name}}/chapter/{{encodeURIComponent this.filename}}">{{this.displayName}}</a>
|
<a href="/book/{{../selectedBook.name}}/chapter/{{this.filename}}">{{this.displayName}}</a>
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
Submodule frontend/public/books/WaysOfTheWorld-Strayer updated: e837c9f329...cc33fa50ef
@@ -162,6 +162,7 @@ body {
|
|||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
width: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chapter-item a {
|
.chapter-item a {
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
<!-- This content will be injected into the layout template -->
|
<!-- Intentionally left empty to inject dynamic content via Handlebars in layout -->
|
||||||
<!-- The main functionality is handled by the layout template and JavaScript -->
|
|
||||||
Reference in New Issue
Block a user