Bug Fixes + Duplicate Support (#1016)
* Update README.md supporter list * Fix: Multiple bug fixes and features bundle (#888, #991, #617, #984) (#1007) * fix: resolve location creation failures, broken image uploads, and invalid URL handling - Add missing addToast import in LocationDetails.svelte for proper error feedback - Add objectId check and error response handling in ImageManagement.svelte to prevent ghost images - Add Content-Type check in +page.server.ts image action to handle non-JSON backend responses - Add client-side URL validation in LocationDetails.svelte (invalid URLs → null) - Improve Django field error extraction for user-friendly toast messages - Clean up empty description fields (whitespace → null) - Update BUGFIX_DOCUMENTATION.md with detailed fix descriptions * feat: bug fixes and new features bundle Bug fixes: - fix: resolve PATCH location with visits (#888) - fix: Wikipedia/URL image upload via server-side proxy (#991) - fix: private/public toggle race condition (#617) - fix: location creation feedback (addToast import) - fix: invalid URL handling for locations and collections - fix: world map country highlighting (bg-*-200 -> bg-*-400) - fix: clipboard API polyfill for HTTP contexts - fix: MultipleObjectsReturned for duplicate images - fix: SvelteKit proxy sessionid cookie forwarding Features: - feat: duplicate location button (list + detail view) - feat: duplicate collection button - feat: i18n translations for 19 languages - feat: improved error handling and user feedback Technical: - Backend: fetch_from_url endpoint with SSRF protection - Backend: validate_link() for collections - Backend: file_permissions filter() instead of get() - Frontend: copyToClipboard() helper function - Frontend: clipboard polyfill via server-side injection * chore: switch docker-compose from image to build Use local source code builds instead of upstream :latest images to preserve our custom patches and fixes. * fix: lodging save errors, AI language support, and i18n improvements - Fix Lodging save: add res.ok checks, error toasts, isSaving state (#984) - Fix URL validation: silently set invalid URLs to null (Lodging, Transportation) - Fix AI description language: pass user locale to Wikipedia API - Fix missing i18n keys: Strava toggle buttons (show/hide) - Add CHANGELOG.md - Remove internal documentation from public tracking - Update .gitignore for Cursor IDE and internal docs Co-authored-by: Cursor <cursoragent@cursor.com> * feat: update location duplication handling, improve UI feedback, and enhance localization support --------- Co-authored-by: AdventureLog Bugfix <bugfix@adventurelog.local> Co-authored-by: madmp87 <info@so-pa.de> Co-authored-by: Mathias Ponnwitz <devuser@dockge-dev.fritz.box> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Sean Morley <mail@seanmorley.com> * Enhance duplication functionality for collections and locations; update UI to reflect changes * Potential fix for code scanning alert no. 49: Information exposure through an exception Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * Update Django and Pillow versions in requirements.txt * Fix error logging for image fetch timeout in ContentImageViewSet * Update requirements.txt to include jaraco.context and wheel for security fixes * Update app version and add security vulnerabilities to .trivyignore * Update backend/server/adventures/views/collection_view.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update frontend/src/lib/types.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Reorder build and image directives in docker-compose.yml for clarity * Refactor code structure for improved readability and maintainability * Remove inline clipboard polyfill script injection from server hooks (#1019) * Initial plan * Remove inline clipboard polyfill script injection from hooks.server.ts Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com> * Fix unhandled promise rejections in copyToClipboard click handlers (#1018) * Initial plan * Fix: make copyToClipboard handlers async with try/catch error toast Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com> * Harden `fetch_from_url` image proxy: require auth, rate-limit, and strengthen SSRF protections (#1017) * Initial plan * Harden fetch_from_url: require auth, rate-limit, block non-standard ports, check all IPs, re-validate redirects Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com> * Fix subregion filtering in world travel page to exclude null values * Update package.json to use caret (^) for versioning in overrides * fix: update package dependencies for compatibility and stability - Added cookie dependency with version constraint <0.7.0 - Updated svelte dependency to allow versions <=5.51.4 - Updated @sveltejs/adapter-vercel dependency to allow versions <6.3.2 * Refactor code structure for improved readability and maintainability --------- Co-authored-by: madmp87 <79420509+madmp87@users.noreply.github.com> Co-authored-by: AdventureLog Bugfix <bugfix@adventurelog.local> Co-authored-by: madmp87 <info@so-pa.de> Co-authored-by: Mathias Ponnwitz <devuser@dockge-dev.fritz.box> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -91,9 +91,18 @@
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
/* Prevent unwanted horizontal scroll caused by positioned dropdowns or transformed ancestors on small screens */
|
||||
:global(html),
|
||||
:global(body) {
|
||||
/* Prevent unwanted horizontal scroll and ensure single scrollbar */
|
||||
:global(html) {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Ensure slot content doesn't create nested scrollbars */
|
||||
:global(body > div) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,7 +63,8 @@ async function handleRequest(
|
||||
}
|
||||
|
||||
// Set the new csrf token in both headers and cookies
|
||||
const cookieHeader = `csrftoken=${csrfToken}; Path=/; HttpOnly; SameSite=Lax`;
|
||||
const sessionId = cookies.get('sessionid');
|
||||
const cookieHeader = `csrftoken=${csrfToken}` + (sessionId ? `; sessionid=${sessionId}` : '');
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
|
||||
@@ -65,7 +65,8 @@ async function handleRequest(
|
||||
}
|
||||
|
||||
// Set the new csrf token in both headers and cookies
|
||||
const cookieHeader = `csrftoken=${csrfToken}; Path=/; HttpOnly; SameSite=Lax`;
|
||||
const sessionId = cookies.get('sessionid');
|
||||
const cookieHeader = `csrftoken=${csrfToken}` + (sessionId ? `; sessionid=${sessionId}` : '');
|
||||
|
||||
try {
|
||||
const response = await fetch(targetUrl, {
|
||||
|
||||
@@ -80,7 +80,6 @@ export const load = (async (event) => {
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions: Actions = {
|
||||
|
||||
restoreData: async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return redirect(302, '/');
|
||||
|
||||
@@ -191,6 +191,21 @@
|
||||
isShowingCollectionModal = false;
|
||||
}
|
||||
|
||||
function duplicateCollectionInList(event: CustomEvent<SlimCollection | Collection>) {
|
||||
const duplicatedCollection = event.detail as SlimCollection;
|
||||
|
||||
collections = [
|
||||
duplicatedCollection,
|
||||
...collections.filter((collection) => collection.id !== duplicatedCollection.id)
|
||||
];
|
||||
|
||||
archivedCollections = archivedCollections.filter(
|
||||
(collection) => collection.id !== duplicatedCollection.id
|
||||
);
|
||||
|
||||
activeView = 'owned';
|
||||
}
|
||||
|
||||
async function editCollection(event: CustomEvent<SlimCollection>) {
|
||||
const slim = event.detail;
|
||||
try {
|
||||
@@ -574,7 +589,7 @@
|
||||
{:else}
|
||||
<!-- Collections Grid -->
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6"
|
||||
class="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-6"
|
||||
>
|
||||
{#each currentCollections as collection (collection.id)}
|
||||
<CollectionCard
|
||||
@@ -584,6 +599,7 @@
|
||||
on:edit={editCollection}
|
||||
on:archive={archiveCollection}
|
||||
on:unarchive={unarchiveCollection}
|
||||
on:duplicate={duplicateCollectionInList}
|
||||
user={data.user}
|
||||
on:leave={(e) => {
|
||||
collectionIdToLeave = e.detail;
|
||||
|
||||
@@ -1179,7 +1179,7 @@
|
||||
<!-- Itinerary View -->
|
||||
{#if currentView === 'itinerary'}
|
||||
<CollectionItineraryPlanner
|
||||
{collection}
|
||||
bind:collection
|
||||
user={data.user}
|
||||
canModify={canModifyCollection}
|
||||
/>
|
||||
|
||||
9
frontend/src/routes/locations/+page.server.ts
Normal file → Executable file
9
frontend/src/routes/locations/+page.server.ts
Normal file → Executable file
@@ -74,6 +74,15 @@ export const actions: Actions = {
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
// Handle non-JSON responses gracefully (e.g. HTML error pages from backend)
|
||||
const contentType = res.headers.get('content-type') || '';
|
||||
if (!contentType.includes('application/json')) {
|
||||
const text = await res.text();
|
||||
console.error(`Image upload failed with status ${res.status}:`, text.substring(0, 200));
|
||||
return { error: `Image upload failed (status ${res.status})` };
|
||||
}
|
||||
|
||||
let data = await res.json();
|
||||
return data;
|
||||
},
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
{:else}
|
||||
<!-- Adventures Grid -->
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6"
|
||||
class="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-6"
|
||||
>
|
||||
{#each adventures as adventure}
|
||||
<LocationCard
|
||||
@@ -261,6 +261,10 @@
|
||||
{adventure}
|
||||
on:delete={deleteAdventure}
|
||||
on:edit={editAdventure}
|
||||
on:duplicate={(e) => {
|
||||
// Add the new location to the beginning of the list
|
||||
adventures = [e.detail, ...adventures];
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
import LightbulbOn from '~icons/mdi/lightbulb-on';
|
||||
import WeatherSunset from '~icons/mdi/weather-sunset';
|
||||
import ClipboardList from '~icons/mdi/clipboard-list';
|
||||
import ContentCopy from '~icons/mdi/content-copy';
|
||||
import DotsVertical from '~icons/mdi/dots-vertical';
|
||||
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
|
||||
import AttachmentCard from '$lib/components/cards/AttachmentCard.svelte';
|
||||
import { getActivityColor, getBasemapUrl, isAllDay } from '$lib';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import { getActivityColor, getBasemapUrl, isAllDay, copyToClipboard } from '$lib';
|
||||
import ActivityCard from '$lib/components/cards/ActivityCard.svelte';
|
||||
import TrailCard from '$lib/components/cards/TrailCard.svelte';
|
||||
import NewLocationModal from '$lib/components/locations/LocationModal.svelte';
|
||||
@@ -126,6 +129,32 @@
|
||||
return measurementSystem === 'imperial' ? totalMeters * 3.28084 : totalMeters;
|
||||
}
|
||||
|
||||
let isDuplicating = false;
|
||||
let isFabMenuOpen = false;
|
||||
|
||||
async function duplicateAdventure() {
|
||||
if (isDuplicating) return;
|
||||
isDuplicating = true;
|
||||
isFabMenuOpen = false;
|
||||
try {
|
||||
const res = await fetch(`/api/locations/${adventure.id}/duplicate/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (res.ok) {
|
||||
const newLocation = await res.json();
|
||||
addToast('success', $t('adventures.location_duplicate_success'));
|
||||
goto(`/locations/${newLocation.id}`);
|
||||
} else {
|
||||
addToast('error', $t('adventures.location_duplicate_error'));
|
||||
}
|
||||
} catch (e) {
|
||||
addToast('error', $t('adventures.location_duplicate_error'));
|
||||
} finally {
|
||||
isDuplicating = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeImageModal() {
|
||||
isImageModalOpen = false;
|
||||
}
|
||||
@@ -183,12 +212,40 @@
|
||||
{#if adventure}
|
||||
{#if data.user?.uuid && adventure.user?.uuid && data.user.uuid === adventure.user.uuid}
|
||||
<div class="fixed bottom-6 right-6 z-50">
|
||||
<button
|
||||
class="btn btn-primary btn-circle w-16 h-16 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-110"
|
||||
on:click={() => (isEditModalOpen = true)}
|
||||
>
|
||||
<ClipboardList class="w-8 h-8" />
|
||||
</button>
|
||||
<div class="dropdown dropdown-top dropdown-end" class:dropdown-open={isFabMenuOpen}>
|
||||
<button
|
||||
class="btn btn-primary btn-circle w-16 h-16 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-110"
|
||||
on:click={() => (isFabMenuOpen = !isFabMenuOpen)}
|
||||
>
|
||||
<DotsVertical class="w-8 h-8" />
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow-lg border border-base-300 mb-2"
|
||||
>
|
||||
<li>
|
||||
<button
|
||||
on:click={() => {
|
||||
isFabMenuOpen = false;
|
||||
isEditModalOpen = true;
|
||||
}}
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<ClipboardList class="w-5 h-5" />
|
||||
{$t('adventures.edit_location')}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
on:click={duplicateAdventure}
|
||||
class="flex items-center gap-2"
|
||||
disabled={isDuplicating}
|
||||
>
|
||||
<ContentCopy class="w-5 h-5" />
|
||||
{isDuplicating ? '...' : $t('adventures.duplicate_location')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -648,19 +705,27 @@
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="btn btn-xs btn-ghost flex-1 text-xs"
|
||||
on:click={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`${adventure.latitude}, ${adventure.longitude}`
|
||||
)}
|
||||
on:click={async () => {
|
||||
try {
|
||||
await copyToClipboard(`${adventure.latitude}, ${adventure.longitude}`);
|
||||
} catch {
|
||||
addToast('error', $t('adventures.copy_failed'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
📋 {$t('adventures.copy_coordinates')}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-xs btn-ghost flex-1 text-xs"
|
||||
on:click={() =>
|
||||
navigator.clipboard.writeText(
|
||||
`https://www.google.com/maps/@${adventure.latitude},${adventure.longitude},15z`
|
||||
)}
|
||||
on:click={async () => {
|
||||
try {
|
||||
await copyToClipboard(
|
||||
`https://www.google.com/maps/@${adventure.latitude},${adventure.longitude},15z`
|
||||
);
|
||||
} catch {
|
||||
addToast('error', $t('adventures.copy_failed'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
🔗 {$t('adventures.copy_link')}
|
||||
</button>
|
||||
|
||||
@@ -294,10 +294,10 @@
|
||||
|
||||
$: showLocalTripTime = Boolean(
|
||||
localTravelWindow &&
|
||||
primaryTripTimezone(
|
||||
transportation?.start_timezone ?? null,
|
||||
transportation?.end_timezone ?? null
|
||||
) !== localTimeZone
|
||||
primaryTripTimezone(
|
||||
transportation?.start_timezone ?? null,
|
||||
transportation?.end_timezone ?? null
|
||||
) !== localTimeZone
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -157,7 +157,13 @@
|
||||
goto(`/worldtravel/${countryCode}`);
|
||||
}
|
||||
|
||||
worldSubregions = [...new Set(allCountries.map((country) => country.subregion))];
|
||||
worldSubregions = [
|
||||
...new Set(
|
||||
allCountries
|
||||
.map((country) => country.subregion)
|
||||
.filter((subregion): subregion is string => subregion !== null)
|
||||
)
|
||||
];
|
||||
worldSubregions = worldSubregions.filter((subregion) => subregion !== '');
|
||||
console.log(worldSubregions);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user