Final
This commit is contained in:
@@ -1,32 +1,21 @@
|
||||
# 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
|
||||
|
||||
This project implements a PDF document management system with the following features:
|
||||
- **Frontend**: Nginx serving static files and Handlebars templates
|
||||
- **Backend**: Node.js/Express server with custom routing and PDF management
|
||||
- **PDF Management**: PDF validation and secure serving system
|
||||
- **Security**: Path validation and access control for PDF serving
|
||||
- **Database Integration**: SQLite database for book and chapter metadata
|
||||
- **Backend**: Node.js/Express server with custom routing
|
||||
- **JSON Metadata**: Book and chapter metadata stored as JSON files
|
||||
- **Containerization**: Docker containers orchestrated with Docker Compose
|
||||
|
||||
## Features
|
||||
|
||||
### PDF Document Management
|
||||
- **PDF Validation Module**: Comprehensive validation before serving any PDF
|
||||
- **Custom Routing**: Dedicated routing module for book and chapter navigation
|
||||
- **Security Controls**: Path validation and access restrictions
|
||||
- **Database Integration**: SQLite database with books and chapters metadata
|
||||
|
||||
### 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)
|
||||
- **JSON Metadata**: Book and chapter information stored in JSON format
|
||||
- **File Serving**: Direct access to PDF files through designated routes
|
||||
|
||||
### User Interface
|
||||
- **Book Browser**: Navigate through available books and chapters
|
||||
@@ -52,14 +41,13 @@ COS498-HW3/
|
||||
│ ├── Dockerfile # Backend container configuration
|
||||
│ ├── package.json # Node.js dependencies and scripts
|
||||
│ ├── package-lock.json # Locked dependency versions
|
||||
│ ├── server.js # Node.js Express server with PDF management
|
||||
│ ├── server.js # Node.js Express server
|
||||
│ ├── database/
|
||||
│ │ ├── backend.db # SQLite database with book metadata
|
||||
│ │ ├── backend.schema # Database schema definition
|
||||
│ │ └── backend_initial.db # Initial database backup
|
||||
│ │ └── backend.schema # Database schema definition
|
||||
│ └── modules/
|
||||
│ ├── RoutingManager.js # Custom routing module
|
||||
│ └── PDFValidationManager.js # PDF validation and security
|
||||
│ ├── PDFValidationManager.js # PDF validation module
|
||||
│ └── PDFDatabaseManager.js # Database management module
|
||||
└── frontend/
|
||||
├── Dockerfile # Frontend container configuration
|
||||
├── default.conf # Nginx configuration
|
||||
@@ -74,7 +62,9 @@ COS498-HW3/
|
||||
│ └── WaysOfTheWorld-Strayer/ # Sample textbook (git submodule)
|
||||
│ ├── README.md # Book information
|
||||
│ ├── .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/
|
||||
└── main.css # Application stylesheet
|
||||
```
|
||||
|
||||
Binary file not shown.
@@ -13,30 +13,4 @@ CREATE TABLE IF NOT EXISTS "chapters" (
|
||||
"book_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY("id" AUTOINCREMENT),
|
||||
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 path = require('path');
|
||||
|
||||
/**
|
||||
* PDF Validation Module - Validates PDF existence and access permissions
|
||||
* Ensures only authorized access to PDFs within designated folders
|
||||
*/
|
||||
// Configuration Constants
|
||||
const BOOK_PATH = "../frontend/public/books";
|
||||
|
||||
// PDF Validation Module - Validates PDF existence and access permissions
|
||||
// Ensures only authorized access to PDFs within designated folders
|
||||
class PDFValidationManager {
|
||||
constructor(baseDir = '/app/frontend/public/books') {
|
||||
this.baseDir = path.resolve(baseDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if a requested PDF document exists and is accessible
|
||||
*/
|
||||
async validatePDF(bookName, filename) {
|
||||
|
||||
// Validate if a requested PDF document exists and is accessible
|
||||
static async validatePDF(bookName, filename) {
|
||||
try {
|
||||
// Construct file path
|
||||
const filePath = path.resolve(this.baseDir, bookName, filename)
|
||||
|
||||
// Check if file exists and get file stats
|
||||
const fileStats = await fs.stat(filePath);
|
||||
if (!fileStats.isFile()) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'File not found or is not a regular file',
|
||||
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
|
||||
};
|
||||
const filePath = path.resolve(BOOK_PATH, bookName, filename)
|
||||
const file = await fs.stat(filePath);
|
||||
if (file.isFile()) {
|
||||
return filePath;
|
||||
} else {
|
||||
throw new Error('Invalid PDF request');
|
||||
}
|
||||
} catch {
|
||||
throw new Error('Invalid PDF request');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+147
-141
@@ -1,154 +1,160 @@
|
||||
// Helper functions for server-side rendering
|
||||
function loadBooks(db) {
|
||||
return new Promise((resolve, reject) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Imports
|
||||
const hbs = require('hbs');
|
||||
const express = require('express');
|
||||
|
||||
function loadChapters(db, 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
|
||||
`;
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Local Module Imports
|
||||
const PDFValidationManager = require('./PDFValidationManager');
|
||||
const PDFDatabaseManager = require('./PDFDatabaseManager');
|
||||
|
||||
// Page Route handlers
|
||||
async function homePage(db, req, res) {
|
||||
try {
|
||||
const books = await loadBooks(db);
|
||||
res.render('index', {
|
||||
title: 'Book Viewer',
|
||||
books: books,
|
||||
selectedBook: null,
|
||||
selectedChapter: null,
|
||||
chapters: []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading books for home page:', error);
|
||||
res.render('index', {
|
||||
title: 'Book Viewer',
|
||||
books: [],
|
||||
selectedBook: null,
|
||||
selectedChapter: null,
|
||||
chapters: [],
|
||||
error: 'Failed to load books'
|
||||
class RoutingManager {
|
||||
constructor(app) {
|
||||
this.databaseManager = new PDFDatabaseManager();
|
||||
this.db = this.databaseManager.db;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
// Setup Routing Manager
|
||||
setup() {
|
||||
this.setupHandlebars();
|
||||
this.middleware();
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
// Setup Handlebars
|
||||
setupHandlebars() {
|
||||
// Handlebars - Views
|
||||
this.app.set('view engine', 'hbs');
|
||||
this.app.set('views', '../frontend/views');
|
||||
// Handlebars - Partials
|
||||
hbs.registerPartials('../frontend/partials');
|
||||
// Handlebars Helpers
|
||||
hbs.registerHelper('eq', function(a, b) {
|
||||
return a === b;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Render page with specific book
|
||||
async function bookPage(db, req, res) {
|
||||
const bookName = req.params.bookName;
|
||||
|
||||
try {
|
||||
const [books, chapters] = await Promise.all([
|
||||
loadBooks(db),
|
||||
loadChapters(db, bookName)
|
||||
]);
|
||||
const selectedBook = books.find(book => book.name === bookName);
|
||||
|
||||
if (!selectedBook) {
|
||||
return res.redirect('/');
|
||||
// Middleware for Application
|
||||
middleware() {
|
||||
this.app.use(express.static('../frontend/public'));
|
||||
this.app.use(express.json());
|
||||
// this.app.use(cookieParser());
|
||||
}
|
||||
|
||||
// Setup Page Routes
|
||||
setupRoutes() {
|
||||
this.app.get('/', async (req, res) => {
|
||||
await this.homePage(req, res);
|
||||
});
|
||||
|
||||
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
|
||||
async function chapterPage(db, req, res) {
|
||||
const bookName = req.params.bookName;
|
||||
const chapterFile = decodeURIComponent(req.params.chapterFile);
|
||||
|
||||
try {
|
||||
const [books, chapters] = await Promise.all([
|
||||
loadBooks(db),
|
||||
loadChapters(db, bookName)
|
||||
]);
|
||||
const selectedBook = books.find(book => book.name === bookName);
|
||||
const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile);
|
||||
|
||||
if (!selectedBook || !selectedChapter) {
|
||||
return res.redirect('/');
|
||||
// Book Page Handler
|
||||
async bookPage(req, res) {
|
||||
const bookName = req.params.bookName;
|
||||
try {
|
||||
const [books, chapters] = await Promise.all([
|
||||
this.databaseManager.loadBooks(),
|
||||
this.databaseManager.loadChapters(bookName)
|
||||
]);
|
||||
const selectedBook = books.find(book => book.name === bookName);
|
||||
|
||||
if (!selectedBook) {
|
||||
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', {
|
||||
title: 'Book Viewer',
|
||||
books: books,
|
||||
selectedBook: selectedBook,
|
||||
selectedChapter: selectedChapter,
|
||||
chapters: chapters
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error loading chapter page:', error);
|
||||
res.redirect('/');
|
||||
try {
|
||||
const [books, chapters] = await Promise.all([
|
||||
this.databaseManager.loadBooks(),
|
||||
this.databaseManager.loadChapters(bookName)
|
||||
]);
|
||||
const selectedBook = books.find(book => book.name === bookName);
|
||||
const selectedChapter = chapters.find(chapter => chapter.filename === chapterFile);
|
||||
|
||||
if (!selectedBook || !selectedChapter) {
|
||||
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
|
||||
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
|
||||
};
|
||||
module.exports = RoutingManager;
|
||||
+5
-75
@@ -1,85 +1,15 @@
|
||||
// Imports
|
||||
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 PORT = process.env.PORT || 3000;
|
||||
|
||||
// Handlebars
|
||||
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
|
||||
// Local Module Imports
|
||||
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
|
||||
RoutingManager.setupRoutes(app, db);
|
||||
|
||||
// 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' });
|
||||
}
|
||||
});
|
||||
|
||||
const routingManager = new RoutingManager(app);
|
||||
routingManager.setup();
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{#if chapters}}
|
||||
{{#each chapters}}
|
||||
<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>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
|
||||
Submodule frontend/public/books/WaysOfTheWorld-Strayer updated: e837c9f329...cc33fa50ef
@@ -162,6 +162,7 @@ body {
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.95rem;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.chapter-item a {
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<!-- This content will be injected into the layout template -->
|
||||
<!-- The main functionality is handled by the layout template and JavaScript -->
|
||||
<!-- Intentionally left empty to inject dynamic content via Handlebars in layout -->
|
||||
Reference in New Issue
Block a user