U4CVCPSGPGYWJ4PA72HHHCHKJSQW3GU2QFK4YFSMKQOEM2MMY3PQC
OWHNUYOKSGQTG6ZSVQZ3DNMYCW3IOEMXGRRS5QRRDAPIG4MOODNAC
UCDTBEK3CF6YT2H6V57HI6FAFW44BIYYAK3Z2QJ5LJE7QWX7OEYAC
YX7LU4WRAUDMWS3DEDXZDSF6DXBHLYDWVSMSRK6KIW3MO6GRXSVQC
RLH37YB4D7O42IFM2T7GJG4AVVAURWBZ7AOTHAWR7YJZRG3JOPLQC
4M3EBLTLSS2BRCM42ZP7WVD4YMRRLGV2P2XF47IAV5XHHJD52HTQC
POIBWSL3JFHT2KN3STFSJX3INSYKEJTX6KSW3N7BVEKWX2GJ6T7QC
details: error instanceof Error ? error.message : String(error),
},
500
);
}
});
/**
* Protected API route to update an existing location
*
* @route PUT /api/locations/:id
* @authentication Required
* @param {string} id - Location ID in URL path
* @param {Object} body - Location data to update (name, description, latitude, longitude, active)
* @returns {Object} Response containing:
* - data: Updated location object
* @description Updates an existing location. Requires EDIT permission on the dataset.
*/
app.put("/api/locations/:id", authenticate, async (c) => {
try {
// Get user from JWT
const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;
const userId = jwtPayload.sub;
// Get location ID from URL parameters
const locationId = c.req.param("id");
// Parse request body
const body = await c.req.json();
const { name, description, latitude, longitude, active } = body;
// Connect to database
const sql = neon(c.env.DATABASE_URL);
const db = drizzle(sql);
// Check if location exists and get its dataset ID
const existingLocation = await db
.select({
id: location.id,
datasetId: location.datasetId,
active: location.active
})
.from(location)
.where(eq(location.id, locationId))
.limit(1);
if (existingLocation.length === 0) {
return c.json({
error: "Location not found"
}, 404);
}
const locationRecord = existingLocation[0];
// Check if user has EDIT permission on the dataset
const hasEditPermission = await checkUserPermission(db, userId, locationRecord.datasetId, 'EDIT');
if (!hasEditPermission) {
return c.json({
error: "You don't have permission to edit this location"
}, 403);
}
// Build update object with only provided fields
const updateData: Record<string, unknown> = {
lastModified: new Date(),
modifiedBy: userId,
};
// Validate and add name if provided
if (name !== undefined) {
if (typeof name !== 'string' || name.trim().length === 0) {
return c.json({
error: "Field 'name' must be a non-empty string"
}, 400);
}
if (name.length > 140) {
return c.json({
error: "Field 'name' must be 140 characters or less"
}, 400);
}
updateData.name = name.trim();
}
// Validate and add description if provided
if (description !== undefined) {
if (description !== null && typeof description !== 'string') {
return c.json({
error: "Field 'description' must be a string or null"
}, 400);
}
if (description && description.length > 255) {
return c.json({
error: "Field 'description' must be 255 characters or less"
}, 400);
}
updateData.description = description?.trim() || null;
}
// Validate and add latitude if provided
if (latitude !== undefined) {
if (latitude !== null) {
const lat = Number(latitude);
if (isNaN(lat) || lat < -90 || lat > 90) {
return c.json({
error: "Field 'latitude' must be a valid number between -90 and 90"
}, 400);
}
updateData.latitude = String(lat);
} else {
updateData.latitude = null;
}
}
// Validate and add longitude if provided
if (longitude !== undefined) {
if (longitude !== null) {
const lng = Number(longitude);
if (isNaN(lng) || lng < -180 || lng > 180) {
return c.json({
error: "Field 'longitude' must be a valid number between -180 and 180"
}, 400);
}
updateData.longitude = String(lng);
} else {
updateData.longitude = null;
}
}
// Add active status if provided (for soft delete)
if (active !== undefined) {
if (typeof active !== 'boolean') {
return c.json({
error: "Field 'active' must be a boolean"
}, 400);
}
updateData.active = active;
}
// Update the location
const result = await db
.update(location)
.set(updateData)
.where(eq(location.id, locationId))
.returning({
id: location.id,
datasetId: location.datasetId,
name: location.name,
description: location.description,
latitude: location.latitude,
longitude: location.longitude,
createdAt: location.createdAt,
createdBy: location.createdBy,
lastModified: location.lastModified,
modifiedBy: location.modifiedBy,
active: location.active,
});
if (result.length === 0) {
return c.json({
error: "Failed to update location"
}, 500);
}
console.log("Updated location:", result[0].id, "by user:", userId);
return c.json({
data: result[0]
});
} catch (error) {
console.error("Error updating location:", error);
return c.json(
{
error: "Failed to update location",
const response = await fetch("/api/locations", {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id,
datasetId,
name: formData.name,
description: formData.description || null,
latitude: formData.latitude ? parseFloat(formData.latitude) : null,
longitude: formData.longitude ? parseFloat(formData.longitude) : null,
}),
});
if (isEditMode && editingLocation) {
// Update existing location
const response = await fetch(`/api/locations/${editingLocation.id}`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.name,
description: formData.description || null,
latitude: formData.latitude ? parseFloat(formData.latitude) : null,
longitude: formData.longitude ? parseFloat(formData.longitude) : null,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
}
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
}
const { data: newLocation } = await response.json();
// Add the new location to the list
setLocations(prev => [newLocation, ...prev]);
const { data: updatedLocation } = await response.json();
// Update the location in the list
setLocations(prev => prev.map(location =>
location.id === updatedLocation.id ? updatedLocation : location
));
} else {
// Create new location
const id = nanoid(12);
const response = await fetch("/api/locations", {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id,
datasetId,
name: formData.name,
description: formData.description || null,
latitude: formData.latitude ? parseFloat(formData.latitude) : null,
longitude: formData.longitude ? parseFloat(formData.longitude) : null,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
}
const { data: newLocation } = await response.json();
// Add the new location to the list
setLocations(prev => [newLocation, ...prev]);
}
setFormError(err instanceof Error ? err.message : "Failed to create location");
console.error("Error creating location:", err);
setFormError(err instanceof Error ? err.message : `Failed to ${isEditMode ? 'update' : 'create'} location`);
console.error(`Error ${isEditMode ? 'updating' : 'creating'} location:`, err);
const handleEditLocation = (location: Location) => {
setIsEditMode(true);
setEditingLocation(location);
setFormData({
name: location.name,
description: location.description || '',
latitude: location.latitude ? String(location.latitude) : '',
longitude: location.longitude ? String(location.longitude) : ''
});
setShowForm(true);
};
const handleDeleteLocation = async (location: Location) => {
if (!isAuthenticated) return;
const confirmDelete = window.confirm(`Are you sure you want to delete "${location.name}"? This action cannot be undone.`);
if (!confirmDelete) return;
try {
const accessToken = await getAccessToken();
const response = await fetch(`/api/locations/${location.id}`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
active: false
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);
}
// Remove the location from the list (soft delete)
setLocations(prev => prev.filter(l => l.id !== location.id));
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete location");
console.error("Error deleting location:", err);
}
};
<TableCell className="font-medium whitespace-normal break-words">{location.name}</TableCell>
<TableCell className="whitespace-normal break-words">
<TableCell
className={`font-medium whitespace-normal break-words ${onLocationSelect ? 'cursor-pointer' : ''}`}
onClick={() => onLocationSelect && onLocationSelect(location.id, location.name)}
>
{location.name}
</TableCell>
<TableCell
className={`whitespace-normal break-words ${onLocationSelect ? 'cursor-pointer' : ''}`}
onClick={() => onLocationSelect && onLocationSelect(location.id, location.name)}
>
<TableCell className="whitespace-normal break-words">{location.description || "—"}</TableCell>
<TableCell
className={`whitespace-normal break-words ${onLocationSelect ? 'cursor-pointer' : ''}`}
onClick={() => onLocationSelect && onLocationSelect(location.id, location.name)}
>
{location.description || "—"}
</TableCell>
{canCreateLocations && (
<TableCell className="text-center">
<div className="flex justify-center gap-2">
<Button
onClick={(e) => {
e.stopPropagation();
handleEditLocation(location);
}}
variant="ghost"
size="sm"
className="p-1 h-8 w-8"
title="Edit location"
>
<Edit className="h-4 w-4" />
</Button>
<Button
onClick={(e) => {
e.stopPropagation();
handleDeleteLocation(location);
}}
variant="ghost"
size="sm"
className="p-1 h-8 w-8 text-red-600 hover:text-red-700 hover:bg-red-50"
title="Delete location"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
)}