OWHNUYOKSGQTG6ZSVQZ3DNMYCW3IOEMXGRRS5QRRDAPIG4MOODNAC
E7L6ECZK5DLB27CDJSB3USBUKU4E2HE4GDNUIQJB6HNU4J65MRMAC
E3WSKRJTPPRD6ZEEURTO77N6GTHYJY7ISWQL2LN2VCCXA4YRH2CAC
UCDTBEK3CF6YT2H6V57HI6FAFW44BIYYAK3Z2QJ5LJE7QWX7OEYAC
JHFIJJSLVMQNYIDE6CXWUK5UTB7BTUSY7NCI33WKCWIRZHCSMBHQC
YX7LU4WRAUDMWS3DEDXZDSF6DXBHLYDWVSMSRK6KIW3MO6GRXSVQC
RLH37YB4D7O42IFM2T7GJG4AVVAURWBZ7AOTHAWR7YJZRG3JOPLQC
4M3EBLTLSS2BRCM42ZP7WVD4YMRRLGV2P2XF47IAV5XHHJD52HTQC
M4UG5FMI5ICQRLJCCNSLV3ZKCGIXDQ65ECJM4PBE3NZYHT3CJLDAC
CVW5G63BAFGQBR5YTZIHTZVVVECP2U3XI76APTKKOODGWHRBEIBQC
POIBWSL3JFHT2KN3STFSJX3INSYKEJTX6KSW3N7BVEKWX2GJ6T7QC
O7W4FZVRKDQDAAXEW4T7P262PPRILRCSSACODMUTQZ6VNR36PVCQC
DOQBQX4IQSDBYYSBP4BEMTMJUKZPSC33KKXPAGZ3A5BRJMMKHCRQC
2OYSY7VNMQ4C3CQMX7VKKD2JSZM72TJ3LL4GEDUQYG6RJTNIROLAC
M3JUJ2WWZGCVMBITKRM5FUJMHFYL2QRMXJUVRUE4AC2RF74AOL5AC
details: error instanceof Error ? error.message : String(error),
},
500
);
}
});
/**
* Protected API route to create a new location
*
* @route POST /api/locations
* @authentication Required
* @param {Object} body - Location data including datasetId, name, description, latitude, longitude
* @returns {Object} Response containing:
* - data: Created location object
* @description Creates a new location with the authenticated user as owner.
* Requires EDIT permission on the dataset to create locations within it.
*/
app.post("/api/locations", authenticate, async (c) => {
try {
// Get user ID from JWT
const jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;
const userId = jwtPayload.sub;
// Database connection
const sql = neon(c.env.DATABASE_URL);
const db = drizzle(sql);
// Parse and validate request body
const body = await c.req.json();
const { id, datasetId, name, description, latitude, longitude } = body;
// Field validation
if (!id || typeof id !== 'string') {
return c.json({
error: "Missing or invalid required field: id"
}, 400);
}
if (!datasetId || typeof datasetId !== 'string') {
return c.json({
error: "Missing or invalid required field: datasetId"
}, 400);
}
if (!name || typeof name !== 'string' || name.trim().length === 0) {
return c.json({
error: "Missing or invalid required field: name"
}, 400);
}
// Length validation
if (id.length !== 12) {
return c.json({
error: "Field 'id' must be exactly 12 characters (nanoid)"
}, 400);
}
if (name.length > 140) {
return c.json({
error: "Field 'name' must be 140 characters or less"
}, 400);
}
if (description && description.length > 255) {
return c.json({
error: "Field 'description' must be 255 characters or less"
}, 400);
}
// Validate latitude and longitude if provided
if (latitude !== null && latitude !== undefined) {
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);
}
}
if (longitude !== null && longitude !== undefined) {
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);
}
}
// Check if user has EDIT permission on the dataset
const hasPermission = await checkUserPermission(db, userId, datasetId.trim(), 'EDIT');
if (!hasPermission) {
return c.json({
error: "You don't have permission to create locations in this dataset"
}, 403);
}
// Create location object
const now = new Date();
const newLocation = {
id: id.trim(),
datasetId: datasetId.trim(),
name: name.trim(),
description: description?.trim() || null,
latitude: latitude !== null && latitude !== undefined ? String(Number(latitude)) : null,
longitude: longitude !== null && longitude !== undefined ? String(Number(longitude)) : null,
createdBy: userId,
createdAt: now,
lastModified: now,
modifiedBy: userId,
active: true,
};
// Insert the location
const result = await db.insert(location).values(newLocation).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,
});
console.log("Created location:", result[0].id, "for dataset:", datasetId, "by user:", userId);
return c.json({
data: result[0]
}, 201);
} catch (error) {
console.error("Error creating location:", error);
// Handle unique constraint violations
if (error instanceof Error && error.message.includes('duplicate key')) {
return c.json({
error: "A location with this ID already exists"
}, 400);
}
return c.json(
{
error: "Failed to create location",
const handleSubmitLocation = async (e: React.FormEvent) => {
e.preventDefault();
if (!isAuthenticated) return;
setFormLoading(true);
setFormError(null);
try {
const accessToken = await getAccessToken();
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]);
// Reset form and close modal
handleCancelForm();
} catch (err) {
setFormError(err instanceof Error ? err.message : "Failed to create location");
console.error("Error creating location:", err);
} finally {
setFormLoading(false);
}
};
const handleFormChange = (field: keyof typeof formData, value: string) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleCancelForm = () => {
setShowForm(false);
setFormError(null);
setFormData({
name: '',
description: '',
latitude: '',
longitude: ''
});
};
const handleCreateNew = () => {
setFormData({
name: '',
description: '',
latitude: '',
longitude: ''
});
setShowForm(true);
};
<div className="card p-6 bg-white shadow-sm rounded-lg">
<>
{/* Header with Add Button */}
{isAuthenticated && !authLoading && canCreateLocations && (
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-semibold">Locations</h2>
<Button
onClick={handleCreateNew}
variant="default"
size="sm"
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Add Location
</Button>
</div>
)}
<div className="card p-6 bg-white shadow-sm rounded-lg">
</div>
</div>
{/* Create Location Modal */}
{showForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 w-full max-w-md mx-4">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">
Create New Location
</h3>
<Button
onClick={handleCancelForm}
variant="ghost"
size="sm"
className="p-1"
>
<X className="h-4 w-4" />
</Button>
</div>
{formError && (
<div className="bg-red-50 border border-red-200 text-red-700 px-3 py-2 rounded mb-4">
{formError}
</div>
)}
<form onSubmit={handleSubmitLocation} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Name *
</label>
<input
type="text"
id="name"
required
maxLength={255}
value={formData.name}
onChange={(e) => handleFormChange('name', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter location name"
/>
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
id="description"
maxLength={255}
value={formData.description}
onChange={(e) => handleFormChange('description', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter description (optional)"
rows={3}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="latitude" className="block text-sm font-medium text-gray-700 mb-1">
Latitude
</label>
<input
type="number"
id="latitude"
step="any"
min="-90"
max="90"
value={formData.latitude}
onChange={(e) => handleFormChange('latitude', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. -45.50608"
/>
</div>
<div>
<label htmlFor="longitude" className="block text-sm font-medium text-gray-700 mb-1">
Longitude
</label>
<input
type="number"
id="longitude"
step="any"
min="-180"
max="180"
value={formData.longitude}
onChange={(e) => handleFormChange('longitude', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="e.g. 167.47822"
/>
</div>
</div>
<div className="flex justify-end space-x-3 pt-4">
<Button
type="button"
onClick={handleCancelForm}
variant="outline"
disabled={formLoading}
>
Cancel
</Button>
<Button
type="submit"
disabled={formLoading || !formData.name.trim()}
>
{formLoading ? "Creating..." : "Create Location"}
</Button>
</div>
</form>
</div>
</div>
)}
</>