353 lines
8.5 KiB
JavaScript
353 lines
8.5 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;
|
|
|
|
app.use(helmet());
|
|
app.use(
|
|
cors({
|
|
origin: process.env.ALLOWED_ORIGINS?.split(",") || [
|
|
"http://localhost:3000",
|
|
],
|
|
credentials: true,
|
|
}),
|
|
);
|
|
app.use(express.json());
|
|
|
|
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();
|
|
};
|
|
|
|
const DISCOUNT_CODES_PATH = path.join(__dirname, "data", "discountCodes.json");
|
|
const PRODUCTS_PATH = path.join(__dirname, "data", "products.js");
|
|
|
|
let cache = {
|
|
discountCodes: null,
|
|
products: null,
|
|
lastUpdated: {
|
|
discountCodes: null,
|
|
products: null,
|
|
},
|
|
};
|
|
|
|
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");
|
|
const match = data.match(
|
|
/export const products: Product\[\] = (\[[\s\S]*?\]);/,
|
|
);
|
|
if (match) {
|
|
const productsString = match[1]
|
|
.replace(/(\w+):/g, '"$1":')
|
|
.replace(/'/g, '"')
|
|
.replace(/,\s*}/g, "}")
|
|
.replace(/,\s*]/g, "]");
|
|
|
|
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:")
|
|
.replace(/"/g, '"')};
|
|
`;
|
|
await fs.writeFile(PRODUCTS_PATH, jsContent, "utf8");
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Error writing products:", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
|
|
app.get("/health", (req, res) => {
|
|
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
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" });
|
|
}
|
|
});
|
|
|
|
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" });
|
|
}
|
|
|
|
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" });
|
|
}
|
|
});
|
|
|
|
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" });
|
|
}
|
|
|
|
clearCache();
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error("Error deleting discount code:", error);
|
|
res.status(500).json({ error: "Failed to delete discount code" });
|
|
}
|
|
});
|
|
|
|
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" });
|
|
}
|
|
});
|
|
|
|
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" });
|
|
}
|
|
|
|
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" });
|
|
}
|
|
});
|
|
|
|
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,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
app.post("/api/clear-cache", authenticateAdmin, (req, res) => {
|
|
clearCache();
|
|
res.json({ success: true, message: "Cache cleared" });
|
|
});
|
|
|
|
app.use((error, req, res, next) => {
|
|
console.error("Unhandled error:", error);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
});
|
|
|
|
app.use((req, res) => {
|
|
res.status(404).json({ error: "Not found" });
|
|
});
|
|
|
|
const initializeDataDirectory = async () => {
|
|
const dataDir = path.join(__dirname, "data");
|
|
try {
|
|
await fs.access(dataDir);
|
|
} catch {
|
|
await fs.mkdir(dataDir, { recursive: true });
|
|
|
|
try {
|
|
await fs.access(DISCOUNT_CODES_PATH);
|
|
} catch {
|
|
await fs.writeFile(DISCOUNT_CODES_PATH, "{}", "utf8");
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
};
|
|
|
|
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();
|