This commit is contained in:
Jake Kasper
2026-04-08 10:19:12 -04:00
parent 4a6728ee4e
commit e83a51a051
7 changed files with 148 additions and 49 deletions

View File

@@ -1,7 +1,12 @@
const express = require('express');
const pool = require('../config/database');
const { validateRequest, validateParams } = require('../utils/validation');
const { productSchema, idParamSchema } = require('../utils/validation');
const {
validateRequest,
validateParams,
productSchema,
idParamSchema,
propertySectionParamSchema
} = require('../utils/validation');
const { requireAdmin } = require('../middleware/auth');
const { AppError } = require('../middleware/errorHandler');
@@ -1172,7 +1177,7 @@ router.get('/properties/:id', validateParams(idParamSchema), async (req, res, ne
});
// Update a lawn section (admin override)
router.put('/properties/:propertyId/sections/:sectionId', validateParams(idParamSchema), async (req, res, next) => {
router.put('/properties/:propertyId/sections/:sectionId', validateParams(propertySectionParamSchema), async (req, res, next) => {
try {
const { propertyId, sectionId } = req.params;
const { name, area, polygonData, grassType, grassTypes, captureMethod, captureMeta } = req.body;

View File

@@ -8,6 +8,7 @@ const { validateRequest } = require('../utils/validation');
const { registerSchema, loginSchema, changePasswordSchema } = require('../utils/validation');
const { AppError } = require('../middleware/errorHandler');
const { authenticateToken } = require('../middleware/auth');
const { getRegistrationEnabled } = require('../services/adminSettingsService');
const router = express.Router();
@@ -338,31 +339,7 @@ router.get('/me', authenticateToken, async (req, res, next) => {
// @access Public
router.get('/registration-status', async (req, res, next) => {
try {
// Create admin_settings table if it doesn't exist
await pool.query(`
CREATE TABLE IF NOT EXISTS admin_settings (
id SERIAL PRIMARY KEY,
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Insert default registration setting if it doesn't exist
await pool.query(`
INSERT INTO admin_settings (setting_key, setting_value)
VALUES ('registrationEnabled', 'true')
ON CONFLICT (setting_key) DO NOTHING
`);
// Get registration setting
const settingResult = await pool.query(
'SELECT setting_value FROM admin_settings WHERE setting_key = $1',
['registrationEnabled']
);
const enabled = settingResult.rows.length > 0 ? settingResult.rows[0].setting_value === 'true' : true;
const enabled = await getRegistrationEnabled();
res.json({
success: true,
@@ -375,4 +352,4 @@ router.get('/registration-status', async (req, res, next) => {
}
});
module.exports = router;
module.exports = router;

View File

@@ -1,9 +1,18 @@
const express = require('express');
const Joi = require('joi');
const pool = require('../config/database');
const { validateRequest, validateParams } = require('../utils/validation');
const { idParamSchema } = require('../utils/validation');
const {
validateRequest,
validateParams,
idParamSchema
} = require('../utils/validation');
const { AppError } = require('../middleware/errorHandler');
const flowRateParamSchema = Joi.object({
nozzleTypeId: Joi.number().integer().positive().required(),
pressurePsi: Joi.number().integer().positive().required()
});
const router = express.Router();
// @route GET /api/nozzles/types
@@ -107,7 +116,7 @@ router.get('/types', async (req, res, next) => {
// @route GET /api/nozzles/flow-rate/:nozzleTypeId/:pressurePsi
// @desc Get flow rate for specific nozzle at specific pressure
// @access Private
router.get('/flow-rate/:nozzleTypeId/:pressurePsi', validateParams(idParamSchema), async (req, res, next) => {
router.get('/flow-rate/:nozzleTypeId/:pressurePsi', validateParams(flowRateParamSchema), async (req, res, next) => {
try {
const { nozzleTypeId, pressurePsi } = req.params;
@@ -855,4 +864,4 @@ router.get('/configurations/:configId/details', validateParams(idParamSchema), a
}
});
module.exports = router;
module.exports = router;

View File

@@ -1,19 +1,31 @@
const express = require('express');
const pool = require('../config/database');
const { validateRequest, validateParams } = require('../utils/validation');
const { propertySchema, lawnSectionSchema, idParamSchema } = require('../utils/validation');
const {
validateRequest,
validateParams,
propertySchema,
lawnSectionSchema,
idParamSchema,
propertySectionParamSchema
} = require('../utils/validation');
const { AppError } = require('../middleware/errorHandler');
// Geometry helpers for sanitization and simplification are defined below
const router = express.Router();
const shouldLogAreaDebug = process.env.PROPERTY_AREA_DEBUG === 'true';
const areaDebug = (...args) => {
if (shouldLogAreaDebug) {
console.log(...args);
}
};
// Helper function to calculate polygon area (in square feet)
const calculatePolygonArea = (coordinates) => {
if (!coordinates || coordinates.length < 3) {
console.log('Invalid coordinates for area calculation:', coordinates);
areaDebug('Invalid coordinates for area calculation:', coordinates);
return 0;
}
console.log('Calculating area for coordinates:', coordinates);
areaDebug('Calculating area for coordinates:', coordinates);
let area = 0;
const n = coordinates.length;
for (let i = 0; i < n; i++) {
@@ -22,15 +34,15 @@ const calculatePolygonArea = (coordinates) => {
area -= coordinates[j][1] * coordinates[i][0];
}
area = Math.abs(area) / 2;
console.log('Raw shoelace area (in deg²):', area);
areaDebug('Raw shoelace area (in deg²):', area);
const avgLat = coordinates.reduce((sum, coord) => sum + coord[0], 0) / n;
console.log('Average latitude:', avgLat);
areaDebug('Average latitude:', avgLat);
const metersPerDegreeLat = 111320;
const metersPerDegreeLng = 111320 * Math.cos(avgLat * Math.PI / 180);
const areaInSquareMeters = area * metersPerDegreeLat * metersPerDegreeLng;
const areaInSquareFeet = areaInSquareMeters * 10.7639;
console.log('Area in square meters:', areaInSquareMeters);
console.log('Area in square feet:', areaInSquareFeet);
areaDebug('Area in square meters:', areaInSquareMeters);
areaDebug('Area in square feet:', areaInSquareFeet);
return areaInSquareFeet;
};
@@ -345,10 +357,10 @@ router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawn
// Calculate area from polygon if provided
let calculatedArea = area;
if (polygonData && polygonData.coordinates && polygonData.coordinates[0]) {
console.log('Original polygon data:', JSON.stringify(polygonData, null, 2));
console.log('Coordinates for calculation:', polygonData.coordinates[0]);
areaDebug('Original polygon data:', JSON.stringify(polygonData, null, 2));
areaDebug('Coordinates for calculation:', polygonData.coordinates[0]);
calculatedArea = calculatePolygonArea(polygonData.coordinates[0]);
console.log('Calculated area:', calculatedArea);
areaDebug('Calculated area:', calculatedArea);
}
// Sanitize polygon before storing
@@ -409,7 +421,7 @@ router.post('/:id/sections', validateParams(idParamSchema), validateRequest(lawn
// @route PUT /api/properties/:propertyId/sections/:sectionId
// @desc Update lawn section
// @access Private
router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
router.put('/:propertyId/sections/:sectionId', validateParams(propertySectionParamSchema), validateRequest(lawnSectionSchema), async (req, res, next) => {
try {
const { propertyId, sectionId } = req.params;
const { name, area, polygonData, grassType, grassTypes, soilType, captureMethod, captureMeta } = req.body;
@@ -490,7 +502,7 @@ router.put('/:propertyId/sections/:sectionId', async (req, res, next) => {
// @route DELETE /api/properties/:propertyId/sections/:sectionId
// @desc Delete lawn section
// @access Private
router.delete('/:propertyId/sections/:sectionId', async (req, res, next) => {
router.delete('/:propertyId/sections/:sectionId', validateParams(propertySectionParamSchema), async (req, res, next) => {
try {
const { propertyId, sectionId } = req.params;

View File

@@ -2,13 +2,14 @@ const express = require('express');
const axios = require('axios');
const pool = require('../config/database');
const { AppError } = require('../middleware/errorHandler');
const { validateParams, propertyIdParamSchema } = require('../utils/validation');
const router = express.Router();
// @route GET /api/weather/:propertyId
// @desc Get current weather for property location
// @access Private
router.get('/:propertyId', async (req, res, next) => {
router.get('/:propertyId', validateParams(propertyIdParamSchema), async (req, res, next) => {
try {
const propertyId = req.params.propertyId;
@@ -165,7 +166,7 @@ router.get('/:propertyId', async (req, res, next) => {
// @route GET /api/weather/:propertyId/forecast
// @desc Get 5-day weather forecast for property
// @access Private
router.get('/:propertyId/forecast', async (req, res, next) => {
router.get('/:propertyId/forecast', validateParams(propertyIdParamSchema), async (req, res, next) => {
try {
const propertyId = req.params.propertyId;

View File

@@ -0,0 +1,68 @@
const pool = require('../config/database');
const REGISTRATION_KEY = 'registrationEnabled';
const CACHE_TTL_MS = parseInt(process.env.ADMIN_SETTINGS_CACHE_TTL_MS, 10) || 60000;
let cache = { value: null, expiresAt: 0 };
let initPromise;
const ensureInitialized = async () => {
if (!initPromise) {
initPromise = (async () => {
await pool.query(`
CREATE TABLE IF NOT EXISTS admin_settings (
id SERIAL PRIMARY KEY,
setting_key VARCHAR(255) UNIQUE NOT NULL,
setting_value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
await pool.query(`
INSERT INTO admin_settings (setting_key, setting_value)
VALUES ($1, $2)
ON CONFLICT (setting_key) DO NOTHING
`, [REGISTRATION_KEY, 'true']);
})().catch((err) => {
initPromise = null;
throw err;
});
}
return initPromise;
};
const readSetting = async (key, fallback = 'true') => {
await ensureInitialized();
const result = await pool.query(
'SELECT setting_value FROM admin_settings WHERE setting_key = $1',
[key]
);
if (result.rows.length === 0) return fallback;
return result.rows[0].setting_value;
};
const getRegistrationEnabled = async () => {
const now = Date.now();
if (cache.expiresAt > now && cache.value !== null) {
return cache.value;
}
const value = await readSetting(REGISTRATION_KEY);
const enabled = value === 'true';
cache = {
value: enabled,
expiresAt: now + CACHE_TTL_MS
};
return enabled;
};
const clearAdminSettingsCache = () => {
cache = { value: null, expiresAt: 0 };
};
module.exports = {
getRegistrationEnabled,
clearAdminSettingsCache
};

View File

@@ -228,7 +228,23 @@ const validateRequest = (schema) => {
const validateParams = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.params, { abortEarly: false });
let paramsToValidate = req.params;
try {
const schemaDescription = schema.describe();
const expectedKeys = schemaDescription?.keys ? Object.keys(schemaDescription.keys) : [];
if (
expectedKeys.length === 1 &&
!Object.prototype.hasOwnProperty.call(req.params, expectedKeys[0]) &&
Object.keys(req.params).length === 1
) {
const [actualKey] = Object.keys(req.params);
paramsToValidate = { [expectedKeys[0]]: req.params[actualKey] };
}
} catch (_) {
// Ignore schema introspection issues and fall back to default validation.
}
const { error } = schema.validate(paramsToValidate, { abortEarly: false });
if (error) {
return next(error);
}
@@ -240,6 +256,15 @@ const idParamSchema = Joi.object({
id: Joi.number().integer().positive().required()
});
const propertyIdParamSchema = Joi.object({
propertyId: Joi.number().integer().positive().required()
});
const propertySectionParamSchema = Joi.object({
propertyId: Joi.number().integer().positive().required(),
sectionId: Joi.number().integer().positive().required()
});
module.exports = {
// Schemas
registerSchema,
@@ -257,6 +282,8 @@ module.exports = {
mowingSessionSchema,
mowingPlanSchema,
idParamSchema,
propertyIdParamSchema,
propertySectionParamSchema,
// Middleware
validateRequest,