Files
location-mqtt-tracker-app/lib/geofence-db.ts
Joachim Hummel 3ac82621c8 Security update: Patch React and Next.js CVE vulnerabilities
Updated React to 19.2.1 and Next.js to 16.0.7 to address critical security vulnerabilities:
- CVE-2025-55182: React Server Components deserialization flaw
- CVE-2025-66478: Next.js RSC implementation vulnerability

Also includes:
- Add PATCH endpoint for geofence updates
- Reorder admin navigation items
- Add geofence update functionality in database layer

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 09:30:47 +00:00

329 lines
7.9 KiB
TypeScript

/**
* Geofence Database Layer
* CRUD operations for Geofence, GeofenceEvent, and GeofenceStatus
*/
import Database from 'better-sqlite3';
import path from 'path';
import type {
Geofence,
GeofenceEvent,
GeofenceStatus,
CreateGeofenceInput,
CreateGeofenceEventInput,
} from './types';
const dbPath = path.join(process.cwd(), 'data', 'database.sqlite');
function getDb() {
return new Database(dbPath);
}
export const geofenceDb = {
/**
* Create a new geofence
*/
create(data: CreateGeofenceInput): Geofence {
const db = getDb();
try {
const stmt = db.prepare(`
INSERT INTO Geofence (
id, name, description, shape_type,
center_latitude, center_longitude, radius_meters,
owner_id, device_id, color
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(
data.id,
data.name,
data.description || null,
'circle',
data.center_latitude,
data.center_longitude,
data.radius_meters,
data.owner_id,
data.device_id,
data.color || '#3b82f6'
);
return this.findById(data.id)!;
} finally {
db.close();
}
},
/**
* Find geofence by ID
*/
findById(id: string): Geofence | null {
const db = getDb();
try {
const stmt = db.prepare('SELECT * FROM Geofence WHERE id = ?');
return stmt.get(id) as Geofence | null;
} finally {
db.close();
}
},
/**
* Find all geofences for an owner
*/
findByOwner(ownerId: string): Geofence[] {
const db = getDb();
try {
const stmt = db.prepare('SELECT * FROM Geofence WHERE owner_id = ? ORDER BY created_at DESC');
return stmt.all(ownerId) as Geofence[];
} finally {
db.close();
}
},
/**
* Find all active geofences for a specific device
*/
findActiveForDevice(deviceId: string): Geofence[] {
const db = getDb();
try {
const stmt = db.prepare(`
SELECT * FROM Geofence
WHERE device_id = ? AND is_active = 1
ORDER BY created_at DESC
`);
return stmt.all(deviceId) as Geofence[];
} finally {
db.close();
}
},
/**
* Update a geofence (partial update)
*/
update(id: string, updates: Partial<Omit<Geofence, 'id' | 'owner_id' | 'device_id' | 'created_at' | 'updated_at'>>): Geofence | null {
const db = getDb();
try {
// Build dynamic UPDATE query for only provided fields
const updateFields: string[] = [];
const values: any[] = [];
if (updates.name !== undefined) {
updateFields.push('name = ?');
values.push(updates.name);
}
if (updates.description !== undefined) {
updateFields.push('description = ?');
values.push(updates.description);
}
if (updates.radius_meters !== undefined) {
updateFields.push('radius_meters = ?');
values.push(updates.radius_meters);
}
if (updates.color !== undefined) {
updateFields.push('color = ?');
values.push(updates.color);
}
if (updates.is_active !== undefined) {
updateFields.push('is_active = ?');
values.push(updates.is_active);
}
if (updateFields.length === 0) {
return this.findById(id);
}
// Always update updated_at
updateFields.push('updated_at = datetime(\'now\')');
const query = `UPDATE Geofence SET ${updateFields.join(', ')} WHERE id = ?`;
values.push(id);
const stmt = db.prepare(query);
const result = stmt.run(...values);
if (result.changes === 0) {
return null;
}
return this.findById(id);
} finally {
db.close();
}
},
/**
* Delete a geofence
*/
delete(id: string): boolean {
const db = getDb();
try {
const stmt = db.prepare('DELETE FROM Geofence WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
} finally {
db.close();
}
},
/**
* Get current status for device/geofence pair
*/
getStatus(deviceId: string, geofenceId: string): GeofenceStatus | null {
const db = getDb();
try {
const stmt = db.prepare(`
SELECT * FROM GeofenceStatus
WHERE device_id = ? AND geofence_id = ?
`);
return stmt.get(deviceId, geofenceId) as GeofenceStatus | null;
} finally {
db.close();
}
},
/**
* Update or create status for device/geofence pair
*/
updateStatus(deviceId: string, geofenceId: string, isInside: boolean): void {
const db = getDb();
try {
const now = new Date().toISOString();
const isInsideInt = isInside ? 1 : 0;
// Try to update existing status
const updateStmt = db.prepare(`
UPDATE GeofenceStatus
SET is_inside = ?,
last_enter_time = CASE WHEN ? = 1 AND is_inside = 0 THEN ? ELSE last_enter_time END,
last_exit_time = CASE WHEN ? = 0 AND is_inside = 1 THEN ? ELSE last_exit_time END,
last_checked_at = ?,
updated_at = ?
WHERE device_id = ? AND geofence_id = ?
`);
const result = updateStmt.run(
isInsideInt,
isInsideInt, now, // for enter
isInsideInt, now, // for exit
now,
now,
deviceId,
geofenceId
);
// If no rows updated, insert new status
if (result.changes === 0) {
const insertStmt = db.prepare(`
INSERT INTO GeofenceStatus (
device_id, geofence_id, is_inside,
last_enter_time, last_exit_time, last_checked_at
) VALUES (?, ?, ?, ?, ?, ?)
`);
insertStmt.run(
deviceId,
geofenceId,
isInsideInt,
isInside ? now : null,
!isInside ? now : null,
now
);
}
} finally {
db.close();
}
},
/**
* Create a new geofence event
*/
createEvent(event: CreateGeofenceEventInput): GeofenceEvent {
const db = getDb();
try {
const stmt = db.prepare(`
INSERT INTO GeofenceEvent (
geofence_id, device_id, location_id,
event_type, latitude, longitude,
distance_from_center, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
event.geofence_id,
event.device_id,
event.location_id,
event.event_type,
event.latitude,
event.longitude,
event.distance_from_center,
event.timestamp
);
// Return the created event
const selectStmt = db.prepare('SELECT * FROM GeofenceEvent WHERE id = ?');
return selectStmt.get(result.lastInsertRowid) as GeofenceEvent;
} finally {
db.close();
}
},
/**
* Find events for a specific device or geofence
*/
findEvents(filters: {
deviceId?: string;
geofenceId?: string;
limit?: number;
}): GeofenceEvent[] {
const db = getDb();
try {
let query = 'SELECT * FROM GeofenceEvent WHERE 1=1';
const params: any[] = [];
if (filters.deviceId) {
query += ' AND device_id = ?';
params.push(filters.deviceId);
}
if (filters.geofenceId) {
query += ' AND geofence_id = ?';
params.push(filters.geofenceId);
}
query += ' ORDER BY timestamp DESC';
if (filters.limit) {
query += ' LIMIT ?';
params.push(filters.limit);
}
const stmt = db.prepare(query);
return stmt.all(...params) as GeofenceEvent[];
} finally {
db.close();
}
},
/**
* Mark notification as sent or failed
*/
markNotificationSent(eventId: number, success: boolean, error?: string): void {
const db = getDb();
try {
const stmt = db.prepare(`
UPDATE GeofenceEvent
SET notification_sent = ?,
notification_error = ?
WHERE id = ?
`);
stmt.run(
success ? 1 : 2, // 1 = sent, 2 = failed
error || null,
eventId
);
} finally {
db.close();
}
},
};