protect against sql injection, xss, csrf
This commit is contained in:
83
backend/middleware/csrf.js
Normal file
83
backend/middleware/csrf.js
Normal file
@@ -0,0 +1,83 @@
|
||||
const csrf = require("csrf");
|
||||
const cookieParser = require("cookie-parser");
|
||||
|
||||
// Initialize CSRF token generator
|
||||
const tokens = new csrf();
|
||||
|
||||
// Generate a secret for signing tokens
|
||||
const secret = tokens.secretSync();
|
||||
|
||||
// CSRF middleware using double submit cookie pattern
|
||||
const csrfProtection = (req, res, next) => {
|
||||
// Skip CSRF for safe methods
|
||||
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Get token from header or body
|
||||
const token =
|
||||
req.headers["x-csrf-token"] || req.body.csrfToken || req.query.csrfToken;
|
||||
|
||||
// Get token from cookie
|
||||
const cookieToken = req.cookies["csrf-token"];
|
||||
|
||||
// Verify both tokens exist and match
|
||||
if (!token || !cookieToken || token !== cookieToken) {
|
||||
return res.status(403).json({
|
||||
error: "Invalid CSRF token",
|
||||
code: "CSRF_TOKEN_MISMATCH",
|
||||
});
|
||||
}
|
||||
|
||||
// Verify token is valid
|
||||
if (!tokens.verify(secret, token)) {
|
||||
return res.status(403).json({
|
||||
error: "Invalid CSRF token",
|
||||
code: "CSRF_TOKEN_INVALID",
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Middleware to generate and send CSRF token
|
||||
const generateCSRFToken = (req, res, next) => {
|
||||
const token = tokens.create(secret);
|
||||
|
||||
// Set token in cookie (httpOnly for security)
|
||||
res.cookie("csrf-token", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV !== "dev",
|
||||
sameSite: "strict",
|
||||
maxAge: 60 * 60 * 1000, // 1 hour
|
||||
});
|
||||
|
||||
// Also provide token in header for client to use
|
||||
res.set("X-CSRF-Token", token);
|
||||
|
||||
// Make token available to response
|
||||
res.locals.csrfToken = token;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Route to get CSRF token (for initial page loads)
|
||||
const getCSRFToken = (req, res) => {
|
||||
const token = tokens.create(secret);
|
||||
|
||||
res.cookie("csrf-token", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV !== "dev",
|
||||
sameSite: "strict",
|
||||
maxAge: 60 * 60 * 1000,
|
||||
});
|
||||
|
||||
res.json({ csrfToken: token });
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
csrfProtection,
|
||||
generateCSRFToken,
|
||||
getCSRFToken,
|
||||
cookieParser: cookieParser(),
|
||||
};
|
||||
271
backend/middleware/validation.js
Normal file
271
backend/middleware/validation.js
Normal file
@@ -0,0 +1,271 @@
|
||||
const { body, validationResult } = require("express-validator");
|
||||
const DOMPurify = require("dompurify");
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
// Create a DOM purify instance for server-side sanitization
|
||||
const window = new JSDOM("").window;
|
||||
const purify = DOMPurify(window);
|
||||
|
||||
// Password strength validation
|
||||
const passwordStrengthRegex =
|
||||
/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
|
||||
const commonPasswords = [
|
||||
"password",
|
||||
"123456",
|
||||
"123456789",
|
||||
"qwerty",
|
||||
"abc123",
|
||||
"password123",
|
||||
"admin",
|
||||
"letmein",
|
||||
"welcome",
|
||||
"monkey",
|
||||
"1234567890",
|
||||
];
|
||||
|
||||
// Sanitization middleware
|
||||
const sanitizeInput = (req, res, next) => {
|
||||
const sanitizeValue = (value) => {
|
||||
if (typeof value === "string") {
|
||||
return purify.sanitize(value, { ALLOWED_TAGS: [] });
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const sanitized = {};
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
sanitized[key] = sanitizeValue(val);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
if (req.body) {
|
||||
req.body = sanitizeValue(req.body);
|
||||
}
|
||||
if (req.query) {
|
||||
req.query = sanitizeValue(req.query);
|
||||
}
|
||||
if (req.params) {
|
||||
req.params = sanitizeValue(req.params);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Validation error handler
|
||||
const handleValidationErrors = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
error: "Validation failed",
|
||||
details: errors.array().map((err) => ({
|
||||
field: err.path,
|
||||
message: err.msg,
|
||||
})),
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Registration validation rules
|
||||
const validateRegistration = [
|
||||
body("email")
|
||||
.isEmail()
|
||||
.normalizeEmail()
|
||||
.withMessage("Please provide a valid email address")
|
||||
.isLength({ max: 255 })
|
||||
.withMessage("Email must be less than 255 characters"),
|
||||
|
||||
body("password")
|
||||
.isLength({ min: 8, max: 128 })
|
||||
.withMessage("Password must be between 8 and 128 characters")
|
||||
.matches(passwordStrengthRegex)
|
||||
.withMessage(
|
||||
"Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
|
||||
)
|
||||
.custom((value) => {
|
||||
if (commonPasswords.includes(value.toLowerCase())) {
|
||||
throw new Error(
|
||||
"Password is too common. Please choose a stronger password"
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
body("firstName")
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 50 })
|
||||
.withMessage("First name must be between 1 and 50 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"First name can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("lastName")
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 50 })
|
||||
.withMessage("Last name must be between 1 and 50 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"Last name can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("username")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 3, max: 30 })
|
||||
.withMessage("Username must be between 3 and 30 characters")
|
||||
.matches(/^[a-zA-Z0-9_-]+$/)
|
||||
.withMessage(
|
||||
"Username can only contain letters, numbers, underscores, and hyphens"
|
||||
),
|
||||
|
||||
body("phone")
|
||||
.optional()
|
||||
.isMobilePhone()
|
||||
.withMessage("Please provide a valid phone number"),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Login validation rules
|
||||
const validateLogin = [
|
||||
body("email")
|
||||
.isEmail()
|
||||
.normalizeEmail()
|
||||
.withMessage("Please provide a valid email address"),
|
||||
|
||||
body("password")
|
||||
.notEmpty()
|
||||
.withMessage("Password is required")
|
||||
.isLength({ max: 128 })
|
||||
.withMessage("Password is too long"),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Google auth validation
|
||||
const validateGoogleAuth = [
|
||||
body("idToken")
|
||||
.notEmpty()
|
||||
.withMessage("Google ID token is required")
|
||||
.isLength({ max: 2048 })
|
||||
.withMessage("Invalid token format"),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Profile update validation
|
||||
const validateProfileUpdate = [
|
||||
body("firstName")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 50 })
|
||||
.withMessage("First name must be between 1 and 50 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"First name can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("lastName")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 50 })
|
||||
.withMessage("Last name must be between 1 and 50 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"Last name can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("phone")
|
||||
.optional()
|
||||
.isMobilePhone()
|
||||
.withMessage("Please provide a valid phone number"),
|
||||
|
||||
body("address1")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 255 })
|
||||
.withMessage("Address line 1 must be less than 255 characters"),
|
||||
|
||||
body("address2")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 255 })
|
||||
.withMessage("Address line 2 must be less than 255 characters"),
|
||||
|
||||
body("city")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage("City must be less than 100 characters")
|
||||
.matches(/^[a-zA-Z\s\-']+$/)
|
||||
.withMessage(
|
||||
"City can only contain letters, spaces, hyphens, and apostrophes"
|
||||
),
|
||||
|
||||
body("state")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage("State must be less than 100 characters"),
|
||||
|
||||
body("zipCode")
|
||||
.optional()
|
||||
.trim()
|
||||
.matches(/^[0-9]{5}(-[0-9]{4})?$/)
|
||||
.withMessage("Please provide a valid ZIP code"),
|
||||
|
||||
body("country")
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage("Country must be less than 100 characters"),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
// Password change validation
|
||||
const validatePasswordChange = [
|
||||
body("currentPassword")
|
||||
.notEmpty()
|
||||
.withMessage("Current password is required"),
|
||||
|
||||
body("newPassword")
|
||||
.isLength({ min: 8, max: 128 })
|
||||
.withMessage("New password must be between 8 and 128 characters")
|
||||
.matches(passwordStrengthRegex)
|
||||
.withMessage(
|
||||
"New password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
|
||||
)
|
||||
.custom((value, { req }) => {
|
||||
if (value === req.body.currentPassword) {
|
||||
throw new Error("New password must be different from current password");
|
||||
}
|
||||
if (commonPasswords.includes(value.toLowerCase())) {
|
||||
throw new Error(
|
||||
"Password is too common. Please choose a stronger password"
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
body("confirmPassword").custom((value, { req }) => {
|
||||
if (value !== req.body.newPassword) {
|
||||
throw new Error("Password confirmation does not match");
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
sanitizeInput,
|
||||
handleValidationErrors,
|
||||
validateRegistration,
|
||||
validateLogin,
|
||||
validateGoogleAuth,
|
||||
validateProfileUpdate,
|
||||
validatePasswordChange,
|
||||
};
|
||||
@@ -104,17 +104,25 @@ const User = sequelize.define(
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
loginAttempts: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
},
|
||||
lockUntil: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
hooks: {
|
||||
beforeCreate: async (user) => {
|
||||
if (user.password) {
|
||||
user.password = await bcrypt.hash(user.password, 10);
|
||||
user.password = await bcrypt.hash(user.password, 12);
|
||||
}
|
||||
},
|
||||
beforeUpdate: async (user) => {
|
||||
if (user.changed("password") && user.password) {
|
||||
user.password = await bcrypt.hash(user.password, 10);
|
||||
user.password = await bcrypt.hash(user.password, 12);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -128,4 +136,41 @@ User.prototype.comparePassword = async function (password) {
|
||||
return bcrypt.compare(password, this.password);
|
||||
};
|
||||
|
||||
// Account lockout constants
|
||||
const MAX_LOGIN_ATTEMPTS = 5;
|
||||
const LOCK_TIME = 2 * 60 * 60 * 1000; // 2 hours
|
||||
|
||||
// Check if account is locked
|
||||
User.prototype.isLocked = function() {
|
||||
return !!(this.lockUntil && this.lockUntil > Date.now());
|
||||
};
|
||||
|
||||
// Increment login attempts and lock account if necessary
|
||||
User.prototype.incLoginAttempts = async function() {
|
||||
// If we have a previous lock that has expired, restart at 1
|
||||
if (this.lockUntil && this.lockUntil < Date.now()) {
|
||||
return this.update({
|
||||
loginAttempts: 1,
|
||||
lockUntil: null
|
||||
});
|
||||
}
|
||||
|
||||
const updates = { loginAttempts: this.loginAttempts + 1 };
|
||||
|
||||
// Lock account after max attempts
|
||||
if (this.loginAttempts + 1 >= MAX_LOGIN_ATTEMPTS && !this.isLocked()) {
|
||||
updates.lockUntil = Date.now() + LOCK_TIME;
|
||||
}
|
||||
|
||||
return this.update(updates);
|
||||
};
|
||||
|
||||
// Reset login attempts after successful login
|
||||
User.prototype.resetLoginAttempts = async function() {
|
||||
return this.update({
|
||||
loginAttempts: 0,
|
||||
lockUntil: null
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = User;
|
||||
|
||||
687
backend/package-lock.json
generated
687
backend/package-lock.json
generated
@@ -12,11 +12,17 @@
|
||||
"@googlemaps/google-maps-services-js": "^3.4.2",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"body-parser": "^2.2.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"csrf": "^3.1.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.1.0",
|
||||
"express-validator": "^7.2.1",
|
||||
"google-auth-library": "^10.3.0",
|
||||
"helmet": "^8.1.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"node-cron": "^3.0.3",
|
||||
@@ -30,6 +36,178 @@
|
||||
"nodemon": "^3.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.4.tgz",
|
||||
"integrity": "sha512-cKjSKvWGmAziQWbCouOsFwb14mp1betm8Y7Fn+yglDMUUu3r9DCbJ9iJbeFDenLMqFbIMC0pQP8K+B8LAxX3OQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.4",
|
||||
"@csstools/css-color-parser": "^3.0.10",
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4",
|
||||
"lru-cache": "^11.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
|
||||
"integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.5.4.tgz",
|
||||
"integrity": "sha512-RNSNk1dnB8lAn+xdjlRoM4CzdVrHlmXZtSXAWs2jyl4PiBRWqTZr9ML5M710qgd9RPTBsVG6P0SLy7dwy0Foig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.1.0",
|
||||
"is-potential-custom-element-name": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz",
|
||||
"integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@googlemaps/google-maps-services-js": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@googlemaps/google-maps-services-js/-/google-maps-services-js-3.4.2.tgz",
|
||||
@@ -103,6 +281,13 @@
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.15.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz",
|
||||
@@ -205,9 +390,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
|
||||
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@@ -248,6 +433,15 @@
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.1",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
||||
@@ -564,6 +758,25 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
@@ -603,6 +816,47 @@
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csrf": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
|
||||
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rndm": "1.2.0",
|
||||
"tsscmp": "1.0.6",
|
||||
"uid-safe": "2.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
|
||||
"integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.12.2",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.0.tgz",
|
||||
"integrity": "sha512-RveJPnk3m7aarYQ2bJ6iw+Urh55S6FzUiqtBq+TihnTDP4cI8y/TYDqGOyqgnG1J1a6BxJXZsV9JFSTulm9Z7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^4.0.3",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.0.14",
|
||||
"css-tree": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
||||
@@ -612,6 +866,19 @@
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
|
||||
"integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^15.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@@ -628,6 +895,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
@@ -654,6 +927,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz",
|
||||
@@ -731,6 +1013,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@@ -853,6 +1147,28 @@
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-validator": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz",
|
||||
"integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"validator": "~13.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-validator/node_modules/validator": {
|
||||
"version": "13.12.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
|
||||
"integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
@@ -1315,6 +1631,27 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -1338,6 +1675,19 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
@@ -1476,6 +1826,12 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
@@ -1528,6 +1884,45 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "27.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz",
|
||||
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/dom-selector": "^6.5.4",
|
||||
"cssstyle": "^5.3.0",
|
||||
"data-urls": "^6.0.0",
|
||||
"decimal.js": "^10.5.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"parse5": "^7.3.0",
|
||||
"rrweb-cssom": "^0.8.0",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^15.0.0",
|
||||
"ws": "^8.18.2",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
@@ -1641,6 +2036,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
|
||||
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||
@@ -1807,6 +2208,25 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
@@ -1990,6 +2410,18 @@
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -2132,6 +2564,35 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
@@ -2196,6 +2657,15 @@
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
@@ -2228,6 +2698,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -2284,6 +2763,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -2320,6 +2808,12 @@
|
||||
"axios": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/rndm": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
|
||||
"integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
@@ -2335,6 +2829,12 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -2359,6 +2859,18 @@
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@@ -2619,6 +3131,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split-on-first": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
|
||||
@@ -2801,6 +3322,30 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.0.14",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.14.tgz",
|
||||
"integrity": "sha512-lMNHE4aSI3LlkMUMicTmAG3tkkitjOQGDTFboPJwAg2kJXKP1ryWEyqujktg5qhrFZOkk5YFzgkxg3jErE+i5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.0.14"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.0.14",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.14.tgz",
|
||||
"integrity": "sha512-viZGNK6+NdluOJWwTO9olaugx0bkKhscIdriQQ+lNNhwitIKvb+SvhbYgnCz6j9p7dX3cJntt4agQAKMXLjJ5g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -2835,6 +3380,39 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsscmp": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.x"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
@@ -2854,6 +3432,18 @@
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uid-safe": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
|
||||
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"random-bytes": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/umzug": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
|
||||
@@ -2927,6 +3517,18 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
@@ -2936,6 +3538,49 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
|
||||
"integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.0.0.tgz",
|
||||
"integrity": "sha512-+0q+Pc6oUhtbbeUfuZd4heMNOLDJDdagYxv756mCf9vnLF+NTj4zvv5UyYNkHJpc3CJIesMVoEIOdhi7L9RObA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.1",
|
||||
"webidl-conversions": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -3047,6 +3692,42 @@
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
@@ -19,11 +19,17 @@
|
||||
"@googlemaps/google-maps-services-js": "^3.4.2",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"body-parser": "^2.2.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"csrf": "^3.1.0",
|
||||
"dompurify": "^3.2.6",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.1.0",
|
||||
"express-validator": "^7.2.1",
|
||||
"google-auth-library": "^10.3.0",
|
||||
"helmet": "^8.1.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"node-cron": "^3.0.3",
|
||||
|
||||
@@ -2,11 +2,17 @@ const express = require("express");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { OAuth2Client } = require("google-auth-library");
|
||||
const { User } = require("../models"); // Import from models/index.js to get models with associations
|
||||
const {
|
||||
sanitizeInput,
|
||||
validateRegistration,
|
||||
validateLogin,
|
||||
validateGoogleAuth
|
||||
} = require("../middleware/validation");
|
||||
const router = express.Router();
|
||||
|
||||
const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
|
||||
|
||||
router.post("/register", async (req, res) => {
|
||||
router.post("/register", sanitizeInput, validateRegistration, async (req, res) => {
|
||||
try {
|
||||
const { username, email, password, firstName, lastName, phone } = req.body;
|
||||
|
||||
@@ -17,7 +23,10 @@ router.post("/register", async (req, res) => {
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: "User already exists" });
|
||||
return res.status(400).json({
|
||||
error: "Registration failed",
|
||||
details: [{ field: "email", message: "An account with this email already exists" }]
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.create({
|
||||
@@ -44,20 +53,40 @@ router.post("/register", async (req, res) => {
|
||||
token,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
console.error('Registration error:', error);
|
||||
res.status(500).json({ error: "Registration failed. Please try again." });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
router.post("/login", sanitizeInput, validateLogin, async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await User.findOne({ where: { email } });
|
||||
|
||||
if (!user || !(await user.comparePassword(password))) {
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Check if account is locked
|
||||
if (user.isLocked()) {
|
||||
return res.status(423).json({
|
||||
error: "Account is temporarily locked due to too many failed login attempts. Please try again later."
|
||||
});
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const isPasswordValid = await user.comparePassword(password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
// Increment login attempts
|
||||
await user.incLoginAttempts();
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Reset login attempts on successful login
|
||||
await user.resetLoginAttempts();
|
||||
|
||||
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
@@ -73,11 +102,12 @@ router.post("/login", async (req, res) => {
|
||||
token,
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ error: "Login failed. Please try again." });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/google", async (req, res) => {
|
||||
router.post("/google", sanitizeInput, validateGoogleAuth, async (req, res) => {
|
||||
try {
|
||||
const { idToken } = req.body;
|
||||
|
||||
@@ -165,9 +195,8 @@ router.post("/google", async (req, res) => {
|
||||
.status(400)
|
||||
.json({ error: "Malformed Google token. Please try again." });
|
||||
}
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: "Failed to authenticate with Google: " + error.message });
|
||||
console.error('Google auth error:', error);
|
||||
res.status(500).json({ error: "Google authentication failed. Please try again." });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ const express = require("express");
|
||||
const cors = require("cors");
|
||||
const bodyParser = require("body-parser");
|
||||
const path = require("path");
|
||||
const helmet = require("helmet");
|
||||
const { sequelize } = require("./models"); // Import from models/index.js to ensure associations are loaded
|
||||
const { cookieParser } = require("./middleware/csrf");
|
||||
|
||||
const authRoutes = require("./routes/auth");
|
||||
const userRoutes = require("./routes/users");
|
||||
@@ -25,9 +27,52 @@ const PayoutProcessor = require("./jobs/payoutProcessor");
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: "5mb" }));
|
||||
app.use(bodyParser.urlencoded({ extended: true, limit: "5mb" }));
|
||||
// Security headers
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "https://cdn.jsdelivr.net"],
|
||||
fontSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "https://accounts.google.com"],
|
||||
imgSrc: ["'self'"],
|
||||
connectSrc: ["'self'"],
|
||||
frameSrc: ["'self'"],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Cookie parser for CSRF
|
||||
app.use(cookieParser);
|
||||
|
||||
// CORS with security settings
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 200,
|
||||
})
|
||||
);
|
||||
|
||||
// Body parsing with size limits
|
||||
app.use(
|
||||
bodyParser.json({
|
||||
limit: "1mb",
|
||||
verify: (req, res, buf) => {
|
||||
// Store raw body for webhook verification
|
||||
req.rawBody = buf;
|
||||
},
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
bodyParser.urlencoded({
|
||||
extended: true,
|
||||
limit: "1mb",
|
||||
parameterLimit: 100, // Limit number of parameters
|
||||
})
|
||||
);
|
||||
|
||||
// Serve static files from uploads directory
|
||||
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
|
||||
@@ -54,10 +99,10 @@ sequelize
|
||||
.sync({ alter: true })
|
||||
.then(() => {
|
||||
console.log("Database synced");
|
||||
|
||||
|
||||
// Start the payout processor
|
||||
const payoutJobs = PayoutProcessor.startScheduledPayouts();
|
||||
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on port ${PORT}`);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import PasswordStrengthMeter from "./PasswordStrengthMeter";
|
||||
|
||||
interface AuthModalProps {
|
||||
show: boolean;
|
||||
@@ -198,6 +199,9 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
{mode === "signup" && (
|
||||
<PasswordStrengthMeter password={password} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface LocationMapProps {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
location: string;
|
||||
itemName: string;
|
||||
}
|
||||
|
||||
const LocationMap: React.FC<LocationMapProps> = ({ latitude, longitude, location, itemName }) => {
|
||||
const mapRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// If we have coordinates, use them directly
|
||||
if (latitude && longitude && mapRef.current) {
|
||||
// Create a simple map using an iframe with OpenStreetMap
|
||||
const mapUrl = `https://www.openstreetmap.org/export/embed.html?bbox=${longitude-0.01},${latitude-0.01},${longitude+0.01},${latitude+0.01}&layer=mapnik&marker=${latitude},${longitude}`;
|
||||
|
||||
mapRef.current.innerHTML = `
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameborder="0"
|
||||
scrolling="no"
|
||||
marginheight="0"
|
||||
marginwidth="0"
|
||||
src="${mapUrl}"
|
||||
style="border: none; border-radius: 8px;"
|
||||
></iframe>
|
||||
`;
|
||||
} else if (location && mapRef.current) {
|
||||
// If we only have a location string, try to show it on the map
|
||||
// For a more robust solution, you'd want to use a geocoding service
|
||||
const encodedLocation = encodeURIComponent(location);
|
||||
const mapUrl = `https://www.openstreetmap.org/export/embed.html?bbox=-180,-90,180,90&layer=mapnik&marker=`;
|
||||
|
||||
// For now, we'll show a static map with a search link
|
||||
mapRef.current.innerHTML = `
|
||||
<div class="text-center p-4">
|
||||
<i class="bi bi-geo-alt-fill text-primary" style="font-size: 3rem;"></i>
|
||||
<p class="mt-2 mb-3"><strong>Location:</strong> ${location}</p>
|
||||
<a
|
||||
href="https://www.openstreetmap.org/search?query=${encodedLocation}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
>
|
||||
<i class="bi bi-map me-2"></i>View on Map
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}, [latitude, longitude, location]);
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h5>Location</h5>
|
||||
<div
|
||||
ref={mapRef}
|
||||
style={{
|
||||
height: '300px',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center justify-content-center h-100">
|
||||
<div className="spinner-border text-primary" role="status">
|
||||
<span className="visually-hidden">Loading map...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{(latitude && longitude) && (
|
||||
<p className="text-muted small mt-2">
|
||||
<i className="bi bi-info-circle me-1"></i>
|
||||
Exact location shown on map
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationMap;
|
||||
127
frontend/src/components/PasswordStrengthMeter.tsx
Normal file
127
frontend/src/components/PasswordStrengthMeter.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
|
||||
interface PasswordStrengthMeterProps {
|
||||
password: string;
|
||||
showRequirements?: boolean;
|
||||
}
|
||||
|
||||
interface PasswordRequirement {
|
||||
regex: RegExp;
|
||||
text: string;
|
||||
met: boolean;
|
||||
}
|
||||
|
||||
const PasswordStrengthMeter: React.FC<PasswordStrengthMeterProps> = ({
|
||||
password,
|
||||
showRequirements = true
|
||||
}) => {
|
||||
const requirements: PasswordRequirement[] = [
|
||||
{
|
||||
regex: /.{8,}/,
|
||||
text: "At least 8 characters",
|
||||
met: /.{8,}/.test(password)
|
||||
},
|
||||
{
|
||||
regex: /[a-z]/,
|
||||
text: "One lowercase letter",
|
||||
met: /[a-z]/.test(password)
|
||||
},
|
||||
{
|
||||
regex: /[A-Z]/,
|
||||
text: "One uppercase letter",
|
||||
met: /[A-Z]/.test(password)
|
||||
},
|
||||
{
|
||||
regex: /\d/,
|
||||
text: "One number",
|
||||
met: /\d/.test(password)
|
||||
},
|
||||
{
|
||||
regex: /[@$!%*?&]/,
|
||||
text: "One special character (@$!%*?&)",
|
||||
met: /[@$!%*?&]/.test(password)
|
||||
}
|
||||
];
|
||||
|
||||
const getPasswordStrength = (): { score: number; label: string; color: string } => {
|
||||
if (!password) return { score: 0, label: '', color: '' };
|
||||
|
||||
const metRequirements = requirements.filter(req => req.met).length;
|
||||
const hasCommonPassword = ['password', '123456', '123456789', 'qwerty', 'abc123', 'password123'].includes(password.toLowerCase());
|
||||
|
||||
if (hasCommonPassword) {
|
||||
return { score: 0, label: 'Too Common', color: 'danger' };
|
||||
}
|
||||
|
||||
switch (metRequirements) {
|
||||
case 0:
|
||||
case 1:
|
||||
return { score: 1, label: 'Very Weak', color: 'danger' };
|
||||
case 2:
|
||||
return { score: 2, label: 'Weak', color: 'warning' };
|
||||
case 3:
|
||||
return { score: 3, label: 'Fair', color: 'info' };
|
||||
case 4:
|
||||
return { score: 4, label: 'Good', color: 'primary' };
|
||||
case 5:
|
||||
return { score: 5, label: 'Strong', color: 'success' };
|
||||
default:
|
||||
return { score: 0, label: '', color: '' };
|
||||
}
|
||||
};
|
||||
|
||||
const strength = getPasswordStrength();
|
||||
const progressPercentage = password ? (strength.score / 5) * 100 : 0;
|
||||
|
||||
if (!password) return null;
|
||||
|
||||
return (
|
||||
<div className="password-strength-meter mt-2">
|
||||
{/* Strength Bar */}
|
||||
<div className="mb-2">
|
||||
<div className="d-flex justify-content-between align-items-center mb-1">
|
||||
<small className="text-muted">Password Strength</small>
|
||||
{strength.label && (
|
||||
<small className={`text-${strength.color} fw-bold`}>
|
||||
{strength.label}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="progress" style={{ height: '4px' }}>
|
||||
<div
|
||||
className={`progress-bar bg-${strength.color}`}
|
||||
role="progressbar"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
aria-valuenow={progressPercentage}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requirements List */}
|
||||
{showRequirements && (
|
||||
<div className="password-requirements">
|
||||
<small className="text-muted d-block mb-1">Password must contain:</small>
|
||||
<ul className="list-unstyled mb-0" style={{ fontSize: '0.75rem' }}>
|
||||
{requirements.map((requirement, index) => (
|
||||
<li key={index} className="d-flex align-items-center mb-1">
|
||||
<i
|
||||
className={`bi ${
|
||||
requirement.met ? 'bi-check-circle-fill text-success' : 'bi-circle text-muted'
|
||||
} me-2`}
|
||||
style={{ fontSize: '0.75rem' }}
|
||||
/>
|
||||
<span className={requirement.met ? 'text-success' : 'text-muted'}>
|
||||
{requirement.text}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordStrengthMeter;
|
||||
Reference in New Issue
Block a user