assco-backend/server.js
2025-09-28 18:33:16 +01:00

378 lines
9.3 KiB
JavaScript

const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const fs = require("fs").promises;
const path = require("path");
require("dotenv").config();
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(helmet());
app.use(
cors({
origin: process.env.ALLOWED_ORIGINS?.split(",") || [
"http://localhost:3000",
],
credentials: true,
}),
);
app.use(express.json());
// Auth middleware
const authenticateAdmin = (req, res, next) => {
const adminPassword = req.headers.authorization?.replace("Bearer ", "");
if (adminPassword !== process.env.ADMIN_PASSWORD) {
return res.status(401).json({ error: "Unauthorized" });
}
next();
};
// File paths
const DISCOUNT_CODES_PATH = path.join(__dirname, "data", "discountCodes.json");
const PRODUCTS_PATH = path.join(__dirname, "data", "products.js");
// In-memory cache
let cache = {
discountCodes: null,
products: null,
lastUpdated: {
discountCodes: null,
products: null,
},
};
// Helper functions
const clearCache = () => {
cache.discountCodes = null;
cache.products = null;
cache.lastUpdated.discountCodes = null;
cache.lastUpdated.products = null;
};
const readDiscountCodes = async () => {
try {
const data = await fs.readFile(DISCOUNT_CODES_PATH, "utf8");
return JSON.parse(data);
} catch (error) {
console.error("Error reading discount codes:", error);
return {};
}
};
const writeDiscountCodes = async (codes) => {
try {
await fs.writeFile(
DISCOUNT_CODES_PATH,
JSON.stringify(codes, null, 2),
"utf8",
);
return true;
} catch (error) {
console.error("Error writing discount codes:", error);
return false;
}
};
const readProducts = async () => {
try {
const data = await fs.readFile(PRODUCTS_PATH, "utf8");
// Extract the products array from the JS file
const match = data.match(
/export const products: Product\[\] = (\[[\s\S]*?\]);/,
);
if (match) {
// Convert JS object notation to JSON
const productsString = match[1]
.replace(/(\w+):/g, '"$1":') // Quote keys
.replace(/'/g, '"') // Single quotes to double quotes
.replace(/,\s*}/g, "}") // Remove trailing commas
.replace(/,\s*]/g, "]"); // Remove trailing commas in arrays
return JSON.parse(productsString);
}
return [];
} catch (error) {
console.error("Error reading products:", error);
return [];
}
};
const writeProducts = async (products) => {
try {
const jsContent = `import type { Product } from "../types/product";
export const products: Product[] = ${JSON.stringify(products, null, 2)
.replace(/"([^"]+)":/g, "$1:") // Remove quotes from keys
.replace(/"/g, '"')}; // Keep string quotes
`;
await fs.writeFile(PRODUCTS_PATH, jsContent, "utf8");
return true;
} catch (error) {
console.error("Error writing products:", error);
return false;
}
};
// Routes
// Health check
app.get("/health", (req, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// Get discount codes
app.get("/api/discount-codes", async (req, res) => {
try {
if (!cache.discountCodes) {
console.log("Loading discount codes from file...");
cache.discountCodes = await readDiscountCodes();
cache.lastUpdated.discountCodes = new Date();
}
res.json(cache.discountCodes);
} catch (error) {
console.error("Error getting discount codes:", error);
res.status(500).json({ error: "Failed to get discount codes" });
}
});
// Add discount code
app.post("/api/discount-codes", authenticateAdmin, async (req, res) => {
try {
const { code, percentage, expiration, description } = req.body;
if (!code || !percentage) {
return res
.status(400)
.json({ error: "Code and percentage are required" });
}
const discountCodes = await readDiscountCodes();
const newCode = {
percentage: parseInt(percentage),
description: description || "",
};
if (expiration) {
newCode.expiration = expiration;
}
discountCodes[code] = newCode;
const success = await writeDiscountCodes(discountCodes);
if (!success) {
return res.status(500).json({ error: "Failed to save discount code" });
}
// Clear cache to force refresh
clearCache();
res.json({ success: true, code: newCode });
} catch (error) {
console.error("Error adding discount code:", error);
res.status(500).json({ error: "Failed to add discount code" });
}
});
// Delete discount code
app.delete("/api/discount-codes/:code", authenticateAdmin, async (req, res) => {
try {
const { code } = req.params;
const discountCodes = await readDiscountCodes();
if (!discountCodes[code]) {
return res.status(404).json({ error: "Discount code not found" });
}
delete discountCodes[code];
const success = await writeDiscountCodes(discountCodes);
if (!success) {
return res.status(500).json({ error: "Failed to delete discount code" });
}
// Clear cache to force refresh
clearCache();
res.json({ success: true });
} catch (error) {
console.error("Error deleting discount code:", error);
res.status(500).json({ error: "Failed to delete discount code" });
}
});
// Get products
app.get("/api/products", async (req, res) => {
try {
if (!cache.products) {
console.log("Loading products from file...");
cache.products = await readProducts();
cache.lastUpdated.products = new Date();
}
res.json(cache.products);
} catch (error) {
console.error("Error getting products:", error);
res.status(500).json({ error: "Failed to get products" });
}
});
// Update product price
app.put("/api/products/:productId", authenticateAdmin, async (req, res) => {
try {
const { productId } = req.params;
const { pricePerStack } = req.body;
if (pricePerStack === undefined) {
return res.status(400).json({ error: "pricePerStack is required" });
}
const products = await readProducts();
const productIndex = products.findIndex((p) => p.id === productId);
if (productIndex === -1) {
return res.status(404).json({ error: "Product not found" });
}
products[productIndex].pricePerStack = parseFloat(pricePerStack);
const success = await writeProducts(products);
if (!success) {
return res.status(500).json({ error: "Failed to update product" });
}
// Clear cache to force refresh
clearCache();
res.json({ success: true, product: products[productIndex] });
} catch (error) {
console.error("Error updating product:", error);
res.status(500).json({ error: "Failed to update product" });
}
});
// Get cache status (for debugging)
app.get("/api/cache-status", (req, res) => {
res.json({
cache: {
discountCodes: {
cached: !!cache.discountCodes,
lastUpdated: cache.lastUpdated.discountCodes,
},
products: {
cached: !!cache.products,
lastUpdated: cache.lastUpdated.products,
},
},
});
});
// Clear cache endpoint (for debugging)
app.post("/api/clear-cache", authenticateAdmin, (req, res) => {
clearCache();
res.json({ success: true, message: "Cache cleared" });
});
// Error handling middleware
app.use((error, req, res, next) => {
console.error("Unhandled error:", error);
res.status(500).json({ error: "Internal server error" });
});
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: "Not found" });
});
// Ensure data directory exists
const initializeDataDirectory = async () => {
const dataDir = path.join(__dirname, "data");
try {
await fs.access(dataDir);
} catch {
await fs.mkdir(dataDir, { recursive: true });
// Create initial discount codes file if it doesn't exist
try {
await fs.access(DISCOUNT_CODES_PATH);
} catch {
await fs.writeFile(DISCOUNT_CODES_PATH, "{}", "utf8");
}
// Create initial products file if it doesn't exist
try {
await fs.access(PRODUCTS_PATH);
} catch {
const initialProducts = `import type { Product } from "../types/product";
export const products: Product[] = [
{
id: "spruce-log",
name: "Spruce Log",
pricePerStack: 1,
},
{
id: "birch-log",
name: "Birch Log",
pricePerStack: 1,
},
{
id: "oak-log",
name: "Oak Log",
pricePerStack: 1,
},
{
id: "jungle-log",
name: "Jungle Log",
pricePerStack: 1,
},
{
id: "cobblestone",
name: "Cobblestone",
pricePerStack: 1 / 16,
defaultStacksPerDia: 16,
variants: ["Cobblestone", "Stone", "Stone Bricks"],
},
{
id: "sand",
name: "Sand",
pricePerStack: 1,
},
{
id: "gunpowder",
name: "Gunpowder",
pricePerStack: 1 / 4,
defaultStacksPerDia: 4,
},
];
`;
await fs.writeFile(PRODUCTS_PATH, initialProducts, "utf8");
}
}
};
// Start server
const startServer = async () => {
try {
await initializeDataDirectory();
app.listen(PORT, () => {
console.log(`🚀 Data server running on port ${PORT}`);
console.log(`📂 Discount codes: ${DISCOUNT_CODES_PATH}`);
console.log(`📂 Products: ${PRODUCTS_PATH}`);
console.log(
`🔐 Admin password: ${process.env.ADMIN_PASSWORD ? "Set" : "Not set"}`,
);
});
} catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
};
startServer();