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>
329 lines
7.9 KiB
TypeScript
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();
|
|
}
|
|
},
|
|
};
|