ice/src/routes/locations.ts
Claude Code 15e117d10c Clean up unused imports and variables to improve code quality
- Remove unused imports: LocationSubmission from types, Location/ProfanityWord from server
- Remove unused variables: wordText, detectedWords in profanity rejection
- Remove unused parameters: req in skip function, wordId/updates in fallback filter
- Fix regex escaping and destructuring patterns
- Remove unused response variables in tests
- Reduce ESLint issues from 45 to 22 (eliminated all 21 errors, keeping only warnings)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-05 22:41:14 -04:00

257 lines
No EOL
10 KiB
TypeScript

import express, { Request, Response, Router } from 'express';
import rateLimit from 'express-rate-limit';
import Location from '../models/Location';
import ProfanityFilterService from '../services/ProfanityFilterService';
// Define interfaces for request bodies
interface LocationPostRequest extends Request {
body: {
address: string;
latitude?: number;
longitude?: number;
description?: string;
};
}
export default (locationModel: Location, profanityFilter: ProfanityFilterService | any): Router => {
const router = express.Router();
// Rate limiting for location submissions to prevent abuse
const submitLocationLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 10, // Limit each IP to 10 location submissions per 15 minutes
message: {
error: 'Too many location reports submitted',
message: 'You can submit up to 10 location reports every 15 minutes. Please wait before submitting more.',
retryAfter: '15 minutes'
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
// Skip rate limiting in test environment
skip: () => process.env.NODE_ENV === 'test'
});
/**
* @swagger
* /api/locations:
* get:
* tags:
* - Public API
* summary: Get active ice condition reports
* description: |
* Retrieves all active ice condition reports. Reports are considered active if they are:
* - Less than 48 hours old, OR
* - Marked as persistent by an administrator
* responses:
* 200:
* description: Active locations retrieved successfully
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/Location'
* examples:
* active_locations:
* summary: Example active locations
* value:
* - id: 123
* address: "Main St & Oak Ave, Grand Rapids, MI"
* latitude: 42.9634
* longitude: -85.6681
* description: "Black ice present, multiple vehicles stuck"
* persistent: false
* created_at: "2025-01-15T10:30:00.000Z"
* - id: 124
* address: "I-96 & US-131, Grand Rapids, MI"
* latitude: 42.9584
* longitude: -85.6706
* description: "Icy on-ramp conditions"
* persistent: true
* created_at: "2025-01-14T08:15:00.000Z"
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.get('/', async (req: Request, res: Response): Promise<void> => {
console.log('Fetching active locations');
try {
const locations = await locationModel.getActive();
console.log(`Fetched ${locations.length} active locations (including persistent)`);
res.json(locations);
} catch (err) {
console.error('Error fetching locations:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
/**
* @swagger
* /api/locations:
* post:
* tags:
* - Public API
* summary: Report a new ice condition
* description: |
* Submit a new ice condition report. The system will:
* - Validate the address is provided
* - Check description for profanity and reject if found
* - Geocode the address if coordinates not provided
* - Store the report for 48 hours (unless made persistent by admin)
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LocationInput'
* examples:
* basic_report:
* summary: Basic ice report with address only
* value:
* address: "Main St & Oak Ave, Grand Rapids, MI"
* description: "Black ice spotted, drive carefully"
* detailed_report:
* summary: Detailed report with coordinates
* value:
* address: "I-96 westbound at Exit 85"
* latitude: 42.9634
* longitude: -85.6681
* description: "Multiple vehicles stuck, road salt needed"
* responses:
* 200:
* description: Location report created successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Location'
* example:
* id: 125
* address: "Main St & Oak Ave, Grand Rapids, MI"
* latitude: 42.9634
* longitude: -85.6681
* description: "Black ice spotted, drive carefully"
* persistent: false
* created_at: "2025-01-15T12:00:00.000Z"
* 400:
* description: Bad request - invalid input or profanity detected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* examples:
* missing_address:
* summary: Missing required address field
* value:
* error: "Address is required"
* profanity_detected:
* summary: Profanity detected in description
* value:
* error: "Submission rejected"
* message: "Your description contains inappropriate language and cannot be posted. Please revise your description to focus on road conditions and keep it professional."
* details:
* severity: "medium"
* wordCount: 1
* detectedCategories: ["general"]
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
router.post('/', submitLocationLimiter, async (req: LocationPostRequest, res: Response): Promise<void> => {
const { address, latitude, longitude, description } = req.body;
console.log(`Attempt to add new location: ${address}`);
// Input validation for security
if (!address) {
console.warn('Failed to add location: Address is required');
res.status(400).json({ error: 'Address is required' });
return;
}
if (typeof address !== 'string' || address.length > 500) {
console.warn(`Failed to add location: Invalid address length (${address?.length || 0})`);
res.status(400).json({ error: 'Address must be a string with maximum 500 characters' });
return;
}
if (description && (typeof description !== 'string' || description.length > 1000)) {
console.warn(`Failed to add location: Invalid description length (${description?.length || 0})`);
res.status(400).json({ error: 'Description must be a string with maximum 1000 characters' });
return;
}
// Validate latitude if provided
if (latitude !== undefined && (typeof latitude !== 'number' || latitude < -90 || latitude > 90)) {
console.warn(`Failed to add location: Invalid latitude (${latitude})`);
res.status(400).json({ error: 'Latitude must be a number between -90 and 90' });
return;
}
// Validate longitude if provided
if (longitude !== undefined && (typeof longitude !== 'number' || longitude < -180 || longitude > 180)) {
console.warn(`Failed to add location: Invalid longitude (${longitude})`);
res.status(400).json({ error: 'Longitude must be a number between -180 and 180' });
return;
}
// Log suspicious activity
if (address.length > 200 || (description && description.length > 500)) {
console.warn(`Unusually long submission from IP: ${req.ip} - Address: ${address.length} chars, Description: ${description?.length || 0} chars`);
}
// Check for profanity in description and reject if any is found
if (description && profanityFilter) {
try {
const analysis = profanityFilter.analyzeProfanity(description);
if (analysis.hasProfanity) {
console.warn(`Submission rejected due to inappropriate language (${analysis.count} word${analysis.count > 1 ? 's' : ''}, severity: ${analysis.severity}) - Original: "${req.body.description}"`);
// Reject any submission with profanity
res.status(400).json({
error: 'Submission rejected',
message: 'Your description contains inappropriate language and cannot be posted. Please revise your description to focus on road conditions and keep it professional.\n\nExample: "Multiple vehicles stuck, black ice present" or "Road very slippery, saw 3 accidents"',
details: {
severity: analysis.severity,
wordCount: analysis.count,
detectedCategories: [...new Set(analysis.matches.map((m: any) => m.category))]
}
});
return;
}
} catch (filterError) {
console.error('Error checking profanity:', filterError);
// Continue with original description if filter fails
}
}
try {
const newLocation = await locationModel.create({
address,
latitude,
longitude,
description
});
console.log(`Location added successfully: ${address}`);
res.json({
...newLocation,
created_at: new Date().toISOString()
});
} catch (err) {
console.error('Error inserting location:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
// DELETE functionality has been moved to admin-only routes for security.
// Use /api/admin/locations/:id (with authentication) for location deletion.
return router;
};