diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index ed664fe..673e7de 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -19,10 +19,10 @@ export default function AdminLayout({ const allNavigation = [ { name: "Dashboard", href: "/admin", roles: ['ADMIN', 'VIEWER'], superAdminOnly: false }, { name: "Devices", href: "/admin/devices", roles: ['ADMIN', 'VIEWER'], superAdminOnly: false }, - { name: "Geofences", href: "/admin/geofences", roles: ['ADMIN'], superAdminOnly: false }, { name: "MQTT Provisioning", href: "/admin/mqtt", roles: ['ADMIN'], superAdminOnly: false }, - { name: "Setup Guide", href: "/admin/setup", roles: ['ADMIN', 'VIEWER'], superAdminOnly: false }, + { name: "Geofences", href: "/admin/geofences", roles: ['ADMIN'], superAdminOnly: false }, { name: "Users", href: "/admin/users", roles: ['ADMIN'], superAdminOnly: false }, + { name: "Setup Guide", href: "/admin/setup", roles: ['ADMIN', 'VIEWER'], superAdminOnly: false }, { name: "Settings", href: "/admin/settings", roles: ['ADMIN'], superAdminOnly: true }, { name: "Emails", href: "/admin/emails", roles: ['ADMIN'], superAdminOnly: true }, ]; diff --git a/app/api/geofences/[id]/route.ts b/app/api/geofences/[id]/route.ts index 62f623c..4e1be14 100644 --- a/app/api/geofences/[id]/route.ts +++ b/app/api/geofences/[id]/route.ts @@ -2,6 +2,89 @@ import { NextResponse } from "next/server"; import { auth } from "@/lib/auth"; import { geofenceDb } from "@/lib/geofence-db"; +// PATCH /api/geofences/[id] - Update a geofence +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const session = await auth(); + + if (!session?.user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const userId = (session.user as any).id; + const { id: geofenceId } = await params; + + // Check if geofence exists + const geofence = geofenceDb.findById(geofenceId); + + if (!geofence) { + return NextResponse.json( + { error: "Geofence not found" }, + { status: 404 } + ); + } + + // Check ownership + if (geofence.owner_id !== userId) { + return NextResponse.json( + { error: "Forbidden: You can only update your own geofences" }, + { status: 403 } + ); + } + + // Parse request body + const body = await request.json(); + const { name, description, radius_meters, color, is_active } = body; + + // Build updates object with only provided fields + const updates: any = {}; + if (name !== undefined) updates.name = name; + if (description !== undefined) updates.description = description; + if (radius_meters !== undefined) updates.radius_meters = radius_meters; + if (color !== undefined) updates.color = color; + if (is_active !== undefined) updates.is_active = is_active; + + // Validate updates if provided + if (radius_meters !== undefined) { + if (typeof radius_meters !== 'number' || radius_meters < 50 || radius_meters > 50000) { + return NextResponse.json( + { error: "Invalid radius_meters (must be between 50 and 50000)" }, + { status: 400 } + ); + } + } + + // Update geofence + const updatedGeofence = geofenceDb.update(geofenceId, updates); + + if (!updatedGeofence) { + return NextResponse.json( + { error: "Failed to update geofence" }, + { status: 500 } + ); + } + + console.log(`[PATCH /api/geofences/${geofenceId}] Updated geofence ${updatedGeofence.name} for user ${userId}`); + + return NextResponse.json({ + success: true, + geofence: updatedGeofence, + }); + } catch (error) { + console.error(`[PATCH /api/geofences/[id]] Error:`, error); + return NextResponse.json( + { + error: "Failed to update geofence", + details: error instanceof Error ? error.message : "Unknown error", + }, + { status: 500 } + ); + } +} + // DELETE /api/geofences/[id] - Delete a geofence export async function DELETE( request: Request, diff --git a/lib/geofence-db.ts b/lib/geofence-db.ts index 51b237d..f4b4de1 100644 --- a/lib/geofence-db.ts +++ b/lib/geofence-db.ts @@ -96,6 +96,60 @@ export const geofenceDb = { } }, + /** + * Update a geofence (partial update) + */ + update(id: string, updates: Partial>): 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 */ diff --git a/package-lock.json b/package-lock.json index 8b2ee27..e79fbe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,11 +20,11 @@ "better-sqlite3": "^12.4.1", "leaflet": "^1.9.4", "mqtt": "^5.14.1", - "next": "^16.0.3", + "next": "^16.0.7", "next-auth": "^5.0.0-beta.30", "nodemailer": "^7.0.10", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", "react-email": "^4.3.2", "react-leaflet": "^5.0.0", "typescript": "^5.9.3", @@ -1906,15 +1906,15 @@ } }, "node_modules/@next/env": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.3.tgz", - "integrity": "sha512-IqgtY5Vwsm14mm/nmQaRMmywCU+yyMIYfk3/MHZ2ZTJvwVbBn3usZnjMi1GacrMVzVcAxJShTCpZlPs26EdEjQ==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz", + "integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.3.tgz", - "integrity": "sha512-MOnbd92+OByu0p6QBAzq1ahVWzF6nyfiH07dQDez4/Nku7G249NjxDVyEfVhz8WkLiOEU+KFVnqtgcsfP2nLXg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz", + "integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==", "cpu": [ "arm64" ], @@ -1928,9 +1928,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.3.tgz", - "integrity": "sha512-i70C4O1VmbTivYdRlk+5lj9xRc2BlK3oUikt3yJeHT1unL4LsNtN7UiOhVanFdc7vDAgZn1tV/9mQwMkWOJvHg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz", + "integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==", "cpu": [ "x64" ], @@ -1944,9 +1944,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.3.tgz", - "integrity": "sha512-O88gCZ95sScwD00mn/AtalyCoykhhlokxH/wi1huFK+rmiP5LAYVs/i2ruk7xST6SuXN4NI5y4Xf5vepb2jf6A==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz", + "integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==", "cpu": [ "arm64" ], @@ -1960,9 +1960,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.3.tgz", - "integrity": "sha512-CEErFt78S/zYXzFIiv18iQCbRbLgBluS8z1TNDQoyPi8/Jr5qhR3e8XHAIxVxPBjDbEMITprqELVc5KTfFj0gg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz", + "integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==", "cpu": [ "arm64" ], @@ -1976,9 +1976,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.3.tgz", - "integrity": "sha512-Tc3i+nwt6mQ+Dwzcri/WNDj56iWdycGVh5YwwklleClzPzz7UpfaMw1ci7bLl6GRYMXhWDBfe707EXNjKtiswQ==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz", + "integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==", "cpu": [ "x64" ], @@ -1992,9 +1992,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.3.tgz", - "integrity": "sha512-zTh03Z/5PBBPdTurgEtr6nY0vI9KR9Ifp/jZCcHlODzwVOEKcKRBtQIGrkc7izFgOMuXDEJBmirwpGqdM/ZixA==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz", + "integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==", "cpu": [ "x64" ], @@ -2008,9 +2008,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.3.tgz", - "integrity": "sha512-Jc1EHxtZovcJcg5zU43X3tuqzl/sS+CmLgjRP28ZT4vk869Ncm2NoF8qSTaL99gh6uOzgM99Shct06pSO6kA6g==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz", + "integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==", "cpu": [ "arm64" ], @@ -2024,9 +2024,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.3.tgz", - "integrity": "sha512-N7EJ6zbxgIYpI/sWNzpVKRMbfEGgsWuOIvzkML7wxAAZhPk1Msxuo/JDu1PKjWGrAoOLaZcIX5s+/pF5LIbBBg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz", + "integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==", "cpu": [ "x64" ], @@ -5054,12 +5054,12 @@ } }, "node_modules/next": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/next/-/next-16.0.3.tgz", - "integrity": "sha512-Ka0/iNBblPFcIubTA1Jjh6gvwqfjrGq1Y2MTI5lbjeLIAfmC+p5bQmojpRZqgHHVu5cG4+qdIiwXiBSm/8lZ3w==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", + "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", "license": "MIT", "dependencies": { - "@next/env": "16.0.3", + "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5072,14 +5072,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.0.3", - "@next/swc-darwin-x64": "16.0.3", - "@next/swc-linux-arm64-gnu": "16.0.3", - "@next/swc-linux-arm64-musl": "16.0.3", - "@next/swc-linux-x64-gnu": "16.0.3", - "@next/swc-linux-x64-musl": "16.0.3", - "@next/swc-win32-arm64-msvc": "16.0.3", - "@next/swc-win32-x64-msvc": "16.0.3", + "@next/swc-darwin-arm64": "16.0.7", + "@next/swc-darwin-x64": "16.0.7", + "@next/swc-linux-arm64-gnu": "16.0.7", + "@next/swc-linux-arm64-musl": "16.0.7", + "@next/swc-linux-x64-gnu": "16.0.7", + "@next/swc-linux-x64-musl": "16.0.7", + "@next/swc-win32-arm64-msvc": "16.0.7", + "@next/swc-win32-x64-msvc": "16.0.7", "sharp": "^0.34.4" }, "peerDependencies": { @@ -5586,24 +5586,24 @@ } }, "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.0" + "react": "^19.2.1" } }, "node_modules/react-email": { diff --git a/package.json b/package.json index d99468e..4da78b8 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,11 @@ "better-sqlite3": "^12.4.1", "leaflet": "^1.9.4", "mqtt": "^5.14.1", - "next": "^16.0.3", + "next": "^16.0.7", "next-auth": "^5.0.0-beta.30", "nodemailer": "^7.0.10", - "react": "^19.2.0", - "react-dom": "^19.2.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", "react-email": "^4.3.2", "react-leaflet": "^5.0.0", "typescript": "^5.9.3",