Public Access
1
0

fix: server.js location

This commit is contained in:
2025-09-01 17:16:33 -04:00
parent eab43fa45a
commit 8d11c9fef6
2 changed files with 2 additions and 2 deletions

167
server.js Normal file
View File

@@ -0,0 +1,167 @@
const express = require('express');
const cors = require('cors');
const fs = require('fs-extra');
const path = require('path');
const rateLimit = require('express-rate-limit');
const { Mutex } = require('async-mutex');
const app = express();
const port = 3000;
// Create mutexes to prevent race conditions when updating JSON files
const viewsMutex = new Mutex();
const likesMutex = new Mutex();
// trust proxy:
// Used by express-rate-limit to obtain the client's IP address.
// '1' means that the first hop (your reverse proxy) is trusted.
app.set('trust proxy', 1);
// --- Security Middleware ---
// 1. CORS Configuration: Only allow requests from your own domain.
// We create a list of allowed origins, so the API works in both
// development (localhost) and production (the APP_URL).
const allowedOrigins = ['http://localhost:8080'];
if (process.env.APP_URL) {
allowedOrigins.push(process.env.APP_URL);
}
const corsOptions = {
origin: allowedOrigins,
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
// 2. Rate Limiting: Apply to all API requests to prevent abuse.
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
});
app.use('/api/', apiLimiter);
// Path to the file where metadata counts will be stored
const dbPath = path.join(__dirname, '_data', 'views.json');
const likesDbPath = path.join(__dirname, '_data', 'likes.json');
// Ensure the data directory and the .json files exist
fs.ensureFileSync(dbPath);
const initialData = fs.readFileSync(dbPath, 'utf8');
if (initialData.length === 0) {
fs.writeJsonSync(dbPath, {});
}
fs.ensureFileSync(likesDbPath);
const initialLikesData = fs.readFileSync(likesDbPath, 'utf8');
if (initialLikesData.length === 0) {
fs.writeJsonSync(likesDbPath, {});
}
app.use(express.json());
// In production, the server also serves the static site from the `_site` directory.
app.use(express.static('_site'));
// 3. Input Validation Middleware
const validateSlug = (req, res, next) => {
const { slug } = req.params;
// Basic slug validation: allow alphanumeric characters, hyphens, underscores, and dots.
if (!/^[a-z0-9_.-]+$/.test(slug)) {
return res.status(400).json({ error: 'Invalid slug format.' });
}
next();
};
// --- API Endpoints ---
// GET: Fetch the view count for a specific post slug
app.get('/api/views/:slug', validateSlug, async (req, res) => {
try {
const { slug } = req.params;
const views = await fs.readJson(dbPath);
const count = views[slug] || 0;
res.json({ count });
} catch (error) {
console.error('Error reading view count:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
// GET: Fetch the like count for a specific post slug
app.get('/api/likes/:slug', validateSlug, async (req, res) => {
try {
const { slug } = req.params;
const likes = await fs.readJson(likesDbPath);
const count = likes[slug] || 0;
res.json({ count });
} catch (error) {
console.error('Error reading like count:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
// POST: Increment the view count for a specific post slug
app.post('/api/views/:slug', validateSlug, async (req, res) => {
const release = await viewsMutex.acquire();
try {
const { slug } = req.params;
const views = await fs.readJson(dbPath);
views[slug] = (views[slug] || 0) + 1;
await fs.writeJson(dbPath, views);
res.json({ count: views[slug] });
} catch (error) {
console.error('Error updating view count:', error);
res.status(500).json({ error: 'Internal Server Error' });
} finally {
release();
}
});
// POST: Increment the like count for a specific post slug
app.post('/api/likes/:slug', validateSlug, async (req, res) => {
const release = await likesMutex.acquire();
try {
const { slug } = req.params;
const likes = await fs.readJson(likesDbPath);
likes[slug] = (likes[slug] || 0) + 1;
await fs.writeJson(likesDbPath, likes);
res.json({ count: likes[slug] });
} catch (error) {
console.error('Error updating like count:', error);
res.status(500).json({ error: 'Internal Server Error' });
} finally {
release();
}
});
// DELETE: Decrement the like count for a specific post slug (unlike)
app.delete('/api/likes/:slug', validateSlug, async (req, res) => {
const release = await likesMutex.acquire();
try {
const { slug } = req.params;
const likes = await fs.readJson(likesDbPath);
// Ensure the count doesn't go below zero
if (likes[slug] > 0) {
likes[slug] -= 1;
} else {
likes[slug] = 0;
}
await fs.writeJson(likesDbPath, likes);
res.json({ count: likes[slug] });
} catch (error) {
console.error('Error updating like count:', error);
res.status(500).json({ error: 'Internal Server Error' });
} finally {
release();
}
});
// --- 404 Handler (Catch-All) ---
// This MUST be the last route or middleware added.
app.use((req, res, next) => {
res.redirect('/404.html');
});
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});