admin
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
68
backend/src/services/adminSettingsService.js
Normal file
68
backend/src/services/adminSettingsService.js
Normal 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
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user