├── .devcontainer └── devcontainer.json ├── .github ├── .docker-compose-database.yml ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── deployment-issue.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── backend-beta.yml │ ├── backend-latest.yml │ ├── backend-release.yml │ ├── backend-test.yml │ ├── cdn-beta.yml │ ├── cdn-latest.yml │ ├── cdn-release.yml │ ├── frontend-beta.yml │ ├── frontend-latest.yml │ ├── frontend-release.yml │ └── frontend-test.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── backend ├── .devcontainer │ └── devcontainer.json ├── .github │ └── dependabot.yml ├── .gitignore ├── Dockerfile ├── entrypoint.sh ├── nginx.conf ├── server │ ├── .env.example │ ├── achievements │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── achievement-seed.py │ │ ├── models.py │ │ ├── tests.py │ │ └── views.py │ ├── adventurelog.txt │ ├── adventures │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── travel-seed.py │ │ ├── managers.py │ │ ├── middleware.py │ │ ├── migrations │ │ │ ├── 0001_adventure_image.py │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_adventureimage.py │ │ │ ├── 0002_alter_adventureimage_adventure.py │ │ │ ├── 0003_adventure_end_date.py │ │ │ ├── 0004_transportation_end_date.py │ │ │ ├── 0005_collection_shared_with.py │ │ │ ├── 0006_alter_adventure_link.py │ │ │ ├── 0007_visit_model.py │ │ │ ├── 0008_remove_date_field.py │ │ │ ├── 0009_alter_adventure_type.py │ │ │ ├── 0010_collection_link.py │ │ │ ├── 0011_category_adventure_category.py │ │ │ ├── 0012_migrate_types_to_categories.py │ │ │ ├── 0013_remove_adventure_type_alter_adventure_category.py │ │ │ ├── 0014_alter_category_unique_together.py │ │ │ ├── 0015_transportation_destination_latitude_and_more.py │ │ │ ├── 0016_alter_adventureimage_image.py │ │ │ ├── 0017_adventureimage_is_primary.py │ │ │ ├── 0018_attachment.py │ │ │ ├── 0019_alter_attachment_file.py │ │ │ ├── 0020_attachment_name.py │ │ │ ├── 0021_alter_attachment_name.py │ │ │ ├── 0022_hotel.py │ │ │ ├── 0023_lodging_delete_hotel.py │ │ │ ├── 0024_alter_attachment_file.py │ │ │ ├── 0025_alter_visit_end_date_alter_visit_start_date.py │ │ │ ├── 0026_visit_timezone.py │ │ │ ├── 0027_transportation_end_timezone_and_more.py │ │ │ ├── 0028_lodging_timezone.py │ │ │ ├── __init__.py │ │ │ ├── migrate_images.py │ │ │ └── migrate_visits_categories.py │ │ ├── models.py │ │ ├── permissions.py │ │ ├── serializers.py │ │ ├── tests.py │ │ ├── urls.py │ │ ├── utils │ │ │ ├── file_permissions.py │ │ │ └── pagination.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── activity_types_view.py │ │ │ ├── adventure_image_view.py │ │ │ ├── adventure_view.py │ │ │ ├── attachment_view.py │ │ │ ├── category_view.py │ │ │ ├── checklist_view.py │ │ │ ├── collection_view.py │ │ │ ├── generate_description_view.py │ │ │ ├── global_search_view.py │ │ │ ├── ics_calendar_view.py │ │ │ ├── lodging_view.py │ │ │ ├── note_view.py │ │ │ ├── overpass_view.py │ │ │ ├── reverse_geocode_view.py │ │ │ ├── stats_view.py │ │ │ └── transportation_view.py │ ├── build_files.sh │ ├── integrations │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── main │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ ├── utils.py │ │ ├── views.py │ │ └── wsgi.py │ ├── manage.py │ ├── requirements.txt │ ├── templates │ │ ├── base.html │ │ ├── fragments │ │ │ ├── email_verification_form.html │ │ │ ├── login_form.html │ │ │ ├── logout_form.html │ │ │ ├── password_change_form.html │ │ │ ├── password_reset_confirm_form.html │ │ │ ├── password_reset_form.html │ │ │ ├── resend_email_verification_form.html │ │ │ ├── signup_form.html │ │ │ └── user_details_form.html │ │ ├── home.html │ │ └── rest_framework │ │ │ └── api.html │ ├── users │ │ ├── __init__.py │ │ ├── adapters.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── backends.py │ │ ├── form_overrides.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_customuser_public_profile.py │ │ │ ├── 0003_alter_customuser_email.py │ │ │ ├── 0004_customuser_disable_password.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── tests.py │ │ └── views.py │ └── worldtravel │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── download-countries.py │ │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_region_name_en.py │ │ ├── 0003_alter_region_name_en.py │ │ ├── 0004_country_geometry.py │ │ ├── 0005_remove_country_geometry_region_geometry.py │ │ ├── 0006_remove_country_continent_country_subregion.py │ │ ├── 0007_remove_region_geometry_remove_region_name_en.py │ │ ├── 0008_region_latitude_region_longitude.py │ │ ├── 0009_alter_country_country_code.py │ │ ├── 0010_country_capital.py │ │ ├── 0011_country_latitude_country_longitude.py │ │ ├── 0012_city.py │ │ ├── 0013_visitedcity.py │ │ ├── 0014_alter_visitedcity_options.py │ │ ├── 0015_city_insert_id_country_insert_id_region_insert_id.py │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py └── supervisord.conf ├── backup.sh ├── brand ├── adventurelog.png ├── adventurelog.svg ├── banner.png └── screenshots │ ├── adventures.png │ ├── countries.png │ ├── dashboard.png │ ├── details.png │ ├── edit.png │ ├── itinerary.png │ ├── map.png │ └── regions.png ├── cdn ├── .gitignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── entrypoint.sh ├── index.html ├── main.py ├── nginx.conf └── requirements.txt ├── deploy.sh ├── docker-compose-traefik.yaml ├── docker-compose.yml ├── documentation ├── .gitignore ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.ts │ │ └── style.css ├── docs │ ├── changelogs │ │ ├── v0-7-0.md │ │ ├── v0-7-1.md │ │ ├── v0-8-0.md │ │ └── v0-9-0.md │ ├── configuration │ │ ├── analytics.md │ │ ├── disable_registration.md │ │ ├── email.md │ │ ├── immich_integration.md │ │ ├── social_auth.md │ │ ├── social_auth │ │ │ ├── authentik.md │ │ │ ├── github.md │ │ │ └── oidc.md │ │ └── updating.md │ ├── guides │ │ ├── admin_panel.md │ │ └── v0-7-1_migration.md │ ├── install │ │ ├── caddy.md │ │ ├── docker.md │ │ ├── getting_started.md │ │ ├── kustomize.md │ │ ├── nginx_proxy_manager.md │ │ ├── proxmox_lxc.md │ │ ├── synology_nas.md │ │ ├── traefik.md │ │ └── unraid.md │ ├── intro │ │ └── adventurelog_overview.md │ ├── troubleshooting │ │ ├── login_unresponsive.md │ │ ├── nginx_failed.md │ │ └── no_images.md │ └── usage │ │ └── usage.md ├── index.md ├── package.json ├── pnpm-lock.yaml ├── public │ ├── adventurelog.png │ ├── adventurelog.svg │ ├── authentik_settings.png │ ├── github_settings.png │ ├── unraid-config-1.png │ ├── unraid-config-2.png │ └── unraid-config-3.png └── static │ └── img │ └── favicon.png ├── frontend ├── .env.example ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src │ ├── app.d.ts │ ├── app.html │ ├── hooks.server.ts │ ├── lib │ │ ├── assets │ │ │ ├── AdventureOverlook.webp │ │ │ ├── MapWithPins.webp │ │ │ ├── immich.svg │ │ │ ├── undraw_lost.svg │ │ │ └── undraw_server_error.svg │ │ ├── components │ │ │ ├── AboutModal.svelte │ │ │ ├── ActivityComplete.svelte │ │ │ ├── AdventureCard.svelte │ │ │ ├── AdventureLink.svelte │ │ │ ├── AdventureModal.svelte │ │ │ ├── AttachmentCard.svelte │ │ │ ├── Avatar.svelte │ │ │ ├── CardCarousel.svelte │ │ │ ├── CategoryDropdown.svelte │ │ │ ├── CategoryFilterDropdown.svelte │ │ │ ├── CategoryModal.svelte │ │ │ ├── ChecklistCard.svelte │ │ │ ├── ChecklistModal.svelte │ │ │ ├── CityCard.svelte │ │ │ ├── CollectionCard.svelte │ │ │ ├── CollectionLink.svelte │ │ │ ├── CollectionModal.svelte │ │ │ ├── CountryCard.svelte │ │ │ ├── DateRangeCollapse.svelte │ │ │ ├── DeleteWarning.svelte │ │ │ ├── ImageDisplayModal.svelte │ │ │ ├── ImageFetcher.svelte │ │ │ ├── ImageInfoModal.svelte │ │ │ ├── ImmichSelect.svelte │ │ │ ├── LocationDropdown.svelte │ │ │ ├── LodgingCard.svelte │ │ │ ├── LodgingModal.svelte │ │ │ ├── MarkdownEditor.svelte │ │ │ ├── Navbar.svelte │ │ │ ├── NotFound.svelte │ │ │ ├── NoteCard.svelte │ │ │ ├── NoteModal.svelte │ │ │ ├── PointSelectionModal.svelte │ │ │ ├── RegionCard.svelte │ │ │ ├── ShareModal.svelte │ │ │ ├── TOTPModal.svelte │ │ │ ├── TimezoneSelector.svelte │ │ │ ├── Toast.svelte │ │ │ ├── TransportationCard.svelte │ │ │ ├── TransportationModal.svelte │ │ │ └── UserCard.svelte │ │ ├── config.ts │ │ ├── dateUtils.ts │ │ ├── index.server.ts │ │ ├── index.ts │ │ ├── json │ │ │ ├── backgrounds.json │ │ │ └── quotes.json │ │ ├── toasts.ts │ │ └── types.ts │ ├── locales │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── it.json │ │ ├── ko.json │ │ ├── nl.json │ │ ├── no.json │ │ ├── pl.json │ │ ├── sv.json │ │ └── zh.json │ ├── routes │ │ ├── +error.svelte │ │ ├── +layout.server.ts │ │ ├── +layout.svelte │ │ ├── +page.server.ts │ │ ├── +page.svelte │ │ ├── activities │ │ │ └── +server.ts │ │ ├── admin │ │ │ └── +page.server.ts │ │ ├── adventures │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── [id] │ │ │ │ ├── +page.server.ts │ │ │ │ └── +page.svelte │ │ ├── api │ │ │ └── [...path] │ │ │ │ └── +server.ts │ │ ├── auth │ │ │ └── [...path] │ │ │ │ └── +server.ts │ │ ├── calendar │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── collections │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ ├── [id] │ │ │ │ ├── +page.server.ts │ │ │ │ └── +page.svelte │ │ │ └── archived │ │ │ │ ├── +page.server.ts │ │ │ │ └── +page.svelte │ │ ├── dashboard │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── gpx │ │ │ └── [file] │ │ │ │ └── +server.ts │ │ ├── immich │ │ │ └── [key] │ │ │ │ └── +server.ts │ │ ├── login │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── map │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── profile │ │ │ └── [uuid] │ │ │ │ ├── +page.server.ts │ │ │ │ └── +page.svelte │ │ ├── search │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── settings │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── shared │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── signup │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ ├── user │ │ │ ├── [uuid] │ │ │ │ ├── +page.server.ts │ │ │ │ └── +page.svelte │ │ │ ├── reset-password │ │ │ │ ├── +page.server.ts │ │ │ │ ├── +page.svelte │ │ │ │ └── [key] │ │ │ │ │ ├── +page.server.ts │ │ │ │ │ └── +page.svelte │ │ │ └── verify-email │ │ │ │ └── [key] │ │ │ │ ├── +page.server.ts │ │ │ │ └── +page.svelte │ │ ├── users │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ │ └── worldtravel │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── [id] │ │ │ ├── +page.server.ts │ │ │ ├── +page.svelte │ │ │ └── [id] │ │ │ ├── +page.server.ts │ │ │ └── +page.svelte │ └── service-worker │ │ └── indes.ts ├── startup.sh ├── static │ ├── adventurelog.svg │ ├── backgrounds │ │ ├── adventurelog_christmas.webp │ │ ├── adventurelog_new_year.webp │ │ ├── adventurelog_showcase_1.webp │ │ ├── adventurelog_showcase_2.webp │ │ ├── adventurelog_showcase_3.webp │ │ ├── adventurelog_showcase_4.webp │ │ ├── adventurelog_showcase_5.webp │ │ └── adventurelog_showcase_6.webp │ ├── favicon.png │ ├── manifest.json │ └── robots.txt ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts └── kustomization.yml /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu 3 | { 4 | "name": "Ubuntu", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/base:jammy", 7 | "features": { 8 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 9 | "ghcr.io/devcontainers/features/node:1": {}, 10 | "ghcr.io/devcontainers/features/python:1": {} 11 | } 12 | 13 | // Features to add to the dev container. More info: https://containers.dev/features. 14 | // "features": {}, 15 | 16 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 17 | // "forwardPorts": [], 18 | 19 | // Use 'postCreateCommand' to run commands after the container is created. 20 | // "postCreateCommand": "uname -a", 21 | 22 | // Configure tool-specific properties. 23 | // "customizations": {}, 24 | 25 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 26 | // "remoteUser": "root" 27 | } 28 | -------------------------------------------------------------------------------- /.github/.docker-compose-database.yml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: postgis/postgis:15-3.3 4 | container_name: adventurelog-db 5 | restart: unless-stopped 6 | ports: 7 | - "127.0.0.1:5432:5432" 8 | environment: 9 | POSTGRES_DB: database 10 | POSTGRES_USER: adventure 11 | POSTGRES_PASSWORD: changeme123 12 | volumes: 13 | - postgres_data:/var/lib/postgresql/data/ 14 | 15 | volumes: 16 | postgres_data: 17 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @seanmorley15 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: seanmorley15 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Detailed bug reports help me diagnose and fix bugs quicker! Thanks! 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Docker Compose** 24 | If the issue is related to deployment and docker, please post an **obfuscated** (remove secrets and confidential information) version of your compose file. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/deployment-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deployment Issue 3 | about: Request help deploying AdventureLog on your machine. The more details, the 4 | better I can help! 5 | title: "[DEPLOYMENT]" 6 | labels: deployment 7 | assignees: '' 8 | 9 | --- 10 | 11 | ## Explain your issue 12 | 13 | ## Provide an **obfuscated** `docker-compose.yml` 14 | 15 | ## Provide any necessary logs from the containers and browser 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for AdventureLog 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/backend-beta.yml: -------------------------------------------------------------------------------- 1 | name: Upload beta backend image to GHCR and Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - development 7 | paths: 8 | - "backend/**" 9 | 10 | env: 11 | IMAGE_NAME: "adventurelog-backend" 12 | 13 | jobs: 14 | upload: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.ACCESS_TOKEN }} 26 | 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: set lower case owner name 40 | run: | 41 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 42 | env: 43 | OWNER: "${{ github.repository_owner }}" 44 | 45 | - name: Build Docker images 46 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./backend 47 | -------------------------------------------------------------------------------- /.github/workflows/backend-latest.yml: -------------------------------------------------------------------------------- 1 | name: Upload latest backend image to GHCR and Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "backend/**" 9 | 10 | env: 11 | IMAGE_NAME: "adventurelog-backend" 12 | 13 | jobs: 14 | upload: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.ACCESS_TOKEN }} 26 | 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: set lower case owner name 40 | run: | 41 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 42 | env: 43 | OWNER: '${{ github.repository_owner }}' 44 | 45 | - name: Build Docker images 46 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest ./backend 47 | -------------------------------------------------------------------------------- /.github/workflows/backend-release.yml: -------------------------------------------------------------------------------- 1 | name: Upload the tagged release backend image to GHCR and Docker Hub 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | env: 8 | IMAGE_NAME: "adventurelog-backend" 9 | 10 | jobs: 11 | upload: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Login to GitHub Container Registry 18 | uses: docker/login-action@v1 19 | with: 20 | registry: ghcr.io 21 | username: ${{ github.actor }} 22 | password: ${{ secrets.ACCESS_TOKEN }} 23 | 24 | - name: Login to Docker Hub 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKERHUB_USERNAME }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v3 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: set lower case owner name 37 | run: | 38 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 39 | env: 40 | OWNER: "${{ github.repository_owner }}" 41 | 42 | - name: Build Docker images 43 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} ./backend 44 | -------------------------------------------------------------------------------- /.github/workflows/backend-test.yml: -------------------------------------------------------------------------------- 1 | name: Test Backend 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | paths: 9 | - 'backend/server/**' 10 | - '.github/workflows/backend-test.yml' 11 | push: 12 | paths: 13 | - 'backend/server/**' 14 | - '.github/workflows/backend-test.yml' 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: set up python 3.12 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: '3.12' 26 | 27 | - name: install dependencies 28 | run: | 29 | sudo apt update -q 30 | sudo apt install -y -q \ 31 | python3-gdal 32 | 33 | - name: start database 34 | run: | 35 | docker compose -f .github/.docker-compose-database.yml up -d 36 | 37 | - name: install python libreries 38 | working-directory: backend/server 39 | run: | 40 | pip install -r requirements.txt 41 | 42 | - name: run server 43 | working-directory: backend/server 44 | env: 45 | PGHOST: "127.0.0.1" 46 | PGDATABASE: "database" 47 | PGUSER: "adventure" 48 | PGPASSWORD: "changeme123" 49 | SECRET_KEY: "changeme123" 50 | DJANGO_ADMIN_USERNAME: "admin" 51 | DJANGO_ADMIN_PASSWORD: "admin" 52 | DJANGO_ADMIN_EMAIL: "admin@example.com" 53 | PUBLIC_URL: "http://localhost:8000" 54 | CSRF_TRUSTED_ORIGINS: "http://localhost:5173,http://localhost:8000" 55 | DEBUG: "True" 56 | FRONTEND_URL: "http://localhost:5173" 57 | run: | 58 | python manage.py migrate 59 | python manage.py runserver & 60 | 61 | - name: wait for backend to boot 62 | run: > 63 | curl -fisS --retry 60 --retry-delay 1 --retry-all-errors 64 | http://localhost:8000/ 65 | -------------------------------------------------------------------------------- /.github/workflows/cdn-beta.yml: -------------------------------------------------------------------------------- 1 | name: Upload beta CDN image to GHCR and Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - development 7 | paths: 8 | - "cdn/**" 9 | 10 | env: 11 | IMAGE_NAME: "adventurelog-cdn" 12 | 13 | jobs: 14 | upload: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.ACCESS_TOKEN }} 26 | 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: set lower case owner name 40 | run: | 41 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 42 | env: 43 | OWNER: "${{ github.repository_owner }}" 44 | 45 | - name: Build Docker images 46 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./cdn 47 | -------------------------------------------------------------------------------- /.github/workflows/cdn-latest.yml: -------------------------------------------------------------------------------- 1 | name: Upload latest CDN image to GHCR and Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "cdn/**" 9 | 10 | env: 11 | IMAGE_NAME: "adventurelog-cdn" 12 | 13 | jobs: 14 | upload: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.ACCESS_TOKEN }} 26 | 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: set lower case owner name 40 | run: | 41 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 42 | env: 43 | OWNER: "${{ github.repository_owner }}" 44 | 45 | - name: Build Docker images 46 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest ./cdn 47 | -------------------------------------------------------------------------------- /.github/workflows/cdn-release.yml: -------------------------------------------------------------------------------- 1 | name: Upload the tagged release CDN image to GHCR and Docker Hub 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | env: 8 | IMAGE_NAME: "adventurelog-cdn" 9 | 10 | jobs: 11 | upload: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Login to GitHub Container Registry 18 | uses: docker/login-action@v1 19 | with: 20 | registry: ghcr.io 21 | username: ${{ github.actor }} 22 | password: ${{ secrets.ACCESS_TOKEN }} 23 | 24 | - name: Login to Docker Hub 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKERHUB_USERNAME }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v3 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: set lower case owner name 37 | run: | 38 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 39 | env: 40 | OWNER: "${{ github.repository_owner }}" 41 | 42 | - name: Build Docker images 43 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} ./cdn 44 | -------------------------------------------------------------------------------- /.github/workflows/frontend-beta.yml: -------------------------------------------------------------------------------- 1 | name: Upload beta frontend image to GHCR and Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - development 7 | paths: 8 | - "frontend/**" 9 | 10 | env: 11 | IMAGE_NAME: "adventurelog-frontend" 12 | 13 | jobs: 14 | upload: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.ACCESS_TOKEN }} 26 | 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: set lower case owner name 40 | run: | 41 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 42 | env: 43 | OWNER: "${{ github.repository_owner }}" 44 | 45 | - name: Build Docker images 46 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:beta -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:beta ./frontend 47 | -------------------------------------------------------------------------------- /.github/workflows/frontend-latest.yml: -------------------------------------------------------------------------------- 1 | name: Upload latest frontend image to GHCR and Docker Hub 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "frontend/**" 9 | 10 | env: 11 | IMAGE_NAME: "adventurelog-frontend" 12 | 13 | jobs: 14 | upload: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Login to GitHub Container Registry 21 | uses: docker/login-action@v1 22 | with: 23 | registry: ghcr.io 24 | username: ${{ github.actor }} 25 | password: ${{ secrets.ACCESS_TOKEN }} 26 | 27 | - name: Login to Docker Hub 28 | uses: docker/login-action@v1 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | 39 | - name: set lower case owner name 40 | run: | 41 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 42 | env: 43 | OWNER: '${{ github.repository_owner }}' 44 | 45 | - name: Build Docker images 46 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:latest -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:latest ./frontend 47 | -------------------------------------------------------------------------------- /.github/workflows/frontend-release.yml: -------------------------------------------------------------------------------- 1 | name: Upload tagged release frontend image to GHCR and Docker Hub 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | env: 8 | IMAGE_NAME: "adventurelog-frontend" 9 | 10 | jobs: 11 | upload: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Login to GitHub Container Registry 18 | uses: docker/login-action@v1 19 | with: 20 | registry: ghcr.io 21 | username: ${{ github.actor }} 22 | password: ${{ secrets.ACCESS_TOKEN }} 23 | 24 | - name: Login to Docker Hub 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKERHUB_USERNAME }} 28 | password: ${{ secrets.DOCKERHUB_TOKEN }} 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@v3 32 | 33 | - name: Set up Docker Buildx 34 | uses: docker/setup-buildx-action@v3 35 | 36 | - name: set lower case owner name 37 | run: | 38 | echo "REPO_OWNER=${OWNER,,}" >>${GITHUB_ENV} 39 | env: 40 | OWNER: "${{ github.repository_owner }}" 41 | 42 | - name: Build Docker images 43 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t ghcr.io/$REPO_OWNER/$IMAGE_NAME:${{ github.event.release.tag_name }} -t ${{ secrets.DOCKERHUB_USERNAME }}/$IMAGE_NAME:${{ github.event.release.tag_name }} ./frontend 44 | -------------------------------------------------------------------------------- /.github/workflows/frontend-test.yml: -------------------------------------------------------------------------------- 1 | name: Test Frontend 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | paths: 9 | - "frontend/**" 10 | - ".github/workflows/frontend-test.yml" 11 | push: 12 | paths: 13 | - "frontend/**" 14 | - ".github/workflows/frontend-test.yml" 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | 26 | - name: install dependencies 27 | working-directory: frontend 28 | run: npm i 29 | 30 | - name: build frontend 31 | working-directory: frontend 32 | run: npm run build 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in the .venv folder 2 | .venv/ 3 | .vscode/settings.json 4 | .pnpm-store/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "lokalise.i18n-ally", 4 | "svelte.svelte-vscode" 5 | ] 6 | } -------------------------------------------------------------------------------- /backend/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", 7 | 8 | 9 | // Features to add to the dev container. More info: https://containers.dev/features. 10 | "features": { 11 | "ghcr.io/devcontainers/features/docker-in-docker:2": {} 12 | } 13 | 14 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 15 | // "forwardPorts": [], 16 | 17 | // Use 'postCreateCommand' to run commands after the container is created. 18 | // "postCreateCommand": "pip3 install --user -r requirements.txt", 19 | 20 | // Configure tool-specific properties. 21 | // "customizations": {}, 22 | 23 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 24 | // "remoteUser": "root" 25 | } 26 | -------------------------------------------------------------------------------- /backend/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | coverage_html/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | test-results/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IDE 78 | .idea 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | demo/react-spa/node_modules/ 111 | demo/react-spa/yarn.lock 112 | 113 | # Visual Studio Code 114 | .vscode/ 115 | 116 | */media/* 117 | 118 | */staticfiles/* -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python slim image as the base image 2 | FROM python:3.10-slim 3 | 4 | LABEL Developers="Sean Morley" 5 | 6 | # Set environment variables 7 | ENV PYTHONDONTWRITEBYTECODE 1 8 | ENV PYTHONUNBUFFERED 1 9 | 10 | # Set the working directory 11 | WORKDIR /code 12 | 13 | # Install system dependencies (Nginx included) 14 | RUN apt-get update \ 15 | && apt-get install -y git postgresql-client gdal-bin libgdal-dev nginx supervisor \ 16 | && apt-get clean \ 17 | && rm -rf /var/lib/apt/lists/* 18 | 19 | # Install Python dependencies 20 | COPY ./server/requirements.txt /code/ 21 | RUN pip install --upgrade pip \ 22 | && pip install -r requirements.txt 23 | 24 | # Create necessary directories 25 | RUN mkdir -p /code/static /code/media 26 | # RUN mkdir -p /code/staticfiles /code/media 27 | 28 | # Copy the Django project code into the Docker image 29 | COPY ./server /code/ 30 | 31 | # Copy Nginx configuration 32 | COPY ./nginx.conf /etc/nginx/nginx.conf 33 | 34 | # Copy Supervisor configuration 35 | COPY ./supervisord.conf /etc/supervisor/conf.d/supervisord.conf 36 | 37 | # Collect static files 38 | RUN python3 manage.py collectstatic --noinput --verbosity 2 39 | 40 | # Set the entrypoint script 41 | COPY ./entrypoint.sh /code/entrypoint.sh 42 | RUN chmod +x /code/entrypoint.sh 43 | 44 | # Expose ports for NGINX and Gunicorn 45 | EXPOSE 80 8000 46 | 47 | # Command to start Supervisor (which starts Nginx and Gunicorn) 48 | CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] 49 | -------------------------------------------------------------------------------- /backend/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include /etc/nginx/mime.types; 9 | default_type application/octet-stream; 10 | 11 | sendfile on; 12 | keepalive_timeout 65; 13 | 14 | client_max_body_size 100M; 15 | 16 | # The backend is running in the same container, so reference localhost 17 | upstream django { 18 | server 127.0.0.1:8000; # Use localhost to point to Gunicorn running internally 19 | } 20 | 21 | server { 22 | listen 80; 23 | server_name localhost; 24 | 25 | location / { 26 | proxy_pass http://django; # Forward to the upstream block 27 | proxy_set_header Host $host; 28 | proxy_set_header X-Real-IP $remote_addr; 29 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 30 | proxy_set_header X-Forwarded-Proto $scheme; 31 | } 32 | 33 | location /static/ { 34 | alias /code/staticfiles/; # Serve static files directly 35 | } 36 | 37 | # Serve protected media files with X-Accel-Redirect 38 | location /protectedMedia/ { 39 | internal; # Only internal requests are allowed 40 | alias /code/media/; # This should match Django MEDIA_ROOT 41 | try_files $uri =404; # Return a 404 if the file doesn't exist 42 | } 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/server/.env.example: -------------------------------------------------------------------------------- 1 | PGHOST='' 2 | PGDATABASE='' 3 | PGUSER='' 4 | PGPASSWORD='' 5 | 6 | SECRET_KEY='pleasechangethisbecauseifyoudontitwillbeverybadandyouwillgethackedinlessthanaminuteguaranteed' 7 | 8 | PUBLIC_URL='http://127.0.0.1:8000' 9 | 10 | DEBUG=True 11 | 12 | FRONTEND_URL='http://localhost:3000' 13 | 14 | EMAIL_BACKEND='console' 15 | 16 | # EMAIL_BACKEND='email' 17 | # EMAIL_HOST='smtp.gmail.com' 18 | # EMAIL_USE_TLS=False 19 | # EMAIL_PORT=587 20 | # EMAIL_USE_SSL=True 21 | # EMAIL_HOST_USER='user' 22 | # EMAIL_HOST_PASSWORD='password' 23 | # DEFAULT_FROM_EMAIL='user@example.com' 24 | 25 | 26 | # ------------------- # 27 | # For Developers to start a Demo Database 28 | # docker run --name adventurelog-development -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin -e POSTGRES_DB=adventurelog -p 5432:5432 -d postgis/postgis:15-3.3 29 | 30 | # PGHOST='localhost' 31 | # PGDATABASE='adventurelog' 32 | # PGUSER='admin' 33 | # PGPASSWORD='admin' 34 | # ------------------- # -------------------------------------------------------------------------------- /backend/server/achievements/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/achievements/__init__.py -------------------------------------------------------------------------------- /backend/server/achievements/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from allauth.account.decorators import secure_admin_login 3 | from achievements.models import Achievement, UserAchievement 4 | 5 | admin.autodiscover() 6 | admin.site.login = secure_admin_login(admin.site.login) 7 | 8 | admin.site.register(Achievement) 9 | admin.site.register(UserAchievement) -------------------------------------------------------------------------------- /backend/server/achievements/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AchievementsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'achievements' 7 | -------------------------------------------------------------------------------- /backend/server/achievements/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/achievements/management/__init__.py -------------------------------------------------------------------------------- /backend/server/achievements/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/achievements/management/commands/__init__.py -------------------------------------------------------------------------------- /backend/server/achievements/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.db import models 3 | from django.contrib.auth import get_user_model 4 | 5 | User = get_user_model() 6 | 7 | VALID_ACHIEVEMENT_TYPES = [ 8 | "adventure_count", 9 | "country_count", 10 | ] 11 | 12 | class Achievement(models.Model): 13 | """Stores all possible achievements""" 14 | name = models.CharField(max_length=255, unique=True) 15 | key = models.CharField(max_length=255, unique=True, default='achievements.other') # Used for frontend lookups, e.g. "achievements.first_adventure" 16 | type = models.CharField(max_length=255, choices=[(tag, tag) for tag in VALID_ACHIEVEMENT_TYPES], default='adventure_count') # adventure_count, country_count, etc. 17 | description = models.TextField() 18 | icon = models.ImageField(upload_to="achievements/", null=True, blank=True) 19 | condition = models.JSONField() # Stores rules like {"type": "adventure_count", "value": 10} 20 | 21 | def __str__(self): 22 | return self.name 23 | 24 | class UserAchievement(models.Model): 25 | """Tracks which achievements a user has earned""" 26 | user = models.ForeignKey(User, on_delete=models.CASCADE) 27 | achievement = models.ForeignKey(Achievement, on_delete=models.CASCADE) 28 | earned_at = models.DateTimeField(auto_now_add=True) 29 | 30 | class Meta: 31 | unique_together = ("user", "achievement") # Prevent duplicates 32 | 33 | def __str__(self): 34 | return f"{self.user.username} - {self.achievement.name}" 35 | -------------------------------------------------------------------------------- /backend/server/achievements/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/server/achievements/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /backend/server/adventurelog.txt: -------------------------------------------------------------------------------- 1 | █████╗ ██████╗ ██╗ ██╗███████╗███╗ ██╗████████╗██╗ ██╗██████╗ ███████╗██╗ ██████╗ ██████╗ 2 | ██╔══██╗██╔══██╗██║ ██║██╔════╝████╗ ██║╚══██╔══╝██║ ██║██╔══██╗██╔════╝██║ ██╔═══██╗██╔════╝ 3 | ███████║██║ ██║██║ ██║█████╗ ██╔██╗ ██║ ██║ ██║ ██║██████╔╝█████╗ ██║ ██║ ██║██║ ███╗ 4 | ██╔══██║██║ ██║╚██╗ ██╔╝██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██╔══██╗██╔══╝ ██║ ██║ ██║██║ ██║ 5 | ██║ ██║██████╔╝ ╚████╔╝ ███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ██║███████╗███████╗╚██████╔╝╚██████╔╝ 6 | ╚═╝ ╚═╝╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚═════╝ 7 | “The world is full of wonderful things you haven't seen yet. Don't ever give up on the chance of seeing them.” - J.K. Rowling 8 | -------------------------------------------------------------------------------- /backend/server/adventures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/adventures/__init__.py -------------------------------------------------------------------------------- /backend/server/adventures/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | from django.conf import settings 3 | 4 | class AdventuresConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'adventures' -------------------------------------------------------------------------------- /backend/server/adventures/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/adventures/management/__init__.py -------------------------------------------------------------------------------- /backend/server/adventures/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/adventures/management/commands/__init__.py -------------------------------------------------------------------------------- /backend/server/adventures/managers.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.db.models import Q 3 | 4 | class AdventureManager(models.Manager): 5 | def retrieve_adventures(self, user, include_owned=False, include_shared=False, include_public=False): 6 | # Initialize the query with an empty Q object 7 | query = Q() 8 | 9 | # Add owned adventures to the query if included 10 | if include_owned: 11 | query |= Q(user_id=user.id) 12 | 13 | # Add shared adventures to the query if included 14 | if include_shared: 15 | query |= Q(collection__shared_with=user.id) 16 | 17 | # Add public adventures to the query if included 18 | if include_public: 19 | query |= Q(is_public=True) 20 | 21 | # Perform the query with the final Q object and remove duplicates 22 | return self.filter(query).distinct() 23 | -------------------------------------------------------------------------------- /backend/server/adventures/middleware.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.deprecation import MiddlewareMixin 3 | import os 4 | 5 | class OverrideHostMiddleware: 6 | def __init__(self, get_response): 7 | self.get_response = get_response 8 | 9 | def __call__(self, request): 10 | public_url = os.getenv('PUBLIC_URL', None) 11 | if public_url: 12 | # Extract host and scheme 13 | scheme, host = public_url.split("://") 14 | request.META['HTTP_HOST'] = host 15 | request.META['wsgi.url_scheme'] = scheme 16 | 17 | # Set X-Forwarded-Proto for Django 18 | request.META['HTTP_X_FORWARDED_PROTO'] = scheme 19 | 20 | response = self.get_response(request) 21 | return response 22 | 23 | class XSessionTokenMiddleware(MiddlewareMixin): 24 | def process_request(self, request): 25 | session_token = request.headers.get('X-Session-Token') 26 | if session_token: 27 | request.COOKIES[settings.SESSION_COOKIE_NAME] = session_token 28 | 29 | class DisableCSRFForSessionTokenMiddleware(MiddlewareMixin): 30 | def process_request(self, request): 31 | if 'X-Session-Token' in request.headers: 32 | setattr(request, '_dont_enforce_csrf_checks', True) 33 | 34 | class DisableCSRFForMobileLoginSignup(MiddlewareMixin): 35 | def process_request(self, request): 36 | is_mobile = request.headers.get('X-Is-Mobile', '').lower() == 'true' 37 | is_login_or_signup = request.path in ['/auth/browser/v1/auth/login', '/auth/browser/v1/auth/signup'] 38 | if is_mobile and is_login_or_signup: 39 | setattr(request, '_dont_enforce_csrf_checks', True) 40 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0001_adventure_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-15 23:20 2 | 3 | import django_resized.forms 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('adventures', 'migrate_images'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='adventure', 16 | name='image', 17 | field=django_resized.forms.ResizedImageField(blank=True, crop=None, force_format='WEBP', keep_meta=True, null=True, quality=75, scale=None, size=[1920, 1080], upload_to='images/'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0002_adventureimage.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-15 23:17 2 | 3 | import django.db.models.deletion 4 | import django_resized.forms 5 | import uuid 6 | from django.conf import settings 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('adventures', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='AdventureImage', 20 | fields=[ 21 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), 22 | ('image', django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, quality=75, scale=None, size=[1920, 1080], upload_to='images/')), 23 | ('adventure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='adventures.adventure')), 24 | ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0002_alter_adventureimage_adventure.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-15 23:31 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('adventures', '0001_adventure_image'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='adventureimage', 16 | name='adventure', 17 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='adventures.adventure'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0003_adventure_end_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-18 16:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0002_alter_adventureimage_adventure'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='adventure', 15 | name='end_date', 16 | field=models.DateField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0004_transportation_end_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-19 20:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0003_adventure_end_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='transportation', 15 | name='end_date', 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0005_collection_shared_with.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-02 13:21 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('adventures', '0004_transportation_end_date'), 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='collection', 17 | name='shared_with', 18 | field=models.ManyToManyField(blank=True, related_name='shared_with', to=settings.AUTH_USER_MODEL), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0006_alter_adventure_link.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-17 14:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0005_collection_shared_with'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='adventure', 15 | name='link', 16 | field=models.URLField(blank=True, max_length=2083, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0007_visit_model.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-23 18:06 2 | 3 | import django.db.models.deletion 4 | import uuid 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('adventures', '0006_alter_adventure_link'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='adventure', 17 | name='type', 18 | field=models.CharField(choices=[('general', 'General 🌍'), ('Outdoor', 'Outdoor 🏞️'), ('lodging', 'Lodging 🛌'), ('dining', 'Dining 🍽️'), ('activity', 'Activity 🏄'), ('attraction', 'Attraction 🎢'), ('shopping', 'Shopping 🛍️'), ('nightlife', 'Nightlife 🌃'), ('event', 'Event 🎉'), ('transportation', 'Transportation 🚗'), ('culture', 'Culture 🎭'), ('water_sports', 'Water Sports 🚤'), ('hiking', 'Hiking 🥾'), ('wildlife', 'Wildlife 🦒'), ('historical_sites', 'Historical Sites 🏛️'), ('music_concerts', 'Music & Concerts 🎶'), ('fitness', 'Fitness 🏋️'), ('art_museums', 'Art & Museums 🎨'), ('festivals', 'Festivals 🎪'), ('spiritual_journeys', 'Spiritual Journeys 🧘\u200d♀️'), ('volunteer_work', 'Volunteer Work 🤝'), ('other', 'Other')], default='general', max_length=100), 19 | ), 20 | migrations.CreateModel( 21 | name='Visit', 22 | fields=[ 23 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), 24 | ('start_date', models.DateField(blank=True, null=True)), 25 | ('end_date', models.DateField(blank=True, null=True)), 26 | ('notes', models.TextField(blank=True, null=True)), 27 | ('created_at', models.DateTimeField(auto_now_add=True)), 28 | ('updated_at', models.DateTimeField(auto_now=True)), 29 | ('adventure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='visits', to='adventures.adventure')), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0008_remove_date_field.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-23 18:06 2 | 3 | import django.db.models.deletion 4 | import uuid 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('adventures', 'migrate_visits_categories'), 12 | ('adventures', 'migrate_images'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField( 17 | model_name='adventure', 18 | name='date', 19 | ), 20 | migrations.RemoveField( 21 | model_name='adventure', 22 | name='end_date', 23 | ), 24 | migrations.RemoveField( 25 | model_name='adventure', 26 | name='image', 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0009_alter_adventure_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-30 00:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0008_remove_date_field'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='adventure', 15 | name='type', 16 | field=models.CharField(choices=[('general', 'General 🌍'), ('outdoor', 'Outdoor 🏞️'), ('lodging', 'Lodging 🛌'), ('dining', 'Dining 🍽️'), ('activity', 'Activity 🏄'), ('attraction', 'Attraction 🎢'), ('shopping', 'Shopping 🛍️'), ('nightlife', 'Nightlife 🌃'), ('event', 'Event 🎉'), ('transportation', 'Transportation 🚗'), ('culture', 'Culture 🎭'), ('water_sports', 'Water Sports 🚤'), ('hiking', 'Hiking 🥾'), ('wildlife', 'Wildlife 🦒'), ('historical_sites', 'Historical Sites 🏛️'), ('music_concerts', 'Music & Concerts 🎶'), ('fitness', 'Fitness 🏋️'), ('art_museums', 'Art & Museums 🎨'), ('festivals', 'Festivals 🎪'), ('spiritual_journeys', 'Spiritual Journeys 🧘\u200d♀️'), ('volunteer_work', 'Volunteer Work 🤝'), ('other', 'Other')], default='general', max_length=100), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0010_collection_link.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-10-08 03:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0009_alter_adventure_type'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='collection', 15 | name='link', 16 | field=models.URLField(blank=True, max_length=2083, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0011_category_adventure_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-11-14 04:30 2 | 3 | from django.conf import settings 4 | import django.db.models.deletion 5 | import uuid 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('adventures', '0010_collection_link'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Category', 18 | fields=[ 19 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), 20 | ('name', models.CharField(max_length=200)), 21 | ('display_name', models.CharField(max_length=200)), 22 | ('icon', models.CharField(default='🌍', max_length=200)), 23 | ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | options={ 26 | 'verbose_name_plural': 'Categories', 27 | }, 28 | ), 29 | migrations.AddField( 30 | model_name='adventure', 31 | name='category', 32 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='adventures.category'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0013_remove_adventure_type_alter_adventure_category.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-11-14 04:51 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('adventures', '0012_migrate_types_to_categories'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='adventure', 16 | name='type', 17 | ), 18 | migrations.AlterField( 19 | model_name='adventure', 20 | name='category', 21 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='adventures.category'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0014_alter_category_unique_together.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-11-17 21:43 2 | 3 | from django.conf import settings 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('adventures', '0013_remove_adventure_type_alter_adventure_category'), 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterUniqueTogether( 16 | name='category', 17 | unique_together={('name', 'user_id')}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0015_transportation_destination_latitude_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-12-19 17:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0014_alter_category_unique_together'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='transportation', 15 | name='destination_latitude', 16 | field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='transportation', 20 | name='destination_longitude', 21 | field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name='transportation', 25 | name='origin_latitude', 26 | field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), 27 | ), 28 | migrations.AddField( 29 | model_name='transportation', 30 | name='origin_longitude', 31 | field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0016_alter_adventureimage_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-01 21:40 2 | 3 | import adventures.models 4 | import django_resized.forms 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('adventures', '0015_transportation_destination_latitude_and_more'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='adventureimage', 17 | name='image', 18 | field=django_resized.forms.ResizedImageField(crop=None, force_format='WEBP', keep_meta=True, quality=75, scale=None, size=[1920, 1080], upload_to=adventures.models.PathAndRename('images/')), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0017_adventureimage_is_primary.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-03 04:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0016_alter_adventureimage_image'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='adventureimage', 15 | name='is_primary', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0018_attachment.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-19 00:39 2 | 3 | import django.db.models.deletion 4 | import uuid 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('adventures', '0017_adventureimage_is_primary'), 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Attachment', 19 | fields=[ 20 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), 21 | ('file', models.FileField(upload_to='attachments/')), 22 | ('adventure', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='adventures.adventure')), 23 | ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 24 | ], 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0019_alter_attachment_file.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-19 22:17 2 | 3 | import adventures.models 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('adventures', '0018_attachment'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='attachment', 16 | name='file', 17 | field=models.FileField(upload_to=adventures.models.PathAndRename('attachments/')), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0020_attachment_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-19 22:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0019_alter_attachment_file'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='attachment', 15 | name='name', 16 | field=models.CharField(default='', max_length=200), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0021_alter_attachment_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-19 22:32 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0020_attachment_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='attachment', 15 | name='name', 16 | field=models.CharField(blank=True, max_length=200, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0024_alter_attachment_file.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-03-17 01:15 2 | 3 | import adventures.models 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('adventures', '0023_lodging_delete_hotel'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='attachment', 16 | name='file', 17 | field=models.FileField(upload_to=adventures.models.PathAndRename('attachments/'), validators=[adventures.models.validate_file_extension]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/0025_alter_visit_end_date_alter_visit_start_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-03-17 21:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('adventures', '0024_alter_attachment_file'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='visit', 15 | name='end_date', 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='visit', 20 | name='start_date', 21 | field=models.DateTimeField(blank=True, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/server/adventures/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/adventures/migrations/__init__.py -------------------------------------------------------------------------------- /backend/server/adventures/migrations/migrate_images.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | def move_images_to_new_model(apps, schema_editor): 4 | Adventure = apps.get_model('adventures', 'Adventure') 5 | AdventureImage = apps.get_model('adventures', 'AdventureImage') 6 | 7 | for adventure in Adventure.objects.all(): 8 | if adventure.image: 9 | AdventureImage.objects.create( 10 | adventure=adventure, 11 | image=adventure.image, 12 | user_id=adventure.user_id, 13 | ) 14 | 15 | 16 | class Migration(migrations.Migration): 17 | 18 | dependencies = [ 19 | ('adventures', '0001_initial'), 20 | ('adventures', '0002_adventureimage'), 21 | ] 22 | 23 | operations = [ 24 | migrations.RunPython(move_images_to_new_model), 25 | migrations.RemoveField( 26 | model_name='Adventure', 27 | name='image', 28 | ), 29 | ] -------------------------------------------------------------------------------- /backend/server/adventures/migrations/migrate_visits_categories.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | from django.db import migrations, models 3 | 4 | def move_images_to_new_model(apps, schema_editor): 5 | Adventure = apps.get_model('adventures', 'Adventure') 6 | Visit = apps.get_model('adventures', 'Visit') 7 | 8 | for adventure in Adventure.objects.all(): 9 | # if the type is visited and there is no date, set note to 'No date provided.' 10 | note = 'No date provided.' if adventure.type == 'visited' and not adventure.date else '' 11 | if adventure.date or adventure.type == 'visited': 12 | Visit.objects.create( 13 | adventure=adventure, 14 | start_date=adventure.date, 15 | end_date=adventure.end_date, 16 | notes=note, 17 | ) 18 | if adventure.type == 'visited' or adventure.type == 'planned': 19 | adventure.type = 'general' 20 | adventure.save() 21 | 22 | 23 | class Migration(migrations.Migration): 24 | 25 | dependencies = [ 26 | ('adventures', '0007_visit_model'), 27 | ] 28 | 29 | operations = [ 30 | migrations.RunPython(move_images_to_new_model), 31 | ] -------------------------------------------------------------------------------- /backend/server/adventures/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/server/adventures/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework.routers import DefaultRouter 3 | from adventures.views import * 4 | 5 | router = DefaultRouter() 6 | router.register(r'adventures', AdventureViewSet, basename='adventures') 7 | router.register(r'collections', CollectionViewSet, basename='collections') 8 | router.register(r'stats', StatsViewSet, basename='stats') 9 | router.register(r'generate', GenerateDescription, basename='generate') 10 | router.register(r'activity-types', ActivityTypesView, basename='activity-types') 11 | router.register(r'transportations', TransportationViewSet, basename='transportations') 12 | router.register(r'notes', NoteViewSet, basename='notes') 13 | router.register(r'checklists', ChecklistViewSet, basename='checklists') 14 | router.register(r'images', AdventureImageViewSet, basename='images') 15 | router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode') 16 | router.register(r'categories', CategoryViewSet, basename='categories') 17 | router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar') 18 | router.register(r'overpass', OverpassViewSet, basename='overpass') 19 | router.register(r'search', GlobalSearchView, basename='search') 20 | router.register(r'attachments', AttachmentViewSet, basename='attachments') 21 | router.register(r'lodging', LodgingViewSet, basename='lodging') 22 | 23 | 24 | urlpatterns = [ 25 | # Include the router under the 'api/' prefix 26 | path('', include(router.urls)), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/server/adventures/utils/file_permissions.py: -------------------------------------------------------------------------------- 1 | from adventures.models import AdventureImage, Attachment 2 | 3 | protected_paths = ['images/', 'attachments/'] 4 | 5 | def checkFilePermission(fileId, user, mediaType): 6 | if mediaType not in protected_paths: 7 | return True 8 | if mediaType == 'images/': 9 | try: 10 | # Construct the full relative path to match the database field 11 | image_path = f"images/{fileId}" 12 | # Fetch the AdventureImage object 13 | adventure = AdventureImage.objects.get(image=image_path).adventure 14 | if adventure.is_public: 15 | return True 16 | elif adventure.user_id == user: 17 | return True 18 | elif adventure.collection: 19 | if adventure.collection.shared_with.filter(id=user.id).exists(): 20 | return True 21 | else: 22 | return False 23 | except AdventureImage.DoesNotExist: 24 | return False 25 | elif mediaType == 'attachments/': 26 | try: 27 | # Construct the full relative path to match the database field 28 | attachment_path = f"attachments/{fileId}" 29 | # Fetch the Attachment object 30 | attachment = Attachment.objects.get(file=attachment_path).adventure 31 | if attachment.is_public: 32 | return True 33 | elif attachment.user_id == user: 34 | return True 35 | elif attachment.collection: 36 | if attachment.collection.shared_with.filter(id=user.id).exists(): 37 | return True 38 | else: 39 | return False 40 | except Attachment.DoesNotExist: 41 | return False -------------------------------------------------------------------------------- /backend/server/adventures/utils/pagination.py: -------------------------------------------------------------------------------- 1 | from rest_framework.pagination import PageNumberPagination 2 | 3 | class StandardResultsSetPagination(PageNumberPagination): 4 | page_size = 25 5 | page_size_query_param = 'page_size' 6 | max_page_size = 1000 -------------------------------------------------------------------------------- /backend/server/adventures/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .activity_types_view import * 2 | from .adventure_image_view import * 3 | from .adventure_view import * 4 | from .category_view import * 5 | from .checklist_view import * 6 | from .collection_view import * 7 | from .generate_description_view import * 8 | from .ics_calendar_view import * 9 | from .note_view import * 10 | from .overpass_view import * 11 | from .reverse_geocode_view import * 12 | from .stats_view import * 13 | from .transportation_view import * 14 | from .global_search_view import * 15 | from .attachment_view import * 16 | from .lodging_view import * -------------------------------------------------------------------------------- /backend/server/adventures/views/activity_types_view.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.decorators import action 3 | from rest_framework.permissions import IsAuthenticated 4 | from rest_framework.response import Response 5 | from adventures.models import Adventure 6 | 7 | class ActivityTypesView(viewsets.ViewSet): 8 | permission_classes = [IsAuthenticated] 9 | 10 | @action(detail=False, methods=['get']) 11 | def types(self, request): 12 | """ 13 | Retrieve a list of distinct activity types for adventures associated with the current user. 14 | 15 | Args: 16 | request (HttpRequest): The HTTP request object. 17 | 18 | Returns: 19 | Response: A response containing a list of distinct activity types. 20 | """ 21 | types = Adventure.objects.filter(user_id=request.user.id).values_list('activity_types', flat=True).distinct() 22 | 23 | allTypes = [] 24 | 25 | for i in types: 26 | if not i: 27 | continue 28 | for x in i: 29 | if x and x not in allTypes: 30 | allTypes.append(x) 31 | 32 | return Response(allTypes) -------------------------------------------------------------------------------- /backend/server/adventures/views/category_view.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | from rest_framework.decorators import action 3 | from rest_framework.permissions import IsAuthenticated 4 | from rest_framework.response import Response 5 | from adventures.models import Category, Adventure 6 | from adventures.serializers import CategorySerializer 7 | 8 | class CategoryViewSet(viewsets.ModelViewSet): 9 | queryset = Category.objects.all() 10 | serializer_class = CategorySerializer 11 | permission_classes = [IsAuthenticated] 12 | 13 | def get_queryset(self): 14 | return Category.objects.filter(user_id=self.request.user) 15 | 16 | @action(detail=False, methods=['get']) 17 | def categories(self, request): 18 | """ 19 | Retrieve a list of distinct categories for adventures associated with the current user. 20 | """ 21 | categories = self.get_queryset().distinct() 22 | serializer = self.get_serializer(categories, many=True) 23 | return Response(serializer.data) 24 | 25 | def destroy(self, request, *args, **kwargs): 26 | instance = self.get_object() 27 | if instance.user_id != request.user: 28 | return Response({"error": "User does not own this category"}, status 29 | =400) 30 | 31 | if instance.name == 'general': 32 | return Response({"error": "Cannot delete the general category"}, status=400) 33 | 34 | # set any adventures with this category to a default category called general before deleting the category, if general does not exist create it for the user 35 | general_category = Category.objects.filter(user_id=request.user, name='general').first() 36 | 37 | if not general_category: 38 | general_category = Category.objects.create(user_id=request.user, name='general', icon='🌍', display_name='General') 39 | 40 | Adventure.objects.filter(category=instance).update(category=general_category) 41 | 42 | return super().destroy(request, *args, **kwargs) -------------------------------------------------------------------------------- /backend/server/build_files.sh: -------------------------------------------------------------------------------- 1 | # build_files.sh 2 | 3 | pip install -r requirements.txt 4 | python3.9 manage.py collectstatic --noinput -------------------------------------------------------------------------------- /backend/server/integrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/integrations/__init__.py -------------------------------------------------------------------------------- /backend/server/integrations/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from allauth.account.decorators import secure_admin_login 3 | 4 | from .models import ImmichIntegration 5 | 6 | admin.autodiscover() 7 | admin.site.login = secure_admin_login(admin.site.login) 8 | 9 | admin.site.register(ImmichIntegration) -------------------------------------------------------------------------------- /backend/server/integrations/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class IntegrationsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'integrations' 7 | -------------------------------------------------------------------------------- /backend/server/integrations/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-02 23:16 2 | 3 | import django.db.models.deletion 4 | import uuid 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='ImmichIntegration', 20 | fields=[ 21 | ('server_url', models.CharField(max_length=255)), 22 | ('api_key', models.CharField(max_length=255)), 23 | ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), 24 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/server/integrations/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/integrations/migrations/__init__.py -------------------------------------------------------------------------------- /backend/server/integrations/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth import get_user_model 3 | import uuid 4 | 5 | User = get_user_model() 6 | 7 | class ImmichIntegration(models.Model): 8 | server_url = models.CharField(max_length=255) 9 | api_key = models.CharField(max_length=255) 10 | user = models.ForeignKey( 11 | User, on_delete=models.CASCADE) 12 | id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) 13 | 14 | def __str__(self): 15 | return self.user.username + ' - ' + self.server_url -------------------------------------------------------------------------------- /backend/server/integrations/serializers.py: -------------------------------------------------------------------------------- 1 | from .models import ImmichIntegration 2 | from rest_framework import serializers 3 | 4 | class ImmichIntegrationSerializer(serializers.ModelSerializer): 5 | class Meta: 6 | model = ImmichIntegration 7 | fields = '__all__' 8 | read_only_fields = ['id', 'user'] 9 | 10 | def to_representation(self, instance): 11 | representation = super().to_representation(instance) 12 | representation.pop('user', None) 13 | return representation 14 | -------------------------------------------------------------------------------- /backend/server/integrations/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/server/integrations/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework.routers import DefaultRouter 3 | from integrations.views import ImmichIntegrationView, IntegrationView, ImmichIntegrationViewSet 4 | 5 | # Create the router and register the ViewSet 6 | router = DefaultRouter() 7 | router.register(r'immich', ImmichIntegrationView, basename='immich') 8 | router.register(r'', IntegrationView, basename='integrations') 9 | router.register(r'immich', ImmichIntegrationViewSet, basename='immich_viewset') 10 | 11 | # Include the router URLs 12 | urlpatterns = [ 13 | path("", include(router.urls)), # Includes /immich/ routes 14 | ] 15 | -------------------------------------------------------------------------------- /backend/server/main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/main/__init__.py -------------------------------------------------------------------------------- /backend/server/main/utils.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | def get_user_uuid(user): 4 | return str(user.uuid) 5 | 6 | class CustomModelSerializer(serializers.ModelSerializer): 7 | def to_representation(self, instance): 8 | representation = super().to_representation(instance) 9 | representation['user_id'] = get_user_uuid(instance.user_id) 10 | return representation -------------------------------------------------------------------------------- /backend/server/main/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from django.middleware.csrf import get_token 3 | from os import getenv 4 | from django.conf import settings 5 | from django.http import HttpResponse, HttpResponseForbidden 6 | from django.views.static import serve 7 | from adventures.utils.file_permissions import checkFilePermission 8 | 9 | def get_csrf_token(request): 10 | csrf_token = get_token(request) 11 | return JsonResponse({'csrfToken': csrf_token}) 12 | 13 | def get_public_url(request): 14 | return JsonResponse({'PUBLIC_URL': getenv('PUBLIC_URL')}) 15 | 16 | protected_paths = ['images/', 'attachments/'] 17 | 18 | def serve_protected_media(request, path): 19 | if any([path.startswith(protected_path) for protected_path in protected_paths]): 20 | image_id = path.split('/')[1] 21 | user = request.user 22 | media_type = path.split('/')[0] + '/' 23 | if checkFilePermission(image_id, user, media_type): 24 | if settings.DEBUG: 25 | # In debug mode, serve the file directly 26 | return serve(request, path, document_root=settings.MEDIA_ROOT) 27 | else: 28 | # In production, use X-Accel-Redirect to serve the file using Nginx 29 | response = HttpResponse() 30 | response['Content-Type'] = '' 31 | response['X-Accel-Redirect'] = '/protectedMedia/' + path 32 | return response 33 | else: 34 | return HttpResponseForbidden() 35 | else: 36 | if settings.DEBUG: 37 | return serve(request, path, document_root=settings.MEDIA_ROOT) 38 | else: 39 | response = HttpResponse() 40 | response['Content-Type'] = '' 41 | response['X-Accel-Redirect'] = '/protectedMedia/' + path 42 | return response -------------------------------------------------------------------------------- /backend/server/main/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "main.settings") 15 | 16 | application = get_wsgi_application() 17 | # add this vercel variable 18 | app = application 19 | -------------------------------------------------------------------------------- /backend/server/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "main.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /backend/server/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==5.0.11 2 | djangorestframework>=3.15.2 3 | django-allauth==0.63.3 4 | drf-yasg==1.21.4 5 | django-cors-headers==4.4.0 6 | coreapi==2.3.3 7 | python-dotenv 8 | psycopg2-binary 9 | Pillow 10 | whitenoise 11 | django-resized 12 | django-geojson 13 | setuptools 14 | gunicorn==23.0.0 15 | qrcode==8.0 16 | slippers==0.6.2 17 | django-allauth-ui==1.5.1 18 | django-widget-tweaks==1.5.0 19 | django-ical==1.9.2 20 | icalendar==6.1.0 21 | ijson==3.3.0 22 | tqdm==4.67.1 23 | overpy==0.7 24 | publicsuffix2==2.20191221 -------------------------------------------------------------------------------- /backend/server/templates/fragments/email_verification_form.html: -------------------------------------------------------------------------------- 1 | 2 |
{% csrf_token %} 3 |
4 | 5 |
6 | 7 |

Put here a key which was sent in verification email

8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /backend/server/templates/fragments/login_form.html: -------------------------------------------------------------------------------- 1 | 2 |
{% csrf_token %} 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /backend/server/templates/fragments/logout_form.html: -------------------------------------------------------------------------------- 1 | {% block content %} 2 | 3 |
{% csrf_token %} 4 |
5 | 6 |
7 | 8 |

Token received after login

9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /backend/server/templates/fragments/password_change_form.html: -------------------------------------------------------------------------------- 1 | 2 |
{% csrf_token %} 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /backend/server/templates/fragments/password_reset_confirm_form.html: -------------------------------------------------------------------------------- 1 | 2 |
{% csrf_token %} 3 |
4 | 5 |
6 | 7 |

Uid value sent in email

8 |
9 |
10 | 11 |
12 | 13 |
14 | 15 |

Token value sent in email

16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /backend/server/templates/fragments/password_reset_form.html: -------------------------------------------------------------------------------- 1 | 2 |
{% csrf_token %} 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /backend/server/templates/fragments/resend_email_verification_form.html: -------------------------------------------------------------------------------- 1 | 2 |
{% csrf_token %} 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /backend/server/templates/fragments/signup_form.html: -------------------------------------------------------------------------------- 1 | 2 |
{% csrf_token %} 3 |
4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /backend/server/templates/fragments/user_details_form.html: -------------------------------------------------------------------------------- 1 | 2 |
{% csrf_token %} 3 | 4 |
5 | 6 |
7 | 8 |
9 |
10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | -------------------------------------------------------------------------------- /backend/server/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block content %} 2 | 3 |
4 |

AdventureLog API Server

5 |

6 | Admin Site 7 | Account Managment 13 |

14 |
15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /backend/server/templates/rest_framework/api.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/base.html" %} 2 | 3 | {% block style %} 4 | {{ block.super }} 5 | 25 | {% endblock %} 26 | 27 | {% block userlinks %} 28 | {% if user.is_authenticated or response.data.access_token %} 29 | 46 | {% else %} 47 | {% url 'rest_login' as login_url %} 48 |
  • Login
  • 49 | {% url 'rest_register' as register_url %} 50 |
  • Register
  • 51 | {% endif %} 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /backend/server/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/users/__init__.py -------------------------------------------------------------------------------- /backend/server/users/adapters.py: -------------------------------------------------------------------------------- 1 | from allauth.account.adapter import DefaultAccountAdapter 2 | from django.conf import settings 3 | 4 | class NoNewUsersAccountAdapter(DefaultAccountAdapter): 5 | """ 6 | Disable new user registration. 7 | """ 8 | def is_open_for_signup(self, request): 9 | is_disabled = getattr(settings, 'DISABLE_REGISTRATION', False) 10 | return not is_disabled -------------------------------------------------------------------------------- /backend/server/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from allauth.account.decorators import secure_admin_login 3 | from django.contrib.sessions.models import Session 4 | 5 | admin.autodiscover() 6 | admin.site.login = secure_admin_login(admin.site.login) 7 | 8 | class SessionAdmin(admin.ModelAdmin): 9 | def _session_data(self, obj): 10 | return obj.get_decoded() 11 | list_display = ['session_key', '_session_data', 'expire_date'] 12 | 13 | admin.site.register(Session, SessionAdmin) -------------------------------------------------------------------------------- /backend/server/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'users' 7 | -------------------------------------------------------------------------------- /backend/server/users/backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.backends import ModelBackend 2 | from allauth.socialaccount.models import SocialAccount 3 | 4 | class NoPasswordAuthBackend(ModelBackend): 5 | def authenticate(self, request, username=None, password=None, **kwargs): 6 | # First, attempt normal authentication 7 | user = super().authenticate(request, username=username, password=password, **kwargs) 8 | if user is None: 9 | return None 10 | 11 | if SocialAccount.objects.filter(user=user).exists() and user.disable_password: 12 | # If yes, disable login via password 13 | return None 14 | 15 | return user 16 | -------------------------------------------------------------------------------- /backend/server/users/form_overrides.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | class CustomSignupForm(forms.Form): 4 | first_name = forms.CharField(max_length=30, required=True) 5 | last_name = forms.CharField(max_length=30, required=True) 6 | 7 | def signup(self, request, user): 8 | # Delay the import to avoid circular import 9 | from allauth.account.forms import SignupForm 10 | 11 | # No need to call super() from CustomSignupForm; use the SignupForm directly if needed 12 | user.first_name = self.cleaned_data['first_name'] 13 | user.last_name = self.cleaned_data['last_name'] 14 | 15 | # Save the user instance 16 | user.save() 17 | return user -------------------------------------------------------------------------------- /backend/server/users/migrations/0002_customuser_public_profile.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-06 23:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='customuser', 15 | name='public_profile', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/users/migrations/0003_alter_customuser_email.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-11-18 14:51 2 | 3 | from django.db import migrations, models 4 | 5 | def check_duplicate_email(apps, schema_editor): 6 | # sets an email to null if there are duplicates 7 | CustomUser = apps.get_model('users', 'CustomUser') 8 | duplicates = CustomUser.objects.values('email').annotate(email_count=models.Count('email')).filter(email_count__gt=1) 9 | for duplicate in duplicates: 10 | CustomUser.objects.filter(email=duplicate['email']).update(email=None) 11 | print(f"Duplicate email: {duplicate['email']}") 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | dependencies = [ 17 | ('users', '0002_customuser_public_profile'), 18 | ] 19 | 20 | operations = [ 21 | migrations.RunPython(check_duplicate_email), 22 | migrations.AlterField( 23 | model_name='customuser', 24 | name='email', 25 | field=models.EmailField(max_length=254, unique=True), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/server/users/migrations/0004_customuser_disable_password.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-03-17 01:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('users', '0003_alter_customuser_email'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='customuser', 15 | name='disable_password', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/users/migrations/__init__.py -------------------------------------------------------------------------------- /backend/server/users/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from django.contrib.auth.models import AbstractUser 3 | from django.db import models 4 | from django_resized import ResizedImageField 5 | 6 | class CustomUser(AbstractUser): 7 | email = models.EmailField(unique=True) # Override the email field with unique constraint 8 | profile_pic = ResizedImageField(force_format="WEBP", quality=75, null=True, blank=True, upload_to='profile-pics/') 9 | uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) 10 | public_profile = models.BooleanField(default=False) 11 | disable_password = models.BooleanField(default=False) 12 | 13 | def __str__(self): 14 | return self.username -------------------------------------------------------------------------------- /backend/server/worldtravel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/worldtravel/__init__.py -------------------------------------------------------------------------------- /backend/server/worldtravel/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from allauth.account.decorators import secure_admin_login 3 | 4 | admin.autodiscover() 5 | admin.site.login = secure_admin_login(admin.site.login) -------------------------------------------------------------------------------- /backend/server/worldtravel/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class WorldtravelConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'worldtravel' 7 | -------------------------------------------------------------------------------- /backend/server/worldtravel/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/worldtravel/management/__init__.py -------------------------------------------------------------------------------- /backend/server/worldtravel/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/worldtravel/management/commands/__init__.py -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.6 on 2024-06-28 01:01 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Country', 19 | fields=[ 20 | ('id', models.AutoField(primary_key=True, serialize=False)), 21 | ('name', models.CharField(max_length=100)), 22 | ('country_code', models.CharField(max_length=2)), 23 | ('continent', models.CharField(choices=[('AF', 'Africa'), ('AN', 'Antarctica'), ('AS', 'Asia'), ('EU', 'Europe'), ('NA', 'North America'), ('OC', 'Oceania'), ('SA', 'South America')], default='AF', max_length=2)), 24 | ], 25 | options={ 26 | 'verbose_name': 'Country', 27 | 'verbose_name_plural': 'Countries', 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='Region', 32 | fields=[ 33 | ('id', models.CharField(primary_key=True, serialize=False)), 34 | ('name', models.CharField(max_length=100)), 35 | ('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='worldtravel.country')), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='VisitedRegion', 40 | fields=[ 41 | ('id', models.AutoField(primary_key=True, serialize=False)), 42 | ('region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='worldtravel.region')), 43 | ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0002_region_name_en.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-20 21:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='region', 15 | name='name_en', 16 | field=models.CharField(default='', max_length=100), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0003_alter_region_name_en.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-21 14:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0002_region_name_en'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='region', 15 | name='name_en', 16 | field=models.CharField(blank=True, max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0004_country_geometry.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-23 17:01 2 | 3 | import django.contrib.gis.db.models.fields 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('worldtravel', '0003_alter_region_name_en'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='country', 16 | name='geometry', 17 | field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0005_remove_country_geometry_region_geometry.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-08-23 17:47 2 | 3 | import django.contrib.gis.db.models.fields 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('worldtravel', '0004_country_geometry'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='country', 16 | name='geometry', 17 | ), 18 | migrations.AddField( 19 | model_name='region', 20 | name='geometry', 21 | field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0006_remove_country_continent_country_subregion.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-11 02:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0005_remove_country_geometry_region_geometry'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='country', 15 | name='continent', 16 | ), 17 | migrations.AddField( 18 | model_name='country', 19 | name='subregion', 20 | field=models.CharField(blank=True, max_length=100, null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0007_remove_region_geometry_remove_region_name_en.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-11 02:20 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0006_remove_country_continent_country_subregion'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='region', 15 | name='geometry', 16 | ), 17 | migrations.RemoveField( 18 | model_name='region', 19 | name='name_en', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0008_region_latitude_region_longitude.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-11 02:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0007_remove_region_geometry_remove_region_name_en'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='region', 15 | name='latitude', 16 | field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='region', 20 | name='longitude', 21 | field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0009_alter_country_country_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-11 02:52 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0008_region_latitude_region_longitude'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='country', 15 | name='country_code', 16 | field=models.CharField(max_length=2, unique=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0010_country_capital.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2024-09-11 19:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0009_alter_country_country_code'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='country', 15 | name='capital', 16 | field=models.CharField(blank=True, max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0011_country_latitude_country_longitude.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-02 00:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0010_country_capital'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='country', 15 | name='latitude', 16 | field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='country', 20 | name='longitude', 21 | field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0012_city.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-09 15:11 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('worldtravel', '0011_country_latitude_country_longitude'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='City', 16 | fields=[ 17 | ('id', models.CharField(primary_key=True, serialize=False)), 18 | ('name', models.CharField(max_length=100)), 19 | ('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), 20 | ('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), 21 | ('region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='worldtravel.region')), 22 | ], 23 | options={ 24 | 'verbose_name_plural': 'Cities', 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0013_visitedcity.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-09 17:00 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('worldtravel', '0012_city'), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='VisitedCity', 18 | fields=[ 19 | ('id', models.AutoField(primary_key=True, serialize=False)), 20 | ('city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='worldtravel.city')), 21 | ('user_id', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0014_alter_visitedcity_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-09 18:08 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0013_visitedcity'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='visitedcity', 15 | options={'verbose_name_plural': 'Visited Cities'}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/0015_city_insert_id_country_insert_id_region_insert_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.8 on 2025-01-13 17:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('worldtravel', '0014_alter_visitedcity_options'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='city', 15 | name='insert_id', 16 | field=models.UUIDField(blank=True, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name='country', 20 | name='insert_id', 21 | field=models.UUIDField(blank=True, null=True), 22 | ), 23 | migrations.AddField( 24 | model_name='region', 25 | name='insert_id', 26 | field=models.UUIDField(blank=True, null=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/server/worldtravel/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/backend/server/worldtravel/migrations/__init__.py -------------------------------------------------------------------------------- /backend/server/worldtravel/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/server/worldtravel/urls.py: -------------------------------------------------------------------------------- 1 | # travel/urls.py 2 | 3 | from django.urls import include, path 4 | from rest_framework.routers import DefaultRouter 5 | from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country, cities_by_region, VisitedCityViewSet, visits_by_region 6 | router = DefaultRouter() 7 | router.register(r'countries', CountryViewSet, basename='countries') 8 | router.register(r'regions', RegionViewSet, basename='regions') 9 | router.register(r'visitedregion', VisitedRegionViewSet, basename='visitedregion') 10 | router.register(r'visitedcity', VisitedCityViewSet, basename='visitedcity') 11 | 12 | urlpatterns = [ 13 | path('', include(router.urls)), 14 | path('/regions/', regions_by_country, name='regions-by-country'), 15 | path('/visits/', visits_by_country, name='visits-by-country'), 16 | path('regions//cities/', cities_by_region, name='cities-by-region'), 17 | path('regions//cities/visits/', visits_by_region, name='visits-by-region'), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | 4 | [program:nginx] 5 | command=/usr/sbin/nginx -g "daemon off;" 6 | autorestart=true 7 | stdout_logfile=/dev/stdout 8 | stderr_logfile=/dev/stderr 9 | 10 | [program:gunicorn] 11 | command=/code/entrypoint.sh 12 | autorestart=true 13 | stdout_logfile=/dev/stdout 14 | stderr_logfile=/dev/stderr 15 | stdout_logfile_maxbytes = 0 16 | stderr_logfile_maxbytes = 0 17 | -------------------------------------------------------------------------------- /backup.sh: -------------------------------------------------------------------------------- 1 | # This script will create a backup of the adventurelog_media volume and store it in the current directory as adventurelog-backup.tar.gz 2 | 3 | docker run --rm \ 4 | -v adventurelog_adventurelog_media:/backup-volume \ 5 | -v "$(pwd)":/backup \ 6 | busybox \ 7 | tar -zcvf /backup/adventurelog-backup.tar.gz /backup-volume -------------------------------------------------------------------------------- /brand/adventurelog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/adventurelog.png -------------------------------------------------------------------------------- /brand/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/banner.png -------------------------------------------------------------------------------- /brand/screenshots/adventures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/screenshots/adventures.png -------------------------------------------------------------------------------- /brand/screenshots/countries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/screenshots/countries.png -------------------------------------------------------------------------------- /brand/screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/screenshots/dashboard.png -------------------------------------------------------------------------------- /brand/screenshots/details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/screenshots/details.png -------------------------------------------------------------------------------- /brand/screenshots/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/screenshots/edit.png -------------------------------------------------------------------------------- /brand/screenshots/itinerary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/screenshots/itinerary.png -------------------------------------------------------------------------------- /brand/screenshots/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/screenshots/map.png -------------------------------------------------------------------------------- /brand/screenshots/regions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/brand/screenshots/regions.png -------------------------------------------------------------------------------- /cdn/.gitignore: -------------------------------------------------------------------------------- 1 | data/ -------------------------------------------------------------------------------- /cdn/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python image as a base 2 | FROM python:3.11-slim 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Install required Python packages 8 | RUN pip install --no-cache-dir requests osm2geojson 9 | 10 | # Copy the script into the container 11 | COPY main.py /app/main.py 12 | 13 | # Run the script to generate the data folder and GeoJSON files (this runs inside the container) 14 | RUN python -u /app/main.py 15 | 16 | # Install Nginx 17 | RUN apt update && apt install -y nginx && rm -rf /var/lib/apt/lists/* 18 | 19 | # Copy the entire generated data folder to the Nginx serving directory 20 | RUN mkdir -p /var/www/html/data && cp -r /app/data/* /var/www/html/data/ 21 | 22 | # Copy Nginx configuration 23 | COPY nginx.conf /etc/nginx/nginx.conf 24 | 25 | # Copy the index.html file to the Nginx serving directory 26 | COPY index.html /usr/share/nginx/html/index.html 27 | 28 | # Expose port 80 for Nginx 29 | EXPOSE 80 30 | 31 | # Copy the entrypoint script into the container 32 | COPY entrypoint.sh /app/entrypoint.sh 33 | RUN chmod +x /app/entrypoint.sh 34 | 35 | # Set the entrypoint script as the default command 36 | ENTRYPOINT ["/app/entrypoint.sh"] 37 | -------------------------------------------------------------------------------- /cdn/README.md: -------------------------------------------------------------------------------- 1 | This folder contains the scripts to generate AdventureLOG CDN files. 2 | 3 | Special thanks to [@larsl-net](https://github.com/larsl-net) for the GeoJSON generation script. 4 | -------------------------------------------------------------------------------- /cdn/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cdn: 3 | build: . 4 | container_name: adventurelog-cdn 5 | ports: 6 | - "8080:80" 7 | restart: unless-stopped 8 | volumes: 9 | - ./data:/app/data # Ensures new data files persist 10 | -------------------------------------------------------------------------------- /cdn/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Any setup tasks or checks can go here (if needed) 4 | echo "AdventureLog CDN has started!" 5 | echo "Refer to the documentation for information about connecting your AdventureLog instance to this CDN." 6 | echo "Thanks to our data providers for making this possible! You can find them on the CDN site." 7 | 8 | # Start Nginx in the foreground (as the main process) 9 | nginx -g 'daemon off;' 10 | -------------------------------------------------------------------------------- /cdn/nginx.conf: -------------------------------------------------------------------------------- 1 | events {} 2 | 3 | http { 4 | server { 5 | listen 80; 6 | server_name _; 7 | 8 | location /data/ { 9 | root /var/www/html; 10 | autoindex on; # Enable directory listing 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cdn/requirements.txt: -------------------------------------------------------------------------------- 1 | osm2geojson==0.2.5 -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | # This script is used to deploy the latest version of AdventureLog to the server. It pulls the latest version of the Docker images and starts the containers. It is a simple script that can be run on the server, possibly as a cron job, to keep the server up to date with the latest version of the application. 2 | 3 | echo "Deploying latest version of AdventureLog" 4 | docker compose pull 5 | echo "Stating containers" 6 | docker compose up -d 7 | echo "All set!" 8 | docker logs adventurelog-backend --follow -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | #build: ./frontend/ 4 | image: ghcr.io/seanmorley15/adventurelog-frontend:latest 5 | container_name: adventurelog-frontend 6 | restart: unless-stopped 7 | environment: 8 | - PUBLIC_SERVER_URL=http://server:8000 # Should be the service name of the backend with port 8000, even if you change the port in the backend service 9 | - ORIGIN=http://localhost:8015 10 | - BODY_SIZE_LIMIT=Infinity 11 | ports: 12 | - "8015:3000" 13 | depends_on: 14 | - server 15 | 16 | db: 17 | image: postgis/postgis:15-3.3 18 | container_name: adventurelog-db 19 | restart: unless-stopped 20 | environment: 21 | POSTGRES_DB: database 22 | POSTGRES_USER: adventure 23 | POSTGRES_PASSWORD: changeme123 24 | volumes: 25 | - postgres_data:/var/lib/postgresql/data/ 26 | 27 | server: 28 | #build: ./backend/ 29 | image: ghcr.io/seanmorley15/adventurelog-backend:latest 30 | container_name: adventurelog-backend 31 | restart: unless-stopped 32 | environment: 33 | - PGHOST=db 34 | - PGDATABASE=database 35 | - PGUSER=adventure 36 | - PGPASSWORD=changeme123 37 | - SECRET_KEY=changeme123 38 | - DJANGO_ADMIN_USERNAME=admin 39 | - DJANGO_ADMIN_PASSWORD=admin 40 | - DJANGO_ADMIN_EMAIL=admin@example.com 41 | - PUBLIC_URL=http://localhost:8016 # Match the outward port, used for the creation of image urls 42 | - CSRF_TRUSTED_ORIGINS=http://localhost:8016,http://localhost:8015 # Comma separated list of trusted origins for CSRF 43 | - DEBUG=False 44 | - FRONTEND_URL=http://localhost:8015 # Used for email generation. This should be the url of the frontend 45 | ports: 46 | - "8016:80" 47 | depends_on: 48 | - db 49 | volumes: 50 | - adventurelog_media:/code/media/ 51 | 52 | volumes: 53 | postgres_data: 54 | adventurelog_media: 55 | -------------------------------------------------------------------------------- /documentation/.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /src/client/shared.ts 3 | /src/node/shared.ts 4 | *.log 5 | *.tgz 6 | .DS_Store 7 | .idea 8 | .temp 9 | .vite_opt_cache 10 | .vscode 11 | dist 12 | cache 13 | temp 14 | examples-temp 15 | node_modules 16 | pnpm-global 17 | TODOs.md 18 | *.timestamp-*.mjs -------------------------------------------------------------------------------- /documentation/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // https://vitepress.dev/guide/custom-theme 2 | import { h } from 'vue' 3 | import type { Theme } from 'vitepress' 4 | import DefaultTheme from 'vitepress/theme' 5 | import './style.css' 6 | 7 | export default { 8 | extends: DefaultTheme, 9 | Layout: () => { 10 | return h(DefaultTheme.Layout, null, { 11 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 12 | }) 13 | }, 14 | enhanceApp({ app, router, siteData }) { 15 | // ... 16 | } 17 | } satisfies Theme 18 | -------------------------------------------------------------------------------- /documentation/docs/configuration/analytics.md: -------------------------------------------------------------------------------- 1 | # Umami Analytics (optional) 2 | 3 | Umami Analytics is a free, open-source, and privacy-focused web analytics tool that can be used as an alternative to Google Analytics. Learn more about Umami Analytics [here](https://umami.is/). 4 | 5 | To enable Umami Analytics for your AdventureLog instance, you can set the following variables in your `docker-compose.yml` under the `web` service: 6 | 7 | ```yaml 8 | PUBLIC_UMAMI_SRC=https://cloud.umami.is/script.js # If you are using the hosted version of Umami 9 | PUBLIC_UMAMI_WEBSITE_ID= 10 | ``` 11 | -------------------------------------------------------------------------------- /documentation/docs/configuration/disable_registration.md: -------------------------------------------------------------------------------- 1 | # Disable Registration 2 | 3 | To disable registration, you can set the following variable in your docker-compose.yml under the server service: 4 | 5 | ```yaml 6 | environment: 7 | - DISABLE_REGISTRATION=True 8 | # OPTIONAL: Set the message to display when registration is disabled 9 | - DISABLE_REGISTRATION_MESSAGE='Registration is disabled for this instance of AdventureLog.' 10 | ``` 11 | -------------------------------------------------------------------------------- /documentation/docs/configuration/email.md: -------------------------------------------------------------------------------- 1 | # Change Email Backend 2 | 3 | To change the email backend, you can set the following variable in your docker-compose.yml under the server service: 4 | 5 | ## Using Console (default) 6 | 7 | ```yaml 8 | environment: 9 | - EMAIL_BACKEND='console' 10 | ``` 11 | 12 | ## With SMTP 13 | 14 | ```yaml 15 | environment: 16 | - EMAIL_BACKEND=email 17 | - EMAIL_HOST=smtp.gmail.com 18 | - EMAIL_USE_TLS=True 19 | - EMAIL_PORT=587 20 | - EMAIL_USE_SSL=False 21 | - EMAIL_HOST_USER=user 22 | - EMAIL_HOST_PASSWORD=password 23 | - DEFAULT_FROM_EMAIL=user@example.com 24 | ``` 25 | 26 | ## Customizing Emails 27 | 28 | By default, the email will display `[example.com]` in the subject. You can customize this in the admin site. 29 | 30 | 1. Go to the admin site (serverurl/admin) 31 | 2. Click on `Sites` 32 | 3. Click on first site, it will probably be `example.com` 33 | 4. Change the `Domain name` and `Display name` to your desired values 34 | 5. Click `Save` 35 | -------------------------------------------------------------------------------- /documentation/docs/configuration/immich_integration.md: -------------------------------------------------------------------------------- 1 | # Immich Integration 2 | 3 | ### What is Immich? 4 | 5 | 6 | 7 | ![Immich Banner](https://repository-images.githubusercontent.com/455229168/ebba3238-9ef5-4891-ad58-a3b0223b12bd) 8 | 9 | Immich is a self-hosted, open-source platform that allows users to backup and manage their photos and videos similar to Google Photos, but with the advantage of storing data on their own private server, ensuring greater privacy and control over their media. 10 | 11 | - [Immich Website and Documentation](https://immich.app/) 12 | - [GitHub Repository](https://github.com/immich-app/immich) 13 | 14 | ### How to integrate Immich with AdventureLog? 15 | 16 | To integrate Immich with AdventureLog, you need to have an Immich server running and accessible from where AdventureLog is running. 17 | 18 | 1. Obtain the Immich API Key from the Immich server. 19 | - In the Immich web interface, click on your user profile picture, go to `Account Settings` > `API Keys`. 20 | - Click `New API Key` and name it something like `AdventureLog`. 21 | - Copy the generated API Key, you will need it in the next step. 22 | 2. Go to the AdventureLog web interface, click on your user profile picture, go to `Settings` and scroll down to the `Immich Integration` section. 23 | - Enter the URL of your Immich server, e.g. `https://immich.example.com/api`. Note that `localhost` or `127.0.0.1` will probably not work because Immich and AdventureLog are running on different docker networks. It is recommended to use the IP address of the server where Immich is running ex `http://my-server-ip:port` or a domain name. 24 | - Paste the API Key you obtained in the previous step. 25 | - Click `Enable Immich` to save the settings. 26 | 3. Now, when you are adding images to an adventure, you will see an option to search for images in Immich or upload from an album. 27 | 28 | Enjoy the privacy and control of managing your travel media with Immich and AdventureLog! 🎉 29 | -------------------------------------------------------------------------------- /documentation/docs/configuration/social_auth.md: -------------------------------------------------------------------------------- 1 | # Social Authentication 2 | 3 | AdventureLog support authentication via 3rd party services and self-hosted identity providers. Once these services are enabled, users can log in to AdventureLog using their accounts from these services and link existing AdventureLog accounts to these services for easier access. 4 | 5 | The steps for each service varies so please refer to the specific service's documentation for more information. 6 | 7 | ## Supported Services 8 | 9 | - [Authentik](social_auth/authentik.md) (self-hosted) 10 | - [GitHub](social_auth/github.md) 11 | - [Open ID Connect](social_auth/oidc.md) 12 | 13 | ## Linking Existing Accounts 14 | 15 | If you already have an AdventureLog account and would like to link it to a 3rd party service, you can do so by logging in to AdventureLog and navigating to the `Account Settings` page. From there, scroll down to `Social and OIDC Authentication` and click the `Launch Account Connections` button. If identity providers have been enabled on your instance, you will see a list of available services to link to. 16 | -------------------------------------------------------------------------------- /documentation/docs/configuration/social_auth/oidc.md: -------------------------------------------------------------------------------- 1 | # OIDC Social Authentication 2 | 3 | AdventureLog can be configured to use OpenID Connect (OIDC) as an identity provider for social authentication. Users can then log in to AdventureLog using their OIDC account. 4 | 5 | The configuration is basically the same as [Authentik](./authentik.md), but you replace the client and secret with the OIDC client and secret provided by your OIDC provider. The `server_url` should be the URL of your OIDC provider where you can find the OIDC configuration. 6 | 7 | Each provider has a different configuration, so you will need to check the documentation of your OIDC provider to find the correct configuration. 8 | -------------------------------------------------------------------------------- /documentation/docs/configuration/updating.md: -------------------------------------------------------------------------------- 1 | # Updating 2 | 3 | Updating AdventureLog when using docker can be quite easy. Run the following commands to pull the latest version and restart the containers. Make sure you backup your instance before updating just in case! 4 | 5 | Note: Make sure you are in the same directory as your `docker-compose.yml` file. 6 | 7 | ```bash 8 | docker compose pull 9 | docker compose up -d 10 | ``` 11 | 12 | ## Updating the Region Data 13 | 14 | Region and Country data in AdventureLog is provided by an open source project: [dr5hn/countries-states-cities-database](https://github.com/dr5hn/countries-states-cities-database). If you would like to update the region data in your AdventureLog instance, you can do so by running the following command. This will make sure your database is up to date with the latest region data for your version of AdventureLog. For security reasons, the region data is not automatically updated to the latest and is release version is controlled in the `settings.py` file. 15 | 16 | ```bash 17 | docker exec -it bash 18 | ``` 19 | 20 | Once you are in the container run the following command to resync the region data. 21 | 22 | ```bash 23 | python manage.py download-countries --force 24 | ``` 25 | -------------------------------------------------------------------------------- /documentation/docs/guides/admin_panel.md: -------------------------------------------------------------------------------- 1 | # AdventureLog Admin Panel 2 | 3 | The AdventureLog Admin Panel, powered by Django, is a web-based interface that allows administrators to manage objects in the AdventureLog database. The Admin Panel is accessible at the `/admin` endpoint of the AdventureLog server. Example: `https://al-server.yourdomain.com/admin`. 4 | 5 | Features of the Admin Panel include: 6 | 7 | - **User Management**: Administrators can view and manage user accounts, including creating new users, updating user information, and deleting users. 8 | - **Adventure Management**: Administrators can view and manage adventures, including creating new adventures, updating adventure information, and deleting adventures. 9 | - **Security**: The Admin Panel enforces access control to ensure that only authorized administrators can access and manage the database. This means that only users with the `is_staff` flag set to `True` can access the Admin Panel. 10 | 11 | Note: the `CSRF_TRUSTED_ORIGINS` setting in your `docker-compose.yml` file must include the domain of the server. For example, if your server is hosted at `https://al-server.yourdomain.com`, you should add `al-server.yourdomain.com` to the `CSRF_TRUSTED_ORIGINS` setting. 12 | -------------------------------------------------------------------------------- /documentation/docs/install/caddy.md: -------------------------------------------------------------------------------- 1 | # Installation with Caddy 2 | 3 | Caddy is a modern HTTP reverse proxy. It automatically integrates with Let's Encrypt (or other certificate providers) to generate TLS certificates for your site. 4 | 5 | As an example, if you want to add Caddy to your Docker compose configuration, add the following service to your `docker-compose.yml`: 6 | 7 | ```yaml 8 | services: 9 | caddy: 10 | image: docker.io/library/caddy:2 11 | container_name: adventurelog-caddy 12 | restart: unless-stopped 13 | cap_add: 14 | - NET_ADMIN 15 | ports: 16 | - "80:80" 17 | - "443:443" 18 | - "443:443/udp" 19 | volumes: 20 | - ./caddy:/etc/caddy 21 | - caddy_data:/data 22 | - caddy_config:/config 23 | 24 | web: ... 25 | server: ... 26 | db: ... 27 | 28 | volumes: 29 | caddy_data: 30 | caddy_config: 31 | ``` 32 | 33 | Since all ingress traffic to the AdventureLog containsers now travels through Caddy, we can also remove the external ports configuration from those containsers in the `docker-compose.yml`. Just delete this configuration: 34 | 35 | ```yaml 36 | web: 37 | ports: 38 | - "8016:80" 39 | … 40 | server: 41 | ports: 42 | - "8015:3000" 43 | ``` 44 | 45 | That's it for the Docker compose changes. Of course, there are other methods to run Caddy which are equally valid. 46 | 47 | However, we also need to configure Caddy. For this, create a file `./caddy/Caddyfile` in which you configure the requests which are proxied to the frontend and backend respectively and what domain Caddy should request a certificate for: 48 | 49 | ``` 50 | adventurelog.example.com { 51 | 52 | @frontend { 53 | not path /media* /admin* /static* /accounts* 54 | } 55 | reverse_proxy @frontend web:3000 56 | 57 | reverse_proxy server:80 58 | } 59 | ``` 60 | 61 | Once configured, you can start up the containsers: 62 | 63 | ```bash 64 | docker compose up 65 | ``` 66 | 67 | Your AdventureLog should now be up and running. 68 | -------------------------------------------------------------------------------- /documentation/docs/install/getting_started.md: -------------------------------------------------------------------------------- 1 | # Install Options for AdventureLog 2 | 3 | AdventureLog can be installed in a variety of ways. The following are the most common methods: 4 | 5 | - [Docker](docker.md) 🐳 6 | - [Proxmox LXC](proxmox_lxc.md) 🐧 7 | - [Synology NAS](synology_nas.md) ☁️ 8 | - [Kubernetes and Kustomize](kustomize.md) 🌐 9 | - [Unraid](unraid.md) 🧡 10 | 11 | ### Other Options 12 | 13 | - [Nginx Proxy Manager](nginx_proxy_manager.md) 🛡 14 | - [Traefik](traefik.md) 🚀 15 | - [Caddy](caddy.md) 🔒 16 | -------------------------------------------------------------------------------- /documentation/docs/install/kustomize.md: -------------------------------------------------------------------------------- 1 | # Kubernetes and Kustomize (k8s) 2 | 3 | _AdventureLog can be run inside a kubernetes cluster using [kustomize](https://kustomize.io/)._ 4 | 5 | ## Prerequisites 6 | 7 | A working kubernetes cluster. AdventureLog has been tested on k8s, but any Kustomize-capable flavor should be easy to use. 8 | 9 | ## Cluster Routing 10 | 11 | Because the AdventureLog backend must be reachable by **both** the web browser and the AdventureLog frontend, k8s-internal routing mechanisms traditional for standing up other similar applications **cannot** be used. 12 | 13 | In order to host AdventureLog in your cluster, you must therefor configure an internally and externally resolvable ingress that routes to your AdventureLog backend container. 14 | 15 | Once you have made said ingress, set `PUBLIC_SERVER_URL` and `PUBLIC_URL` env variables below to the url of that ingress. 16 | 17 | ## Tailscale and Headscale 18 | 19 | Many k8s homelabs choose to use [Tailscale](https://tailscale.com/) or similar projects to remove the need for open ports in your home firewall. 20 | 21 | The [Tailscale k8s Operator](https://tailscale.com/kb/1185/kubernetes/) will set up an externally resolvable service/ingress for your AdventureLog instance, 22 | but it will fail to resolve internally. 23 | 24 | You must [expose tailnet IPs to your cluster](https://tailscale.com/kb/1438/kubernetes-operator-cluster-egress#expose-a-tailnet-https-service-to-your-cluster-workloads) so the AdventureLog pods can resolve them. 25 | 26 | ## Getting Started 27 | 28 | Take a look at the [example config](https://github.com/seanmorley15/AdventureLog/blob/main/kustomization.yml) and modify it for your use case. 29 | 30 | ## Environment Variables 31 | 32 | Look at the [environment variable summary](docker.md#configuration) in the docker install section to see available and required configuration options. 33 | 34 | Enjoy AdventureLog! 🎉 35 | -------------------------------------------------------------------------------- /documentation/docs/install/proxmox_lxc.md: -------------------------------------------------------------------------------- 1 | # Proxmox LXC 🐧 2 | 3 | AdventureLog can be installed in a Proxmox LXC container. This script created by the community will help you install AdventureLog in a Proxmox LXC container. 4 | [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=adventurelog) 5 | -------------------------------------------------------------------------------- /documentation/docs/install/synology_nas.md: -------------------------------------------------------------------------------- 1 | # Installation on a Synology NAS 2 | 3 | AdventureLog can be deployed on a Synology NAS using Docker. This guide from Marius Hosting will walk you through the process. 4 | 5 | [Read the guide Here](https://mariushosting.com/how-to-install-adventurelog-on-your-synology-nas/) 6 | -------------------------------------------------------------------------------- /documentation/docs/install/traefik.md: -------------------------------------------------------------------------------- 1 | # Installation with Traefik 2 | 3 | Traefik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy. It is designed to be simple to use and configure, and it integrates well with Docker. 4 | 5 | AdventureLog has a built-in Traefik configuration that makes it easy to deploy and manage your AdventureLog instance. 6 | 7 | The most recent version of the Traefik `docker-compose.yml` file can be found in the [AdventureLog GitHub repository](https://github.com/seanmorley15/AdventureLog/blob/main/docker-compose-traefik.yaml). 8 | -------------------------------------------------------------------------------- /documentation/docs/troubleshooting/login_unresponsive.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting: Login and Registration Unresponsive 2 | 3 | When you encounter issues with the login and registration pages being unresponsive in AdventureLog, it can be due to various reasons. This guide will help you troubleshoot and resolve the unresponsive login and registration pages in AdventureLog. 4 | 5 | 1. Check to make sure the backend container is running and accessible. 6 | 7 | - Check the backend container logs to see if there are any errors or issues blocking the container from running. 8 | 2. Check the connection between the frontend and backend containers. 9 | 10 | - Attempt login with the browser console network tab open to see if there are any errors or issues with the connection between the frontend and backend containers. If there is a connection issue, the code will show an error like `Failed to load resource: net::ERR_CONNECTION_REFUSED`. If this is the case, check the `PUBLIC_SERVER_URL` in the frontend container and refer to the installation docs to ensure the correct URL is set. 11 | - If the error is `403`, continue to the next step. 12 | 13 | 3. The error most likely is due to a CSRF security config issue in either the backend or frontend. 14 | 15 | - Check that the `ORIGIN` variable in the frontend is set to the URL where the frontend is access and you are accessing the app from currently. 16 | - Check that the `CSRF_TRUSTED_ORIGINS` variable in the backend is set to a comma separated list of the origins where you use your backend server and frontend. One of these values should match the `ORIGIN` variable in the frontend. 17 | 18 | 4. If you are still experiencing issues, please refer to the [AdventureLog Discord Server](https://discord.gg/wRbQ9Egr8C) for further assistance, providing as much detail as possible about the issue you are experiencing! 19 | -------------------------------------------------------------------------------- /documentation/docs/troubleshooting/no_images.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting: Images Not Displayed in AdventureLog 2 | 3 | The AdventureLog backend container uses a built-in Nginx container to serve media to the frontend. The `PUBLIC_URL` environment variable is set to the external URL of the **backend** container. This URL is used to generate the URLs for the images in the frontend. If this URL is not set correctly or not accessible from the frontend, the images will not be displayed. 4 | 5 | If you're experiencing issues with images not displaying in AdventureLog, follow these troubleshooting steps to resolve the issue. 6 | 7 | 1. **Check the `PUBLIC_URL` Environment Variable**: 8 | 9 | - Verify that the `PUBLIC_URL` environment variable is set correctly in the `docker-compose.yml` file for the `server` service. 10 | - The `PUBLIC_URL` should be set to the external URL of the backend container. For example: 11 | ``` 12 | PUBLIC_URL=http://backend.example.com 13 | ``` 14 | 15 | 2. **Check `CSRF_TRUSTED_ORIGINS` Environment Variable**: 16 | - If you have set the `CSRF_TRUSTED_ORIGINS` environment variable in the `docker-compose.yml` file, ensure that it includes the frontend URL and the backend URL. 17 | - For example: 18 | ``` 19 | CSRF_TRUSTED_ORIGINS=http://frontend.example.com,http://backend.example.com 20 | ``` 21 | -------------------------------------------------------------------------------- /documentation/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "AdventureLog" 7 | text: "The ultimate travel companion." 8 | tagline: Discover new places, track your adventures, and share your experiences with friends and family. 9 | actions: 10 | - theme: brand 11 | text: Get Started 12 | link: /docs/install/getting_started 13 | - theme: alt 14 | text: About 15 | link: /docs/intro/adventurelog_overview 16 | - theme: alt 17 | text: Demo 18 | link: https://demo.adventurelog.app 19 | image: 20 | src: ./adventurelog.svg 21 | alt: AdventureLog Map Logo 22 | 23 | features: 24 | - title: "Track Your Adventures" 25 | details: "Log your adventures and keep track of where you've been on the world map." 26 | icon: 📍 27 | - title: "Plan Your Next Trip" 28 | details: "Take the guesswork out of planning your next adventure with an easy-to-use itinerary planner." 29 | icon: 📅 30 | - title: "Share Your Experiences" 31 | details: "Share your adventures with friends and family and collaborate on trips together." 32 | icon: 📸 33 | --- 34 | -------------------------------------------------------------------------------- /documentation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "vitepress": "^1.5.0" 4 | }, 5 | "scripts": { 6 | "docs:dev": "vitepress dev", 7 | "docs:build": "vitepress build", 8 | "docs:preview": "vitepress preview" 9 | }, 10 | "dependencies": { 11 | "prettier": "^3.3.3", 12 | "vue": "^3.5.13" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /documentation/public/adventurelog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/documentation/public/adventurelog.png -------------------------------------------------------------------------------- /documentation/public/authentik_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/documentation/public/authentik_settings.png -------------------------------------------------------------------------------- /documentation/public/github_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/documentation/public/github_settings.png -------------------------------------------------------------------------------- /documentation/public/unraid-config-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/documentation/public/unraid-config-1.png -------------------------------------------------------------------------------- /documentation/public/unraid-config-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/documentation/public/unraid-config-2.png -------------------------------------------------------------------------------- /documentation/public/unraid-config-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/documentation/public/unraid-config-3.png -------------------------------------------------------------------------------- /documentation/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/documentation/static/img/favicon.png -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_SERVER_URL=http://127.0.0.1:8000 2 | BODY_SIZE_LIMIT=Infinity 3 | 4 | 5 | # OPTIONAL VARIABLES FOR UMAMI ANALYTICS 6 | PUBLIC_UMAMI_SRC= 7 | PUBLIC_UMAMI_WEBSITE_ID= -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /frontend/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 8 | } 9 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use this image as the platform to build the app 2 | FROM node:22-alpine AS external-website 3 | 4 | # A small line inside the image to show who made it 5 | LABEL Developers="Sean Morley" 6 | 7 | # The WORKDIR instruction sets the working directory for everything that will happen next 8 | WORKDIR /app 9 | 10 | # Copy all local files into the image 11 | COPY . . 12 | 13 | # Remove the development .env file if present 14 | RUN rm -f .env 15 | 16 | # Install pnpm 17 | RUN npm install -g pnpm 18 | 19 | # Clean install all node modules using pnpm 20 | RUN pnpm install 21 | 22 | # Build SvelteKit app 23 | RUN pnpm run build 24 | 25 | # Expose the port that the app is listening on 26 | EXPOSE 3000 27 | 28 | # Run the app 29 | RUN chmod +x ./startup.sh 30 | 31 | # The USER instruction sets the user name to use as the default user for the remainder of the current stage 32 | USER node:node 33 | 34 | # Run startup.sh instead of the default command 35 | CMD ["./startup.sh"] 36 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adventurelog-frontend", 3 | "version": "0.9.0", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "django": "cd .. && cd backend/server && python3 manage.py runserver", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 11 | "lint": "prettier --check .", 12 | "format": "prettier --write ." 13 | }, 14 | "devDependencies": { 15 | "@event-calendar/core": "^3.7.1", 16 | "@event-calendar/day-grid": "^3.7.1", 17 | "@event-calendar/time-grid": "^3.7.1", 18 | "@iconify-json/mdi": "^1.1.67", 19 | "@sveltejs/adapter-node": "^5.2.0", 20 | "@sveltejs/adapter-vercel": "^5.4.1", 21 | "@sveltejs/kit": "^2.8.3", 22 | "@sveltejs/vite-plugin-svelte": "^3.1.1", 23 | "@tailwindcss/typography": "^0.5.13", 24 | "@types/node": "^22.5.4", 25 | "@types/qrcode": "^1.5.5", 26 | "autoprefixer": "^10.4.19", 27 | "daisyui": "^4.12.6", 28 | "postcss": "^8.4.38", 29 | "prettier": "^3.3.2", 30 | "prettier-plugin-svelte": "^3.2.5", 31 | "svelte": "^4.2.19", 32 | "svelte-check": "^3.8.1", 33 | "tailwindcss": "^3.4.4", 34 | "tslib": "^2.6.3", 35 | "typescript": "^5.5.2", 36 | "unplugin-icons": "^0.19.0", 37 | "vite": "^5.4.12" 38 | }, 39 | "type": "module", 40 | "dependencies": { 41 | "@lukulent/svelte-umami": "^0.0.3", 42 | "@mapbox/togeojson": "^0.16.2", 43 | "dompurify": "^3.2.4", 44 | "emoji-picker-element": "^1.26.0", 45 | "gsap": "^3.12.7", 46 | "luxon": "^3.6.1", 47 | "marked": "^15.0.4", 48 | "psl": "^1.15.0", 49 | "qrcode": "^1.5.4", 50 | "svelte-i18n": "^4.0.1", 51 | "svelte-maplibre": "^0.9.8" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | interface Locals { 7 | user: { 8 | pk: number; 9 | username: string; 10 | first_name: string | null; 11 | last_name: string | null; 12 | email: string | null; 13 | date_joined: string | null; 14 | is_staff: boolean; 15 | profile_pic: string | null; 16 | uuid: string; 17 | public_profile: boolean; 18 | has_password: boolean; 19 | disable_password: boolean; 20 | } | null; 21 | locale: string; 22 | } 23 | // interface PageData {} 24 | // interface PageState {} 25 | // interface Platform {} 26 | } 27 | } 28 | 29 | export {}; 30 | -------------------------------------------------------------------------------- /frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 |
    %sveltekit.body%
    12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/lib/assets/AdventureOverlook.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/src/lib/assets/AdventureOverlook.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/MapWithPins.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/src/lib/assets/MapWithPins.webp -------------------------------------------------------------------------------- /frontend/src/lib/assets/immich.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /frontend/src/lib/components/Avatar.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 51 | -------------------------------------------------------------------------------- /frontend/src/lib/components/CategoryFilterDropdown.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
    39 | 40 |
    41 | {$t('adventures.category_filter')} 42 |
    43 |
    44 | 47 | {#each adventure_types as type} 48 |
  • 49 | 58 |
  • 59 | {/each} 60 |
    61 |
    62 | -------------------------------------------------------------------------------- /frontend/src/lib/components/CollectionLink.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 | 45 | 46 | 47 | 59 | 60 | -------------------------------------------------------------------------------- /frontend/src/lib/components/CountryCard.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    19 |
    20 | 21 | No image available 22 |
    23 |
    24 |

    {country.name}

    25 |
    26 | {#if country.subregion} 27 |
    {country.subregion}
    28 | {/if} 29 | {#if country.capital} 30 |
    31 | {country.capital} 32 |
    33 | {/if} 34 | {#if country.num_visits > 0 && country.num_visits != country.num_regions} 35 |
    36 | Visited {country.num_visits} Region{country.num_visits > 1 ? 's' : ''} 37 |
    38 | {:else if country.num_visits > 0 && country.num_visits === country.num_regions} 39 |
    {$t('adventures.visited')}
    40 | {:else} 41 |
    {$t('adventures.not_visited')}
    42 | {/if} 43 |
    44 | 45 |
    46 | 47 | 48 |
    49 |
    50 |
    51 | -------------------------------------------------------------------------------- /frontend/src/lib/components/DeleteWarning.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 38 | 39 | 47 | 48 | -------------------------------------------------------------------------------- /frontend/src/lib/components/ImageInfoModal.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 | 29 | 30 | 31 | 56 | 57 | -------------------------------------------------------------------------------- /frontend/src/lib/components/NotFound.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
    10 |
    11 |
    12 | Lost 13 |
    14 |

    15 | {$t('adventures.no_adventures_found')} 16 |

    17 | {#if !error} 18 |

    19 | {$t('adventures.adventure_not_found')} 20 |

    21 | {:else} 22 |

    {error}

    23 | {/if} 24 |
    25 |
    26 | -------------------------------------------------------------------------------- /frontend/src/lib/components/Toast.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
    13 | {#each toastList as { type, message, id, duration }} 14 | {#if type == 'success'} 15 |
    16 | {message} 17 |
    18 | {/if} 19 | {#if type == 'error'} 20 |
    21 | {message} 22 |
    23 | {/if} 24 | {#if type == 'info'} 25 |
    26 | {message} 27 |
    28 | {/if} 29 | {#if type == 'warning'} 30 |
    31 | {message} 32 |
    33 | {/if} 34 | {/each} 35 |
    36 | -------------------------------------------------------------------------------- /frontend/src/lib/config.ts: -------------------------------------------------------------------------------- 1 | export let appVersion = 'v0.9.0'; 2 | export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.9.0'; 3 | export let appTitle = 'AdventureLog'; 4 | export let copyrightYear = '2023-2025'; 5 | -------------------------------------------------------------------------------- /frontend/src/lib/index.server.ts: -------------------------------------------------------------------------------- 1 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 2 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 3 | 4 | export const fetchCSRFToken = async () => { 5 | const csrfTokenFetch = await fetch(`${serverEndpoint}/csrf/`); 6 | if (csrfTokenFetch.ok) { 7 | const csrfToken = await csrfTokenFetch.json(); 8 | return csrfToken.csrfToken; 9 | } else { 10 | return null; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/src/lib/json/backgrounds.json: -------------------------------------------------------------------------------- 1 | { 2 | "backgrounds": [ 3 | { 4 | "url": "backgrounds/adventurelog_showcase_1.webp", 5 | "author": "Sean Morley", 6 | "location": "Franconia Notch State Park, New Hampshire, USA" 7 | }, 8 | { 9 | "url": "backgrounds/adventurelog_showcase_2.webp", 10 | "author": "Sean Morley", 11 | "location": "Tumbledown Mountain, Maine, USA" 12 | }, 13 | { 14 | "url": "backgrounds/adventurelog_showcase_3.webp", 15 | "author": "Sean Morley", 16 | "location": "Philmont Scout Ranch, New Mexico, USA" 17 | }, 18 | { 19 | "url": "backgrounds/adventurelog_showcase_4.webp", 20 | "author": "Sean Morley", 21 | "location": "Great Sand Dunes National Park, Colorado, USA" 22 | }, 23 | { 24 | "url": "backgrounds/adventurelog_showcase_5.webp", 25 | "author": "Sean Morley", 26 | "location": "Hoboken, New Jersey, USA" 27 | }, 28 | { 29 | "url": "backgrounds/adventurelog_showcase_6.webp", 30 | "author": "Sean Morley", 31 | "location": "Smugglers' Notch Resort, Vermont, USA" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/lib/json/quotes.json: -------------------------------------------------------------------------------- 1 | { 2 | "quotes": [ 3 | { 4 | "quote": "A journey of a thousand miles begins with a single step.", 5 | "author": "Lao Tzu" 6 | }, 7 | { 8 | "quote": "If we were meant to stay in one place, we’d have roots instead of feet.", 9 | "author": "Rachel Wolchin" 10 | }, 11 | { 12 | "quote": "Adventure isn’t hanging on a rope off the side of a mountain. Adventure is an attitude that we must apply to the day to day obstacles in life.", 13 | "author": "John Amatt" 14 | }, 15 | { 16 | "quote": "Wherever you go, go with all your heart.", 17 | "author": "Confucius" 18 | }, 19 | { 20 | "quote": "Until you step into the unknown, you don’t know what you’re made of.", 21 | "author": "Roy T. Bennett" 22 | }, 23 | { 24 | "quote": "You can’t control the past, but you can control where you go next.", 25 | "author": "Kirsten Hubbard" 26 | }, 27 | { 28 | "quote": "Life isn’t about finding yourself. Life is about creating yourself.", 29 | "author": "George Bernard Shaw" 30 | }, 31 | { 32 | "quote": "It is not the mountain we conquer, but ourselves.", 33 | "author": "Edmund Hillary" 34 | }, 35 | { 36 | "quote": "I am not the same, having seen the moon shine on the other side of the world.", 37 | "author": "Mary Anne Radmacher" 38 | }, 39 | { 40 | "quote": "A mind that is stretched by a new experience can never go back to its old dimensions.", 41 | "author": "Oliver Wendell Holmes" 42 | }, 43 | { 44 | "quote": "Life is short and the world is wide.", 45 | "author": "Simon Raven" 46 | }, 47 | { 48 | "quote": "Only those who risk going too far can possibly find out how far they can go.", 49 | "author": "T.S. Eliot" 50 | }, 51 | { 52 | "quote": "Believe you can and you're halfway there.", 53 | "author": "Theodore Roosevelt" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/lib/toasts.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | export const toasts = writable<{ type: any; message: any; id: number }[]>([]); 4 | 5 | export const addToast = (type: any, message: any, duration = 5000) => { 6 | const id = Date.now(); 7 | toasts.update((currentToasts) => { 8 | return [...currentToasts, { type, message, id, duration }]; 9 | }); 10 | setTimeout(() => { 11 | removeToast(id); 12 | }, duration); 13 | }; 14 | 15 | export const removeToast = (id: number) => { 16 | toasts.update((currentToasts) => { 17 | return currentToasts.filter((toast) => toast.id !== id); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { locale } from 'svelte-i18n'; 2 | import type { LayoutServerLoad } from './$types'; 3 | 4 | export const load: LayoutServerLoad = async (event) => { 5 | if (event.locals.user) { 6 | return { 7 | user: event.locals.user, 8 | locale: event.locals.locale 9 | }; 10 | } 11 | return { 12 | user: null, 13 | locale: event.locals.locale 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/routes/activities/+server.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@sveltejs/kit'; 2 | import type { RequestHandler } from '@sveltejs/kit'; 3 | import { fetchCSRFToken } from '$lib/index.server'; 4 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 5 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 6 | 7 | export const POST: RequestHandler = async (event) => { 8 | let allActivities: string[] = []; 9 | let csrfToken = await fetchCSRFToken(); 10 | let sessionId = event.cookies.get('sessionid'); 11 | let res = await event.fetch(`${endpoint}/api/activity-types/types/`, { 12 | headers: { 13 | 'X-CSRFToken': csrfToken, 14 | Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}` 15 | }, 16 | credentials: 'include' 17 | }); 18 | let data = await res.json(); 19 | if (data) { 20 | allActivities = data; 21 | } 22 | return json({ activities: allActivities }); 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/routes/admin/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from '../$types'; 3 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 4 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 5 | 6 | export const load: PageServerLoad = async (event) => { 7 | let publicUrlFetch = await fetch(`${endpoint}/public-url/`); 8 | let publicUrl = ''; 9 | if (!publicUrlFetch.ok) { 10 | return redirect(302, '/'); 11 | } else { 12 | let publicUrlJson = await publicUrlFetch.json(); 13 | publicUrl = publicUrlJson.PUBLIC_URL; 14 | } 15 | 16 | return redirect(302, publicUrl + '/admin/'); 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/routes/calendar/+page.server.ts: -------------------------------------------------------------------------------- 1 | import type { Adventure } from '$lib/types'; 2 | import type { PageServerLoad } from './$types'; 3 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 4 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 5 | 6 | export const load = (async (event) => { 7 | let sessionId = event.cookies.get('sessionid'); 8 | let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, { 9 | headers: { 10 | Cookie: `sessionid=${sessionId}` 11 | } 12 | }); 13 | let adventures = (await visitedFetch.json()) as Adventure[]; 14 | 15 | let dates: Array<{ 16 | id: string; 17 | start: string; 18 | end: string; 19 | title: string; 20 | backgroundColor?: string; 21 | }> = []; 22 | adventures.forEach((adventure) => { 23 | adventure.visits.forEach((visit) => { 24 | if (visit.start_date) { 25 | dates.push({ 26 | id: adventure.id, 27 | start: visit.start_date, 28 | end: visit.end_date || visit.start_date, 29 | title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '') 30 | }); 31 | } 32 | }); 33 | }); 34 | 35 | let icsFetch = await fetch(`${endpoint}/api/ics-calendar/generate`, { 36 | headers: { 37 | Cookie: `sessionid=${sessionId}` 38 | } 39 | }); 40 | let ics_calendar = await icsFetch.text(); 41 | 42 | return { 43 | props: { 44 | adventures, 45 | dates, 46 | ics_calendar 47 | } 48 | }; 49 | }) satisfies PageServerLoad; 50 | -------------------------------------------------------------------------------- /frontend/src/routes/calendar/+page.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |

    {$t('adventures.adventure_calendar')}

    30 | 31 | 32 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /frontend/src/routes/collections/archived/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 4 | import type { Adventure } from '$lib/types'; 5 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 6 | 7 | export const load = (async (event) => { 8 | if (!event.locals.user) { 9 | return redirect(302, '/login'); 10 | } else { 11 | let sessionId = event.cookies.get('sessionid'); 12 | let adventures: Adventure[] = []; 13 | let initialFetch = await fetch(`${serverEndpoint}/api/collections/archived/`, { 14 | headers: { 15 | Cookie: `sessionid=${sessionId}` 16 | } 17 | }); 18 | if (!initialFetch.ok) { 19 | console.error('Failed to fetch visited adventures'); 20 | return redirect(302, '/login'); 21 | } else { 22 | let res = await initialFetch.json(); 23 | let visited = res as Adventure[]; 24 | adventures = [...adventures, ...visited]; 25 | } 26 | 27 | return { 28 | props: { 29 | adventures 30 | } 31 | }; 32 | } 33 | }) satisfies PageServerLoad; 34 | -------------------------------------------------------------------------------- /frontend/src/routes/collections/archived/+page.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
    18 |
    19 | 20 |

    {$t('adventures.archived_collections')}

    21 | {#if collections.length === 0} 22 | 23 | {/if} 24 |
    25 |
    26 | {#each collections as collection} 27 | 28 | {/each} 29 |
    30 |
    31 |
    32 |
    33 | 34 | 35 | Collections 36 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/src/routes/dashboard/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 4 | import type { Adventure } from '$lib/types'; 5 | 6 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 7 | 8 | export const load = (async (event) => { 9 | if (!event.locals.user) { 10 | return redirect(302, '/login'); 11 | } else { 12 | let adventures: Adventure[] = []; 13 | 14 | let initialFetch = await event.fetch(`${serverEndpoint}/api/adventures/`, { 15 | headers: { 16 | Cookie: `sessionid=${event.cookies.get('sessionid')}` 17 | }, 18 | credentials: 'include' 19 | }); 20 | 21 | let stats = null; 22 | 23 | let res = await event.fetch( 24 | `${serverEndpoint}/api/stats/counts/${event.locals.user.username}/`, 25 | { 26 | headers: { 27 | Cookie: `sessionid=${event.cookies.get('sessionid')}` 28 | } 29 | } 30 | ); 31 | if (!res.ok) { 32 | console.error('Failed to fetch user stats'); 33 | } else { 34 | stats = await res.json(); 35 | } 36 | 37 | if (!initialFetch.ok) { 38 | let error_message = await initialFetch.json(); 39 | console.error(error_message); 40 | console.error('Failed to fetch visited adventures'); 41 | return redirect(302, '/login'); 42 | } else { 43 | let res = await initialFetch.json(); 44 | let visited = res.results as Adventure[]; 45 | // only get the first 3 adventures or less if there are less than 3 46 | adventures = visited.slice(0, 3); 47 | } 48 | 49 | return { 50 | props: { 51 | adventures, 52 | stats 53 | } 54 | }; 55 | } 56 | }) satisfies PageServerLoad; 57 | -------------------------------------------------------------------------------- /frontend/src/routes/gpx/[file]/+server.ts: -------------------------------------------------------------------------------- 1 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 2 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 3 | 4 | /** @type {import('./$types').RequestHandler} */ 5 | export async function GET(event) { 6 | let sessionid = event.cookies.get('sessionid'); 7 | let fileName = event.params.file; 8 | let res = await fetch(`${endpoint}/media/attachments/${fileName}`, { 9 | method: 'GET', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | Cookie: `sessionid=${sessionid}` 13 | } 14 | }); 15 | let data = await res.text(); 16 | return new Response(data, { 17 | status: res.status, 18 | headers: { 19 | 'Content-Type': 'application/xml' 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/routes/immich/[key]/+server.ts: -------------------------------------------------------------------------------- 1 | import type { RequestHandler } from './$types'; 2 | 3 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 4 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 5 | 6 | export const GET: RequestHandler = async (event) => { 7 | try { 8 | const key = event.params.key; 9 | 10 | // Forward the session ID from cookies 11 | const sessionid = event.cookies.get('sessionid'); 12 | if (!sessionid) { 13 | return new Response(JSON.stringify({ error: 'Session ID is missing' }), { 14 | status: 401, 15 | headers: { 'Content-Type': 'application/json' } 16 | }); 17 | } 18 | 19 | // Proxy the request to the backend 20 | const res = await fetch(`${endpoint}/api/integrations/immich/get/${key}`, { 21 | method: 'GET', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | Cookie: `sessionid=${sessionid}` 25 | } 26 | }); 27 | 28 | if (!res.ok) { 29 | // Return an error response if the backend request fails 30 | const errorData = await res.json(); 31 | return new Response(JSON.stringify(errorData), { 32 | status: res.status, 33 | headers: { 'Content-Type': 'application/json' } 34 | }); 35 | } 36 | 37 | // Get the image as a Blob 38 | const image = await res.blob(); 39 | 40 | // Create a Response to pass the image back 41 | return new Response(image, { 42 | status: res.status, 43 | headers: { 44 | 'Content-Type': res.headers.get('Content-Type') || 'image/jpeg' 45 | } 46 | }); 47 | } catch (error) { 48 | console.error('Error proxying request:', error); 49 | return new Response(JSON.stringify({ error: 'Failed to fetch image' }), { 50 | status: 500, 51 | headers: { 'Content-Type': 'application/json' } 52 | }); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /frontend/src/routes/map/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 4 | import type { Adventure, VisitedRegion } from '$lib/types'; 5 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 6 | 7 | export const load = (async (event) => { 8 | if (!event.locals.user) { 9 | return redirect(302, '/login'); 10 | } else { 11 | let sessionId = event.cookies.get('sessionid'); 12 | let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, { 13 | headers: { 14 | Cookie: `sessionid=${sessionId}` 15 | } 16 | }); 17 | 18 | let visitedRegionsFetch = await fetch(`${endpoint}/api/visitedregion/`, { 19 | headers: { 20 | Cookie: `sessionid=${sessionId}` 21 | } 22 | }); 23 | 24 | let visitedRegions = (await visitedRegionsFetch.json()) as VisitedRegion[]; 25 | let adventures = (await visitedFetch.json()) as Adventure[]; 26 | 27 | if (!visitedRegionsFetch.ok) { 28 | console.error('Failed to fetch visited regions'); 29 | return redirect(302, '/login'); 30 | } else if (!visitedFetch.ok) { 31 | console.error('Failed to fetch visited adventures'); 32 | return redirect(302, '/login'); 33 | } else { 34 | return { 35 | props: { 36 | visitedRegions, 37 | adventures 38 | } 39 | }; 40 | } 41 | } 42 | }) satisfies PageServerLoad; 43 | -------------------------------------------------------------------------------- /frontend/src/routes/profile/[uuid]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit'; 2 | import type { PageServerLoad, RequestEvent } from '../../$types'; 3 | import { t } from 'svelte-i18n'; 4 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 5 | 6 | export const load: PageServerLoad = async (event: RequestEvent) => { 7 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 8 | 9 | // @ts-ignore 10 | let username = event.params.uuid as string; 11 | 12 | if (!username) { 13 | return error(404, 'Not found'); 14 | } 15 | 16 | // let sessionId = event.cookies.get('sessionid'); 17 | let stats = null; 18 | 19 | let res = await event.fetch(`${endpoint}/api/stats/counts/${username}`, { 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | Cookie: `sessionid=${event.cookies.get('sessionid')}` 23 | } 24 | }); 25 | if (!res.ok) { 26 | console.error('Failed to fetch user stats'); 27 | } else { 28 | stats = await res.json(); 29 | } 30 | 31 | let userData = await event.fetch(`${endpoint}/auth/user/${username}/`, { 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | Cookie: `sessionid=${event.cookies.get('sessionid')}` 35 | } 36 | }); 37 | if (!userData.ok) { 38 | return error(404, 'Not found'); 39 | } 40 | 41 | let data = await userData.json(); 42 | 43 | return { 44 | user: data.user, 45 | adventures: data.adventures, 46 | collections: data.collections, 47 | stats: stats 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /frontend/src/routes/search/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | 4 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 5 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 6 | 7 | export const load = (async (event) => { 8 | if (!event.locals.user) { 9 | return redirect(302, '/login'); 10 | } 11 | 12 | const query = event.url.searchParams.get('query'); 13 | 14 | if (!query) { 15 | return { data: [] }; 16 | } 17 | 18 | let sessionId = event.cookies.get('sessionid'); 19 | 20 | let res = await fetch(`${serverEndpoint}/api/search/?query=${query}`, { 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | Cookie: `sessionid=${sessionId}` 24 | } 25 | }); 26 | 27 | if (!res.ok) { 28 | console.error('Failed to fetch search data'); 29 | let error = await res.json(); 30 | return { error: error.error }; 31 | } 32 | 33 | let data = await res.json(); 34 | 35 | return { 36 | adventures: data.adventures, 37 | collections: data.collections, 38 | users: data.users, 39 | countries: data.countries, 40 | regions: data.regions, 41 | cities: data.cities, 42 | visited_cities: data.visited_cities, 43 | visited_regions: data.visited_regions 44 | }; 45 | }) satisfies PageServerLoad; 46 | -------------------------------------------------------------------------------- /frontend/src/routes/shared/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | 4 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 5 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 6 | 7 | export const load = (async (event) => { 8 | if (!event.locals.user) { 9 | return redirect(302, '/login'); 10 | } else { 11 | let sessionId = event.cookies.get('sessionid'); 12 | let res = await fetch(`${serverEndpoint}/api/collections/shared/`, { 13 | headers: { 14 | Cookie: `sessionid=${sessionId}` 15 | } 16 | }); 17 | if (!res.ok) { 18 | return redirect(302, '/login'); 19 | } else { 20 | return { 21 | props: { 22 | collections: await res.json() 23 | } 24 | }; 25 | } 26 | } 27 | }) satisfies PageServerLoad; 28 | -------------------------------------------------------------------------------- /frontend/src/routes/shared/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if collections.length > 0} 14 |
    15 | {#each collections as collection} 16 | 17 | {/each} 18 |
    19 | {:else} 20 |

    21 | {$t('share.no_shared_found')} 22 | {#if data.user && !data.user?.public_profile} 23 |

    {$t('share.set_public')}

    24 | 27 | {/if} 28 |

    29 | {/if} 30 | 31 | 32 | Shared Collections 33 | 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/routes/user/[uuid]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 4 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 5 | 6 | export const load = (async (event) => { 7 | let sessionId = event.cookies.get('sessionid'); 8 | if (!sessionId) { 9 | return redirect(302, '/login'); 10 | } 11 | const uuid = event.params.uuid; 12 | if (!uuid) { 13 | return redirect(302, '/users'); 14 | } 15 | let res = await fetch(`${serverEndpoint}/auth/user/${uuid}/`, { 16 | headers: { 17 | Cookie: `sessionid=${sessionId}` 18 | } 19 | }); 20 | if (!res.ok) { 21 | return redirect(302, '/users'); 22 | } else { 23 | const data = await res.json(); 24 | return { 25 | props: { 26 | user: data 27 | } 28 | }; 29 | } 30 | }) satisfies PageServerLoad; 31 | -------------------------------------------------------------------------------- /frontend/src/routes/user/[uuid]/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | {#if user.profile_pic} 10 |
    11 |
    12 | {user.username} 13 |
    14 |
    15 | {/if} 16 | 17 |

    {user.first_name} {user.last_name}

    18 |

    {user.username}

    19 | 20 |
    21 | {#if user.is_staff} 22 |
    Admin
    23 | {/if} 24 |
    25 | 26 |
    27 |

    28 | {user.date_joined ? 'Joined ' + new Date(user.date_joined).toLocaleDateString() : ''} 29 |

    30 |
    31 | 32 | 33 | {user.username} | AdventureLog 34 | 35 | 36 | -------------------------------------------------------------------------------- /frontend/src/routes/user/reset-password/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { fetchCSRFToken } from '$lib/index.server'; 2 | import { fail, type Actions } from '@sveltejs/kit'; 3 | 4 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 5 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 6 | 7 | export const actions: Actions = { 8 | forgotPassword: async (event) => { 9 | const formData = await event.request.formData(); 10 | 11 | const email = formData.get('email') as string | null | undefined; 12 | 13 | if (!email) { 14 | return fail(400, { message: 'missing_email' }); 15 | } 16 | 17 | let csrfToken = await fetchCSRFToken(); 18 | 19 | let res = await fetch(`${endpoint}/auth/browser/v1/auth/password/request`, { 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | 'X-CSRFToken': csrfToken, 24 | Cookie: `csrftoken=${csrfToken}`, 25 | Referer: event.url.origin // Include Referer header 26 | }, 27 | body: JSON.stringify({ 28 | email 29 | }) 30 | }); 31 | 32 | if (!res.ok) { 33 | let message = await res.json(); 34 | return fail(res.status, message); 35 | } 36 | return { success: true }; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /frontend/src/routes/user/reset-password/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
    8 |

    {$t('settings.reset_password')}

    9 | 10 |
    11 |
    12 |
    13 | 16 | 24 |
    25 | 26 |
    27 | 30 |
    31 | 32 | {#if $page.form?.message} 33 |
    34 | {$t(`settings.${$page.form?.message}`)} 35 |
    36 | {/if} 37 | 38 | {#if $page.form?.success} 39 |
    40 | {$t('settings.possible_reset')} 41 |
    42 | {/if} 43 |
    44 |
    45 |
    46 | 47 | 48 | Reset Password 49 | 50 | 51 | -------------------------------------------------------------------------------- /frontend/src/routes/user/reset-password/[key]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { fail, redirect } from '@sveltejs/kit'; 2 | import { fetchCSRFToken } from '$lib/index.server'; 3 | import type { PageServerLoad, Actions } from './$types'; 4 | 5 | export const load = (async ({ params }) => { 6 | const key = params.key; 7 | if (!key) { 8 | throw redirect(302, '/'); 9 | } 10 | return { key }; 11 | }) satisfies PageServerLoad; 12 | 13 | export const actions: Actions = { 14 | default: async (event) => { 15 | const formData = await event.request.formData(); 16 | const password = formData.get('password'); 17 | const confirm_password = formData.get('confirm_password'); 18 | const key = event.params.key; 19 | 20 | if (!password || !confirm_password) { 21 | return fail(400, { message: 'auth.both_passwords_required' }); 22 | } 23 | 24 | if (password !== confirm_password) { 25 | return fail(400, { message: 'settings.password_does_not_match' }); 26 | } 27 | 28 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 29 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 30 | const csrfToken = await fetchCSRFToken(); 31 | 32 | const response = await event.fetch(`${serverEndpoint}/auth/browser/v1/auth/password/reset`, { 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | Cookie: `csrftoken=${csrfToken}`, 36 | 'X-CSRFToken': csrfToken, 37 | Referer: event.url.origin // Include Referer header 38 | }, 39 | method: 'POST', 40 | credentials: 'include', 41 | body: JSON.stringify({ key: key, password: password }) 42 | }); 43 | 44 | if (response.status !== 401) { 45 | const error_message = await response.json(); 46 | console.error(error_message); 47 | console.log(response); 48 | return fail(response.status, { message: 'auth.reset_failed' }); 49 | } 50 | 51 | return redirect(302, '/login'); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /frontend/src/routes/user/reset-password/[key]/+page.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
    11 |

    12 | {$t('settings.change_password')} 13 |

    14 | 15 |
    16 |
    17 |
    18 | 21 | 29 |
    30 | 31 |
    32 | 35 | 43 |
    44 | 45 |
    46 | 49 |
    50 | 51 | {#if $page.form?.message} 52 |
    53 | {$t($page.form?.message)} 54 |
    55 | {/if} 56 |
    57 |
    58 |
    59 | 60 | 61 | Change Password 62 | 66 | 67 | -------------------------------------------------------------------------------- /frontend/src/routes/user/verify-email/[key]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { fetchCSRFToken } from '$lib/index.server'; 2 | import type { PageServerLoad } from './$types'; 3 | 4 | export const load = (async (event) => { 5 | // get key from route params 6 | const key = event.params.key; 7 | if (!key) { 8 | return { status: 404 }; 9 | } 10 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 11 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 12 | const csrfToken = await fetchCSRFToken(); 13 | 14 | let verifyFetch = await event.fetch(`${serverEndpoint}/auth/browser/v1/auth/email/verify`, { 15 | headers: { 16 | Cookie: `csrftoken=${csrfToken}`, 17 | 'X-CSRFToken': csrfToken 18 | }, 19 | method: 'POST', 20 | credentials: 'include', 21 | 22 | body: JSON.stringify({ key: key }) 23 | }); 24 | if (verifyFetch.ok || verifyFetch.status == 401) { 25 | return { 26 | verified: true 27 | }; 28 | } else { 29 | let error_message = await verifyFetch.json(); 30 | console.error(error_message); 31 | console.error('Failed to verify email'); 32 | return { status: 404 }; 33 | } 34 | }) satisfies PageServerLoad; 35 | -------------------------------------------------------------------------------- /frontend/src/routes/user/verify-email/[key]/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 |
    10 | {#if data.verified} 11 |

    12 | {$t('settings.email_verified')} 13 |

    14 |

    15 | {$t('settings.email_verified_success')} 16 |

    17 | {:else} 18 |

    19 | {$t('settings.email_verified_error')} 20 |

    21 |

    22 | {$t('settings.email_verified_erorr_desc')} 23 |

    24 | {/if} 25 |
    26 |
    27 | 28 | 29 | Email Verification 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/routes/users/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import type { PageServerLoad } from './$types'; 3 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 4 | const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 5 | 6 | export const load = (async (event) => { 7 | let sessionId = event.cookies.get('sessionid'); 8 | if (!sessionId) { 9 | return redirect(302, '/login'); 10 | } 11 | 12 | const res = await fetch(`${serverEndpoint}/auth/users`, { 13 | headers: { 14 | Cookie: `sessionid=${sessionId}` 15 | } 16 | }); 17 | if (!res.ok) { 18 | return redirect(302, '/login'); 19 | } else { 20 | const data = await res.json(); 21 | return { 22 | props: { 23 | users: data 24 | } 25 | }; 26 | } 27 | }) satisfies PageServerLoad; 28 | -------------------------------------------------------------------------------- /frontend/src/routes/users/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |

    AdventureLog {$t('navbar.users')}

    13 |
    14 | {#each users as user (user.uuid)} 15 | 16 | {/each} 17 |
    18 | 19 | {#if users.length === 0} 20 |

    {$t('users.no_users_found')}

    21 | {/if} 22 | 23 | 24 | Users 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/routes/worldtravel/+page.server.ts: -------------------------------------------------------------------------------- 1 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 2 | import type { Country } from '$lib/types'; 3 | import { redirect, type Actions } from '@sveltejs/kit'; 4 | import type { PageServerLoad } from './$types'; 5 | import { fetchCSRFToken } from '$lib/index.server'; 6 | 7 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 8 | 9 | export const load = (async (event) => { 10 | if (!event.locals.user) { 11 | return redirect(302, '/login'); 12 | } else { 13 | const res = await event.fetch(`${endpoint}/api/countries/`, { 14 | method: 'GET', 15 | headers: { 16 | Cookie: `sessionid=${event.cookies.get('sessionid')}` 17 | }, 18 | credentials: 'include' 19 | }); 20 | if (!res.ok) { 21 | console.error('Failed to fetch countries'); 22 | return { status: 500 }; 23 | } else { 24 | const countries = (await res.json()) as Country[]; 25 | return { 26 | props: { 27 | countries 28 | } 29 | }; 30 | } 31 | } 32 | }) satisfies PageServerLoad; 33 | -------------------------------------------------------------------------------- /frontend/src/routes/worldtravel/[id]/+page.server.ts: -------------------------------------------------------------------------------- 1 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 2 | import type { Country, Region, VisitedRegion } from '$lib/types'; 3 | import { redirect } from '@sveltejs/kit'; 4 | import type { PageServerLoad } from './$types'; 5 | 6 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 7 | 8 | export const load = (async (event) => { 9 | const id = event.params.id.toUpperCase(); 10 | 11 | let regions: Region[] = []; 12 | let visitedRegions: VisitedRegion[] = []; 13 | let country: Country; 14 | 15 | let sessionId = event.cookies.get('sessionid'); 16 | 17 | if (!sessionId) { 18 | return redirect(302, '/login'); 19 | } 20 | 21 | let res = await fetch(`${endpoint}/api/${id}/regions/`, { 22 | method: 'GET', 23 | headers: { 24 | Cookie: `sessionid=${sessionId}` 25 | } 26 | }); 27 | if (!res.ok) { 28 | console.error('Failed to fetch regions'); 29 | return redirect(302, '/404'); 30 | } else { 31 | regions = (await res.json()) as Region[]; 32 | } 33 | 34 | res = await fetch(`${endpoint}/api/${id}/visits/`, { 35 | method: 'GET', 36 | headers: { 37 | Cookie: `sessionid=${sessionId}` 38 | } 39 | }); 40 | if (!res.ok) { 41 | console.error('Failed to fetch visited regions'); 42 | return { status: 500 }; 43 | } else { 44 | visitedRegions = (await res.json()) as VisitedRegion[]; 45 | } 46 | 47 | res = await fetch(`${endpoint}/api/countries/${regions[0].country}/`, { 48 | method: 'GET', 49 | headers: { 50 | Cookie: `sessionid=${sessionId}` 51 | } 52 | }); 53 | if (!res.ok) { 54 | console.error('Failed to fetch country'); 55 | return { status: 500 }; 56 | } else { 57 | country = (await res.json()) as Country; 58 | } 59 | 60 | return { 61 | props: { 62 | regions, 63 | visitedRegions, 64 | country 65 | } 66 | }; 67 | }) satisfies PageServerLoad; 68 | -------------------------------------------------------------------------------- /frontend/src/routes/worldtravel/[id]/[id]/+page.server.ts: -------------------------------------------------------------------------------- 1 | const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; 2 | import type { City, Country, Region, VisitedCity, VisitedRegion } from '$lib/types'; 3 | import { redirect } from '@sveltejs/kit'; 4 | import type { PageServerLoad } from './$types'; 5 | 6 | const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 7 | 8 | export const load = (async (event) => { 9 | const id = event.params.id.toUpperCase(); 10 | 11 | let cities: City[] = []; 12 | let region = {} as Region; 13 | let visitedCities: VisitedCity[] = []; 14 | 15 | let sessionId = event.cookies.get('sessionid'); 16 | 17 | if (!sessionId) { 18 | return redirect(302, '/login'); 19 | } 20 | 21 | let res = await fetch(`${endpoint}/api/regions/${id}/cities/`, { 22 | method: 'GET', 23 | headers: { 24 | Cookie: `sessionid=${sessionId}` 25 | } 26 | }); 27 | if (!res.ok) { 28 | console.error('Failed to fetch regions'); 29 | return redirect(302, '/404'); 30 | } else { 31 | cities = (await res.json()) as City[]; 32 | } 33 | 34 | res = await fetch(`${endpoint}/api/regions/${id}/`, { 35 | method: 'GET', 36 | headers: { 37 | Cookie: `sessionid=${sessionId}` 38 | } 39 | }); 40 | if (!res.ok) { 41 | console.error('Failed to fetch country'); 42 | return { status: 500 }; 43 | } else { 44 | region = (await res.json()) as Region; 45 | } 46 | 47 | res = await fetch(`${endpoint}/api/regions/${region.id}/cities/visits/`, { 48 | method: 'GET', 49 | headers: { 50 | Cookie: `sessionid=${sessionId}` 51 | } 52 | }); 53 | if (!res.ok) { 54 | console.error('Failed to fetch visited regions'); 55 | return { status: 500 }; 56 | } else { 57 | visitedCities = (await res.json()) as VisitedCity[]; 58 | } 59 | 60 | return { 61 | props: { 62 | cities, 63 | region, 64 | visitedCities 65 | } 66 | }; 67 | }) satisfies PageServerLoad; 68 | -------------------------------------------------------------------------------- /frontend/src/service-worker/indes.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { build, files, version } from '$service-worker'; 4 | 5 | const CACHE = `cache-${version}`; 6 | 7 | const ASSETS = [ 8 | ...build, // the app itself 9 | ...files // everything in `static` 10 | ]; 11 | 12 | self.addEventListener('install', (event) => { 13 | // Create a new cache and add all files to it 14 | async function addFilesToCache() { 15 | const cache = await caches.open(CACHE); 16 | await cache.addAll(ASSETS); 17 | } 18 | event.waitUntil(addFilesToCache()); 19 | }); 20 | 21 | self.addEventListener('activate', (event) => { 22 | // Remove previous cached data from disk 23 | async function deleteOldCaches() { 24 | for (const key of await caches.keys()) { 25 | if (key !== CACHE) await caches.delete(key); 26 | } 27 | } 28 | event.waitUntil(deleteOldCaches()); 29 | }); 30 | 31 | self.addEventListener('fetch', (event) => { 32 | // ignore POST requests, etc 33 | if (event.request.method !== 'GET') return; 34 | 35 | async function respond() { 36 | const url = new URL(event.request.url); 37 | const cache = await caches.open(CACHE); 38 | 39 | // `build`/`files` can always be served from the cache 40 | if (ASSETS.includes(url.pathname)) { 41 | return cache.match(url.pathname); 42 | } 43 | 44 | // for everything else, try the network first, but 45 | // fall back to the cache if we're offline 46 | try { 47 | const response = await fetch(event.request); 48 | 49 | if (response.status === 200) { 50 | cache.put(event.request, response.clone()); 51 | } 52 | 53 | return response; 54 | } catch { 55 | return cache.match(event.request); 56 | } 57 | } 58 | 59 | event.respondWith(respond()); 60 | }); 61 | -------------------------------------------------------------------------------- /frontend/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "The origin to be set is: $ORIGIN" 4 | # Start the application 5 | ORIGIN=$ORIGIN exec node build 6 | -------------------------------------------------------------------------------- /frontend/static/backgrounds/adventurelog_christmas.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/backgrounds/adventurelog_christmas.webp -------------------------------------------------------------------------------- /frontend/static/backgrounds/adventurelog_new_year.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/backgrounds/adventurelog_new_year.webp -------------------------------------------------------------------------------- /frontend/static/backgrounds/adventurelog_showcase_1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/backgrounds/adventurelog_showcase_1.webp -------------------------------------------------------------------------------- /frontend/static/backgrounds/adventurelog_showcase_2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/backgrounds/adventurelog_showcase_2.webp -------------------------------------------------------------------------------- /frontend/static/backgrounds/adventurelog_showcase_3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/backgrounds/adventurelog_showcase_3.webp -------------------------------------------------------------------------------- /frontend/static/backgrounds/adventurelog_showcase_4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/backgrounds/adventurelog_showcase_4.webp -------------------------------------------------------------------------------- /frontend/static/backgrounds/adventurelog_showcase_5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/backgrounds/adventurelog_showcase_5.webp -------------------------------------------------------------------------------- /frontend/static/backgrounds/adventurelog_showcase_6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/backgrounds/adventurelog_showcase_6.webp -------------------------------------------------------------------------------- /frontend/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanmorley15/AdventureLog/5dfe39468ae95bff4f5a7ac925309f4a23e9bcc3/frontend/static/favicon.png -------------------------------------------------------------------------------- /frontend/static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "AdventureLog", 3 | "name": "AdventureLog", 4 | "start_url": "/dashboard", 5 | "icons": [ 6 | { 7 | "src": "adventurelog.svg", 8 | "type": "image/svg+xml", 9 | "sizes": "any" 10 | } 11 | ], 12 | "background_color": "#2a323c", 13 | "display": "standalone", 14 | "scope": "/", 15 | "description": "Self-hostable travel tracker and trip planner." 16 | } -------------------------------------------------------------------------------- /frontend/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | # Google adsbot ignores robots.txt unless specifically named! 5 | User-agent: AdsBot-Google 6 | Allow: / 7 | 8 | 9 | User-agent: GPTBot 10 | Disallow: / -------------------------------------------------------------------------------- /frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | 3 | import adapterNode from '@sveltejs/adapter-node'; 4 | import adapterVercel from '@sveltejs/adapter-vercel'; 5 | 6 | let adapter; 7 | if (process.env.VERCEL) { 8 | adapter = adapterVercel; 9 | } else { 10 | adapter = adapterNode; 11 | } 12 | 13 | /** @type {import('@sveltejs/kit').Config} */ 14 | const config = { 15 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 16 | // for more information about preprocessors 17 | preprocess: vitePreprocess(), 18 | 19 | kit: { 20 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 21 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 22 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 23 | adapter: adapter() 24 | } 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | "types": ["unplugin-icons/types/svelte"] // add this line 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { defineConfig } from 'vite'; 3 | import { sveltekit } from '@sveltejs/kit/vite'; 4 | import Icons from 'unplugin-icons/vite'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | sveltekit(), 9 | Icons({ 10 | compiler: 'svelte' 11 | }) 12 | ] 13 | }); 14 | --------------------------------------------------------------------------------