Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions api/controllers/adminController.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,52 @@
const jwt = require('jsonwebtoken');
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");

function login(req, res) {
const { username, password } = req.body || {};

const ADMIN_USERNAME = process.env.ADMIN_USERNAME;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
const ADMIN_JWT_SECRET = process.env.ADMIN_JWT_SECRET;
const ADMIN_JWT_EXPIRES_IN = process.env.ADMIN_JWT_EXPIRES_IN || '2h';
const ADMIN_JWT_EXPIRES_IN = process.env.ADMIN_JWT_EXPIRES_IN || "2h";

if (!ADMIN_USERNAME || !ADMIN_PASSWORD || !ADMIN_JWT_SECRET) {
return res.status(500).json({ success: false, message: 'Admin auth not configured' });
return res
.status(500)
.json({ success: false, message: "Admin auth not configured" });
}

// Fail fast (and loudly, in the server logs) if ADMIN_PASSWORD isn't a bcrypt
// hash. This removes the old plaintext-comparison fallback entirely, so a
// misconfigured .env can no longer silently downgrade auth to `===`.
const isBcryptHash = /^\$2[aby]\$\d{2}\$/.test(ADMIN_PASSWORD);
if (!isBcryptHash) {
console.error(
"[admin-auth] ADMIN_PASSWORD is not a bcrypt hash. Generate one with " +
"`node -e \"console.log(require('bcryptjs').hashSync(process.argv[1], 12))\" yourPassword` " +
"and set the result as ADMIN_PASSWORD.",
);
return res
.status(500)
.json({ success: false, message: "Admin auth not configured" });
}

if (!username || !password) {
return res.status(400).json({ success: false, message: 'Username and password are required' });
return res
.status(400)
.json({ success: false, message: "Username and password are required" });
}

const bcrypt = require('bcryptjs');
const isBcrypt = ADMIN_PASSWORD.startsWith('$2a$') || ADMIN_PASSWORD.startsWith('$2b$');
const passwordMatch = isBcrypt
? bcrypt.compareSync(password, ADMIN_PASSWORD)
: password === ADMIN_PASSWORD;
const passwordMatch = bcrypt.compareSync(password, ADMIN_PASSWORD);

if (username !== ADMIN_USERNAME || !passwordMatch) {
return res.status(401).json({ success: false, message: 'Invalid credentials' });
return res
.status(401)
.json({ success: false, message: "Invalid credentials" });
}

const token = jwt.sign(
{ username: ADMIN_USERNAME },
ADMIN_JWT_SECRET,
{ expiresIn: ADMIN_JWT_EXPIRES_IN }
);
const token = jwt.sign({ username: ADMIN_USERNAME }, ADMIN_JWT_SECRET, {
expiresIn: ADMIN_JWT_EXPIRES_IN,
});

return res.json({ success: true, token, expiresIn: ADMIN_JWT_EXPIRES_IN });
}
Expand Down
16 changes: 16 additions & 0 deletions api/middlewares/adminLoginLimiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const rateLimit = require("express-rate-limit");

// Throttles the admin login endpoint so a leaked/plaintext password (or just
// a weak one) can't be brute-forced. Keyed on IP; tune window/max to taste.
const adminLoginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per IP per window
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
message: "Too many login attempts. Please try again later.",
},
});

module.exports = { adminLoginLimiter };
7 changes: 4 additions & 3 deletions api/routes/adminRoutes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const express = require('express');
const express = require("express");
const router = express.Router();
const { login } = require('../controllers/adminController');
const { login } = require("../controllers/adminController");
const { adminLoginLimiter } = require("../middlewares/adminLoginLimiter");

router.post('/login', login);
router.post("/login", adminLoginLimiter, login);

module.exports = router;
16 changes: 16 additions & 0 deletions middlewares/adminLoginLimiter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const rateLimit = require("express-rate-limit");

// Throttles the admin login endpoint so a leaked/plaintext password (or just
// a weak one) can't be brute-forced. Keyed on IP; tune window/max to taste.
const adminLoginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per IP per window
standardHeaders: true,
legacyHeaders: false,
message: {
success: false,
message: "Too many login attempts. Please try again later.",
},
});

module.exports = { adminLoginLimiter };