├── .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 |
19 |
--------------------------------------------------------------------------------
/backend/server/templates/fragments/login_form.html:
--------------------------------------------------------------------------------
1 |
2 |
25 |
--------------------------------------------------------------------------------
/backend/server/templates/fragments/logout_form.html:
--------------------------------------------------------------------------------
1 | {% block content %}
2 |
3 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/backend/server/templates/fragments/password_change_form.html:
--------------------------------------------------------------------------------
1 |
2 |
25 |
--------------------------------------------------------------------------------
/backend/server/templates/fragments/password_reset_confirm_form.html:
--------------------------------------------------------------------------------
1 |
2 |
41 |
--------------------------------------------------------------------------------
/backend/server/templates/fragments/password_reset_form.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/backend/server/templates/fragments/resend_email_verification_form.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/backend/server/templates/fragments/signup_form.html:
--------------------------------------------------------------------------------
1 |
2 |
39 |
--------------------------------------------------------------------------------
/backend/server/templates/fragments/user_details_form.html:
--------------------------------------------------------------------------------
1 |
2 |
40 |
--------------------------------------------------------------------------------
/backend/server/templates/home.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %} {% block content %}
2 |
3 |
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 |
30 |
31 | {% firstof user.username 'Registered' %}
32 |
33 |
34 |
45 |
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 | 
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 |
15 |
16 |
17 | {#if user.profile_pic}
18 |

19 | {:else}
20 |
{letter}
21 | {/if}
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 | {$t('navbar.greeting')}, {user.first_name
34 | ? `${user.first_name} ${user.last_name}`
35 | : user.username}
36 |
37 | -
38 |
39 |
40 |
41 |
42 | {#if user.is_staff}
43 |
44 | {/if}
45 |
46 |
49 |
50 |
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 |
60 |
--------------------------------------------------------------------------------
/frontend/src/lib/components/CountryCard.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
19 |
20 |
21 |
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 |
48 |
--------------------------------------------------------------------------------
/frontend/src/lib/components/ImageInfoModal.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
57 |
--------------------------------------------------------------------------------
/frontend/src/lib/components/NotFound.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
10 |
11 |
12 |

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 |

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 |
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 |
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 |
--------------------------------------------------------------------------------