Added liking and security measures
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
const crypto = require('crypto');
|
||||
const pool = require('../db');
|
||||
|
||||
const DEFAULT_TOKEN_LIFETIME_SECONDS = 60 * 60;
|
||||
|
||||
@@ -14,7 +15,7 @@ function getJwtSecret() {
|
||||
return secret;
|
||||
}
|
||||
|
||||
function signToken(userId) {
|
||||
function signToken(userId, { jti, tokenVersion } = {}) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const configuredLifetime = Number.parseInt(process.env.JWT_EXPIRES_IN, 10);
|
||||
const lifetime = Number.isInteger(configuredLifetime) && configuredLifetime > 0
|
||||
@@ -25,6 +26,8 @@ function signToken(userId) {
|
||||
iat: now,
|
||||
exp: now + lifetime,
|
||||
};
|
||||
if (jti) payload.jti = jti;
|
||||
if (tokenVersion !== undefined) payload.tvr = tokenVersion;
|
||||
const encodedHeader = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
||||
const encodedPayload = base64url(JSON.stringify(payload));
|
||||
const content = `${encodedHeader}.${encodedPayload}`;
|
||||
@@ -57,7 +60,7 @@ function getTokenFromRequest(req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function authenticateToken(req, res, next) {
|
||||
async function authenticateToken(req, res, next) {
|
||||
const token = getTokenFromRequest(req);
|
||||
|
||||
if (!token) {
|
||||
@@ -92,6 +95,34 @@ function authenticateToken(req, res, next) {
|
||||
}
|
||||
|
||||
req.user = { id: payload.user_id };
|
||||
|
||||
if (payload.jti || payload.tvr !== undefined) {
|
||||
const { rows } = await pool.query(
|
||||
'SELECT token_version FROM people WHERE id = $1',
|
||||
[payload.user_id],
|
||||
);
|
||||
if (rows.length === 0 || rows[0].token_version !== payload.tvr) {
|
||||
return res.status(401).send('Session has been invalidated');
|
||||
}
|
||||
|
||||
if (payload.jti) {
|
||||
const sessionCheck = await pool.query(
|
||||
'SELECT 1 FROM sessions WHERE id = $1 AND user_id = $2',
|
||||
[payload.jti, payload.user_id],
|
||||
);
|
||||
if (sessionCheck.rowCount === 0) {
|
||||
return res.status(401).send('Session has been revoked');
|
||||
}
|
||||
}
|
||||
|
||||
req.user.session_id = payload.jti || null;
|
||||
}
|
||||
|
||||
if (payload.jti) {
|
||||
pool.query('UPDATE sessions SET last_used_at = NOW() WHERE id = $1', [payload.jti])
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
if (err.message.startsWith('JWT_SECRET')) {
|
||||
|
||||
@@ -14,6 +14,38 @@ const extensionByMimeType = {
|
||||
"image/webp": ".webp",
|
||||
};
|
||||
|
||||
// File signatures (magic bytes) that must match the declared Content-Type.
|
||||
const signatureByMimeType = {
|
||||
"image/jpeg": [Buffer.from([0xff, 0xd8, 0xff])],
|
||||
"image/png": [Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])],
|
||||
"image/gif": [Buffer.from("GIF87a"), Buffer.from("GIF89a")],
|
||||
};
|
||||
|
||||
function hasValidImageSignature(file) {
|
||||
const signatures = signatureByMimeType[file.mimetype];
|
||||
if (!signatures) {
|
||||
return isWebP(file.path);
|
||||
}
|
||||
const header = Buffer.alloc(12);
|
||||
const fd = fs.openSync(file.path, "r");
|
||||
fs.readSync(fd, header, 0, 12, 0);
|
||||
fs.closeSync(fd);
|
||||
return signatures.some((signature) =>
|
||||
header.subarray(0, signature.length).equals(signature),
|
||||
);
|
||||
}
|
||||
|
||||
function isWebP(filePath) {
|
||||
const header = Buffer.alloc(12);
|
||||
const fd = fs.openSync(filePath, "r");
|
||||
fs.readSync(fd, header, 0, 12, 0);
|
||||
fs.closeSync(fd);
|
||||
return (
|
||||
header.subarray(0, 4).equals(Buffer.from("RIFF")) &&
|
||||
header.subarray(8, 12).equals(Buffer.from("WEBP"))
|
||||
);
|
||||
}
|
||||
|
||||
fs.mkdirSync(uploadDirectory, { recursive: true });
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
@@ -38,7 +70,13 @@ const ALLOWED_IMAGE_PREFIXES = ["/posts/image/", "/profiles/image/"];
|
||||
|
||||
function handleImageUpload(req, res, next) {
|
||||
uploadImage(req, res, (err) => {
|
||||
if (!err) return next();
|
||||
if (!err) {
|
||||
if (req.file && !hasValidImageSignature(req.file)) {
|
||||
fs.promises.unlink(req.file.path).catch(() => {});
|
||||
return res.status(400).send("Uploaded file is not a valid image");
|
||||
}
|
||||
return next();
|
||||
}
|
||||
if (err.code === "LIMIT_FILE_SIZE") {
|
||||
return res.status(413).send("Image exceeds the maximum allowed size");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const rateLimit = require("express-rate-limit");
|
||||
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
limit: 20,
|
||||
standardHeaders: "draft-7",
|
||||
legacyHeaders: false,
|
||||
message: "Too many attempts, please try again later",
|
||||
});
|
||||
|
||||
const globalLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
limit: 300,
|
||||
standardHeaders: "draft-7",
|
||||
legacyHeaders: false,
|
||||
message: "Too many requests, please try again later",
|
||||
});
|
||||
|
||||
module.exports = { authLimiter, globalLimiter };
|
||||
@@ -121,6 +121,16 @@ function cleanProfileUpdateInput(input) {
|
||||
}
|
||||
}
|
||||
|
||||
if (body.private !== undefined) {
|
||||
if (typeof body.private === 'boolean') {
|
||||
values.private = body.private;
|
||||
} else if (body.private === 'true' || body.private === 'false') {
|
||||
values.private = body.private === 'true';
|
||||
} else {
|
||||
values.private = null;
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
@@ -148,10 +158,14 @@ function validateOptionalPersonField(field, value) {
|
||||
|
||||
function validateProfileUpdateInput(values, { hasImageChange = false } = {}) {
|
||||
const changeFields = ['name', 'username', 'email', 'password'].filter((field) => values[field] !== undefined);
|
||||
if (changeFields.length === 0 && !hasImageChange) {
|
||||
if (changeFields.length === 0 && values.private === undefined && !hasImageChange) {
|
||||
throw new Error('At least one profile field must be changed');
|
||||
}
|
||||
|
||||
if (values.private === null) {
|
||||
throw new Error('private must be true or false');
|
||||
}
|
||||
|
||||
if (values.password !== undefined && values.current_password === undefined) {
|
||||
throw new Error('current_password is required when changing password');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user