This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
PORT=5000
|
||||
DB_URI=mongodb://192.168.1.7:27017/
|
||||
NODE_ENV=production
|
||||
|
||||
# Admin dashboard (used once to seed the admin account on first boot)
|
||||
ADMIN_USERNAME=
|
||||
ADMIN_PASSWORD=
|
||||
# Secret for signing admin JWT tokens (generate: openssl rand -hex 32)
|
||||
JWT_SECRET=
|
||||
|
||||
131
backend/package-lock.json
generated
131
backend/package-lock.json
generated
@@ -9,9 +9,11 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mongoose": "^9.3.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"socket.io": "^4.8.3"
|
||||
@@ -118,6 +120,15 @@
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -187,6 +198,12 @@
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -357,6 +374,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -806,6 +832,49 @@
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz",
|
||||
@@ -815,6 +884,48 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -1208,6 +1319,26 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
"dev": "nodemon src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mongoose": "^9.3.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"socket.io": "^4.8.3"
|
||||
|
||||
@@ -25,12 +25,16 @@ const io = new Server(httpServer, {
|
||||
});
|
||||
|
||||
const connectDB = require("./db");
|
||||
const seedAdmin = require("./seedAdmin");
|
||||
const gridCellsRouter = require("./routes/gridCells");
|
||||
const authRouter = require("./routes/auth");
|
||||
const adminRouter = require("./routes/admin");
|
||||
const hiboliaRouter = require("./routes/hibolia");
|
||||
|
||||
gridCellsRouter.setIO(io);
|
||||
|
||||
// connect database
|
||||
connectDB();
|
||||
connectDB().then(seedAdmin);
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
@@ -56,6 +60,9 @@ app.get("/api/test", (req, res) => {
|
||||
});
|
||||
|
||||
app.use("/api/grid-cells", gridCellsRouter);
|
||||
app.use("/api/auth", authRouter);
|
||||
app.use("/api/admin", adminRouter);
|
||||
app.use("/api/hibolia", hiboliaRouter);
|
||||
|
||||
app.get("/api/status", (req, res) => {
|
||||
const mem = process.memoryUsage();
|
||||
|
||||
33
backend/src/middleware/auth.js
Normal file
33
backend/src/middleware/auth.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const crypto = require("crypto");
|
||||
|
||||
let fallbackSecret = null;
|
||||
|
||||
function getJwtSecret() {
|
||||
if (process.env.JWT_SECRET) return process.env.JWT_SECRET;
|
||||
if (!fallbackSecret) {
|
||||
fallbackSecret = crypto.randomBytes(32).toString("hex");
|
||||
console.warn(
|
||||
"JWT_SECRET is not set — using an ephemeral secret. Sessions will reset on every restart."
|
||||
);
|
||||
}
|
||||
return fallbackSecret;
|
||||
}
|
||||
|
||||
function requireAuth(req, res, next) {
|
||||
const header = req.headers.authorization || "";
|
||||
const [scheme, token] = header.split(" ");
|
||||
|
||||
if (scheme !== "Bearer" || !token) {
|
||||
return res.status(401).json({ error: "Unauthorized" });
|
||||
}
|
||||
|
||||
try {
|
||||
req.user = jwt.verify(token, getJwtSecret());
|
||||
return next();
|
||||
} catch (e) {
|
||||
return res.status(401).json({ error: "Invalid or expired session" });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { requireAuth, getJwtSecret };
|
||||
35
backend/src/routes/admin.js
Normal file
35
backend/src/routes/admin.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const express = require("express");
|
||||
const mongoose = require("mongoose");
|
||||
const GridCell = require("../schemas/GridCell");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
// GET /api/admin/stats — dashboard overview
|
||||
router.get("/stats", async (req, res) => {
|
||||
try {
|
||||
const uptime = Math.floor(process.uptime());
|
||||
const mem = process.memoryUsage();
|
||||
|
||||
const totalCells = await GridCell.countDocuments();
|
||||
|
||||
res.json({
|
||||
status: "online",
|
||||
mongo: mongoose.connection.readyState === 1 ? "connected" : "disconnected",
|
||||
uptime,
|
||||
memory: {
|
||||
rss: mem.rss,
|
||||
heapUsed: mem.heapUsed,
|
||||
},
|
||||
grid: {
|
||||
totalCells,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
82
backend/src/routes/auth.js
Normal file
82
backend/src/routes/auth.js
Normal file
@@ -0,0 +1,82 @@
|
||||
const express = require("express");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const AdminUser = require("../schemas/AdminUser");
|
||||
const { requireAuth, getJwtSecret } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const MAX_ATTEMPTS = 5;
|
||||
const WINDOW_MS = 15 * 60 * 1000;
|
||||
const attempts = new Map();
|
||||
|
||||
function isRateLimited(key) {
|
||||
const entry = attempts.get(key);
|
||||
if (!entry) return false;
|
||||
if (Date.now() > entry.resetAt) {
|
||||
attempts.delete(key);
|
||||
return false;
|
||||
}
|
||||
return entry.count >= MAX_ATTEMPTS;
|
||||
}
|
||||
|
||||
function registerFailure(key) {
|
||||
const entry = attempts.get(key);
|
||||
if (!entry || Date.now() > entry.resetAt) {
|
||||
attempts.set(key, { count: 1, resetAt: Date.now() + WINDOW_MS });
|
||||
} else {
|
||||
entry.count++;
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/auth/login
|
||||
router.post("/login", async (req, res) => {
|
||||
try {
|
||||
const key = req.ip;
|
||||
if (isRateLimited(key)) {
|
||||
return res.status(429).json({ error: "Too many attempts. Try again later." });
|
||||
}
|
||||
|
||||
const { username, password } = req.body || {};
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ error: "username and password are required" });
|
||||
}
|
||||
|
||||
const admin = await AdminUser.findOne({
|
||||
username: String(username).trim().toLowerCase(),
|
||||
});
|
||||
const valid = admin && (await bcrypt.compare(String(password), admin.passwordHash));
|
||||
|
||||
if (!valid) {
|
||||
registerFailure(key);
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
attempts.delete(key);
|
||||
admin.lastLoginAt = new Date();
|
||||
await admin.save();
|
||||
|
||||
const token = jwt.sign(
|
||||
{ sub: admin._id.toString(), username: admin.username },
|
||||
getJwtSecret(),
|
||||
{ expiresIn: "12h" }
|
||||
);
|
||||
|
||||
res.json({ token, username: admin.username });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/auth/me — validates the stored token
|
||||
router.get("/me", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const admin = await AdminUser.findById(req.user.sub).lean();
|
||||
if (!admin) return res.status(401).json({ error: "Invalid or expired session" });
|
||||
res.json({ username: admin.username, lastLoginAt: admin.lastLoginAt });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
51
backend/src/routes/hibolia.js
Normal file
51
backend/src/routes/hibolia.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const express = require("express");
|
||||
const HiboliaOrder = require("../schemas/HiboliaOrder");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
// POST /api/hibolia/orders — public order submission
|
||||
router.post("/orders", async (req, res) => {
|
||||
try {
|
||||
const { name, surname, email, message, color, shipping } = req.body || {};
|
||||
if (!name || !surname || !email || !message || !color || !shipping) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "name, surname, email, message, color and shipping are required" });
|
||||
}
|
||||
if (!HiboliaOrder.COLORS.includes(color)) {
|
||||
return res.status(400).json({ error: "invalid color" });
|
||||
}
|
||||
if (!EMAIL_RE.test(String(email))) {
|
||||
return res.status(400).json({ error: "invalid email" });
|
||||
}
|
||||
const order = await HiboliaOrder.create({
|
||||
name,
|
||||
surname,
|
||||
email,
|
||||
message,
|
||||
color,
|
||||
shipping,
|
||||
});
|
||||
res.status(201).json({ id: order._id });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/hibolia/orders — admin only (dashboard)
|
||||
router.get("/orders", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const orders = await HiboliaOrder.find()
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(500)
|
||||
.lean();
|
||||
res.json(orders);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
20
backend/src/schemas/AdminUser.js
Normal file
20
backend/src/schemas/AdminUser.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const adminUserSchema = new mongoose.Schema(
|
||||
{
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
},
|
||||
passwordHash: { type: String, required: true },
|
||||
lastLoginAt: { type: Date },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const AdminUser = mongoose.model("AdminUser", adminUserSchema);
|
||||
|
||||
module.exports = AdminUser;
|
||||
34
backend/src/schemas/HiboliaOrder.js
Normal file
34
backend/src/schemas/HiboliaOrder.js
Normal file
@@ -0,0 +1,34 @@
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const COLORS = [
|
||||
"red",
|
||||
"gold",
|
||||
"silver",
|
||||
"orange",
|
||||
"yellow",
|
||||
"green",
|
||||
"blue",
|
||||
"purple",
|
||||
"black",
|
||||
"white",
|
||||
"pink",
|
||||
"none",
|
||||
];
|
||||
|
||||
const hiboliaOrderSchema = new mongoose.Schema(
|
||||
{
|
||||
name: { type: String, required: true, trim: true, maxlength: 100 },
|
||||
surname: { type: String, required: true, trim: true, maxlength: 100 },
|
||||
email: { type: String, required: true, trim: true, lowercase: true },
|
||||
message: { type: String, required: true, trim: true, maxlength: 2000 },
|
||||
color: { type: String, required: true, enum: COLORS },
|
||||
shipping: { type: String, required: true, trim: true, maxlength: 2000 },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const HiboliaOrder = mongoose.model("HiboliaOrder", hiboliaOrderSchema);
|
||||
|
||||
HiboliaOrder.COLORS = COLORS;
|
||||
|
||||
module.exports = HiboliaOrder;
|
||||
30
backend/src/seedAdmin.js
Normal file
30
backend/src/seedAdmin.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const bcrypt = require("bcryptjs");
|
||||
const AdminUser = require("./schemas/AdminUser");
|
||||
|
||||
// Seeds the single admin account from env vars on first boot.
|
||||
// ADMIN_PASSWORD is only used to generate the initial bcrypt hash;
|
||||
// after seeding, change credentials directly in the database.
|
||||
const seedAdmin = async () => {
|
||||
try {
|
||||
const count = await AdminUser.countDocuments();
|
||||
if (count > 0) return;
|
||||
|
||||
const username = process.env.ADMIN_USERNAME;
|
||||
const password = process.env.ADMIN_PASSWORD;
|
||||
|
||||
if (!username || !password) {
|
||||
console.warn(
|
||||
"No admin account exists. Set ADMIN_USERNAME and ADMIN_PASSWORD to seed one."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
await AdminUser.create({ username, passwordHash });
|
||||
console.log(`Admin account seeded for user "${username}"`);
|
||||
} catch (error) {
|
||||
console.error("Admin seeding error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = seedAdmin;
|
||||
Reference in New Issue
Block a user