feat: Add multer to admin DB backup/restore and improve validation
Co-authored-by: mathieumoran1 <[email protected]>
This commit is contained in:
co-authored by
mathieumoran1
parent
ee483e342c
commit
7f0b7834e6
+41
-1
@@ -6,6 +6,7 @@ const path = require("path");
|
||||
const zlib = require("zlib");
|
||||
const readline = require("readline");
|
||||
const multer = require("multer");
|
||||
const { Transform } = require("stream");
|
||||
|
||||
// Use MongoDB Extended JSON to preserve ObjectId/Date/etc.
|
||||
// bson is a transitive dependency of mongoose (mongodb driver).
|
||||
@@ -20,6 +21,26 @@ const router = express.Router();
|
||||
|
||||
const BACKUP_DIR = path.join(process.cwd(), "backups");
|
||||
const MAX_UPLOAD_BYTES = 250 * 1024 * 1024; // 250MB
|
||||
// SECURITY: protect against gzip "zip bombs" (small upload -> massive output).
|
||||
// This caps the *decompressed* stream size during restore.
|
||||
const MAX_RESTORE_UNZIPPED_BYTES = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
function createByteLimitStream(maxBytes) {
|
||||
let seen = 0;
|
||||
return new Transform({
|
||||
transform(chunk, _enc, cb) {
|
||||
seen += chunk.length;
|
||||
if (seen > maxBytes) {
|
||||
const err = new Error("Restore failed: decompressed data is too large");
|
||||
// mimic our handler's error shape
|
||||
err.statusCode = 413;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
cb(null, chunk);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function requireAdmin(req) {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
@@ -251,12 +272,28 @@ router.post("/db/restore", upload.single("backup"), async (req, res) => {
|
||||
return res.status(400).json({ message: "No backup file uploaded" });
|
||||
}
|
||||
uploadedPath = file.path;
|
||||
// Best-effort: refuse obviously wrong file names (UI expects .ndjson.gz).
|
||||
const original = String(file.originalname || "").trim().toLowerCase();
|
||||
if (original && !original.endsWith(".gz")) {
|
||||
const err = new Error("Invalid backup file (expected a .gz file)");
|
||||
err.statusCode = 400;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const db = mongoose.connection.db;
|
||||
const gunzip = zlib.createGunzip();
|
||||
const input = fs.createReadStream(uploadedPath).pipe(gunzip);
|
||||
const byteLimit = createByteLimitStream(MAX_RESTORE_UNZIPPED_BYTES);
|
||||
const input = fs.createReadStream(uploadedPath).pipe(gunzip).pipe(byteLimit);
|
||||
|
||||
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
||||
// Ensure stream errors don't become unhandled.
|
||||
let streamError = null;
|
||||
input.on("error", (e) => {
|
||||
streamError = e;
|
||||
try {
|
||||
rl.close();
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
let meta = null;
|
||||
let currentCollection = null;
|
||||
@@ -347,6 +384,9 @@ router.post("/db/restore", upload.single("backup"), async (req, res) => {
|
||||
}
|
||||
|
||||
await flushBatch();
|
||||
if (streamError) {
|
||||
throw streamError;
|
||||
}
|
||||
|
||||
// Cleanup upload after successful restore
|
||||
try {
|
||||
|
||||
+17
-3
@@ -169,7 +169,7 @@ router.get("/stats", verifyToken, async (req, res) => {
|
||||
|
||||
// Register User
|
||||
router.post("/register", async (req, res) => {
|
||||
const { name, email, password, role, verifoof } = req.body;
|
||||
const { name, email, password, verifoof } = req.body;
|
||||
const dogCheckEnabled = woof.length > 0;
|
||||
|
||||
function registerVerification() {
|
||||
@@ -179,6 +179,19 @@ router.post("/register", async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const nameNorm = typeof name === "string" ? name.trim() : "";
|
||||
const emailNorm = typeof email === "string" ? email.trim() : "";
|
||||
|
||||
if (!nameNorm) {
|
||||
throw new Error("Le nom est obligatoire");
|
||||
}
|
||||
if (!emailNorm) {
|
||||
throw new Error("L'email est obligatoire");
|
||||
}
|
||||
// Basic sanity check (not a full RFC validator)
|
||||
if (!/^\S+@\S+\.\S+$/.test(emailNorm)) {
|
||||
throw new Error("Email invalide.");
|
||||
}
|
||||
if (!password) {
|
||||
throw new Error("Le mot de passe est obligatoire et ne peut être indéfini");
|
||||
}
|
||||
@@ -189,14 +202,15 @@ router.post("/register", async (req, res) => {
|
||||
return res.status(400).json({ message: "Mauvais nom de chien" });
|
||||
}
|
||||
|
||||
const existingUser = await User.findOne({ email });
|
||||
const existingUser = await User.findOne({ email: emailNorm });
|
||||
if (existingUser) {
|
||||
return res.status(409).json({ message: "L'email existe déjà, veuillez en choisir un autre." });
|
||||
}
|
||||
|
||||
// Hash password and create user
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const user = new User({ name, email, password: hashedPassword, role });
|
||||
// SECURITY: never accept role escalation from a public endpoint.
|
||||
const user = new User({ name: nameNorm, email: emailNorm, password: hashedPassword, role: 0 });
|
||||
await user.save();
|
||||
|
||||
res.status(201).json({ message: "L'utilisateur s'est enregistré avec succès !" });
|
||||
|
||||
Reference in New Issue
Block a user