diff --git a/README.md b/README.md index 9ff2821..3724be9 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,9 @@ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/seanmorley15) -- **[Documentation](https://adventurelog.app)** -- **[Demo](https://demo.adventurelog.app)** -- **[Join the AdventureLog Community Discord Server](https://discord.gg/wRbQ9Egr8C)** - +- **[Documentation](https://adventurelog.app)** +- **[Demo](https://demo.adventurelog.app)** +- **[Join the AdventureLog Community Discord Server](https://discord.gg/wRbQ9Egr8C)** # Table of Contents @@ -59,18 +58,18 @@ Here is a summary of the configuration options available in the `docker-compose. ### Backend Container (server) -| Name | Required | Description | Default Value | -| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -| `PGHOST` | Yes | Databse host. | db | -| `PGDATABASE` | Yes | Database. | database | -| `PGUSER` | Yes | Database user. | adventure | -| `PGPASSWORD` | Yes | Database password. | changeme123 | -| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin | -| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin | -| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com | -| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | 'http://localhost:8016' | -| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | http://localhost:8016 | -| `FRONTEND_URL` | Yes | This is the publicly accessible url to the **frontend** container. This link should be accessible for all users. Used for email generation. | 'http://localhost:8015' | +| Name | Required | Description | Default Value | +| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| `PGHOST` | Yes | Databse host. | db | +| `PGDATABASE` | Yes | Database. | database | +| `PGUSER` | Yes | Database user. | adventure | +| `PGPASSWORD` | Yes | Database password. | changeme123 | +| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin | +| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin | +| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com | +| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | http://localhost:8016 | +| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | http://localhost:8016 | +| `FRONTEND_URL` | Yes | This is the publicly accessible url to the **frontend** container. This link should be accessible for all users. Used for email generation. | http://localhost:8015 | ## Running the Containers diff --git a/backend/LICENSE b/backend/LICENSE deleted file mode 100644 index 01c7bc7..0000000 --- a/backend/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -# Preface - -AdventureLog uses DjRestAuth, a Django REST Framework authentication backend for Django Rest Framework. DjRestAuth is licensed under the MIT License. - ---- - -## The MIT License (MIT) - -Copyright (c) 2014 iMerica https://github.com/iMerica/ - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 349b7be..0000000 --- a/backend/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# AdventureLog Django Backend -A demo of a possible AdventureLog 2.0 version using Django as the backend with a REST API. - -Based of django-rest-framework and dj-rest-auth. \ No newline at end of file diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 6160f60..1beac0f 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -3,6 +3,11 @@ from django.contrib import admin from django.utils.html import mark_safe from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category from worldtravel.models import Country, Region, VisitedRegion +from allauth.account.decorators import secure_admin_login + +admin.autodiscover() +admin.site.login = secure_admin_login(admin.site.login) + class AdventureAdmin(admin.ModelAdmin): @@ -54,9 +59,9 @@ from users.models import CustomUser class CustomUserAdmin(UserAdmin): model = CustomUser - list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display'] + list_display = ['username', 'is_staff', 'is_active', 'image_display'] readonly_fields = ('uuid',) - search_fields = ('username', 'email') + search_fields = ('username',) fieldsets = UserAdmin.fieldsets + ( (None, {'fields': ('profile_pic', 'uuid', 'public_profile')}), ) diff --git a/backend/server/adventures/middleware.py b/backend/server/adventures/middleware.py index af54b68..3cd9713 100644 --- a/backend/server/adventures/middleware.py +++ b/backend/server/adventures/middleware.py @@ -11,3 +11,13 @@ class AppVersionMiddleware: response['X-AdventureLog-Version'] = '1.0.0' return response + +# make a middlewra that prints all of the request cookies +class PrintCookiesMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + print(request.COOKIES) + response = self.get_response(request) + return response \ No newline at end of file diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index 9b7566d..d035522 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -1,6 +1,6 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet +from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet, IcsCalendarGeneratorViewSet router = DefaultRouter() router.register(r'adventures', AdventureViewSet, basename='adventures') @@ -14,6 +14,7 @@ router.register(r'checklists', ChecklistViewSet, basename='checklists') router.register(r'images', AdventureImageViewSet, basename='images') router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode') router.register(r'categories', CategoryViewSet, basename='categories') +router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar') urlpatterns = [ diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 9ad79c3..f9d9aca 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -17,6 +17,9 @@ from rest_framework.pagination import PageNumberPagination from django.shortcuts import get_object_or_404 from rest_framework import status from django.contrib.auth import get_user_model +from icalendar import Calendar, Event, vText, vCalAddress +from django.http import HttpResponse +from datetime import datetime User = get_user_model() @@ -73,6 +76,7 @@ class AdventureViewSet(viewsets.ModelViewSet): return queryset.order_by(ordering) def get_queryset(self): + print(self.request.user) # if the user is not authenticated return only public adventures for retrieve action if not self.request.user.is_authenticated: if self.action == 'retrieve': @@ -1201,4 +1205,57 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): visited_region.save() new_region_count += 1 new_regions[region.id] = region.name - return Response({"new_regions": new_region_count, "regions": new_regions}) \ No newline at end of file + return Response({"new_regions": new_region_count, "regions": new_regions}) + + +from django.http import HttpResponse +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from icalendar import Calendar, Event, vText, vCalAddress +from datetime import datetime, timedelta + +class IcsCalendarGeneratorViewSet(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + @action(detail=False, methods=['get']) + def generate(self, request): + adventures = Adventure.objects.filter(user_id=request.user) + serializer = AdventureSerializer(adventures, many=True) + user = request.user + name = f"{user.first_name} {user.last_name}" + print(serializer.data) + + cal = Calendar() + cal.add('prodid', '-//My Adventure Calendar//example.com//') + cal.add('version', '2.0') + + for adventure in serializer.data: + if adventure['visits']: + for visit in adventure['visits']: + event = Event() + event.add('summary', adventure['name']) + start_date = datetime.strptime(visit['start_date'], '%Y-%m-%d').date() + end_date = datetime.strptime(visit['end_date'], '%Y-%m-%d').date() + timedelta(days=1) if visit['end_date'] else start_date + timedelta(days=1) + event.add('dtstart', start_date) + event.add('dtend', end_date) + event.add('dtstamp', datetime.now()) + event.add('transp', 'TRANSPARENT') + event.add('class', 'PUBLIC') + event.add('created', datetime.now()) + event.add('last-modified', datetime.now()) + event.add('description', adventure['description']) + if adventure.get('location'): + event.add('location', adventure['location']) + if adventure.get('link'): + event.add('url', adventure['link']) + + organizer = vCalAddress(f'MAILTO:{user.email}') + organizer.params['cn'] = vText(name) + event.add('organizer', organizer) + + cal.add_component(event) + + response = HttpResponse(cal.to_ical(), content_type='text/calendar') + response['Content-Disposition'] = 'attachment; filename=adventures.ics' + return response diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 44aa5d4..7e4973b 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -11,7 +11,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os from dotenv import load_dotenv -from datetime import timedelta from os import getenv from pathlib import Path # Load environment variables from .env file @@ -35,8 +34,6 @@ DEBUG = getenv('DEBUG', 'True') == 'True' # ] ALLOWED_HOSTS = ['*'] -# Application definition - INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', @@ -47,19 +44,19 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'rest_framework', 'rest_framework.authtoken', - 'dj_rest_auth', 'allauth', 'allauth.account', - 'dj_rest_auth.registration', + 'allauth.mfa', + 'allauth.headless', 'allauth.socialaccount', - 'allauth.socialaccount.providers.facebook', + # "widget_tweaks", + # "slippers", 'drf_yasg', 'corsheaders', 'adventures', 'worldtravel', 'users', 'django.contrib.gis', - ) MIDDLEWARE = ( @@ -83,7 +80,6 @@ CACHES = { } } - # For backwards compatibility for Django 1.8 MIDDLEWARE_CLASSES = MIDDLEWARE @@ -91,11 +87,6 @@ ROOT_URLCONF = 'main.urls' # WSGI_APPLICATION = 'demo.wsgi.application' -SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), - "REFRESH_TOKEN_LIFETIME": timedelta(days=365), -} - # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases @@ -114,6 +105,7 @@ DATABASES = { } + # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ @@ -127,6 +119,8 @@ USE_L10N = True USE_TZ = True + + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ @@ -139,7 +133,15 @@ MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' STATICFILES_DIRS = [BASE_DIR / 'static'] -# TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')] +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + } +} + TEMPLATES = [ { @@ -157,32 +159,34 @@ TEMPLATES = [ }, ] -REST_AUTH = { - 'SESSION_LOGIN': True, - 'USE_JWT': True, - 'JWT_AUTH_COOKIE': 'auth', - 'JWT_AUTH_HTTPONLY': False, - 'REGISTER_SERIALIZER': 'users.serializers.RegisterSerializer', - 'USER_DETAILS_SERIALIZER': 'users.serializers.CustomUserDetailsSerializer', - 'PASSWORD_RESET_SERIALIZER': 'users.serializers.MyPasswordResetSerializer' -} +# Authentication settings DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True' DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.') -STORAGES = { - "staticfiles": { - "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", - }, - "default": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - } -} +ALLAUTH_UI_THEME = "dark" +SILENCED_SYSTEM_CHECKS = ["slippers.E001"] AUTH_USER_MODEL = 'users.CustomUser' +ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter' + +ACCOUNT_SIGNUP_FORM_CLASS = 'users.form_overrides.CustomSignupForm' + +SESSION_SAVE_EVERY_REQUEST = True + FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000') +HEADLESS_FRONTEND_URLS = { + "account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}", + "account_reset_password": f"{FRONTEND_URL}/user/reset-password", + "account_reset_password_from_key": f"{FRONTEND_URL}/user/reset-password/{{key}}", + "account_signup": f"{FRONTEND_URL}/signup", + # Fallback in case the state containing the `next` URL is lost and the handshake + # with the third-party provider fails. + "socialaccount_login_error": f"{FRONTEND_URL}/account/provider/callback", +} + EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' SITE_ID = 1 ACCOUNT_EMAIL_REQUIRED = True @@ -214,13 +218,8 @@ else: REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.TokenAuthentication', - 'dj_rest_auth.jwt_auth.JWTCookieAuthentication' ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', - # 'DEFAULT_PERMISSION_CLASSES': [ - # 'rest_framework.permissions.IsAuthenticated', - # ], } SWAGGER_SETTINGS = { @@ -228,12 +227,11 @@ SWAGGER_SETTINGS = { 'LOGOUT_URL': 'logout', } -# For demo purposes only. Use a white list in the real world. -CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()] -from os import getenv CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()] + DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' LOGGING = { @@ -260,6 +258,5 @@ LOGGING = { }, }, } - # https://github.com/dr5hn/countries-states-cities-database/tags COUNTRY_REGION_JSON_VERSION = 'v2.4' \ No newline at end of file diff --git a/backend/server/main/urls.py b/backend/server/main/urls.py index fe098bd..3e3c53f 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -3,8 +3,7 @@ from django.contrib import admin from django.views.generic import RedirectView, TemplateView from django.conf import settings from django.conf.urls.static import static -from adventures import urls as adventures -from users.views import ChangeEmailView, IsRegistrationDisabled, PublicUserListView, PublicUserDetailView +from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView from .views import get_csrf_token from drf_yasg.views import get_schema_view @@ -19,56 +18,27 @@ schema_view = get_schema_view( urlpatterns = [ path('api/', include('adventures.urls')), path('api/', include('worldtravel.urls')), + path("_allauth/", include("allauth.headless.urls")), - path('auth/change-email/', ChangeEmailView.as_view(), name='change_email'), path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'), path('auth/users/', PublicUserListView.as_view(), name='public-user-list'), path('auth/user//', PublicUserDetailView.as_view(), name='public-user-detail'), + path('auth/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'), + + path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'), path('csrf/', get_csrf_token, name='get_csrf_token'), - re_path(r'^$', TemplateView.as_view( - template_name="home.html"), name='home'), - re_path(r'^signup/$', TemplateView.as_view(template_name="signup.html"), - name='signup'), - re_path(r'^email-verification/$', - TemplateView.as_view(template_name="email_verification.html"), - name='email-verification'), - re_path(r'^login/$', TemplateView.as_view(template_name="login.html"), - name='login'), - re_path(r'^logout/$', TemplateView.as_view(template_name="logout.html"), - name='logout'), - re_path(r'^password-reset/$', - TemplateView.as_view(template_name="password_reset.html"), - name='password-reset'), - re_path(r'^password-reset/confirm/$', - TemplateView.as_view(template_name="password_reset_confirm.html"), - name='password-reset-confirm'), - - re_path(r'^user-details/$', - TemplateView.as_view(template_name="user_details.html"), - name='user-details'), - re_path(r'^password-change/$', - TemplateView.as_view(template_name="password_change.html"), - name='password-change'), - re_path(r'^resend-email-verification/$', - TemplateView.as_view( - template_name="resend_email_verification.html"), - name='resend-email-verification'), - - - # this url is used to generate email content - re_path(r'^password-reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$', - TemplateView.as_view(template_name="password_reset_confirm.html"), - name='password_reset_confirm'), - - re_path(r'^auth/', include('dj_rest_auth.urls')), - re_path(r'^auth/registration/', - include('dj_rest_auth.registration.urls')), - re_path(r'^account/', include('allauth.urls')), + + path('', TemplateView.as_view(template_name='home.html')), + re_path(r'^admin/', admin.site.urls), re_path(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'), re_path(r'^docs/$', schema_view.with_ui('swagger', cache_timeout=0), name='api_docs'), # path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'), + path("accounts/", include("allauth.urls")), + + # Include the API endpoints: + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index 0458487..bae189f 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -1,7 +1,5 @@ Django==5.0.8 -dj-rest-auth @ git+https://github.com/iMerica/dj-rest-auth.git@master djangorestframework>=3.15.2 -djangorestframework-simplejwt==5.3.1 django-allauth==0.63.3 drf-yasg==1.21.4 django-cors-headers==4.4.0 @@ -13,4 +11,10 @@ whitenoise django-resized django-geojson setuptools -gunicorn==23.0.0 \ No newline at end of file +gunicorn==23.0.0 +qrcode==8.0 +# slippers==0.6.2 +# django-allauth-ui==1.5.1 +# django-widget-tweaks==1.5.0 +django-ical==1.9.2 +icalendar==6.1.0 \ No newline at end of file diff --git a/backend/server/templates/account/email/password_reset_key_message.txt b/backend/server/templates/account/email/password_reset_key_message.txt deleted file mode 100644 index 42473bf..0000000 --- a/backend/server/templates/account/email/password_reset_key_message.txt +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "account/email/base_message.txt" %} -{% load i18n %} - -{% block content %}{% autoescape off %}{% blocktrans %}You're receiving this email because you or someone else has requested a password reset for your user account. - -It can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %} - -{{ frontend_url }}/settings/forgot-password/confirm?token={{ temp_key }}&uid={{ user_pk }} - -{% if username %} - - -{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}{% endif %}{% endautoescape %}{% endblock content %} \ No newline at end of file diff --git a/backend/server/templates/base.html b/backend/server/templates/base.html index f8a7bd5..be712b7 100644 --- a/backend/server/templates/base.html +++ b/backend/server/templates/base.html @@ -4,8 +4,8 @@ - - + + AdventureLog API Server @@ -31,39 +31,6 @@ diff --git a/frontend/src/lib/components/AdventureLink.svelte b/frontend/src/lib/components/AdventureLink.svelte index db02e4e..9d7d094 100644 --- a/frontend/src/lib/components/AdventureLink.svelte +++ b/frontend/src/lib/components/AdventureLink.svelte @@ -1,5 +1,4 @@
@@ -59,23 +71,36 @@
-
- - - +
+
+ + + + +
+ + {#if isEmojiPickerVisible} +
+ +
+ {/if}
+
{#each categories diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index ce0bb86..fbe8337 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -5,14 +5,10 @@ import type { SubmitFunction } from '@sveltejs/kit'; import DotsHorizontal from '~icons/mdi/dots-horizontal'; - import WeatherSunny from '~icons/mdi/weather-sunny'; - import WeatherNight from '~icons/mdi/weather-night'; - import Forest from '~icons/mdi/forest'; - import Water from '~icons/mdi/water'; + import Calendar from '~icons/mdi/calendar'; import AboutModal from './AboutModal.svelte'; import AccountMultiple from '~icons/mdi/account-multiple'; import Avatar from './Avatar.svelte'; - import PaletteOutline from '~icons/mdi/palette-outline'; import { page } from '$app/stores'; import { t, locale, locales } from 'svelte-i18n'; import { themes } from '$lib'; @@ -91,6 +87,9 @@
  • +
  • + +
  • @@ -157,6 +156,9 @@
  • +
  • + +
  • - + diff --git a/frontend/src/lib/components/TOTPModal.svelte b/frontend/src/lib/components/TOTPModal.svelte new file mode 100644 index 0000000..77da717 --- /dev/null +++ b/frontend/src/lib/components/TOTPModal.svelte @@ -0,0 +1,184 @@ + + + + + + + diff --git a/frontend/src/lib/index.server.ts b/frontend/src/lib/index.server.ts index 3ff2206..12f9885 100644 --- a/frontend/src/lib/index.server.ts +++ b/frontend/src/lib/index.server.ts @@ -10,27 +10,3 @@ export const fetchCSRFToken = async () => { return null; } }; - -export const tryRefreshToken = async (refreshToken: string) => { - const csrfToken = await fetchCSRFToken(); - const refreshFetch = await fetch(`${serverEndpoint}/auth/token/refresh/`, { - method: 'POST', - headers: { - 'X-CSRFToken': csrfToken, - 'Content-Type': 'application/json' // Corrected header name - }, - body: JSON.stringify({ refresh: refreshToken }) - }); - - if (refreshFetch.ok) { - const refresh = await refreshFetch.json(); - const token = `auth=${refresh.access}`; - return token; - // event.cookies.set('auth', `auth=${refresh.access}`, { - // httpOnly: true, - // sameSite: 'lax', - // expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - // path: '/' - // }); - } -}; diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts index 0426345..1d1a3bd 100644 --- a/frontend/src/lib/index.ts +++ b/frontend/src/lib/index.ts @@ -313,5 +313,6 @@ export let themes = [ { name: 'forest', label: 'Forest' }, { name: 'aqua', label: 'Aqua' }, { name: 'aestheticLight', label: 'Aesthetic Light' }, - { name: 'aestheticDark', label: 'Aesthetic Dark' } + { name: 'aestheticDark', label: 'Aesthetic Dark' }, + { name: 'northernLights', label: 'Northern Lights' } ]; diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 203722a..25e2b41 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -188,7 +188,14 @@ "add_a_tag": "Fügen Sie ein Tag hinzu", "tags": "Schlagworte", "set_to_pin": "Auf „Anpinnen“ setzen", - "category_fetch_error": "Fehler beim Abrufen der Kategorien" + "category_fetch_error": "Fehler beim Abrufen der Kategorien", + "copied_to_clipboard": "In die Zwischenablage kopiert!", + "copy_failed": "Das Kopieren ist fehlgeschlagen", + "adventure_calendar": "Abenteuerkalender", + "emoji_picker": "Emoji-Picker", + "hide": "Verstecken", + "show": "Zeigen", + "download_calendar": "Kalender herunterladen" }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", @@ -226,14 +233,16 @@ "light": "Licht", "night": "Nacht", "aestheticDark": "Ästhetisches Dunkel", - "aestheticLight": "Ästhetisches Licht" + "aestheticLight": "Ästhetisches Licht", + "northernLights": "Nordlicht" }, "users": "Benutzer", "worldtravel": "Weltreisen", "my_tags": "Meine Tags", "tag": "Etikett", "language_selection": "Sprache", - "support": "Unterstützung" + "support": "Unterstützung", + "calendar": "Kalender" }, "auth": { "confirm_password": "Passwort bestätigen", @@ -249,7 +258,11 @@ "username": "Benutzername", "profile_picture": "Profilbild", "public_profile": "Öffentliches Profil", - "public_tooltip": "Mit einem öffentlichen Profil können Benutzer Sammlungen mit Ihnen teilen und Ihr Profil auf der Benutzerseite anzeigen." + "public_tooltip": "Mit einem öffentlichen Profil können Benutzer Sammlungen mit Ihnen teilen und Ihr Profil auf der Benutzerseite anzeigen.", + "email_required": "E-Mail ist erforderlich", + "both_passwords_required": "Beide Passwörter sind erforderlich", + "new_password": "Neues Passwort", + "reset_failed": "Passwort konnte nicht zurückgesetzt werden" }, "users": { "no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden." @@ -291,7 +304,50 @@ "about_this_background": "Über diesen Hintergrund", "join_discord": "Treten Sie dem Discord bei", "join_discord_desc": "um Ihre eigenen Fotos zu teilen. \nVeröffentlichen Sie sie im", - "photo_by": "Foto von" + "photo_by": "Foto von", + "change_password_error": "Passwort kann nicht geändert werden. \nUngültiges aktuelles Passwort oder ungültiges neues Passwort.", + "current_password": "Aktuelles Passwort", + "password_change_lopout_warning": "Nach der Passwortänderung werden Sie abgemeldet.", + "authenticator_code": "Authentifikatorcode", + "copy": "Kopie", + "disable_mfa": "Deaktivieren Sie MFA", + "email_added": "E-Mail erfolgreich hinzugefügt!", + "email_added_error": "Fehler beim Hinzufügen der E-Mail", + "email_removed": "E-Mail erfolgreich entfernt!", + "email_removed_error": "Fehler beim Entfernen der E-Mail", + "email_set_primary": "E-Mail erfolgreich als primäre E-Mail-Adresse festgelegt!", + "email_set_primary_error": "Fehler beim Festlegen der E-Mail-Adresse als primär", + "email_verified": "E-Mail erfolgreich bestätigt!", + "email_verified_erorr_desc": "Ihre E-Mail-Adresse konnte nicht bestätigt werden. \nBitte versuchen Sie es erneut.", + "email_verified_error": "Fehler bei der E-Mail-Bestätigung", + "email_verified_success": "Ihre E-Mail-Adresse wurde bestätigt. \nSie können sich jetzt anmelden.", + "enable_mfa": "Aktivieren Sie MFA", + "error_change_password": "Fehler beim Ändern des Passworts. \nBitte überprüfen Sie Ihr aktuelles Passwort und versuchen Sie es erneut.", + "generic_error": "Bei der Bearbeitung Ihrer Anfrage ist ein Fehler aufgetreten.", + "invalid_code": "Ungültiger MFA-Code", + "invalid_credentials": "Ungültiger Benutzername oder Passwort", + "make_primary": "Machen Sie primär", + "mfa_disabled": "Multi-Faktor-Authentifizierung erfolgreich deaktiviert!", + "mfa_enabled": "Multi-Faktor-Authentifizierung erfolgreich aktiviert!", + "mfa_not_enabled": "MFA ist nicht aktiviert", + "mfa_page_title": "Multi-Faktor-Authentifizierung", + "mfa_required": "Eine Multi-Faktor-Authentifizierung ist erforderlich", + "no_emai_set": "Keine E-Mail-Adresse festgelegt", + "not_verified": "Nicht verifiziert", + "primary": "Primär", + "recovery_codes": "Wiederherstellungscodes", + "recovery_codes_desc": "Dies sind Ihre Wiederherstellungscodes. \nBewahren Sie sie sicher auf. \nSie werden sie nicht mehr sehen können.", + "reset_session_error": "Bitte melden Sie sich ab und wieder an, um Ihre Sitzung zu aktualisieren, und versuchen Sie es erneut.", + "verified": "Verifiziert", + "verify": "Verifizieren", + "verify_email_error": "Fehler bei der E-Mail-Bestätigung. \nVersuchen Sie es in ein paar Minuten noch einmal.", + "verify_email_success": "E-Mail-Bestätigung erfolgreich gesendet!", + "add_email_blocked": "Sie können keine E-Mail-Adresse zu einem Konto hinzufügen, das durch die Zwei-Faktor-Authentifizierung geschützt ist.", + "required": "Dieses Feld ist erforderlich", + "csrf_failed": "CSRF-Token konnte nicht abgerufen werden", + "duplicate_email": "Diese E-Mail-Adresse wird bereits verwendet.", + "email_taken": "Diese E-Mail-Adresse wird bereits verwendet.", + "username_taken": "Dieser Benutzername wird bereits verwendet." }, "checklist": { "add_item": "Artikel hinzufügen", @@ -411,5 +467,14 @@ "no_categories_found": "Keine Kategorien gefunden.", "select_category": "Kategorie auswählen", "update_after_refresh": "Die Abenteuerkarten werden aktualisiert, sobald Sie die Seite aktualisieren." + }, + "dashboard": { + "add_some": "Warum beginnen Sie nicht mit der Planung Ihres nächsten Abenteuers? \nSie können ein neues Abenteuer hinzufügen, indem Sie auf die Schaltfläche unten klicken.", + "countries_visited": "Besuchte Länder", + "no_recent_adventures": "Keine aktuellen Abenteuer?", + "recent_adventures": "Aktuelle Abenteuer", + "total_adventures": "Totale Abenteuer", + "total_visited_regions": "Insgesamt besuchte Regionen", + "welcome_back": "Willkommen zurück" } } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 9c8e719..cd9b824 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -1,415 +1,480 @@ { - "navbar": { - "adventures": "Adventures", - "collections": "Collections", - "worldtravel": "World Travel", - "map": "Map", - "users": "Users", - "search": "Search", - "profile": "Profile", - "greeting": "Hi", - "my_adventures": "My Adventures", - "my_tags": "My Tags", - "tag": "Tag", - "shared_with_me": "Shared With Me", - "settings": "Settings", - "logout": "Logout", - "about": "About AdventureLog", - "documentation": "Documentation", - "discord": "Discord", - "language_selection": "Language", - "support": "Support", - "theme_selection": "Theme Selection", - "themes": { - "light": "Light", - "dark": "Dark", - "night": "Night", - "forest": "Forest", - "aestheticLight": "Aesthetic Light", - "aestheticDark": "Aesthetic Dark", - "aqua": "Aqua" - } - }, - "about": { - "about": "About", - "license": "Licensed under the GPL-3.0 License.", - "source_code": "Source Code", - "message": "Made with ❤️ in the United States.", - "oss_attributions": "Open Source Attributions", - "nominatim_1": "Location Search and Geocoding is provided by", - "nominatim_2": "Their data is liscensed under the ODbL license.", - "other_attributions": "Additional attributions can be found in the README file.", - "close": "Close" - }, - "home": { - "hero_1": "Discover the World's Most Thrilling Adventures", - "hero_2": "Discover and plan your next adventure with AdventureLog. Explore breathtaking destinations, create custom itineraries, and stay connected on the go.", - "go_to": "Go To AdventureLog", - "key_features": "Key Features", - "desc_1": "Discover, Plan, and Explore with Ease", - "desc_2": "AdventureLog is designed to simplify your journey, providing you with the tools and resources to plan, pack, and navigate your next unforgettable adventure.", - "feature_1": "Travel Log", - "feature_1_desc": "Keep track of your adventures with a personalized travel log and share your experiences with friends and family.", - "feature_2": "Trip Planning", - "feature_2_desc": "Easily create custom itineraries and get a day-by-day breakdown of your trip.", - "feature_3": "Travel Map", - "feature_3_desc": "View your travels throughout the world with an interactive map and explore new destinations." - }, - "adventures": { - "collection_remove_success": "Adventure removed from collection successfully!", - "collection_remove_error": "Error removing adventure from collection", - "collection_link_success": "Adventure linked to collection successfully!", - "no_image_found": "No image found", - "collection_link_error": "Error linking adventure to collection", - "adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.", - "open_details": "Open Details", - "edit_adventure": "Edit Adventure", - "remove_from_collection": "Remove from Collection", - "add_to_collection": "Add to Collection", - "delete": "Delete", - "not_found": "Adventure not found", - "not_found_desc": "The adventure you were looking for could not be found. Please try a different adventure or check back later.", - "homepage": "Homepage", - "adventure_details": "Adventure Details", - "collection": "Collection", - "adventure_type": "Adventure Type", - "longitude": "Longitude", - "latitude": "Latitude", - "visit": "Visit", - "visits": "Visits", - "create_new": "Create New...", - "adventure": "Adventure", - "count_txt": "results matching your search", - "sort": "Sort", - "order_by": "Order By", - "order_direction": "Order Direction", - "ascending": "Ascending", - "descending": "Descending", - "updated": "Updated", - "name": "Name", - "date": "Date", - "activity_types": "Activity Types", - "tags": "Tags", - "add_a_tag": "Add a tag", - "date_constrain": "Constrain to collection dates", - "rating": "Rating", - "my_images": "My Images", - "add_an_activity": "Add an activity", - "no_images": "No Images", - "upload_images_here": "Upload images here", - "share_adventure": "Share this Adventure!", - "copy_link": "Copy Link", - "image": "Image", - "upload_image": "Upload Image", - "url": "URL", - "fetch_image": "Fetch Image", - "wikipedia": "Wikipedia", - "add_notes": "Add notes", - "warning": "Warning", - "my_adventures": "My Adventures", - "no_linkable_adventures": "No adventures found that can be linked to this collection.", - "add": "Add", - "save_next": "Save & Next", - "end_date": "End Date", - "my_visits": "My Visits", - "start_date": "Start Date", - "remove": "Remove", - "location": "Location", - "search_for_location": "Search for a location", - "clear_map": "Clear map", - "search_results": "Searh results", - "no_results": "No results found", - "wiki_desc": "Pulls excerpt from Wikipedia article matching the name of the adventure.", - "generate_desc": "Generate Description", - "public_adventure": "Public Adventure", - "location_information": "Location Information", - "link": "Link", - "links": "Links", - "description": "Description", - "sources": "Sources", - "collection_adventures": "Include Collection Adventures", - "filter": "Filter", - "category_filter": "Category Filter", - "category": "Category", - "select_adventure_category": "Select Adventure Category", - "clear": "Clear", - "my_collections": "My Collections", - "open_filters": "Open Filters", - "close_filters": "Close Filters", - "archived_collections": "Archived Collections", - "share": "Share", - "private": "Private", - "public": "Public", - "archived": "Archived", - "edit_collection": "Edit Collection", - "unarchive": "Unarchive", - "archive": "Archive", - "no_collections_found": "No collections found to add this adventure to.", - "not_visited": "Not Visited", - "archived_collection_message": "Collection archived successfully!", - "unarchived_collection_message": "Collection unarchived successfully!", - "delete_collection_success": "Collection deleted successfully!", - "delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.", - "cancel": "Cancel", - "delete_collection": "Delete Collection", - "delete_adventure": "Delete Adventure", - "adventure_delete_success": "Adventure deleted successfully!", - "visited": "Visited", - "planned": "Planned", - "duration": "Duration", - "all": "All", - "image_removed_success": "Image removed successfully!", - "image_removed_error": "Error removing image", - "no_image_url": "No image found at that URL.", - "image_upload_success": "Image uploaded successfully!", - "image_upload_error": "Error uploading image", - "dates": "Dates", - "wiki_image_error": "Error fetching image from Wikipedia", - "start_before_end_error": "Start date must be before end date", - "activity": "Activity", - "actions": "Actions", - "no_end_date": "Please enter an end date", - "see_adventures": "See Adventures", - "image_fetch_failed": "Failed to fetch image", - "no_location": "Please enter a location", - "no_start_date": "Please enter a start date", - "no_description_found": "No description found", - "adventure_created": "Adventure created", - "adventure_create_error": "Failed to create adventure", - "adventure_updated": "Adventure updated", - "adventure_update_error": "Failed to update adventure", - "set_to_pin": "Set to Pin", - "category_fetch_error": "Error fetching categories", - "new_adventure": "New Adventure", - "basic_information": "Basic Information", - "adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!", - "no_adventures_found": "No adventures found", - "mark_region_as_visited": "Mark region {region}, {country} as visited?", - "mark_visited": "Mark Visited", - "error_updating_regions": "Error updating regions", - "regions_updated": "regions updated", - "visited_region_check": "Visited Region Check", - "visited_region_check_desc": "By selecting this, the server will check all of your visited adventures and mark the regions they are located in as visited in world travel.", - "update_visited_regions": "Update Visited Regions", - "update_visited_regions_disclaimer": "This may take a while depending on the number of adventures you have visited.", - "link_new": "Link New...", - "add_new": "Add New...", - "transportation": "Transportation", - "note": "Note", - "checklist": "Checklist", - "collection_archived": "This collection has been archived.", - "visit_link": "Visit Link", - "collection_completed": "You've completed this collection!", - "collection_stats": "Collection Stats", - "keep_exploring": "Keep Exploring!", - "linked_adventures": "Linked Adventures", - "notes": "Notes", - "checklists": "Checklists", - "transportations": "Transportations", - "day": "Day", - "itineary_by_date": "Itinerary by Date", - "nothing_planned": "Nothing planned for this day. Enjoy the journey!", - "days": "days", - "activities": { - "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 🧘‍♀️", - "volunteer_work": "Volunteer Work 🤝", - "other": "Other" - } - }, - "worldtravel": { - "country_list": "Country List", - "num_countries": "countries found", - "all": "All", - "partially_visited": "Partially Visited", - "not_visited": "Not Visited", - "completely_visited": "Completely Visited", - "all_subregions": "All Subregions", - "clear_search": "Clear Search", - "no_countries_found": "No countries found" - }, - "auth": { - "username": "Username", - "password": "Password", - "forgot_password": "Forgot Password?", - "signup": "Signup", - "login_error": "Unable to login with the provided credentials.", - "login": "Login", - "email": "Email", - "first_name": "First Name", - "last_name": "Last Name", - "confirm_password": "Confirm Password", - "registration_disabled": "Registration is currently disabled.", - "profile_picture": "Profile Picture", - "public_profile": "Public Profile", - "public_tooltip": "With a public profile, users can share collections with you and view your profile on the users page." - }, - "users": { - "no_users_found": "No users found with public profiles." - }, - "settings": { - "update_error": "Error updating settings", - "update_success": "Settings updated successfully!", - "settings_page": "Settings Page", - "account_settings": "User Account Settings", - "update": "Update", - "password_change": "Change Password", - "new_password": "New Password", - "confirm_new_password": "Confirm New Password", - "email_change": "Change Email", - "current_email": "Current Email", - "no_email_set": "No email set", - "new_email": "New Email", - "change_password": "Change Password", - "login_redir": "You will then be redirected to the login page.", - "token_required": "Token and UID are required for password reset.", - "reset_password": "Reset Password", - "possible_reset": "If the email address you provided is associated with an account, you will receive an email with instructions to reset your password!", - "missing_email": "Please enter an email address", - "submit": "Submit", - "password_does_not_match": "Passwords do not match", - "password_is_required": "Password is required", - "invalid_token": "Token is invalid or has expired", - "about_this_background": "About this background", - "photo_by": "Photo by", - "join_discord": "Join the Discord", - "join_discord_desc": "to share your own photos. Post them in the #travel-share channel." - }, - "collection": { - "collection_created": "Collection created successfully!", - "error_creating_collection": "Error creating collection", - "new_collection": "New Collection", - "create": "Create", - "collection_edit_success": "Collection edited successfully!", - "error_editing_collection": "Error editing collection", - "edit_collection": "Edit Collection" - }, - "notes": { - "note_deleted": "Note deleted successfully!", - "note_delete_error": "Error deleting note", - "open": "Open", - "failed_to_save": "Failed to save note", - "note_editor": "Note Editor", - "editing_note": "Editing note", - "content": "Content", - "save": "Save", - "note_public": "This note is public because it is in a public collection.", - "add_a_link": "Add a link", - "invalid_url": "Invalid URL" - }, - "checklist": { - "checklist_deleted": "Checklist deleted successfully!", - "checklist_delete_error": "Error deleting checklist", - "failed_to_save": "Failed to save checklist", - "checklist_editor": "Checklist Editor", - "editing_checklist": "Editing checklist", - "item": "Item", - "items": "Items", - "add_item": "Add Item", - "new_item": "New Item", - "save": "Save", - "checklist_public": "This checklist is public because it is in a public collection.", - "item_cannot_be_empty": "Item cannot be empty", - "item_already_exists": "Item already exists" - }, - "transportation": { - "transportation_deleted": "Transportation deleted successfully!", - "transportation_delete_error": "Error deleting transportation", - "provide_start_date": "Please provide a start date", - "transport_type": "Transport Type", - "type": "Type", - "transportation_added": "Transportation added successfully!", - "error_editing_transportation": "Error editing transportation", - "new_transportation": "New Transportation", - "date_time": "Start Date & Time", - "end_date_time": "End Date & Time", - "flight_number": "Flight Number", - "from_location": "From Location", - "to_location": "To Location", - "edit": "Edit", - "modes": { - "car": "Car", - "plane": "Plane", - "train": "Train", - "bus": "Bus", - "boat": "Boat", - "bike": "Bike", - "walking": "Walking", - "other": "Other" - }, - "transportation_edit_success": "Transportation edited successfully!", - "edit_transportation": "Edit Transportation", - "start": "Start", - "date_and_time": "Date & Time" - }, - "search": { - "adventurelog_results": "AdventureLog Results", - "public_adventures": "Public Adventures", - "online_results": "Online Results" - }, - "map": { - "view_details": "View Details", - "adventure_map": "Adventure Map", - "map_options": "Map Options", - "show_visited_regions": "Show Visited Regions", - "add_adventure_at_marker": "Add New Adventure at Marker", - "clear_marker": "Clear Marker", - "add_adventure": "Add New Adventure" - }, - "share": { - "shared": "Shared", - "with": "with", - "unshared": "Unshared", - "share_desc": "Share this collection with other users.", - "shared_with": "Shared With", - "no_users_shared": "No users shared with", - "not_shared_with": "Not Shared With", - "no_shared_found": "No collections found that are shared with you.", - "set_public": "In order to allow users to share with you, you need your profile set to public.", - "go_to_settings": "Go to settings" - }, - "languages": { - "en": "English", - "de": "German", - "es": "Spanish", - "fr": "French", - "it": "Italian", - "nl": "Dutch", - "sv": "Swedish", - "zh": "Chinese", - "pl": "Polish" - }, - "profile": { - "member_since": "Member since", - "user_stats": "User Stats", - "visited_countries": "Visited Countries", - "visited_regions": "Visited Regions" - }, - "categories": { - "manage_categories": "Manage Categories", - "no_categories_found": "No categories found.", - "edit_category": "Edit Category", - "icon": "Icon", - "update_after_refresh": "The adventure cards will be updated once you refresh the page.", - "select_category": "Select Category", - "category_name": "Category Name" - } + "navbar": { + "adventures": "Adventures", + "collections": "Collections", + "worldtravel": "World Travel", + "map": "Map", + "users": "Users", + "search": "Search", + "profile": "Profile", + "greeting": "Hi", + "my_adventures": "My Adventures", + "my_tags": "My Tags", + "tag": "Tag", + "shared_with_me": "Shared With Me", + "settings": "Settings", + "logout": "Logout", + "about": "About AdventureLog", + "documentation": "Documentation", + "discord": "Discord", + "language_selection": "Language", + "support": "Support", + "calendar": "Calendar", + "theme_selection": "Theme Selection", + "themes": { + "light": "Light", + "dark": "Dark", + "night": "Night", + "forest": "Forest", + "aestheticLight": "Aesthetic Light", + "aestheticDark": "Aesthetic Dark", + "aqua": "Aqua", + "northernLights": "Northern Lights" + } + }, + "about": { + "about": "About", + "license": "Licensed under the GPL-3.0 License.", + "source_code": "Source Code", + "message": "Made with ❤️ in the United States.", + "oss_attributions": "Open Source Attributions", + "nominatim_1": "Location Search and Geocoding is provided by", + "nominatim_2": "Their data is liscensed under the ODbL license.", + "other_attributions": "Additional attributions can be found in the README file.", + "close": "Close" + }, + "home": { + "hero_1": "Discover the World's Most Thrilling Adventures", + "hero_2": "Discover and plan your next adventure with AdventureLog. Explore breathtaking destinations, create custom itineraries, and stay connected on the go.", + "go_to": "Go To AdventureLog", + "key_features": "Key Features", + "desc_1": "Discover, Plan, and Explore with Ease", + "desc_2": "AdventureLog is designed to simplify your journey, providing you with the tools and resources to plan, pack, and navigate your next unforgettable adventure.", + "feature_1": "Travel Log", + "feature_1_desc": "Keep track of your adventures with a personalized travel log and share your experiences with friends and family.", + "feature_2": "Trip Planning", + "feature_2_desc": "Easily create custom itineraries and get a day-by-day breakdown of your trip.", + "feature_3": "Travel Map", + "feature_3_desc": "View your travels throughout the world with an interactive map and explore new destinations." + }, + "adventures": { + "collection_remove_success": "Adventure removed from collection successfully!", + "collection_remove_error": "Error removing adventure from collection", + "collection_link_success": "Adventure linked to collection successfully!", + "no_image_found": "No image found", + "collection_link_error": "Error linking adventure to collection", + "adventure_delete_confirm": "Are you sure you want to delete this adventure? This action cannot be undone.", + "open_details": "Open Details", + "edit_adventure": "Edit Adventure", + "remove_from_collection": "Remove from Collection", + "add_to_collection": "Add to Collection", + "delete": "Delete", + "not_found": "Adventure not found", + "not_found_desc": "The adventure you were looking for could not be found. Please try a different adventure or check back later.", + "homepage": "Homepage", + "adventure_details": "Adventure Details", + "collection": "Collection", + "adventure_type": "Adventure Type", + "longitude": "Longitude", + "latitude": "Latitude", + "visit": "Visit", + "visits": "Visits", + "create_new": "Create New...", + "adventure": "Adventure", + "count_txt": "results matching your search", + "sort": "Sort", + "order_by": "Order By", + "order_direction": "Order Direction", + "ascending": "Ascending", + "descending": "Descending", + "updated": "Updated", + "name": "Name", + "date": "Date", + "activity_types": "Activity Types", + "tags": "Tags", + "add_a_tag": "Add a tag", + "date_constrain": "Constrain to collection dates", + "rating": "Rating", + "my_images": "My Images", + "add_an_activity": "Add an activity", + "no_images": "No Images", + "upload_images_here": "Upload images here", + "share_adventure": "Share this Adventure!", + "copy_link": "Copy Link", + "image": "Image", + "upload_image": "Upload Image", + "url": "URL", + "fetch_image": "Fetch Image", + "wikipedia": "Wikipedia", + "add_notes": "Add notes", + "warning": "Warning", + "my_adventures": "My Adventures", + "no_linkable_adventures": "No adventures found that can be linked to this collection.", + "add": "Add", + "save_next": "Save & Next", + "end_date": "End Date", + "my_visits": "My Visits", + "start_date": "Start Date", + "remove": "Remove", + "location": "Location", + "search_for_location": "Search for a location", + "clear_map": "Clear map", + "search_results": "Searh results", + "no_results": "No results found", + "wiki_desc": "Pulls excerpt from Wikipedia article matching the name of the adventure.", + "generate_desc": "Generate Description", + "public_adventure": "Public Adventure", + "location_information": "Location Information", + "link": "Link", + "links": "Links", + "description": "Description", + "sources": "Sources", + "collection_adventures": "Include Collection Adventures", + "filter": "Filter", + "category_filter": "Category Filter", + "category": "Category", + "select_adventure_category": "Select Adventure Category", + "clear": "Clear", + "my_collections": "My Collections", + "open_filters": "Open Filters", + "close_filters": "Close Filters", + "archived_collections": "Archived Collections", + "share": "Share", + "private": "Private", + "public": "Public", + "archived": "Archived", + "edit_collection": "Edit Collection", + "unarchive": "Unarchive", + "archive": "Archive", + "no_collections_found": "No collections found to add this adventure to.", + "not_visited": "Not Visited", + "archived_collection_message": "Collection archived successfully!", + "unarchived_collection_message": "Collection unarchived successfully!", + "delete_collection_success": "Collection deleted successfully!", + "delete_collection_warning": "Are you sure you want to delete this collection? This will also delete all of the linked adventures. This action cannot be undone.", + "cancel": "Cancel", + "delete_collection": "Delete Collection", + "delete_adventure": "Delete Adventure", + "adventure_delete_success": "Adventure deleted successfully!", + "visited": "Visited", + "planned": "Planned", + "duration": "Duration", + "all": "All", + "image_removed_success": "Image removed successfully!", + "image_removed_error": "Error removing image", + "no_image_url": "No image found at that URL.", + "image_upload_success": "Image uploaded successfully!", + "image_upload_error": "Error uploading image", + "dates": "Dates", + "wiki_image_error": "Error fetching image from Wikipedia", + "start_before_end_error": "Start date must be before end date", + "activity": "Activity", + "actions": "Actions", + "no_end_date": "Please enter an end date", + "see_adventures": "See Adventures", + "image_fetch_failed": "Failed to fetch image", + "no_location": "Please enter a location", + "no_start_date": "Please enter a start date", + "no_description_found": "No description found", + "adventure_created": "Adventure created", + "adventure_create_error": "Failed to create adventure", + "adventure_updated": "Adventure updated", + "adventure_update_error": "Failed to update adventure", + "set_to_pin": "Set to Pin", + "category_fetch_error": "Error fetching categories", + "new_adventure": "New Adventure", + "basic_information": "Basic Information", + "adventure_not_found": "There are no adventures to display. Add some using the plus button at the bottom right or try changing filters!", + "no_adventures_found": "No adventures found", + "mark_region_as_visited": "Mark region {region}, {country} as visited?", + "mark_visited": "Mark Visited", + "error_updating_regions": "Error updating regions", + "regions_updated": "regions updated", + "visited_region_check": "Visited Region Check", + "visited_region_check_desc": "By selecting this, the server will check all of your visited adventures and mark the regions they are located in as visited in world travel.", + "update_visited_regions": "Update Visited Regions", + "update_visited_regions_disclaimer": "This may take a while depending on the number of adventures you have visited.", + "link_new": "Link New...", + "add_new": "Add New...", + "transportation": "Transportation", + "note": "Note", + "checklist": "Checklist", + "collection_archived": "This collection has been archived.", + "visit_link": "Visit Link", + "collection_completed": "You've completed this collection!", + "collection_stats": "Collection Stats", + "keep_exploring": "Keep Exploring!", + "linked_adventures": "Linked Adventures", + "notes": "Notes", + "checklists": "Checklists", + "transportations": "Transportations", + "adventure_calendar": "Adventure Calendar", + "day": "Day", + "itineary_by_date": "Itinerary by Date", + "nothing_planned": "Nothing planned for this day. Enjoy the journey!", + "copied_to_clipboard": "Copied to clipboard!", + "copy_failed": "Copy failed", + "show": "Show", + "hide": "Hide", + "emoji_picker": "Emoji Picker", + "download_calendar": "Download Calendar", + "days": "days", + "activities": { + "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 🧘‍♀️", + "volunteer_work": "Volunteer Work 🤝", + "other": "Other" + } + }, + "worldtravel": { + "country_list": "Country List", + "num_countries": "countries found", + "all": "All", + "partially_visited": "Partially Visited", + "not_visited": "Not Visited", + "completely_visited": "Completely Visited", + "all_subregions": "All Subregions", + "clear_search": "Clear Search", + "no_countries_found": "No countries found" + }, + "auth": { + "username": "Username", + "password": "Password", + "forgot_password": "Forgot Password?", + "signup": "Signup", + "login_error": "Unable to login with the provided credentials.", + "login": "Login", + "email": "Email", + "first_name": "First Name", + "last_name": "Last Name", + "confirm_password": "Confirm Password", + "registration_disabled": "Registration is currently disabled.", + "profile_picture": "Profile Picture", + "public_profile": "Public Profile", + "public_tooltip": "With a public profile, users can share collections with you and view your profile on the users page.", + "email_required": "Email is required", + "new_password": "New Password", + "both_passwords_required": "Both passwords are required", + "reset_failed": "Failed to reset password" + }, + "users": { + "no_users_found": "No users found with public profiles." + }, + "settings": { + "update_error": "Error updating settings", + "update_success": "Settings updated successfully!", + "settings_page": "Settings Page", + "account_settings": "User Account Settings", + "update": "Update", + "password_change": "Change Password", + "new_password": "New Password", + "confirm_new_password": "Confirm New Password", + "email_change": "Change Email", + "current_email": "Current Email", + "no_email_set": "No email set", + "new_email": "New Email", + "change_password": "Change Password", + "login_redir": "You will then be redirected to the login page.", + "token_required": "Token and UID are required for password reset.", + "reset_password": "Reset Password", + "possible_reset": "If the email address you provided is associated with an account, you will receive an email with instructions to reset your password!", + "missing_email": "Please enter an email address", + "submit": "Submit", + "password_does_not_match": "Passwords do not match", + "password_is_required": "Password is required", + "invalid_token": "Token is invalid or has expired", + "about_this_background": "About this background", + "photo_by": "Photo by", + "join_discord": "Join the Discord", + "join_discord_desc": "to share your own photos. Post them in the #travel-share channel.", + "current_password": "Current Password", + "change_password_error": "Unable to change password. Invalid current password or invalid new password.", + "password_change_lopout_warning": "You will be logged out after changing your password.", + "generic_error": "An error occurred while processing your request.", + "email_removed": "Email removed successfully!", + "email_removed_error": "Error removing email", + "verify_email_success": "Email verification sent successfully!", + "verify_email_error": "Error verifying email. Try again in a few minutes.", + "email_added": "Email added successfully!", + "email_added_error": "Error adding email", + "email_set_primary": "Email set as primary successfully!", + "email_set_primary_error": "Error setting email as primary", + "verified": "Verified", + "primary": "Primary", + "not_verified": "Not Verified", + "make_primary": "Make Primary", + "verify": "Verify", + "no_emai_set": "No email set", + "error_change_password": "Error changing password. Please check your current password and try again.", + "mfa_disabled": "Multi-factor authentication disabled successfully!", + "mfa_page_title": "Multi-factor Authentication", + "enable_mfa": "Enable MFA", + "disable_mfa": "Disable MFA", + "mfa_not_enabled": "MFA is not enabled", + "mfa_enabled": "Multi-factor authentication enabled successfully!", + "copy": "Copy", + "recovery_codes": "Recovery Codes", + "recovery_codes_desc": "These are your recovery codes. Keep them safe. You will not be able to see them again.", + "reset_session_error": "Please logout and back in to refresh your session and try again.", + "authenticator_code": "Authenticator Code", + "email_verified": "Email verified successfully!", + "email_verified_success": "Your email has been verified. You can now log in.", + "email_verified_error": "Error verifying email", + "email_verified_erorr_desc": "Your email could not be verified. Please try again.", + "invalid_code": "Invalid MFA code", + "invalid_credentials": "Invalid username or password", + "mfa_required": "Multi-factor authentication is required", + "required": "This field is required", + "add_email_blocked": "You cannot add an email address to an account protected by two-factor authentication.", + "duplicate_email": "This email address is already in use.", + "csrf_failed": "Failed to fetch CSRF token", + "email_taken": "This email address is already in use.", + "username_taken": "This username is already in use." + }, + "collection": { + "collection_created": "Collection created successfully!", + "error_creating_collection": "Error creating collection", + "new_collection": "New Collection", + "create": "Create", + "collection_edit_success": "Collection edited successfully!", + "error_editing_collection": "Error editing collection", + "edit_collection": "Edit Collection" + }, + "notes": { + "note_deleted": "Note deleted successfully!", + "note_delete_error": "Error deleting note", + "open": "Open", + "failed_to_save": "Failed to save note", + "note_editor": "Note Editor", + "editing_note": "Editing note", + "content": "Content", + "save": "Save", + "note_public": "This note is public because it is in a public collection.", + "add_a_link": "Add a link", + "invalid_url": "Invalid URL" + }, + "checklist": { + "checklist_deleted": "Checklist deleted successfully!", + "checklist_delete_error": "Error deleting checklist", + "failed_to_save": "Failed to save checklist", + "checklist_editor": "Checklist Editor", + "editing_checklist": "Editing checklist", + "item": "Item", + "items": "Items", + "add_item": "Add Item", + "new_item": "New Item", + "save": "Save", + "checklist_public": "This checklist is public because it is in a public collection.", + "item_cannot_be_empty": "Item cannot be empty", + "item_already_exists": "Item already exists" + }, + "transportation": { + "transportation_deleted": "Transportation deleted successfully!", + "transportation_delete_error": "Error deleting transportation", + "provide_start_date": "Please provide a start date", + "transport_type": "Transport Type", + "type": "Type", + "transportation_added": "Transportation added successfully!", + "error_editing_transportation": "Error editing transportation", + "new_transportation": "New Transportation", + "date_time": "Start Date & Time", + "end_date_time": "End Date & Time", + "flight_number": "Flight Number", + "from_location": "From Location", + "to_location": "To Location", + "edit": "Edit", + "modes": { + "car": "Car", + "plane": "Plane", + "train": "Train", + "bus": "Bus", + "boat": "Boat", + "bike": "Bike", + "walking": "Walking", + "other": "Other" + }, + "transportation_edit_success": "Transportation edited successfully!", + "edit_transportation": "Edit Transportation", + "start": "Start", + "date_and_time": "Date & Time" + }, + "search": { + "adventurelog_results": "AdventureLog Results", + "public_adventures": "Public Adventures", + "online_results": "Online Results" + }, + "map": { + "view_details": "View Details", + "adventure_map": "Adventure Map", + "map_options": "Map Options", + "show_visited_regions": "Show Visited Regions", + "add_adventure_at_marker": "Add New Adventure at Marker", + "clear_marker": "Clear Marker", + "add_adventure": "Add New Adventure" + }, + "share": { + "shared": "Shared", + "with": "with", + "unshared": "Unshared", + "share_desc": "Share this collection with other users.", + "shared_with": "Shared With", + "no_users_shared": "No users shared with", + "not_shared_with": "Not Shared With", + "no_shared_found": "No collections found that are shared with you.", + "set_public": "In order to allow users to share with you, you need your profile set to public.", + "go_to_settings": "Go to settings" + }, + "languages": { + "en": "English", + "de": "German", + "es": "Spanish", + "fr": "French", + "it": "Italian", + "nl": "Dutch", + "sv": "Swedish", + "zh": "Chinese", + "pl": "Polish" + }, + "profile": { + "member_since": "Member since", + "user_stats": "User Stats", + "visited_countries": "Visited Countries", + "visited_regions": "Visited Regions" + }, + "categories": { + "manage_categories": "Manage Categories", + "no_categories_found": "No categories found.", + "edit_category": "Edit Category", + "icon": "Icon", + "update_after_refresh": "The adventure cards will be updated once you refresh the page.", + "select_category": "Select Category", + "category_name": "Category Name" + }, + "dashboard": { + "welcome_back": "Welcome back", + "countries_visited": "Countries Visited", + "total_adventures": "Total Adventures", + "total_visited_regions": "Total Visited Regions", + "recent_adventures": "Recent Adventures", + "no_recent_adventures": "No recent adventures?", + "add_some": "Why not start planning your next adventure? You can add a new adventure by clicking the button below." + } } diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 767697d..42da49e 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -23,12 +23,14 @@ "forest": "Bosque", "aqua": "Aqua", "aestheticDark": "Estética Oscura", - "aestheticLight": "Luz estetica" + "aestheticLight": "Luz estetica", + "northernLights": "Aurora boreal" }, "my_tags": "Mis etiquetas", "tag": "Etiqueta", "language_selection": "Idioma", - "support": "Apoyo" + "support": "Apoyo", + "calendar": "Calendario" }, "about": { "about": "Acerca de", @@ -233,7 +235,14 @@ "add_a_tag": "Agregar una etiqueta", "tags": "Etiquetas", "set_to_pin": "Establecer en Fijar", - "category_fetch_error": "Error al buscar categorías" + "category_fetch_error": "Error al buscar categorías", + "copied_to_clipboard": "¡Copiado al portapapeles!", + "copy_failed": "Copia fallida", + "adventure_calendar": "Calendario de aventuras", + "emoji_picker": "Selector de emojis", + "hide": "Esconder", + "show": "Espectáculo", + "download_calendar": "Descargar Calendario" }, "worldtravel": { "all": "Todo", @@ -260,7 +269,11 @@ "registration_disabled": "El registro está actualmente deshabilitado.", "profile_picture": "Foto de perfil", "public_profile": "Perfil público", - "public_tooltip": "Con un perfil público, los usuarios pueden compartir colecciones con usted y ver su perfil en la página de usuarios." + "public_tooltip": "Con un perfil público, los usuarios pueden compartir colecciones con usted y ver su perfil en la página de usuarios.", + "email_required": "Se requiere correo electrónico", + "both_passwords_required": "Se requieren ambas contraseñas", + "new_password": "Nueva contraseña", + "reset_failed": "No se pudo restablecer la contraseña" }, "users": { "no_users_found": "No se encontraron usuarios con perfiles públicos." @@ -291,7 +304,50 @@ "about_this_background": "Sobre este trasfondo", "join_discord": "Únete a la discordia", "join_discord_desc": "para compartir tus propias fotos. \nPublicarlos en el", - "photo_by": "Foto por" + "photo_by": "Foto por", + "change_password_error": "No se puede cambiar la contraseña. \nContraseña actual no válida o contraseña nueva no válida.", + "current_password": "Contraseña actual", + "password_change_lopout_warning": "Se cerrará su sesión después de cambiar su contraseña.", + "generic_error": "Se produjo un error al procesar su solicitud.", + "email_added": "¡Correo electrónico agregado exitosamente!", + "email_added_error": "Error al agregar correo electrónico", + "email_removed": "¡El correo electrónico se eliminó correctamente!", + "email_removed_error": "Error al eliminar el correo electrónico", + "email_set_primary": "¡El correo electrónico se configuró como principal correctamente!", + "email_set_primary_error": "Error al configurar el correo electrónico como principal", + "make_primary": "Hacer primario", + "no_emai_set": "No hay correo electrónico configurado", + "not_verified": "No verificado", + "primary": "Primario", + "verified": "Verificado", + "verify": "Verificar", + "verify_email_error": "Error al verificar el correo electrónico. \nInténtalo de nuevo en unos minutos.", + "verify_email_success": "¡La verificación por correo electrónico se envió correctamente!", + "error_change_password": "Error al cambiar la contraseña. \nPor favor verifique su contraseña actual e inténtelo nuevamente.", + "disable_mfa": "Deshabilitar MFA", + "enable_mfa": "Habilitar MFA", + "mfa_disabled": "¡La autenticación multifactor se deshabilitó correctamente!", + "mfa_not_enabled": "MFA no está habilitado", + "mfa_page_title": "Autenticación multifactor", + "copy": "Copiar", + "mfa_enabled": "¡La autenticación multifactor se habilitó correctamente!", + "recovery_codes": "Códigos de recuperación", + "recovery_codes_desc": "Estos son tus códigos de recuperación. \nMantenlos a salvo. \nNo podrás volver a verlos.", + "reset_session_error": "Por favor cierre sesión y vuelva a iniciarla para actualizar su sesión e inténtelo nuevamente.", + "authenticator_code": "Código de autenticación", + "email_verified": "¡Correo electrónico verificado exitosamente!", + "email_verified_erorr_desc": "Su correo electrónico no pudo ser verificado. \nPor favor inténtalo de nuevo.", + "email_verified_error": "Error al verificar el correo electrónico", + "email_verified_success": "Su correo electrónico ha sido verificado. \nAhora puedes iniciar sesión.", + "invalid_code": "Código MFA no válido", + "invalid_credentials": "Nombre de usuario o contraseña no válidos", + "mfa_required": "Se requiere autenticación multifactor", + "add_email_blocked": "No puede agregar una dirección de correo electrónico a una cuenta protegida por autenticación de dos factores.", + "required": "Este campo es obligatorio", + "csrf_failed": "No se pudo recuperar el token CSRF", + "duplicate_email": "Esta dirección de correo electrónico ya está en uso.", + "email_taken": "Esta dirección de correo electrónico ya está en uso.", + "username_taken": "Este nombre de usuario ya está en uso." }, "checklist": { "add_item": "Agregar artículo", @@ -411,5 +467,14 @@ "no_categories_found": "No se encontraron categorías.", "select_category": "Seleccionar categoría", "update_after_refresh": "Las tarjetas de aventuras se actualizarán una vez que actualices la página." + }, + "dashboard": { + "add_some": "¿Por qué no empezar a planificar tu próxima aventura? \nPuedes agregar una nueva aventura haciendo clic en el botón de abajo.", + "countries_visited": "Países visitados", + "no_recent_adventures": "¿Sin aventuras recientes?", + "recent_adventures": "Aventuras recientes", + "total_adventures": "Aventuras totales", + "total_visited_regions": "Total de regiones visitadas", + "welcome_back": "Bienvenido de nuevo" } } diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index f728ab9..9a532de 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -188,7 +188,14 @@ "add_a_tag": "Ajouter une balise", "tags": "Balises", "set_to_pin": "Définir sur Épingler", - "category_fetch_error": "Erreur lors de la récupération des catégories" + "category_fetch_error": "Erreur lors de la récupération des catégories", + "copied_to_clipboard": "Copié dans le presse-papier !", + "copy_failed": "Échec de la copie", + "adventure_calendar": "Calendrier d'aventure", + "emoji_picker": "Sélecteur d'émoticônes", + "hide": "Cacher", + "show": "Montrer", + "download_calendar": "Télécharger le calendrier" }, "home": { "desc_1": "Découvrez, planifiez et explorez en toute simplicité", @@ -226,14 +233,16 @@ "aqua": "Aqua", "dark": "Sombre", "aestheticDark": "Esthétique sombre", - "aestheticLight": "Lumière esthétique" + "aestheticLight": "Lumière esthétique", + "northernLights": "Aurores boréales" }, "users": "Utilisateurs", "worldtravel": "Voyage dans le monde", "my_tags": "Mes balises", "tag": "Étiqueter", "language_selection": "Langue", - "support": "Soutien" + "support": "Soutien", + "calendar": "Calendrier" }, "auth": { "confirm_password": "Confirmez le mot de passe", @@ -249,7 +258,11 @@ "username": "Nom d'utilisateur", "profile_picture": "Photo de profil", "public_profile": "Profil public", - "public_tooltip": "Avec un profil public, les utilisateurs peuvent partager des collections avec vous et afficher votre profil sur la page des utilisateurs." + "public_tooltip": "Avec un profil public, les utilisateurs peuvent partager des collections avec vous et afficher votre profil sur la page des utilisateurs.", + "email_required": "L'e-mail est requis", + "both_passwords_required": "Les deux mots de passe sont requis", + "new_password": "Nouveau mot de passe", + "reset_failed": "Échec de la réinitialisation du mot de passe" }, "users": { "no_users_found": "Aucun utilisateur trouvé avec des profils publics." @@ -291,7 +304,50 @@ "about_this_background": "À propos de ce contexte", "join_discord": "Rejoignez le Discord", "join_discord_desc": "pour partager vos propres photos. \nPostez-les dans le", - "photo_by": "Photo par" + "photo_by": "Photo par", + "change_password_error": "Impossible de changer le mot de passe. \nMot de passe actuel invalide ou nouveau mot de passe invalide.", + "current_password": "Mot de passe actuel", + "password_change_lopout_warning": "Vous serez déconnecté après avoir modifié votre mot de passe.", + "authenticator_code": "Code d'authentification", + "copy": "Copie", + "disable_mfa": "Désactiver MFA", + "email_added": "E-mail ajouté avec succès !", + "email_added_error": "Erreur lors de l'ajout de l'e-mail", + "email_removed": "E-mail supprimé avec succès !", + "email_removed_error": "Erreur lors de la suppression de l'e-mail", + "email_set_primary": "E-mail défini comme principal avec succès !", + "email_set_primary_error": "Erreur lors de la définition de l'adresse e-mail comme adresse principale", + "email_verified": "E-mail vérifié avec succès !", + "email_verified_erorr_desc": "Votre email n'a pas pu être vérifié. \nVeuillez réessayer.", + "email_verified_error": "Erreur lors de la vérification de l'e-mail", + "email_verified_success": "Votre email a été vérifié. \nVous pouvez maintenant vous connecter.", + "enable_mfa": "Activer l'authentification multifacteur", + "error_change_password": "Erreur lors du changement de mot de passe. \nVeuillez vérifier votre mot de passe actuel et réessayer.", + "generic_error": "Une erreur s'est produite lors du traitement de votre demande.", + "invalid_code": "Code MFA invalide", + "invalid_credentials": "Nom d'utilisateur ou mot de passe invalide", + "make_primary": "Rendre primaire", + "mfa_disabled": "Authentification multifacteur désactivée avec succès !", + "mfa_enabled": "Authentification multifacteur activée avec succès !", + "mfa_not_enabled": "MFA n'est pas activé", + "mfa_page_title": "Authentification multifacteur", + "mfa_required": "Une authentification multifacteur est requise", + "no_emai_set": "Aucune adresse e-mail définie", + "not_verified": "Non vérifié", + "primary": "Primaire", + "recovery_codes": "Codes de récupération", + "recovery_codes_desc": "Ce sont vos codes de récupération. \nGardez-les en sécurité. \nVous ne pourrez plus les revoir.", + "reset_session_error": "Veuillez vous déconnecter, puis vous reconnecter pour actualiser votre session et réessayer.", + "verified": "Vérifié", + "verify": "Vérifier", + "verify_email_error": "Erreur lors de la vérification de l'e-mail. \nRéessayez dans quelques minutes.", + "verify_email_success": "Vérification par e-mail envoyée avec succès !", + "add_email_blocked": "Vous ne pouvez pas ajouter une adresse e-mail à un compte protégé par une authentification à deux facteurs.", + "required": "Ce champ est obligatoire", + "csrf_failed": "Échec de la récupération du jeton CSRF", + "duplicate_email": "Cette adresse e-mail est déjà utilisée.", + "email_taken": "Cette adresse e-mail est déjà utilisée.", + "username_taken": "Ce nom d'utilisateur est déjà utilisé." }, "checklist": { "add_item": "Ajouter un article", @@ -411,5 +467,14 @@ "no_categories_found": "Aucune catégorie trouvée.", "select_category": "Sélectionnez une catégorie", "update_after_refresh": "Les cartes d'aventure seront mises à jour une fois que vous aurez actualisé la page." + }, + "dashboard": { + "add_some": "Pourquoi ne pas commencer à planifier votre prochaine aventure ? \nVous pouvez ajouter une nouvelle aventure en cliquant sur le bouton ci-dessous.", + "countries_visited": "Pays visités", + "no_recent_adventures": "Pas d'aventures récentes ?", + "recent_adventures": "Aventures récentes", + "total_adventures": "Aventures totales", + "total_visited_regions": "Total des régions visitées", + "welcome_back": "Content de te revoir" } } diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 122c4e9..fa0b1fb 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -188,7 +188,14 @@ "add_a_tag": "Aggiungi un'etichetta", "tags": "Tag", "set_to_pin": "Imposta su Blocca", - "category_fetch_error": "Errore durante il recupero delle categorie" + "category_fetch_error": "Errore durante il recupero delle categorie", + "copied_to_clipboard": "Copiato negli appunti!", + "copy_failed": "Copia non riuscita", + "adventure_calendar": "Calendario delle avventure", + "emoji_picker": "Selettore di emoji", + "hide": "Nascondere", + "show": "Spettacolo", + "download_calendar": "Scarica Calendario" }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", @@ -226,14 +233,16 @@ "light": "Leggero", "night": "Notte", "aestheticDark": "Estetico scuro", - "aestheticLight": "Luce estetica" + "aestheticLight": "Luce estetica", + "northernLights": "Aurora boreale" }, "users": "Utenti", "worldtravel": "Viaggio nel mondo", "my_tags": "I miei tag", "tag": "Etichetta", "language_selection": "Lingua", - "support": "Supporto" + "support": "Supporto", + "calendar": "Calendario" }, "auth": { "confirm_password": "Conferma password", @@ -249,7 +258,11 @@ "username": "Nome utente", "profile_picture": "Immagine del profilo", "public_profile": "Profilo pubblico", - "public_tooltip": "Con un profilo pubblico, gli utenti possono condividere raccolte con te e visualizzare il tuo profilo nella pagina degli utenti." + "public_tooltip": "Con un profilo pubblico, gli utenti possono condividere raccolte con te e visualizzare il tuo profilo nella pagina degli utenti.", + "email_required": "L'e-mail è obbligatoria", + "both_passwords_required": "Sono necessarie entrambe le password", + "new_password": "Nuova parola d'ordine", + "reset_failed": "Impossibile reimpostare la password" }, "users": { "no_users_found": "Nessun utente trovato con profili pubblici." @@ -291,7 +304,50 @@ "about_this_background": "A proposito di questo contesto", "join_discord": "Unisciti alla Discordia", "join_discord_desc": "per condividere le tue foto. \nPubblicateli in", - "photo_by": "Foto di" + "photo_by": "Foto di", + "change_password_error": "Impossibile modificare la password. \nPassword attuale non valida o nuova password non valida.", + "current_password": "password attuale", + "password_change_lopout_warning": "Verrai disconnesso dopo aver modificato la password.", + "authenticator_code": "Codice Autenticatore", + "copy": "Copia", + "disable_mfa": "Disabilita MFA", + "email_added": "Email aggiunta con successo!", + "email_added_error": "Errore durante l'aggiunta dell'e-mail", + "email_removed": "Email rimossa con successo!", + "email_removed_error": "Errore durante la rimozione dell'e-mail", + "email_set_primary": "Email impostata come primaria con successo!", + "email_set_primary_error": "Errore durante l'impostazione dell'e-mail come principale", + "email_verified": "Email verificata con successo!", + "email_verified_erorr_desc": "Non è stato possibile verificare la tua email. \nPer favore riprova.", + "email_verified_error": "Errore durante la verifica dell'e-mail", + "email_verified_success": "La tua email è stata verificata. \nOra puoi accedere.", + "enable_mfa": "Abilita MFA", + "error_change_password": "Errore durante la modifica della password. \nControlla la tua password attuale e riprova.", + "generic_error": "Si è verificato un errore durante l'elaborazione della tua richiesta.", + "invalid_code": "Codice MFA non valido", + "invalid_credentials": "Nome utente o password non validi", + "make_primary": "Rendi primario", + "mfa_disabled": "Autenticazione a più fattori disabilitata correttamente!", + "mfa_enabled": "Autenticazione a più fattori abilitata correttamente!", + "mfa_not_enabled": "L'MFA non è abilitata", + "mfa_page_title": "Autenticazione a più fattori", + "mfa_required": "È richiesta l'autenticazione a più fattori", + "no_emai_set": "Nessuna e-mail impostata", + "not_verified": "Non verificato", + "primary": "Primario", + "recovery_codes": "Codici di ripristino", + "recovery_codes_desc": "Questi sono i tuoi codici di ripristino. \nTeneteli al sicuro. \nNon potrai vederli più.", + "reset_session_error": "Esci, effettua nuovamente l'accesso per aggiornare la sessione e riprova.", + "verified": "Verificato", + "verify_email_success": "Verifica email inviata con successo!", + "verify": "Verificare", + "verify_email_error": "Errore durante la verifica dell'e-mail. \nRiprova tra qualche minuto.", + "add_email_blocked": "Non è possibile aggiungere un indirizzo email a un account protetto dall'autenticazione a due fattori.", + "required": "Questo campo è obbligatorio", + "csrf_failed": "Impossibile recuperare il token CSRF", + "duplicate_email": "Questo indirizzo email è già in uso.", + "email_taken": "Questo indirizzo email è già in uso.", + "username_taken": "Questo nome utente è già in uso." }, "checklist": { "add_item": "Aggiungi articolo", @@ -411,5 +467,14 @@ "no_categories_found": "Nessuna categoria trovata.", "select_category": "Seleziona Categoria", "update_after_refresh": "Le carte avventura verranno aggiornate una volta aggiornata la pagina." + }, + "dashboard": { + "add_some": "Perché non iniziare a pianificare la tua prossima avventura? \nPuoi aggiungere una nuova avventura facendo clic sul pulsante in basso.", + "countries_visited": "Paesi visitati", + "no_recent_adventures": "Nessuna avventura recente?", + "recent_adventures": "Avventure recenti", + "total_adventures": "Avventure totali", + "total_visited_regions": "Totale regioni visitate", + "welcome_back": "Bentornato" } } diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index f3e60c7..90ac331 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -1,13 +1,13 @@ { "about": { "about": "Over", - "close": "Dichtbij", + "close": "Sluiten", "license": "Gelicentieerd onder de GPL-3.0-licentie.", - "message": "Gemaakt met ❤️ in de Verenigde Staten.", + "message": "Met ❤️ gemaakt in de Verenigde Staten.", "nominatim_1": "Locatie zoeken en geocodering wordt verzorgd door", "nominatim_2": "Hun gegevens zijn in licentie gegeven onder de ODbL-licentie.", - "oss_attributions": "Open source-attributies", - "other_attributions": "Aanvullende toeschrijvingen zijn te vinden in het README-bestand.", + "oss_attributions": "Open source gebruik", + "other_attributions": "Aanvullende vermeldingen zijn te vinden in het README-bestand.", "source_code": "Broncode" }, "adventures": { @@ -33,38 +33,38 @@ "transportation": "Vervoer 🚗", "volunteer_work": "Vrijwilligerswerk 🤝", "water_sports": "Watersport 🚤", - "wildlife": "Dieren in het wild 🦒" + "wildlife": "Wilde dieren 🦒" }, "add_to_collection": "Toevoegen aan collectie", "adventure": "Avontuur", "adventure_delete_confirm": "Weet je zeker dat je dit avontuur wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", "adventure_details": "Avontuurdetails", "adventure_type": "Avontuurtype", - "archive": "Archief", + "archive": "Archiveer", "archived": "Gearchiveerd", "archived_collection_message": "Collectie succesvol gearchiveerd!", "archived_collections": "Gearchiveerde collecties", "ascending": "Oplopend", "cancel": "Annuleren", "category_filter": "Categoriefilter", - "clear": "Duidelijk", - "close_filters": "Sluit Filters", - "collection": "Verzameling", + "clear": "Leeg maken", + "close_filters": "Sluit filters", + "collection": "Collectie", "collection_adventures": "Inclusief collectie-avonturen", "collection_link_error": "Fout bij het koppelen van avontuur aan collectie", "collection_link_success": "Avontuur succesvol gekoppeld aan collectie!", - "collection_remove_error": "Fout bij verwijderen van avontuur uit verzameling", + "collection_remove_error": "Fout bij verwijderen van avontuur uit collectie", "collection_remove_success": "Avontuur is succesvol uit de collectie verwijderd!", "count_txt": "resultaten die overeenkomen met uw zoekopdracht", "create_new": "Maak nieuwe...", "date": "Datum", "delete": "Verwijderen", - "delete_collection": "Verzameling verwijderen", + "delete_collection": "Collectie verwijderen", "delete_collection_success": "Collectie succesvol verwijderd!", - "delete_collection_warning": "Weet u zeker dat u deze verzameling wilt verwijderen? \nHiermee worden ook alle gekoppelde avonturen verwijderd. \nDeze actie kan niet ongedaan worden gemaakt.", + "delete_collection_warning": "Weet u zeker dat u deze collectie wilt verwijderen? \nHiermee worden ook alle gekoppelde avonturen verwijderd. \nDeze actie kan niet ongedaan worden gemaakt.", "descending": "Aflopend", "edit_adventure": "Avontuur bewerken", - "edit_collection": "Verzameling bewerken", + "edit_collection": "Collectie bewerken", "filter": "Filter", "homepage": "Startpagina", "latitude": "Breedte", @@ -73,21 +73,21 @@ "name": "Naam", "no_image_found": "Geen afbeelding gevonden", "not_found": "Avontuur niet gevonden", - "not_found_desc": "Het avontuur waar je naar op zoek was, kon niet gevonden worden. \nProbeer een ander avontuur of kom later nog eens terug.", + "not_found_desc": "Het avontuur waar je naar op zoek was, kon niet worden gevonden. \nProbeer een ander avontuur of kom later nog eens terug.", "open_details": "Details openen", "open_filters": "Filters openen", - "order_by": "Bestel per", - "order_direction": "Bestelrichting", + "order_by": "Sorteer op", + "order_direction": "Sorteervolgorde", "private": "Privé", "public": "Openbaar", "rating": "Beoordeling", "remove_from_collection": "Verwijderen uit collectie", "share": "Deel", - "sort": "Soort", + "sort": "Sorteer", "sources": "Bronnen", "unarchive": "Uit het archief halen", "unarchived_collection_message": "Collectie is succesvol gedearchiveerd!", - "updated": "Bijgewerkt", + "updated": "Gewijzigd", "visit": "Bezoek", "visits": "Bezoeken", "adventure_delete_success": "Avontuur succesvol verwijderd!", @@ -108,14 +108,14 @@ "activity_types": "Activiteitstypen", "add": "Toevoegen", "add_an_activity": "Voeg een activiteit toe", - "add_notes": "Voeg notities toe", - "adventure_create_error": "Kan geen avontuur creëren", - "adventure_created": "Avontuur gecreëerd", - "adventure_update_error": "Kan avontuur niet updaten", - "adventure_updated": "Avontuur bijgewerkt", + "add_notes": "Voeg opmerkingen toe", + "adventure_create_error": "Kan geen avontuur aanmaken", + "adventure_created": "Avontuur aangemaakt", + "adventure_update_error": "Kan avontuur niet wijzigen", + "adventure_updated": "Avontuur gewijzigd", "basic_information": "Basisinformatie", "category": "Categorie", - "clear_map": "Duidelijke kaart", + "clear_map": "Kaart leegmaken", "copy_link": "Kopieer link", "date_constrain": "Beperk u tot ophaaldata", "description": "Beschrijving", @@ -137,7 +137,7 @@ "no_start_date": "Voer een startdatum in", "public_adventure": "Openbaar avontuur", "remove": "Verwijderen", - "save_next": "Redden", + "save_next": "Opslaan & Volgende", "search_for_location": "Zoek een locatie", "search_results": "Zoekresultaten", "see_adventures": "Zie Avonturen", @@ -152,27 +152,27 @@ "wikipedia": "Wikipedia", "adventure_not_found": "Er zijn geen avonturen om weer te geven. \nVoeg er een paar toe via de plusknop rechtsonder of probeer de filters te wijzigen!", "all": "Alle", - "error_updating_regions": "Fout bij updaten van regio's", - "mark_visited": "Mark bezocht", + "error_updating_regions": "Fout bij wijzigen van regio's", + "mark_visited": "Markeer bezocht", "my_adventures": "Mijn avonturen", "no_adventures_found": "Geen avonturen gevonden", "no_collections_found": "Er zijn geen collecties gevonden waar dit avontuur aan kan worden toegevoegd.", "no_linkable_adventures": "Er zijn geen avonturen gevonden die aan deze collectie kunnen worden gekoppeld.", "not_visited": "Niet bezocht", "regions_updated": "regio's bijgewerkt", - "update_visited_regions": "Update bezochte regio's", - "update_visited_regions_disclaimer": "Dit kan even duren, afhankelijk van het aantal avonturen dat je hebt bezocht.", - "visited_region_check": "Regiocheck bezocht", - "visited_region_check_desc": "Door dit te selecteren, controleert de server al uw bezochte avonturen en markeert de regio's waarin ze zich bevinden als bezocht in de wereldreizen.", + "update_visited_regions": "Werk bezochte regio's bij", + "update_visited_regions_disclaimer": "Dit kan even duren, afhankelijk van het aantal avondturen dat je hebt beleefd.", + "visited_region_check": "Check bezochte regio's", + "visited_region_check_desc": "Door dit te selecteren, controleert de server alle avonturen die je beleefde en markeert hun regio's als bezocht in de wereldreizen.", "add_new": "Nieuw toevoegen...", "checklist": "Controlelijst", "checklists": "Controlelijsten", "collection_archived": "Deze collectie is gearchiveerd.", - "collection_completed": "Je hebt deze verzameling voltooid!", - "collection_stats": "Verzamelstatistieken", + "collection_completed": "Je hebt deze collectie voltooid!", + "collection_stats": "Collectiestatistieken", "days": "dagen", "itineary_by_date": "Reisplan op datum", - "keep_exploring": "Blijf verkennen!", + "keep_exploring": "Blijf ontdekken!", "link_new": "Nieuwe link...", "linked_adventures": "Gekoppelde avonturen", "links": "Koppelingen", @@ -182,13 +182,20 @@ "notes": "Opmerkingen", "nothing_planned": "Niets gepland voor deze dag. \nGeniet van de reis!", "transportation": "Vervoer", - "transportations": "Transporten", + "transportations": "Vervoer", "visit_link": "Bezoek Link", "day": "Dag", "add_a_tag": "Voeg een label toe", "tags": "Labels", - "set_to_pin": "Stel in op Vastzetten", - "category_fetch_error": "Fout bij ophalen van categorieën" + "set_to_pin": "Stel in op pin", + "category_fetch_error": "Fout bij ophalen van categorieën", + "copied_to_clipboard": "Gekopieerd naar klembord!", + "copy_failed": "Kopiëren mislukt", + "adventure_calendar": "Avonturenkalender", + "emoji_picker": "Emoji-kiezer", + "hide": "Verbergen", + "show": "Show", + "download_calendar": "Agenda downloaden" }, "home": { "desc_1": "Ontdek, plan en verken met gemak", @@ -201,14 +208,14 @@ "feature_3_desc": "Bekijk uw reizen over de hele wereld met een interactieve kaart en ontdek nieuwe bestemmingen.", "go_to": "Ga naar AdventureLog", "hero_2": "Ontdek en plan je volgende avontuur met AdventureLog. \nOntdek adembenemende bestemmingen, maak aangepaste reisroutes en blijf onderweg verbonden.", - "hero_1": "Ontdek 's werelds meest opwindende avonturen", + "hero_1": "ontdek 's werelds meest opwindende avonturen", "key_features": "Belangrijkste kenmerken" }, "navbar": { "about": "Over AdventureLog", "adventures": "Avonturen", "collections": "Collecties", - "discord": "Meningsverschil", + "discord": "discord", "documentation": "Documentatie", "greeting": "Hoi", "logout": "Uitloggen", @@ -218,7 +225,7 @@ "search": "Zoekopdracht", "settings": "Instellingen", "shared_with_me": "Gedeeld met mij", - "theme_selection": "Thema Selectie", + "theme_selection": "Thema selectie", "themes": { "aqua": "Aqua", "dark": "Donker", @@ -226,14 +233,16 @@ "light": "Licht", "night": "Nacht", "aestheticDark": "Esthetisch donker", - "aestheticLight": "Esthetisch licht" + "aestheticLight": "Esthetisch licht", + "northernLights": "Noorderlicht" }, "users": "Gebruikers", "worldtravel": "Wereldreizen", - "my_tags": "Mijn tags", + "my_tags": "Mijn labels", "tag": "Label", "language_selection": "Taal", - "support": "Steun" + "support": "Steun", + "calendar": "Kalender" }, "auth": { "confirm_password": "Bevestig wachtwoord", @@ -249,7 +258,11 @@ "username": "Gebruikersnaam", "profile_picture": "Profielfoto", "public_profile": "Openbaar profiel", - "public_tooltip": "Met een openbaar profiel kunnen gebruikers collecties met u delen en uw profiel bekijken op de gebruikerspagina." + "public_tooltip": "Met een openbaar profiel kunnen gebruikers collecties met u delen en uw profiel bekijken op de gebruikerspagina.", + "email_required": "E-mail is vereist", + "both_passwords_required": "Beide wachtwoorden zijn vereist", + "new_password": "Nieuw wachtwoord", + "reset_failed": "Kan het wachtwoord niet opnieuw instellen" }, "users": { "no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen." @@ -266,18 +279,18 @@ "partially_visited": "Gedeeltelijk bezocht" }, "settings": { - "account_settings": "Gebruikersaccountinstellingen", + "account_settings": "Gebruikersaccount instellingen", "confirm_new_password": "Bevestig nieuw wachtwoord", - "current_email": "Huidige e-mail", + "current_email": "Huidig e-mailadres", "email_change": "Wijzig e-mailadres", - "new_email": "Nieuwe e-mail", + "new_email": "Nieuw e-mailadres", "new_password": "Nieuw wachtwoord", "no_email_set": "Geen e-mailadres ingesteld", "password_change": "Wachtwoord wijzigen", "settings_page": "Instellingenpagina", - "update": "Update", - "update_error": "Fout bij updaten van instellingen", - "update_success": "Instellingen succesvol bijgewerkt!", + "update": "Wijzig", + "update_error": "Fout bij wijzigen van instellingen", + "update_success": "Instellingen succesvol gewijzigd!", "change_password": "Wachtwoord wijzigen", "invalid_token": "Token is ongeldig of verlopen", "login_redir": "Vervolgens wordt u doorgestuurd naar de inlogpagina.", @@ -289,54 +302,97 @@ "submit": "Indienen", "token_required": "Token en UID zijn vereist voor het opnieuw instellen van het wachtwoord.", "about_this_background": "Over deze achtergrond", - "join_discord": "Sluit je aan bij de onenigheid", + "join_discord": "Sluit je aan bij Discord", "join_discord_desc": "om uw eigen foto's te delen. \nPlaats ze in de", - "photo_by": "Foto door" + "photo_by": "Foto door", + "change_password_error": "Kan wachtwoord niet wijzigen. \nOngeldig huidig ​​wachtwoord of ongeldig nieuw wachtwoord.", + "current_password": "Huidig ​​wachtwoord", + "password_change_lopout_warning": "Na het wijzigen van uw wachtwoord wordt u uitgelogd.", + "authenticator_code": "Authenticatiecode", + "copy": "Kopiëren", + "disable_mfa": "Schakel MFA uit", + "email_added": "E-mailadres succesvol toegevoegd!", + "email_added_error": "Fout bij het toevoegen van e-mailadres", + "email_removed": "E-mail succesvol verwijderd!", + "email_removed_error": "Fout bij verwijderen van e-mail", + "email_set_primary": "E-mailadres is succesvol ingesteld als primair!", + "email_set_primary_error": "Fout bij het instellen van e-mail als primair", + "email_verified": "E-mail succesvol geverifieerd!", + "email_verified_erorr_desc": "Uw e-mailadres kan niet worden geverifieerd. \nProbeer het opnieuw.", + "email_verified_error": "Fout bij het verifiëren van e-mailadres", + "email_verified_success": "Uw e-mailadres is geverifieerd. \nU kunt nu inloggen.", + "enable_mfa": "Schakel MFA in", + "error_change_password": "Fout bij wijzigen van wachtwoord. \nControleer uw huidige wachtwoord en probeer het opnieuw.", + "generic_error": "Er is een fout opgetreden tijdens het verwerken van uw verzoek.", + "invalid_code": "Ongeldige MFA-code", + "invalid_credentials": "Ongeldige gebruikersnaam of wachtwoord", + "make_primary": "Maak primair", + "mfa_disabled": "Multi-factor authenticatie is succesvol uitgeschakeld!", + "mfa_enabled": "Multi-factor authenticatie succesvol ingeschakeld!", + "mfa_not_enabled": "MFA is niet ingeschakeld", + "mfa_page_title": "Authenticatie met meerdere factoren", + "mfa_required": "Multi-factor authenticatie is vereist", + "no_emai_set": "Geen e-mailadres ingesteld", + "not_verified": "Niet geverifieerd", + "primary": "Primair", + "recovery_codes": "Herstelcodes", + "recovery_codes_desc": "Dit zijn uw herstelcodes. \nBewaar ze veilig. \nJe zult ze niet meer kunnen zien.", + "reset_session_error": "Meld u af en weer aan om uw sessie te vernieuwen en het opnieuw te proberen.", + "verified": "Geverifieerd", + "verify": "Verifiëren", + "verify_email_error": "Fout bij het verifiëren van e-mailadres. \nProbeer het over een paar minuten opnieuw.", + "verify_email_success": "E-mailverificatie succesvol verzonden!", + "add_email_blocked": "U kunt geen e-mailadres toevoegen aan een account dat is beveiligd met tweefactorauthenticatie.", + "required": "Dit veld is verplicht", + "csrf_failed": "Kan CSRF-token niet ophalen", + "duplicate_email": "Dit e-mailadres is al in gebruik.", + "email_taken": "Dit e-mailadres is al in gebruik.", + "username_taken": "Deze gebruikersnaam is al in gebruik." }, "checklist": { "add_item": "Artikel toevoegen", - "checklist_delete_error": "Fout bij verwijderen van checklist", + "checklist_delete_error": "Fout bij verwijderen van controlelijst", "checklist_deleted": "Controlelijst succesvol verwijderd!", "checklist_editor": "Controlelijst-editor", - "checklist_public": "Deze checklist is openbaar omdat deze zich in een openbare collectie bevindt.", + "checklist_public": "Deze controlelijst is openbaar omdat deze zich in een openbare collectie bevindt.", "editing_checklist": "Controlelijst bewerken", - "failed_to_save": "Kan checklist niet opslaan", - "item": "Item", + "failed_to_save": "Kan controlelijst niet opslaan", + "item": "Artikel", "item_already_exists": "Artikel bestaat al", "item_cannot_be_empty": "Artikel mag niet leeg zijn", "items": "Artikelen", "new_item": "Nieuw artikel", - "save": "Redden" + "save": "Opslaan" }, "collection": { "collection_created": "Collectie succesvol aangemaakt!", "collection_edit_success": "Collectie succesvol bewerkt!", - "create": "Creëren", - "edit_collection": "Verzameling bewerken", - "error_creating_collection": "Fout bij maken collectie", + "create": "Aanmaken", + "edit_collection": "Collectie bewerken", + "error_creating_collection": "Fout bij aanmaken collectie", "error_editing_collection": "Fout bij bewerken collectie", "new_collection": "Nieuwe collectie" }, "notes": { "add_a_link": "Voeg een link toe", "content": "Inhoud", - "editing_note": "Notitie bewerken", - "failed_to_save": "Kan notitie niet opslaan", - "note_delete_error": "Fout bij verwijderen van notitie", + "editing_note": "Opmerking bewerken", + "failed_to_save": "Kan opmerking niet opslaan", + "note_delete_error": "Fout bij verwijderen van opmerking", "note_deleted": "Opmerking succesvol verwijderd!", - "note_editor": "Notitie-editor", - "note_public": "Deze notitie is openbaar omdat deze zich in een openbare collectie bevindt.", + "note_editor": "Opmerking-editor", + "note_public": "Deze opmerking is openbaar omdat deze zich in een openbare collectie bevindt.", "open": "Open", - "save": "Redden", + "save": "Opslaan", "invalid_url": "Ongeldige URL" }, "transportation": { "date_and_time": "Datum", "date_time": "Startdatum", "edit": "Bewerking", - "edit_transportation": "Transport bewerken", + "edit_transportation": "Vervoer bewerken", "end_date_time": "Einddatum", - "error_editing_transportation": "Fout bij bewerken van transport", + "error_editing_transportation": "Fout bij bewerken van vervoer", "flight_number": "Vluchtnummer", "from_location": "Van locatie", "modes": { @@ -350,15 +406,15 @@ "walking": "Lopen" }, "to_location": "Naar locatie", - "transportation_edit_success": "Transport succesvol bewerkt!", + "transportation_edit_success": "Vervoer succesvol bewerkt!", "type": "Type", - "new_transportation": "Nieuw transport", + "new_transportation": "Nieuw vervoer", "provide_start_date": "Geef een startdatum op", "start": "Begin", - "transport_type": "Transporttype", - "transportation_added": "Transport succesvol toegevoegd!", - "transportation_delete_error": "Fout bij verwijderen transport", - "transportation_deleted": "Transport succesvol verwijderd!" + "transport_type": "Vervoerstype", + "transportation_added": "Vervoer succesvol toegevoegd!", + "transportation_delete_error": "Fout bij verwijderen vervoer", + "transportation_deleted": "Vervoer succesvol verwijderd!" }, "search": { "adventurelog_results": "AdventureLog-resultaten", @@ -367,9 +423,9 @@ }, "map": { "add_adventure": "Voeg nieuw avontuur toe", - "add_adventure_at_marker": "Voeg een nieuw avontuur toe bij Marker", + "add_adventure_at_marker": "Voeg een nieuw avontuur toe bij markeerpunt", "adventure_map": "Avonturenkaart", - "clear_marker": "Duidelijke markering", + "clear_marker": "Verwijder markeerpunt", "map_options": "Kaartopties", "show_visited_regions": "Toon bezochte regio's", "view_details": "Details bekijken" @@ -388,7 +444,7 @@ "share": { "no_users_shared": "Er zijn geen gebruikers gedeeld", "not_shared_with": "Niet gedeeld met", - "share_desc": "Deel deze verzameling met andere gebruikers.", + "share_desc": "Deel deze collectie met andere gebruikers.", "shared": "Gedeeld", "shared_with": "Gedeeld met", "unshared": "Niet gedeeld", @@ -406,10 +462,19 @@ "categories": { "category_name": "Categorienaam", "edit_category": "Categorie bewerken", - "icon": "Icon", + "icon": "Ikoon", "manage_categories": "Beheer categorieën", "no_categories_found": "Geen categorieën gevonden.", - "select_category": "Selecteer Categorie", + "select_category": "Selecteer categorie", "update_after_refresh": "De avonturenkaarten worden bijgewerkt zodra u de pagina vernieuwt." + }, + "dashboard": { + "add_some": "Waarom begint u niet met het plannen van uw volgende avontuur? \nJe kunt een nieuw avontuur toevoegen door op de onderstaande knop te klikken.", + "countries_visited": "Bezochte landen", + "no_recent_adventures": "Geen recente avonturen?", + "recent_adventures": "Recente avonturen", + "total_adventures": "Totale avonturen", + "total_visited_regions": "Totaal bezochte regio's", + "welcome_back": "Welkom terug" } } diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index d51a88f..db3e758 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -1,154 +1,156 @@ { "navbar": { - "adventures": "Podróże", - "collections": "Kolekcje", - "worldtravel": "Podróże po świecie", - "map": "Mapa", - "users": "Użytkownicy", - "search": "Szukaj", - "profile": "Profil", - "greeting": "Cześć", - "my_adventures": "Moje podróże", - "my_tags": "Moje tagi", - "tag": "Tag", - "shared_with_me": "Udostępnione ze mną", - "settings": "Ustawienia", - "logout": "Wyloguj się", - "about": "O AdventureLog", - "documentation": "Dokumentacja", - "discord": "Discord", - "language_selection": "Wybór języka", - "support": "Wsparcie", - "theme_selection": "Wybór motywu", - "themes": { - "light": "Jasny", - "dark": "Ciemny", - "night": "Nocny", - "forest": "Leśny", - "aestheticLight": "Estetyczny Jasny", - "aestheticDark": "Estetyczny Ciemny", - "aqua": "Aqua" - } + "adventures": "Podróże", + "collections": "Kolekcje", + "worldtravel": "Podróże po świecie", + "map": "Mapa", + "users": "Użytkownicy", + "search": "Szukaj", + "profile": "Profil", + "greeting": "Cześć", + "my_adventures": "Moje podróże", + "my_tags": "Moje tagi", + "tag": "Tag", + "shared_with_me": "Udostępnione ze mną", + "settings": "Ustawienia", + "logout": "Wyloguj się", + "about": "O AdventureLog", + "documentation": "Dokumentacja", + "discord": "Discord", + "language_selection": "Wybór języka", + "support": "Wsparcie", + "theme_selection": "Wybór motywu", + "themes": { + "light": "Jasny", + "dark": "Ciemny", + "night": "Nocny", + "forest": "Leśny", + "aestheticLight": "Estetyczny Jasny", + "aestheticDark": "Estetyczny Ciemny", + "aqua": "Aqua", + "northernLights": "Zorza Polarna" + }, + "calendar": "Kalendarz" }, "about": { - "about": "O aplikacji", - "license": "Licencjonowane na licencji GPL-3.0.", - "source_code": "Kod źródłowy", - "message": "Stworzone z ❤️ w Stanach Zjednoczonych.", - "oss_attributions": "Atrybucje Open Source", - "nominatim_1": "Wyszukiwanie lokalizacji i geokodowanie zapewnia", - "nominatim_2": "Ich dane są licencjonowane na licencji ODbL.", - "other_attributions": "Dodatkowe atrybucje można znaleźć w pliku README.", - "close": "Zamknij" + "about": "O aplikacji", + "license": "Licencjonowane na licencji GPL-3.0.", + "source_code": "Kod źródłowy", + "message": "Stworzone z ❤️ w Stanach Zjednoczonych.", + "oss_attributions": "Atrybucje Open Source", + "nominatim_1": "Wyszukiwanie lokalizacji i geokodowanie zapewnia", + "nominatim_2": "Ich dane są licencjonowane na licencji ODbL.", + "other_attributions": "Dodatkowe atrybucje można znaleźć w pliku README.", + "close": "Zamknij" }, "home": { - "hero_1": "Odkryj najbardziej ekscytujące podróże na świecie", - "hero_2": "Odkrywaj i planuj swoją kolejną podróż z AdventureLog. Poznaj zapierające dech w piersiach miejsca, twórz spersonalizowane plany podróży i bądź w kontakcie w trakcie podróży.", - "go_to": "Przejdź do AdventureLog", - "key_features": "Najważniejsze funkcje", - "desc_1": "Odkrywaj, planuj i eksploruj z łatwością", - "desc_2": "AdventureLog został zaprojektowany, aby uprościć Twoją podróż, oferując narzędzia i zasoby do planowania, pakowania i poruszania się po niezapomnianej podróży.", - "feature_1": "Dziennik podróży", - "feature_1_desc": "Śledź swoje podróże dzięki spersonalizowanemu dziennikowi podróży i dziel się swoimi doświadczeniami z przyjaciółmi i rodziną.", - "feature_2": "Planowanie podróży", - "feature_2_desc": "Łatwo twórz spersonalizowane plany podróży i uzyskaj szczegółowy rozkład swojego wyjazdu na każdy dzień.", - "feature_3": "Mapa podróży", - "feature_3_desc": "Zobacz swoje podróże na całym świecie dzięki interaktywnej mapie i odkrywaj nowe miejsca." + "hero_1": "Odkryj najbardziej ekscytujące podróże na świecie", + "hero_2": "Odkrywaj i planuj swoją kolejną podróż z AdventureLog. Poznaj zapierające dech w piersiach miejsca, twórz spersonalizowane plany podróży i bądź w kontakcie w trakcie podróży.", + "go_to": "Przejdź do AdventureLog", + "key_features": "Najważniejsze funkcje", + "desc_1": "Odkrywaj, planuj i eksploruj z łatwością", + "desc_2": "AdventureLog został zaprojektowany, aby uprościć Twoją podróż, oferując narzędzia i zasoby do planowania, pakowania i poruszania się po niezapomnianej podróży.", + "feature_1": "Dziennik podróży", + "feature_1_desc": "Śledź swoje podróże dzięki spersonalizowanemu dziennikowi podróży i dziel się swoimi doświadczeniami z przyjaciółmi i rodziną.", + "feature_2": "Planowanie podróży", + "feature_2_desc": "Łatwo twórz spersonalizowane plany podróży i uzyskaj szczegółowy rozkład swojego wyjazdu na każdy dzień.", + "feature_3": "Mapa podróży", + "feature_3_desc": "Zobacz swoje podróże na całym świecie dzięki interaktywnej mapie i odkrywaj nowe miejsca." }, "adventures": { - "collection_remove_success": "Podróż została pomyślnie usunięta z kolekcji!", - "collection_remove_error": "Błąd podczas usuwania podróży z kolekcji", - "collection_link_success": "Podróż została pomyślnie dodana do kolekcji!", - "no_image_found": "Nie znaleziono obrazu", - "collection_link_error": "Błąd podczas dodawania podróży do kolekcji", - "adventure_delete_confirm": "Czy na pewno chcesz usunąć tę podróż? Ta operacja jest nieodwracalna.", - "open_details": "Otwórz szczegóły", - "edit_adventure": "Edytuj podróż", - "remove_from_collection": "Usuń z kolekcji", - "add_to_collection": "Dodaj do kolekcji", - "delete": "Usuń", - "not_found": "Podróż nie znaleziona", - "not_found_desc": "Podróży, której szukasz, nie można znaleźć. Spróbuj poszukać innej podróży lub sprawdź później.", - "homepage": "Strona główna", - "adventure_details": "Szczegóły podróży", - "collection": "Kolekcja", - "adventure_type": "Typ podróży", - "longitude": "Długość geograficzna", - "latitude": "Szerokość geograficzna", - "visit": "Odwiedź", - "visits": "Odwiedziny", - "create_new": "Utwórz nową...", - "adventure": "Podróż", - "count_txt": "wyniki pasujące do Twojego wyszukiwania", - "sort": "Sortuj", - "order_by": "Sortuj według", - "order_direction": "Kierunek sortowania", - "ascending": "Rosnąco", - "descending": "Malejąco", - "updated": "Zaktualizowano", - "name": "Nazwa", - "date": "Data", - "activity_types": "Rodzaje aktywności", - "tags": "Tagi", - "add_a_tag": "Dodaj tag", - "date_constrain": "Ogranicz do dat kolekcji", - "rating": "Ocena", - "my_images": "Moje obrazy", - "add_an_activity": "Dodaj aktywność", - "no_images": "Brak obrazów", - "upload_images_here": "Prześlij obrazy tutaj", - "share_adventure": "Podziel się tą podróżą!", - "copy_link": "Kopiuj link", - "image": "Obraz", - "upload_image": "Prześlij obraz", - "url": "URL", - "fetch_image": "Pobierz obraz", - "wikipedia": "Wikipedia", - "add_notes": "Dodaj notatki", - "warning": "Ostrzeżenie", - "my_adventures": "Moje podróże", - "no_linkable_adventures": "Nie znaleziono podróży, które można połączyć z tą kolekcją.", - "add": "Dodaj", - "save_next": "Zapisz i następny", - "end_date": "Data zakończenia", - "my_visits": "Moje wizyty", - "start_date": "Data rozpoczęcia", - "remove": "Usuń", - "location": "Lokalizacja", - "search_for_location": "Szukaj lokalizacji", - "clear_map": "Wyczyść mapę", - "search_results": "Wyniki wyszukiwania", - "no_results": "Nie znaleziono wyników", - "wiki_desc": "Pobiera fragment artykułu z Wikipedii pasującego do nazwy podróży.", - "generate_desc": "Generuj opis", - "public_adventure": "Publiczna podróż", - "location_information": "Informacje o lokalizacji", - "link": "Link", - "links": "Linki", - "description": "Opis", - "sources": "Źródła", - "collection_adventures": "Uwzględnij podróże z kolekcji", - "filter": "Filtr", - "category_filter": "Filtr kategorii", - "category": "Kategoria", - "select_adventure_category": "Wybierz kategorię podróży", - "clear": "Wyczyść", - "my_collections": "Moje kolekcje", - "open_filters": "Otwórz filtry", - "close_filters": "Zamknij filtry", - "archived_collections": "Zarchiwizowane kolekcje", - "share": "Podziel się", - "private": "Prywatne", - "public": "Publiczne", - "archived": "Zarchiwizowane", - "edit_collection": "Edytuj kolekcję", - "unarchive": "Przywróć z archiwum", - "archive": "Archiwizuj", - "no_collections_found": "Nie znaleziono kolekcji, do których można dodać tę podróż.", - "not_visited": "Nie odwiedzone", - "archived_collection_message": "Kolekcja została pomyślnie zarchiwizowana!", - "unarchived_collection_message": "Kolekcja została pomyślnie przywrócona z archiwum!", - "delete_collection_success": "Kolekcja została pomyślnie usunięta!", + "collection_remove_success": "Podróż została pomyślnie usunięta z kolekcji!", + "collection_remove_error": "Błąd podczas usuwania podróży z kolekcji", + "collection_link_success": "Podróż została pomyślnie dodana do kolekcji!", + "no_image_found": "Nie znaleziono obrazu", + "collection_link_error": "Błąd podczas dodawania podróży do kolekcji", + "adventure_delete_confirm": "Czy na pewno chcesz usunąć tę podróż? Ta operacja jest nieodwracalna.", + "open_details": "Otwórz szczegóły", + "edit_adventure": "Edytuj podróż", + "remove_from_collection": "Usuń z kolekcji", + "add_to_collection": "Dodaj do kolekcji", + "delete": "Usuń", + "not_found": "Podróż nie znaleziona", + "not_found_desc": "Podróży, której szukasz, nie można znaleźć. Spróbuj poszukać innej podróży lub sprawdź później.", + "homepage": "Strona główna", + "adventure_details": "Szczegóły podróży", + "collection": "Kolekcja", + "adventure_type": "Typ podróży", + "longitude": "Długość geograficzna", + "latitude": "Szerokość geograficzna", + "visit": "Odwiedź", + "visits": "Odwiedziny", + "create_new": "Utwórz nową...", + "adventure": "Podróż", + "count_txt": "wyniki pasujące do Twojego wyszukiwania", + "sort": "Sortuj", + "order_by": "Sortuj według", + "order_direction": "Kierunek sortowania", + "ascending": "Rosnąco", + "descending": "Malejąco", + "updated": "Zaktualizowano", + "name": "Nazwa", + "date": "Data", + "activity_types": "Rodzaje aktywności", + "tags": "Tagi", + "add_a_tag": "Dodaj tag", + "date_constrain": "Ogranicz do dat kolekcji", + "rating": "Ocena", + "my_images": "Moje obrazy", + "add_an_activity": "Dodaj aktywność", + "no_images": "Brak obrazów", + "upload_images_here": "Prześlij obrazy tutaj", + "share_adventure": "Podziel się tą podróżą!", + "copy_link": "Kopiuj link", + "image": "Obraz", + "upload_image": "Prześlij obraz", + "url": "URL", + "fetch_image": "Pobierz obraz", + "wikipedia": "Wikipedia", + "add_notes": "Dodaj notatki", + "warning": "Ostrzeżenie", + "my_adventures": "Moje podróże", + "no_linkable_adventures": "Nie znaleziono podróży, które można połączyć z tą kolekcją.", + "add": "Dodaj", + "save_next": "Zapisz i następny", + "end_date": "Data zakończenia", + "my_visits": "Moje wizyty", + "start_date": "Data rozpoczęcia", + "remove": "Usuń", + "location": "Lokalizacja", + "search_for_location": "Szukaj lokalizacji", + "clear_map": "Wyczyść mapę", + "search_results": "Wyniki wyszukiwania", + "no_results": "Nie znaleziono wyników", + "wiki_desc": "Pobiera fragment artykułu z Wikipedii pasującego do nazwy podróży.", + "generate_desc": "Generuj opis", + "public_adventure": "Publiczna podróż", + "location_information": "Informacje o lokalizacji", + "link": "Link", + "links": "Linki", + "description": "Opis", + "sources": "Źródła", + "collection_adventures": "Uwzględnij podróże z kolekcji", + "filter": "Filtr", + "category_filter": "Filtr kategorii", + "category": "Kategoria", + "select_adventure_category": "Wybierz kategorię podróży", + "clear": "Wyczyść", + "my_collections": "Moje kolekcje", + "open_filters": "Otwórz filtry", + "close_filters": "Zamknij filtry", + "archived_collections": "Zarchiwizowane kolekcje", + "share": "Podziel się", + "private": "Prywatne", + "public": "Publiczne", + "archived": "Zarchiwizowane", + "edit_collection": "Edytuj kolekcję", + "unarchive": "Przywróć z archiwum", + "archive": "Archiwizuj", + "no_collections_found": "Nie znaleziono kolekcji, do których można dodać tę podróż.", + "not_visited": "Nie odwiedzone", + "archived_collection_message": "Kolekcja została pomyślnie zarchiwizowana!", + "unarchived_collection_message": "Kolekcja została pomyślnie przywrócona z archiwum!", + "delete_collection_success": "Kolekcja została pomyślnie usunięta!", "delete_collection_warning": "Czy na pewno chcesz usunąć tę kolekcję? Spowoduje to również usunięcie wszystkich powiązanych podróży. Ta akcja jest nieodwracalna.", "cancel": "Anuluj", "delete_collection": "Usuń kolekcję", @@ -211,32 +213,39 @@ "nothing_planned": "Nic nie zaplanowane na ten dzień. Ciesz się podróżą!", "days": "dni", "activities": { - "general": "Ogólne 🌍", - "outdoor": "Na świeżym powietrzu 🏞️", - "lodging": "Zakwaterowanie 🛌", - "dining": "Posiłki 🍽️", - "activity": "Aktywność 🏄", - "attraction": "Atrakcje 🎢", - "shopping": "Zakupy 🛍️", - "nightlife": "Życie nocne 🌃", - "event": "Wydarzenie 🎉", - "transportation": "Transport 🚗", - "culture": "Kultura 🎭", - "water_sports": "Sporty wodne 🚤", - "hiking": "Wędrówki 🥾", - "wildlife": "Dzika przyroda 🦒", - "historical_sites": "Miejsca historyczne 🏛️", - "music_concerts": "Muzyka i koncerty 🎶", - "fitness": "Fitness 🏋️", - "art_museums": "Sztuka i muzea 🎨", - "festivals": "Festiwale 🎪", - "spiritual_journeys": "Podróże duchowe 🧘‍♀️", - "volunteer_work": "Praca wolontariacka 🤝", - "other": "Inne" - } + "general": "Ogólne 🌍", + "outdoor": "Na świeżym powietrzu 🏞️", + "lodging": "Zakwaterowanie 🛌", + "dining": "Posiłki 🍽️", + "activity": "Aktywność 🏄", + "attraction": "Atrakcje 🎢", + "shopping": "Zakupy 🛍️", + "nightlife": "Życie nocne 🌃", + "event": "Wydarzenie 🎉", + "transportation": "Transport 🚗", + "culture": "Kultura 🎭", + "water_sports": "Sporty wodne 🚤", + "hiking": "Wędrówki 🥾", + "wildlife": "Dzika przyroda 🦒", + "historical_sites": "Miejsca historyczne 🏛️", + "music_concerts": "Muzyka i koncerty 🎶", + "fitness": "Fitness 🏋️", + "art_museums": "Sztuka i muzea 🎨", + "festivals": "Festiwale 🎪", + "spiritual_journeys": "Podróże duchowe 🧘‍♀️", + "volunteer_work": "Praca wolontariacka 🤝", + "other": "Inne" + }, + "copied_to_clipboard": "Skopiowano do schowka!", + "copy_failed": "Kopiowanie nie powiodło się", + "adventure_calendar": "Kalendarz przygód", + "emoji_picker": "Wybór emoji", + "hide": "Ukrywać", + "show": "Pokazywać", + "download_calendar": "Pobierz Kalendarz" }, "worldtravel": { - "country_list": "Lista krajów", + "country_list": "Lista krajów", "num_countries": "znalezione kraje", "all": "Wszystkie", "partially_visited": "Częściowo odwiedzone", @@ -260,10 +269,14 @@ "registration_disabled": "Rejestracja jest obecnie wyłączona.", "profile_picture": "Zdjęcie profilowe", "public_profile": "Publiczny profil", - "public_tooltip": "Dzięki publicznemu profilowi użytkownicy mogą dzielić się z Tobą kolekcjami i oglądać Twój profil na stronie użytkowników." + "public_tooltip": "Dzięki publicznemu profilowi użytkownicy mogą dzielić się z Tobą kolekcjami i oglądać Twój profil na stronie użytkowników.", + "email_required": "Adres e-mail jest wymagany", + "both_passwords_required": "Obydwa hasła są wymagane", + "new_password": "Nowe hasło", + "reset_failed": "Nie udało się zresetować hasła" }, "users": { - "no_users_found": "Nie znaleziono użytkowników z publicznymi profilami." + "no_users_found": "Nie znaleziono użytkowników z publicznymi profilami." }, "settings": { "update_error": "Błąd podczas aktualizacji ustawień", @@ -291,7 +304,50 @@ "about_this_background": "O tym tle", "photo_by": "Zdjęcie autorstwa", "join_discord": "Dołącz do Discorda", - "join_discord_desc": "aby podzielić się swoimi zdjęciami. Zamieść je w kanale #travel-share." + "join_discord_desc": "aby podzielić się swoimi zdjęciami. Zamieść je w kanale #travel-share.", + "change_password_error": "Nie można zmienić hasła. \nNieprawidłowe bieżące hasło lub nieprawidłowe nowe hasło.", + "current_password": "Aktualne hasło", + "password_change_lopout_warning": "Po zmianie hasła nastąpi wylogowanie.", + "authenticator_code": "Kod uwierzytelniający", + "copy": "Kopia", + "disable_mfa": "Wyłącz usługę MFA", + "email_added": "Adres e-mail został pomyślnie dodany!", + "email_added_error": "Błąd podczas dodawania adresu e-mail", + "email_removed": "E-mail został pomyślnie usunięty!", + "email_removed_error": "Błąd podczas usuwania wiadomości e-mail", + "email_set_primary": "Adres e-mail został pomyślnie ustawiony jako podstawowy!", + "email_set_primary_error": "Błąd podczas ustawiania adresu e-mail jako głównego", + "email_verified": "E-mail zweryfikowany pomyślnie!", + "email_verified_erorr_desc": "Nie udało się zweryfikować Twojego adresu e-mail. \nSpróbuj ponownie.", + "email_verified_error": "Błąd podczas weryfikacji adresu e-mail", + "email_verified_success": "Twój e-mail został zweryfikowany. \nMożesz się teraz zalogować.", + "enable_mfa": "Włącz usługę MFA", + "error_change_password": "Błąd podczas zmiany hasła. \nSprawdź swoje aktualne hasło i spróbuj ponownie.", + "generic_error": "Wystąpił błąd podczas przetwarzania Twojego żądania.", + "invalid_code": "Nieprawidłowy kod MFA", + "invalid_credentials": "Nieprawidłowa nazwa użytkownika lub hasło", + "make_primary": "Ustaw jako podstawowy", + "mfa_disabled": "Uwierzytelnianie wieloskładnikowe zostało pomyślnie wyłączone!", + "mfa_enabled": "Uwierzytelnianie wieloskładnikowe zostało pomyślnie włączone!", + "mfa_not_enabled": "Usługa MFA nie jest włączona", + "mfa_page_title": "Uwierzytelnianie wieloskładnikowe", + "mfa_required": "Wymagane jest uwierzytelnianie wieloskładnikowe", + "no_emai_set": "Nie ustawiono adresu e-mail", + "not_verified": "Nie zweryfikowano", + "primary": "Podstawowy", + "recovery_codes": "Kody odzyskiwania", + "recovery_codes_desc": "To są Twoje kody odzyskiwania. \nZapewnij im bezpieczeństwo. \nNie będziesz mógł ich ponownie zobaczyć.", + "reset_session_error": "Wyloguj się i zaloguj ponownie, aby odświeżyć sesję i spróbuj ponownie.", + "verified": "Zweryfikowano", + "verify": "Zweryfikować", + "verify_email_error": "Błąd podczas weryfikacji adresu e-mail. \nSpróbuj ponownie za kilka minut.", + "verify_email_success": "Weryfikacja e-mailowa została wysłana pomyślnie!", + "add_email_blocked": "Nie można dodać adresu e-mail do konta chronionego uwierzytelnianiem dwuskładnikowym.", + "required": "To pole jest wymagane", + "csrf_failed": "Nie udało się pobrać tokena CSRF", + "duplicate_email": "Ten adres e-mail jest już używany.", + "email_taken": "Ten adres e-mail jest już używany.", + "username_taken": "Ta nazwa użytkownika jest już używana." }, "collection": { "collection_created": "Kolekcja została pomyślnie utworzona!", @@ -316,7 +372,7 @@ "invalid_url": "Nieprawidłowy URL" }, "checklist": { - "checklist_deleted": "Lista kontrolna została pomyślnie usunięta!", + "checklist_deleted": "Lista kontrolna została pomyślnie usunięta!", "checklist_delete_error": "Błąd podczas usuwania listy kontrolnej", "failed_to_save": "Nie udało się zapisać listy kontrolnej", "checklist_editor": "Edytor listy kontrolnej", @@ -346,14 +402,14 @@ "to_location": "Miejsce docelowe", "edit": "Edytuj", "modes": { - "car": "Samochód", - "plane": "Samolot", - "train": "Pociąg", - "bus": "Autobus", - "boat": "Łódź", - "bike": "Rower", - "walking": "Pieszo", - "other": "Inne" + "car": "Samochód", + "plane": "Samolot", + "train": "Pociąg", + "bus": "Autobus", + "boat": "Łódź", + "bike": "Rower", + "walking": "Pieszo", + "other": "Inne" }, "transportation_edit_success": "Transport edytowany pomyślnie!", "edit_transportation": "Edytuj transport", @@ -380,36 +436,45 @@ "unshared": "Nieudostępnione", "share_desc": "Udostępnij tę kolekcję innym użytkownikom.", "shared_with": "Współdzielone z", - "no_users_shared": "Brak użytkowników, którym udostępniono", + "no_users_shared": "Brak użytkowników, którym udostępniono", "not_shared_with": "Brak udostępnionych", "no_shared_found": "Brak kolekcji udostępnionych Tobie.", - "set_public": "Aby umożliwić użytkownikom udostępnianie Tobie, musisz ustawić swój profil jako publiczny.", + "set_public": "Aby umożliwić użytkownikom udostępnianie Tobie, musisz ustawić swój profil jako publiczny.", "go_to_settings": "Przejdź do ustawień" }, "languages": { - "en": "Angielski", - "de": "Niemiecki", - "es": "Hiszpański", - "fr": "Francuski", - "it": "Włoski", - "nl": "Holenderski", - "sv": "Szwedzki", - "zh": "Chiński", - "pl": "Polski" + "en": "Angielski", + "de": "Niemiecki", + "es": "Hiszpański", + "fr": "Francuski", + "it": "Włoski", + "nl": "Holenderski", + "sv": "Szwedzki", + "zh": "Chiński", + "pl": "Polski" }, "profile": { - "member_since": "Użytkownik od", - "user_stats": "Statystyki użytkownika", - "visited_countries": "Odwiedzone kraje", - "visited_regions": "Odwiedzone regiony" + "member_since": "Użytkownik od", + "user_stats": "Statystyki użytkownika", + "visited_countries": "Odwiedzone kraje", + "visited_regions": "Odwiedzone regiony" }, "categories": { - "manage_categories": "Zarządzaj kategoriami", - "no_categories_found": "Brak kategorii.", - "edit_category": "Edytuj kategorię", - "icon": "Ikona", - "update_after_refresh": "Karty podróży zostaną zaktualizowane po odświeżeniu strony.", - "select_category": "Wybierz kategorię", - "category_name": "Nazwa kategorii" + "manage_categories": "Zarządzaj kategoriami", + "no_categories_found": "Brak kategorii.", + "edit_category": "Edytuj kategorię", + "icon": "Ikona", + "update_after_refresh": "Karty podróży zostaną zaktualizowane po odświeżeniu strony.", + "select_category": "Wybierz kategorię", + "category_name": "Nazwa kategorii" + }, + "dashboard": { + "add_some": "Dlaczego nie zacząć planować kolejnej przygody? \nMożesz dodać nową przygodę, klikając przycisk poniżej.", + "countries_visited": "Odwiedzone kraje", + "no_recent_adventures": "Brak nowych przygód?", + "recent_adventures": "Ostatnie przygody", + "total_adventures": "Totalne przygody", + "total_visited_regions": "Łączna liczba odwiedzonych regionów", + "welcome_back": "Witamy z powrotem" } } diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index c8f995e..aae55d5 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -188,7 +188,14 @@ "add_a_tag": "Lägg till en tagg", "tags": "Taggar", "set_to_pin": "Ställ in på Pin", - "category_fetch_error": "Det gick inte att hämta kategorier" + "category_fetch_error": "Det gick inte att hämta kategorier", + "copied_to_clipboard": "Kopierat till urklipp!", + "copy_failed": "Kopieringen misslyckades", + "adventure_calendar": "Äventyrskalender", + "emoji_picker": "Emoji-väljare", + "hide": "Dölja", + "show": "Visa", + "download_calendar": "Ladda ner kalender" }, "home": { "desc_1": "Upptäck, planera och utforska med lätthet", @@ -226,14 +233,16 @@ "light": "Ljus", "night": "Natt", "aestheticDark": "Estetisk mörk", - "aestheticLight": "Estetiskt ljus" + "aestheticLight": "Estetiskt ljus", + "northernLights": "Norrsken" }, "users": "Användare", "worldtravel": "Världsresor", "my_tags": "Mina taggar", "tag": "Märka", "language_selection": "Språk", - "support": "Stöd" + "support": "Stöd", + "calendar": "Kalender" }, "worldtravel": { "all": "Alla", @@ -260,7 +269,11 @@ "username": "Användarnamn", "public_tooltip": "Med en offentlig profil kan användare dela samlingar med dig och se din profil på användarsidan.", "profile_picture": "Profilbild", - "public_profile": "Offentlig profil" + "public_profile": "Offentlig profil", + "email_required": "E-post krävs", + "both_passwords_required": "Båda lösenorden krävs", + "new_password": "Nytt lösenord", + "reset_failed": "Det gick inte att återställa lösenordet" }, "users": { "no_users_found": "Inga användare hittades med offentliga profiler." @@ -291,7 +304,50 @@ "about_this_background": "Om denna bakgrund", "join_discord": "Gå med i Discord", "join_discord_desc": "för att dela dina egna foton. \nLägg upp dem i", - "photo_by": "Foto av" + "photo_by": "Foto av", + "change_password_error": "Det går inte att ändra lösenord. \nOgiltigt nuvarande lösenord eller ogiltigt nytt lösenord.", + "current_password": "Aktuellt lösenord", + "password_change_lopout_warning": "Du kommer att loggas ut efter att du har ändrat ditt lösenord.", + "authenticator_code": "Autentiseringskod", + "copy": "Kopiera", + "disable_mfa": "Inaktivera MFA", + "email_added": "E-post har lagts till!", + "email_added_error": "Det gick inte att lägga till e-post", + "email_removed": "E-post har tagits bort!", + "email_removed_error": "Det gick inte att ta bort e-post", + "email_set_primary": "E-post har angetts som primärt!", + "email_set_primary_error": "Det gick inte att ställa in e-post som primär", + "email_verified": "E-post har verifierats!", + "email_verified_erorr_desc": "Din e-postadress kunde inte verifieras. \nFörsök igen.", + "email_verified_error": "Fel vid verifiering av e-post", + "email_verified_success": "Din e-postadress har verifierats. \nDu kan nu logga in.", + "enable_mfa": "Aktivera MFA", + "error_change_password": "Fel vid byte av lösenord. \nKontrollera ditt nuvarande lösenord och försök igen.", + "generic_error": "Ett fel uppstod när din begäran bearbetades.", + "invalid_code": "Ogiltig MFA-kod", + "invalid_credentials": "Ogiltigt användarnamn eller lösenord", + "make_primary": "Gör Primär", + "mfa_disabled": "Multifaktorautentisering har inaktiverats!", + "mfa_enabled": "Multifaktorautentisering har aktiverats!", + "mfa_not_enabled": "MFA är inte aktiverat", + "mfa_page_title": "Multifaktorautentisering", + "mfa_required": "Flerfaktorsautentisering krävs", + "no_emai_set": "Ingen e-post inställd", + "not_verified": "Ej verifierad", + "primary": "Primär", + "recovery_codes": "Återställningskoder", + "recovery_codes_desc": "Det här är dina återställningskoder. \nHåll dem säkra. \nDu kommer inte att kunna se dem igen.", + "reset_session_error": "Logga ut och in igen för att uppdatera din session och försök igen.", + "verified": "Verifierad", + "verify": "Kontrollera", + "verify_email_error": "Fel vid verifiering av e-post. \nFörsök igen om några minuter.", + "verify_email_success": "E-postverifiering har skickats!", + "add_email_blocked": "Du kan inte lägga till en e-postadress till ett konto som skyddas av tvåfaktorsautentisering.", + "required": "Detta fält är obligatoriskt", + "csrf_failed": "Det gick inte att hämta CSRF-token", + "duplicate_email": "Den här e-postadressen används redan.", + "email_taken": "Den här e-postadressen används redan.", + "username_taken": "Detta användarnamn används redan." }, "checklist": { "add_item": "Lägg till objekt", @@ -411,5 +467,14 @@ "no_categories_found": "Inga kategorier hittades.", "select_category": "Välj Kategori", "update_after_refresh": "Äventyrskorten kommer att uppdateras när du uppdaterar sidan." + }, + "dashboard": { + "add_some": "Varför inte börja planera ditt nästa äventyr? \nDu kan lägga till ett nytt äventyr genom att klicka på knappen nedan.", + "countries_visited": "Besökta länder", + "no_recent_adventures": "Inga nya äventyr?", + "recent_adventures": "Senaste äventyr", + "total_adventures": "Totala äventyr", + "total_visited_regions": "Totalt antal besökta regioner", + "welcome_back": "Välkommen tillbaka" } } diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 5bb7b43..56cf01f 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -188,7 +188,14 @@ "add_a_tag": "添加标签", "tags": "标签", "set_to_pin": "设置为固定", - "category_fetch_error": "获取类别时出错" + "category_fetch_error": "获取类别时出错", + "copied_to_clipboard": "已复制到剪贴板!", + "copy_failed": "复制失败", + "adventure_calendar": "冒险日历", + "emoji_picker": "表情符号选择器", + "hide": "隐藏", + "show": "展示", + "download_calendar": "下载日历" }, "home": { "desc_1": "轻松发现、规划和探索", @@ -226,14 +233,16 @@ "light": "光", "night": "夜晚", "aestheticDark": "审美黑暗", - "aestheticLight": "美学之光" + "aestheticLight": "美学之光", + "northernLights": "北极光" }, "users": "用户", "worldtravel": "环球旅行", "my_tags": "我的标签", "tag": "标签", "language_selection": "语言", - "support": "支持" + "support": "支持", + "calendar": "日历" }, "auth": { "forgot_password": "忘记密码?", @@ -249,7 +258,11 @@ "registration_disabled": "目前已禁用注册。", "profile_picture": "个人资料图片", "public_profile": "公开资料", - "public_tooltip": "通过公开个人资料,用户可以与您共享收藏并在用户页面上查看您的个人资料。" + "public_tooltip": "通过公开个人资料,用户可以与您共享收藏并在用户页面上查看您的个人资料。", + "email_required": "电子邮件为必填项", + "both_passwords_required": "两个密码都需要", + "new_password": "新密码", + "reset_failed": "重置密码失败" }, "worldtravel": { "all": "全部", @@ -291,7 +304,50 @@ "about_this_background": "关于这个背景", "join_discord": "加入不和谐", "join_discord_desc": "分享您自己的照片。\n将它们张贴在", - "photo_by": "摄影:" + "photo_by": "摄影:", + "change_password_error": "无法更改密码。\n当前密码无效或新密码无效。", + "current_password": "当前密码", + "password_change_lopout_warning": "更改密码后您将退出。", + "authenticator_code": "验证码", + "copy": "复制", + "disable_mfa": "禁用 MFA", + "email_added": "邮箱添加成功!", + "email_added_error": "添加电子邮件时出错", + "email_removed": "电子邮件删除成功!", + "email_removed_error": "删除电子邮件时出错", + "email_set_primary": "成功将电子邮件设置为主!", + "email_set_primary_error": "将电子邮件设置为主要电子邮件时出错", + "email_verified": "邮箱验证成功!", + "email_verified_erorr_desc": "无法验证您的电子邮件。\n请再试一次。", + "email_verified_error": "验证电子邮件时出错", + "email_verified_success": "您的电子邮件已被验证。\n您现在可以登录了。", + "enable_mfa": "启用 MFA", + "error_change_password": "更改密码时出错。\n请检查您当前的密码,然后重试。", + "generic_error": "处理您的请求时发生错误。", + "invalid_code": "MFA 代码无效", + "invalid_credentials": "用户名或密码无效", + "make_primary": "设为主要", + "mfa_disabled": "多重身份验证已成功禁用!", + "mfa_enabled": "多重身份验证启用成功!", + "mfa_not_enabled": "MFA 未启用", + "mfa_page_title": "多重身份验证", + "mfa_required": "需要多重身份验证", + "no_emai_set": "没有设置电子邮件", + "not_verified": "未验证", + "primary": "基本的", + "recovery_codes": "恢复代码", + "recovery_codes_desc": "这些是您的恢复代码。\n确保他们的安全。\n你将无法再见到他们。", + "reset_session_error": "请注销并重新登录以刷新您的会话,然后重试。", + "verified": "已验证", + "verify": "核实", + "verify_email_error": "验证电子邮件时出错。\n几分钟后重试。", + "verify_email_success": "邮箱验证发送成功!", + "add_email_blocked": "您无法将电子邮件地址添加到受双因素身份验证保护的帐户。", + "required": "此字段是必需的", + "csrf_failed": "获取 CSRF 令牌失败", + "duplicate_email": "该电子邮件地址已被使用。", + "email_taken": "该电子邮件地址已被使用。", + "username_taken": "该用户名已被使用。" }, "checklist": { "add_item": "添加项目", @@ -411,5 +467,14 @@ "no_categories_found": "未找到类别。", "select_category": "选择类别", "update_after_refresh": "刷新页面后,冒险卡将更新。" + }, + "dashboard": { + "add_some": "为什么不开始计划你的下一次冒险呢?\n您可以通过单击下面的按钮添加新的冒险。", + "countries_visited": "访问国家", + "no_recent_adventures": "最近没有冒险吗?", + "recent_adventures": "最近的冒险", + "total_adventures": "全面冒险", + "total_visited_regions": "总访问地区", + "welcome_back": "欢迎回来" } } diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts index c45b004..8d0446a 100644 --- a/frontend/src/routes/+page.server.ts +++ b/frontend/src/routes/+page.server.ts @@ -1,9 +1,17 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import { redirect, type Actions } from '@sveltejs/kit'; import { themes } from '$lib'; +import { fetchCSRFToken } from '$lib/index.server'; +import type { PageServerLoad } from './$types'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +export const load = (async (event) => { + if (event.locals.user) { + return redirect(302, '/dashboard'); + } +}) satisfies PageServerLoad; + export const actions: Actions = { setTheme: async ({ url, cookies }) => { const theme = url.searchParams.get('theme'); @@ -16,23 +24,24 @@ export const actions: Actions = { }); } }, - logout: async ({ cookies }: { cookies: any }) => { - const cookie = cookies.get('auth') || null; + logout: async (event) => { + let sessionId = event.cookies.get('sessionid'); + let csrfToken = await fetchCSRFToken(); - if (!cookie) { + if (!sessionId) { return; } - const res = await fetch(`${serverEndpoint}/auth/logout/`, { - method: 'POST', + const res = await fetch(`${serverEndpoint}/_allauth/browser/v1/auth/session`, { + method: 'DELETE', headers: { 'Content-Type': 'application/json', - Cookie: cookies.get('auth') - } + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken + }, + credentials: 'include' }); - if (res.ok) { - cookies.delete('auth', { path: '/', secure: false }); - cookies.delete('refresh', { path: '/', secure: false }); + if (res.status == 401) { return redirect(302, '/login'); } else { return redirect(302, '/'); diff --git a/frontend/src/routes/_allauth/[...path]/+server.ts b/frontend/src/routes/_allauth/[...path]/+server.ts new file mode 100644 index 0000000..681a3fa --- /dev/null +++ b/frontend/src/routes/_allauth/[...path]/+server.ts @@ -0,0 +1,94 @@ +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +import { fetchCSRFToken } from '$lib/index.server'; +import { json } from '@sveltejs/kit'; + +/** @type {import('./$types').RequestHandler} */ +export async function GET(event) { + const { url, params, request, fetch, cookies } = event; + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam); +} + +/** @type {import('./$types').RequestHandler} */ +export async function POST({ url, params, request, fetch, cookies }) { + const searchParam = url.search ? `${url.search}` : ''; + return handleRequest(url, params, request, fetch, cookies, searchParam, false); +} + +export async function PATCH({ url, params, request, fetch, cookies }) { + const searchParam = url.search ? `${url.search}` : ''; + return handleRequest(url, params, request, fetch, cookies, searchParam, false); +} + +export async function PUT({ url, params, request, fetch, cookies }) { + const searchParam = url.search ? `${url.search}` : ''; + return handleRequest(url, params, request, fetch, cookies, searchParam, false); +} + +export async function DELETE({ url, params, request, fetch, cookies }) { + const searchParam = url.search ? `${url.search}` : ''; + return handleRequest(url, params, request, fetch, cookies, searchParam, false); +} + +async function handleRequest( + url: any, + params: any, + request: any, + fetch: any, + cookies: any, + searchParam: string, + requreTrailingSlash: boolean | undefined = false +) { + const path = params.path; + let targetUrl = `${endpoint}/_allauth/${path}`; + + // Ensure the path ends with a trailing slash + if (requreTrailingSlash && !targetUrl.endsWith('/')) { + targetUrl += '/'; + } + + // Append query parameters to the path correctly + targetUrl += searchParam; // This will add ?format=json or &format=json to the URL + + const headers = new Headers(request.headers); + + const csrfToken = await fetchCSRFToken(); + if (!csrfToken) { + return json({ error: 'CSRF token is missing or invalid' }, { status: 400 }); + } + + try { + const response = await fetch(targetUrl, { + method: request.method, + headers: { + ...Object.fromEntries(headers), + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}` + }, + body: + request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined, + credentials: 'include' // This line ensures cookies are sent with the request + }); + + if (response.status === 204) { + return new Response(null, { + status: 204, + headers: response.headers + }); + } + + const responseData = await response.text(); + // Create a new Headers object without the 'set-cookie' header + const cleanHeaders = new Headers(response.headers); + cleanHeaders.delete('set-cookie'); + + return new Response(responseData, { + status: response.status, + headers: cleanHeaders + }); + } catch (error) { + console.error('Error forwarding request:', error); + return json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/frontend/src/routes/activities/+page.server.ts b/frontend/src/routes/activities/+page.server.ts index 4407eda..238e6b4 100644 --- a/frontend/src/routes/activities/+page.server.ts +++ b/frontend/src/routes/activities/+page.server.ts @@ -1,5 +1,6 @@ import { redirect, type Actions } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; +import { fetchCSRFToken } from '$lib/index.server'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -7,13 +8,16 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } + let csrfToken = await fetchCSRFToken(); let allActivities: string[] = []; - let res = await fetch(`${endpoint}/api/activity-types/types/`, { + let res = await event.fetch(`${endpoint}/api/activity-types/types/`, { headers: { - 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` - } + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}` + }, + credentials: 'include' }); + console.log(res); let data = await res.json(); if (data) { allActivities = data; @@ -27,13 +31,16 @@ export const load = (async (event) => { export const actions: Actions = { getActivities: async (event) => { + let csrfToken = await fetchCSRFToken(); let allActivities: string[] = []; let res = await fetch(`${endpoint}/api/activity-types/types/`, { headers: { + 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` + Cookie: `csrftoken=${csrfToken}` } }); + console.log(res); let data = await res.json(); if (data) { allActivities = data; diff --git a/frontend/src/routes/activities/+server.ts b/frontend/src/routes/activities/+server.ts index ebb4252..c5143dc 100644 --- a/frontend/src/routes/activities/+server.ts +++ b/frontend/src/routes/activities/+server.ts @@ -1,15 +1,19 @@ import { json } from '@sveltejs/kit'; -import type { RequestHandler } from '../data/$types'; +import type { RequestHandler } from '@sveltejs/kit'; +import { fetchCSRFToken } from '$lib/index.server'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const POST: RequestHandler = async (event) => { let allActivities: string[] = []; - let res = await fetch(`${endpoint}/api/activity-types/types/`, { + let csrfToken = await fetchCSRFToken(); + let sessionId = event.cookies.get('sessionid'); + let res = await event.fetch(`${endpoint}/api/activity-types/types/`, { headers: { - 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` - } + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}` + }, + credentials: 'include' }); let data = await res.json(); if (data) { diff --git a/frontend/src/routes/adventures/+page.server.ts b/frontend/src/routes/adventures/+page.server.ts index e0a0f12..da69fdf 100644 --- a/frontend/src/routes/adventures/+page.server.ts +++ b/frontend/src/routes/adventures/+page.server.ts @@ -4,8 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { Adventure } from '$lib/types'; import type { Actions } from '@sveltejs/kit'; -import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; -import { checkLink } from '$lib'; +import { fetchCSRFToken } from '$lib/index.server'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -29,12 +28,13 @@ export const load = (async (event) => { const page = event.url.searchParams.get('page') || '1'; const is_visited = event.url.searchParams.get('is_visited') || 'all'; - let initialFetch = await fetch( + let initialFetch = await event.fetch( `${serverEndpoint}/api/adventures/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' } ); @@ -61,371 +61,15 @@ export const load = (async (event) => { }) satisfies PageServerLoad; export const actions: Actions = { - create: async (event) => { - const formData = await event.request.formData(); - - const type = formData.get('type') as string; - const name = formData.get('name') as string; - const location = formData.get('location') as string | null; - let date = (formData.get('date') as string | null) ?? null; - const description = formData.get('description') as string | null; - const activity_types = formData.get('activity_types') - ? (formData.get('activity_types') as string).split(',') - : null; - const rating = formData.get('rating') ? Number(formData.get('rating')) : null; - let link = formData.get('link') as string | null; - let latitude = formData.get('latitude') as string | null; - let longitude = formData.get('longitude') as string | null; - let collection = formData.get('collection') as string | null; - let is_public = formData.get('is_public') as string | null | boolean; - - if (is_public) { - is_public = true; - } else { - is_public = false; - } - - // check if latitude and longitude are valid - if (latitude && longitude) { - if (isNaN(Number(latitude)) || isNaN(Number(longitude))) { - return { - status: 400, - body: { error: 'Invalid latitude or longitude' } - }; - } - } - - // round latitude and longitude to 6 decimal places - if (latitude) { - latitude = Number(latitude).toFixed(6); - } - if (longitude) { - longitude = Number(longitude).toFixed(6); - } - - const image = formData.get('image') as File; - - if (!type || !name) { - return { - status: 400, - body: { error: 'Missing required fields' } - }; - } - - if (date == null || date == '') { - date = null; - } - - if (link) { - link = checkLink(link); - } - - const formDataToSend = new FormData(); - formDataToSend.append('type', type); - formDataToSend.append('name', name); - formDataToSend.append('location', location || ''); - formDataToSend.append('date', date || ''); - formDataToSend.append('description', description || ''); - formDataToSend.append('latitude', latitude || ''); - formDataToSend.append('longitude', longitude || ''); - formDataToSend.append('is_public', is_public.toString()); - - if (!isNaN(Number(collection))) { - if (collection !== null) { - formDataToSend.append('collection', collection); - } - } - - if (activity_types) { - // Filter out empty and duplicate activity types, then trim each activity type - const cleanedActivityTypes = Array.from( - new Set( - activity_types - .map((activity_type) => activity_type.trim()) - .filter((activity_type) => activity_type !== '' && activity_type !== ',') - ) - ); - - // Append each cleaned activity type to formDataToSend - cleanedActivityTypes.forEach((activity_type) => { - formDataToSend.append('activity_types', activity_type); - }); - } - formDataToSend.append('rating', rating ? rating.toString() : ''); - formDataToSend.append('link', link || ''); - // formDataToSend.append('image', image); - - // log each key-value pair in the FormData - for (let pair of formDataToSend.entries()) { - console.log(pair[0] + ', ' + pair[1]); - } - - let auth = event.cookies.get('auth'); - - if (!auth) { - const refresh = event.cookies.get('refresh'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - - if (!auth) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - - const csrfToken = await fetchCSRFToken(); - - if (!csrfToken) { - return { - status: 500, - body: { message: 'Failed to fetch CSRF token' } - }; - } - - const res = await fetch(`${serverEndpoint}/api/adventures/`, { - method: 'POST', - headers: { - 'X-CSRFToken': csrfToken, - Cookie: auth - }, - body: formDataToSend - }); - - let new_id = await res.json(); - - if (!res.ok) { - const errorBody = await res.json(); - return { - status: res.status, - body: { error: errorBody } - }; - } - - let id = new_id.id; - let user_id = new_id.user_id; - let image_url = new_id.image; - let link_url = new_id.link; - - if (image && image.size > 0) { - let imageForm = new FormData(); - imageForm.append('image', image); - imageForm.append('adventure', id); - let imageRes = await fetch(`${serverEndpoint}/api/images/`, { - method: 'POST', - headers: { - Cookie: `${event.cookies.get('auth')}` - }, - body: imageForm - }); - let data = await imageRes.json(); - console.log(data); - } - - return { id, user_id, image_url, link }; - }, - edit: async (event) => { - const formData = await event.request.formData(); - - const adventureId = formData.get('adventureId') as string; - const type = formData.get('type') as string; - const name = formData.get('name') as string; - const location = formData.get('location') as string | null; - let date = (formData.get('date') as string | null) ?? null; - const description = formData.get('description') as string | null; - let activity_types = formData.get('activity_types') - ? (formData.get('activity_types') as string).split(',') - : null; - const rating = formData.get('rating') ? Number(formData.get('rating')) : null; - let link = formData.get('link') as string | null; - let latitude = formData.get('latitude') as string | null; - let longitude = formData.get('longitude') as string | null; - let is_public = formData.get('is_public') as string | null | boolean; - - if (is_public) { - is_public = true; - } else { - is_public = false; - } - - // check if latitude and longitude are valid - if (latitude && longitude) { - if (isNaN(Number(latitude)) || isNaN(Number(longitude))) { - return { - status: 400, - body: { error: 'Invalid latitude or longitude' } - }; - } - } - - // round latitude and longitude to 6 decimal places - if (latitude) { - latitude = Number(latitude).toFixed(6); - } - if (longitude) { - longitude = Number(longitude).toFixed(6); - } - - const image = formData.get('image') as File; - - // console.log(activity_types); - - if (!type || !name) { - return { - status: 400, - body: { error: 'Missing required fields' } - }; - } - - if (date == null || date == '') { - date = null; - } - - if (link) { - link = checkLink(link); - } - - const formDataToSend = new FormData(); - formDataToSend.append('type', type); - formDataToSend.append('name', name); - formDataToSend.append('location', location || ''); - formDataToSend.append('date', date || ''); - formDataToSend.append('description', description || ''); - formDataToSend.append('latitude', latitude || ''); - formDataToSend.append('longitude', longitude || ''); - formDataToSend.append('is_public', is_public.toString()); - - let csrfToken = await fetchCSRFToken(); - - if (activity_types) { - // Filter out empty and duplicate activity types, then trim each activity type - const cleanedActivityTypes = Array.from( - new Set( - activity_types - .map((activity_type) => activity_type.trim()) - .filter((activity_type) => activity_type !== '' && activity_type !== ',') - ) - ); - - // Append each cleaned activity type to formDataToSend - cleanedActivityTypes.forEach((activity_type) => { - formDataToSend.append('activity_types', activity_type); - }); - } else { - let res = await fetch(`${serverEndpoint}/api/adventures/${adventureId}/`, { - method: 'PATCH', - headers: { - Cookie: `${event.cookies.get('auth')}`, - 'X-CSRFToken': csrfToken, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ activity_types: [] }) - }); - if (!res.ok) { - const errorBody = await res.json(); - return { - status: res.status, - body: { error: errorBody } - }; - } - } - formDataToSend.append('rating', rating ? rating.toString() : ''); - formDataToSend.append('link', link || ''); - - if (image && image.size > 0) { - formDataToSend.append('image', image); - } - - let auth = event.cookies.get('auth'); - - if (!auth) { - const refresh = event.cookies.get('refresh'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - - if (!auth) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - - if (!csrfToken) { - return { - status: 500, - body: { message: 'Failed to fetch CSRF token' } - }; - } - - const res = await fetch(`${serverEndpoint}/api/adventures/${adventureId}/`, { - method: 'PATCH', - headers: { - 'X-CSRFToken': csrfToken, - Cookie: auth - }, - body: formDataToSend - }); - - if (!res.ok) { - const errorBody = await res.json(); - return { - status: res.status, - body: { error: errorBody } - }; - } - - let adventure = await res.json(); - - let image_url = adventure.image; - let link_url = adventure.link; - return { image_url, link_url }; - }, image: async (event) => { let formData = await event.request.formData(); + let csrfToken = await fetchCSRFToken(); + let sessionId = event.cookies.get('sessionid'); let res = await fetch(`${serverEndpoint}/api/images/`, { method: 'POST', headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`, + 'X-CSRFToken': csrfToken }, body: formData }); diff --git a/frontend/src/routes/adventures/+page.svelte b/frontend/src/routes/adventures/+page.svelte index 5ed3be5..ea5e415 100644 --- a/frontend/src/routes/adventures/+page.svelte +++ b/frontend/src/routes/adventures/+page.svelte @@ -226,7 +226,6 @@ {#each adventures as adventure} { const id = event.params as { id: string }; let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' }); if (!request.ok) { console.error('Failed to fetch adventure ' + id.id); @@ -24,8 +25,9 @@ export const load = (async (event) => { if (adventure.collection) { let res2 = await fetch(`${endpoint}/api/collections/${adventure.collection}/`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' }); collection = await res2.json(); } @@ -39,8 +41,8 @@ export const load = (async (event) => { } }) satisfies PageServerLoad; -import type { Actions } from '@sveltejs/kit'; -import { tryRefreshToken } from '$lib/index.server'; +import { redirect, type Actions } from '@sveltejs/kit'; +import { fetchCSRFToken } from '$lib/index.server'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -50,29 +52,7 @@ export const actions: Actions = { const adventureId = id.id; if (!event.locals.user) { - const refresh = event.cookies.get('refresh'); - let auth = event.cookies.get('auth'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } + return redirect(302, '/login'); } if (!adventureId) { return { @@ -81,12 +61,15 @@ export const actions: Actions = { }; } + let csrfToken = await fetchCSRFToken(); + let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}`, { method: 'DELETE', headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' - } + Cookie: `sessionid=${event.cookies.get('sessionid')}; csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken + }, + credentials: 'include' }); console.log(res); if (!res.ok) { diff --git a/frontend/src/routes/api/[...path]/+server.ts b/frontend/src/routes/api/[...path]/+server.ts index 981debb..33c2e2a 100644 --- a/frontend/src/routes/api/[...path]/+server.ts +++ b/frontend/src/routes/api/[...path]/+server.ts @@ -1,69 +1,77 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +import { fetchCSRFToken } from '$lib/index.server'; import { json } from '@sveltejs/kit'; /** @type {import('./$types').RequestHandler} */ -export async function GET({ url, params, request, fetch, cookies }) { - // add the param format = json to the url or add additional if anothre param is already present - if (url.search) { - url.search = url.search + '&format=json'; - } else { - url.search = '?format=json'; - } - return handleRequest(url, params, request, fetch, cookies); +export async function GET(event) { + const { url, params, request, fetch, cookies } = event; + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam); } /** @type {import('./$types').RequestHandler} */ export async function POST({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies, true); + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam, true); } export async function PATCH({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies, true); + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam, true); } export async function PUT({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies, true); + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam, true); } export async function DELETE({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies, true); + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam, true); } -// Implement other HTTP methods as needed (PUT, DELETE, etc.) - async function handleRequest( url: any, params: any, request: any, fetch: any, cookies: any, + searchParam: string, requreTrailingSlash: boolean | undefined = false ) { const path = params.path; - let targetUrl = `${endpoint}/api/${path}${url.search}`; + let targetUrl = `${endpoint}/api/${path}`; + // Ensure the path ends with a trailing slash if (requreTrailingSlash && !targetUrl.endsWith('/')) { targetUrl += '/'; } + // Append query parameters to the path correctly + targetUrl += searchParam; // This will add ?format=json or &format=json to the URL + const headers = new Headers(request.headers); - const authCookie = cookies.get('auth'); - - if (authCookie) { - headers.set('Cookie', `${authCookie}`); + const csrfToken = await fetchCSRFToken(); + if (!csrfToken) { + return json({ error: 'CSRF token is missing or invalid' }, { status: 400 }); } try { const response = await fetch(targetUrl, { method: request.method, - headers: headers, - body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined + headers: { + ...Object.fromEntries(headers), + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}` + }, + body: + request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined, + credentials: 'include' // This line ensures cookies are sent with the request }); if (response.status === 204) { - // For 204 No Content, return a response with no body return new Response(null, { status: 204, headers: response.headers @@ -71,10 +79,13 @@ async function handleRequest( } const responseData = await response.text(); + // Create a new Headers object without the 'set-cookie' header + const cleanHeaders = new Headers(response.headers); + cleanHeaders.delete('set-cookie'); return new Response(responseData, { status: response.status, - headers: response.headers + headers: cleanHeaders }); } catch (error) { console.error('Error forwarding request:', error); diff --git a/frontend/src/routes/calendar/+page.server.ts b/frontend/src/routes/calendar/+page.server.ts new file mode 100644 index 0000000..6c9ede0 --- /dev/null +++ b/frontend/src/routes/calendar/+page.server.ts @@ -0,0 +1,47 @@ +import type { Adventure } from '$lib/types'; +import type { PageServerLoad } from './$types'; +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + +export const load = (async (event) => { + let sessionId = event.cookies.get('sessionid'); + let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, { + headers: { + Cookie: `sessionid=${sessionId}` + } + }); + let adventures = (await visitedFetch.json()) as Adventure[]; + + let dates: Array<{ + id: string; + start: string; + end: string; + title: string; + backgroundColor?: string; + }> = []; + adventures.forEach((adventure) => { + adventure.visits.forEach((visit) => { + dates.push({ + id: adventure.id, + start: visit.start_date, + end: visit.end_date || visit.start_date, + title: adventure.name + (adventure.category?.icon ? ' ' + adventure.category.icon : '') + }); + }); + }); + + let icsFetch = await fetch(`${endpoint}/api/ics-calendar/generate`, { + headers: { + Cookie: `sessionid=${sessionId}` + } + }); + let ics_calendar = await icsFetch.text(); + + return { + props: { + adventures, + dates, + ics_calendar + } + }; +}) satisfies PageServerLoad; diff --git a/frontend/src/routes/calendar/+page.svelte b/frontend/src/routes/calendar/+page.svelte new file mode 100644 index 0000000..476519d --- /dev/null +++ b/frontend/src/routes/calendar/+page.svelte @@ -0,0 +1,37 @@ + + +

    {$t('adventures.adventure_calendar')}

    + + + + + diff --git a/frontend/src/routes/collections/+page.server.ts b/frontend/src/routes/collections/+page.server.ts index 9d2477f..f88e5ee 100644 --- a/frontend/src/routes/collections/+page.server.ts +++ b/frontend/src/routes/collections/+page.server.ts @@ -4,7 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { Adventure, Collection } from '$lib/types'; import type { Actions, RequestEvent } from '@sveltejs/kit'; -import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; +import { fetchCSRFToken } from '$lib/index.server'; import { checkLink } from '$lib'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -17,10 +17,12 @@ export const load = (async (event) => { let previous = null; let count = 0; let adventures: Adventure[] = []; + let sessionId = event.cookies.get('sessionid'); let initialFetch = await fetch(`${serverEndpoint}/api/collections/?order_by=updated_at`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${sessionId}` + }, + credentials: 'include' }); if (!initialFetch.ok) { console.error('Failed to fetch visited adventures'); @@ -72,34 +74,9 @@ export const actions: Actions = { formDataToSend.append('start_date', start_date || ''); formDataToSend.append('end_date', end_date || ''); formDataToSend.append('link', link || ''); - let auth = event.cookies.get('auth'); + let sessionid = event.cookies.get('sessionid'); - if (!auth) { - const refresh = event.cookies.get('refresh'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - - if (!auth) { + if (!sessionid) { return { status: 401, body: { message: 'Unauthorized' } @@ -119,7 +96,7 @@ export const actions: Actions = { method: 'POST', headers: { 'X-CSRFToken': csrfToken, - Cookie: auth + Cookie: `sessionid=${sessionid}; csrftoken=${csrfToken}` }, body: formDataToSend }); @@ -175,34 +152,9 @@ export const actions: Actions = { formDataToSend.append('end_date', end_date || ''); formDataToSend.append('link', link || ''); - let auth = event.cookies.get('auth'); + let sessionId = event.cookies.get('sessionid'); - if (!auth) { - const refresh = event.cookies.get('refresh'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - - if (!auth) { + if (!sessionId) { return { status: 401, body: { message: 'Unauthorized' } @@ -222,9 +174,10 @@ export const actions: Actions = { method: 'PATCH', headers: { 'X-CSRFToken': csrfToken, - Cookie: auth + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}` }, - body: formDataToSend + body: formDataToSend, + credentials: 'include' }); if (!res.ok) { @@ -241,6 +194,10 @@ export const actions: Actions = { }, get: async (event) => { if (!event.locals.user) { + return { + status: 401, + body: { message: 'Unauthorized' } + }; } const formData = await event.request.formData(); @@ -263,19 +220,20 @@ export const actions: Actions = { let previous = null; let count = 0; - let visitedFetch = await fetch( + let collectionsFetch = await fetch( `${serverEndpoint}/api/collections/?order_by=${order_by}&order_direction=${order_direction}`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' } ); - if (!visitedFetch.ok) { + if (!collectionsFetch.ok) { console.error('Failed to fetch visited adventures'); return redirect(302, '/login'); } else { - let res = await visitedFetch.json(); + let res = await collectionsFetch.json(); let visited = res.results as Adventure[]; next = res.next; previous = res.previous; @@ -332,15 +290,16 @@ export const actions: Actions = { } const fullUrl = `${serverEndpoint}${url}`; - console.log(fullUrl); - console.log(serverEndpoint); + + let sessionId = event.cookies.get('sessionid'); try { const response = await fetch(fullUrl, { headers: { 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${sessionId}` + }, + credentials: 'include' }); if (!response.ok) { diff --git a/frontend/src/routes/collections/[id]/+page.server.ts b/frontend/src/routes/collections/[id]/+page.server.ts index 80d7ef9..bf54a5b 100644 --- a/frontend/src/routes/collections/[id]/+page.server.ts +++ b/frontend/src/routes/collections/[id]/+page.server.ts @@ -6,9 +6,10 @@ const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { const id = event.params as { id: string }; + let sessionid = event.cookies.get('sessionid'); let request = await fetch(`${endpoint}/api/collections/${id.id}/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionid}` } }); if (!request.ok) { @@ -30,7 +31,7 @@ export const load = (async (event) => { }) satisfies PageServerLoad; import type { Actions } from '@sveltejs/kit'; -import { tryRefreshToken } from '$lib/index.server'; +import { fetchCSRFToken } from '$lib/index.server'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -39,31 +40,6 @@ export const actions: Actions = { const id = event.params as { id: string }; const adventureId = id.id; - if (!event.locals.user) { - const refresh = event.cookies.get('refresh'); - let auth = event.cookies.get('auth'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } if (!adventureId) { return { status: 400, @@ -71,15 +47,27 @@ export const actions: Actions = { }; } + let sessionId = event.cookies.get('sessionid'); + + if (!sessionId) { + return { + status: 401, + error: new Error('Unauthorized') + }; + } + + let csrfToken = await fetchCSRFToken(); + let res = await fetch(`${serverEndpoint}/api/collections/${event.params.id}`, { method: 'DELETE', headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' - } + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include' }); - console.log(res); if (!res.ok) { return { status: res.status, diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 846d763..10bc85b 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -398,7 +398,6 @@ user={data.user} on:edit={editAdventure} on:delete={deleteAdventure} - type={adventure.type} {adventure} {collection} /> @@ -521,7 +520,6 @@ user={data.user} on:edit={editAdventure} on:delete={deleteAdventure} - type={adventure.type} {adventure} /> {/each} diff --git a/frontend/src/routes/collections/archived/+page.server.ts b/frontend/src/routes/collections/archived/+page.server.ts index c951a3a..7b0f8c5 100644 --- a/frontend/src/routes/collections/archived/+page.server.ts +++ b/frontend/src/routes/collections/archived/+page.server.ts @@ -8,13 +8,11 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { - let next = null; - let previous = null; - let count = 0; + let sessionId = event.cookies.get('sessionid'); let adventures: Adventure[] = []; let initialFetch = await fetch(`${serverEndpoint}/api/collections/archived/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!initialFetch.ok) { diff --git a/frontend/src/routes/dashboard/+page.server.ts b/frontend/src/routes/dashboard/+page.server.ts new file mode 100644 index 0000000..5db5aef --- /dev/null +++ b/frontend/src/routes/dashboard/+page.server.ts @@ -0,0 +1,53 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; +import type { Adventure } from '$lib/types'; + +const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + +export const load = (async (event) => { + if (!event.locals.user) { + return redirect(302, '/login'); + } else { + let adventures: Adventure[] = []; + + let initialFetch = await event.fetch(`${serverEndpoint}/api/adventures/`, { + headers: { + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' + }); + + let stats = null; + + let res = await event.fetch(`${serverEndpoint}/api/stats/counts/`, { + headers: { + Cookie: `sessionid=${event.cookies.get('sessionid')}` + } + }); + if (!res.ok) { + console.error('Failed to fetch user stats'); + } else { + stats = await res.json(); + } + + if (!initialFetch.ok) { + let error_message = await initialFetch.json(); + console.error(error_message); + console.error('Failed to fetch visited adventures'); + return redirect(302, '/login'); + } else { + let res = await initialFetch.json(); + let visited = res.results as Adventure[]; + // only get the first 3 adventures or less if there are less than 3 + adventures = visited.slice(0, 3); + } + + return { + props: { + adventures, + stats + } + }; + } +}) satisfies PageServerLoad; diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte new file mode 100644 index 0000000..2115977 --- /dev/null +++ b/frontend/src/routes/dashboard/+page.svelte @@ -0,0 +1,102 @@ + + +
    + +
    +

    {$t('dashboard.welcome_back')}, {user?.first_name}!

    +
    + + +
    +
    +
    + +
    +
    {$t('dashboard.countries_visited')}
    +
    {stats.country_count}
    +
    +
    +
    + +
    +
    {$t('dashboard.total_adventures')}
    +
    {stats.adventure_count}
    +
    +
    +
    + +
    +
    {$t('dashboard.total_visited_regions')}
    +
    {stats.visited_region_count}
    +
    +
    + + + {#if recentAdventures.length > 0} +

    {$t('dashboard.recent_adventures')}

    +
    + {#each recentAdventures as adventure} + + {/each} +
    + {/if} + + + {#if recentAdventures.length === 0} +
    +

    {$t('dashboard.no_recent_adventures')}

    +

    + {$t('dashboard.add_some')} +

    + {$t('map.add_adventure')} +
    + {/if} +
    + + + Dashboard | AdventureLog + + diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index fcafe72..1b50518 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -1,7 +1,8 @@ -import { fail, redirect } from '@sveltejs/kit'; +import { fail, redirect, type RequestEvent } from '@sveltejs/kit'; -import type { Actions, PageServerLoad } from './$types'; +import type { Actions, PageServerLoad, RouteParams } from './$types'; import { getRandomBackground, getRandomQuote } from '$lib'; +import { fetchCSRFToken } from '$lib/index.server'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; export const load: PageServerLoad = async (event) => { @@ -24,65 +25,99 @@ export const actions: Actions = { default: async (event) => { const formData = await event.request.formData(); const formUsername = formData.get('username'); - const formPassword = formData.get('password'); - - let username = formUsername?.toString().toLocaleLowerCase(); - + const username = formUsername?.toString().toLowerCase(); const password = formData.get('password'); + const totp = formData.get('totp'); const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; - const csrfTokenFetch = await event.fetch(`${serverEndpoint}/csrf/`); + const csrfToken = await fetchCSRFToken(); - if (!csrfTokenFetch.ok) { - console.error('Failed to fetch CSRF token'); - event.locals.user = null; - return fail(500, { - message: 'Failed to fetch CSRF token' - }); - } - - const tokenPromise = await csrfTokenFetch.json(); - const csrfToken = tokenPromise.csrfToken; - - const loginFetch = await event.fetch(`${serverEndpoint}/auth/login/`, { + // Initial login attempt + const loginFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/login`, { method: 'POST', headers: { 'X-CSRFToken': csrfToken, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Cookie: `csrftoken=${csrfToken}` }, - body: JSON.stringify({ - username, - password - }) + body: JSON.stringify({ username, password }), + credentials: 'include' }); - const loginResponse = await loginFetch.json(); - if (!loginFetch.ok) { - // get the value of the first key in the object - const firstKey = Object.keys(loginResponse)[0] || 'error'; - const error = loginResponse[firstKey][0] || 'Invalid username or password'; - return fail(400, { - message: error - }); - } else { - const token = loginResponse.access; - const tokenFormatted = `auth=${token}`; - const refreshToken = `${loginResponse.refresh}`; - event.cookies.set('auth', tokenFormatted, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/', - secure: false - }); - event.cookies.set('refresh', refreshToken, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year - path: '/', - secure: false - }); + if (loginFetch.status === 200) { + // Login successful without MFA + handleSuccessfulLogin(event, loginFetch); return redirect(302, '/'); + } else if (loginFetch.status === 401) { + // MFA required + if (!totp) { + return fail(401, { + message: 'settings.mfa_required', + mfa_required: true + }); + } else { + // Attempt MFA authentication + const sessionId = extractSessionId(loginFetch.headers.get('Set-Cookie')); + const mfaLoginFetch = await event.fetch( + `${serverEndpoint}/_allauth/browser/v1/auth/2fa/authenticate`, + { + method: 'POST', + headers: { + 'X-CSRFToken': csrfToken, + 'Content-Type': 'application/json', + Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}` + }, + body: JSON.stringify({ code: totp }), + credentials: 'include' + } + ); + + if (mfaLoginFetch.ok) { + // MFA successful + handleSuccessfulLogin(event, mfaLoginFetch); + return redirect(302, '/'); + } else { + // MFA failed + const mfaLoginResponse = await mfaLoginFetch.json(); + return fail(401, { + message: mfaLoginResponse.error || 'settings.invalid_code', + mfa_required: true + }); + } + } + } else { + // Login failed + const loginResponse = await loginFetch.json(); + const firstKey = Object.keys(loginResponse)[0] || 'error'; + const error = loginResponse[firstKey][0] || 'settings.invalid_credentials'; + return fail(400, { message: error }); } } }; + +function handleSuccessfulLogin(event: RequestEvent, response: Response) { + const setCookieHeader = response.headers.get('Set-Cookie'); + if (setCookieHeader) { + const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/; + const match = setCookieHeader.match(sessionIdRegex); + if (match) { + const [, sessionId, expiryString] = match; + event.cookies.set('sessionid', sessionId, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: true, + expires: new Date(expiryString) + }); + } + } +} + +function extractSessionId(setCookieHeader: string | null) { + if (setCookieHeader) { + const sessionIdRegex = /sessionid=([^;]+)/; + const match = setCookieHeader.match(sessionIdRegex); + return match ? match[1] : ''; + } + return ''; +} diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index e126f99..3bf6c98 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -51,12 +51,21 @@ id="password" class="block input input-bordered w-full max-w-xs" />
    + {#if $page.form?.mfa_required} + +
    + {/if} @@ -64,7 +73,7 @@ {#if ($page.form?.message && $page.form?.message.length > 1) || $page.form?.type === 'error'}
    - {$page.form.message || $t('auth.login_error')} + {$t($page.form.message) || $t('auth.login_error')}
    {/if}
    diff --git a/frontend/src/routes/map/+page.server.ts b/frontend/src/routes/map/+page.server.ts index 98d036c..96db704 100644 --- a/frontend/src/routes/map/+page.server.ts +++ b/frontend/src/routes/map/+page.server.ts @@ -8,15 +8,16 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { + let sessionId = event.cookies.get('sessionid'); let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); let visitedRegionsFetch = await fetch(`${endpoint}/api/visitedregion/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); diff --git a/frontend/src/routes/profile/+page.server.ts b/frontend/src/routes/profile/+page.server.ts index 2e4e39a..825a867 100644 --- a/frontend/src/routes/profile/+page.server.ts +++ b/frontend/src/routes/profile/+page.server.ts @@ -4,15 +4,16 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; export const load: PageServerLoad = async (event: RequestEvent) => { const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; - if (!event.locals.user || !event.cookies.get('auth')) { + if (!event.locals.user || !event.cookies.get('sessionid')) { return redirect(302, '/login'); } + let sessionId = event.cookies.get('sessionid'); let stats = null; let res = await event.fetch(`${endpoint}/api/stats/counts/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/src/routes/search/+page.server.ts b/frontend/src/routes/search/+page.server.ts index cf8c4f6..8ff7e23 100644 --- a/frontend/src/routes/search/+page.server.ts +++ b/frontend/src/routes/search/+page.server.ts @@ -14,12 +14,14 @@ export const load = (async (event) => { return { data: [] }; } + let sessionId = event.cookies.get('sessionid'); + let res = await fetch( `${serverEndpoint}/api/adventures/search/?query=${query}&property=${property}`, { headers: { 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } } ); diff --git a/frontend/src/routes/search/+page.svelte b/frontend/src/routes/search/+page.svelte index 83728fd..ac3735e 100644 --- a/frontend/src/routes/search/+page.svelte +++ b/frontend/src/routes/search/+page.svelte @@ -155,7 +155,6 @@ {#each myAdventures as adventure} {$t('search.public_adventures')}
    {#each publicAdventures as adventure} - + {/each}
    {/if} diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts index 9b97697..beb8432 100644 --- a/frontend/src/routes/settings/+page.server.ts +++ b/frontend/src/routes/settings/+page.server.ts @@ -2,29 +2,65 @@ import { fail, redirect, type Actions } from '@sveltejs/kit'; import type { PageServerLoad } from '../$types'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { User } from '$lib/types'; +import { fetchCSRFToken } from '$lib/index.server'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +type MFAAuthenticatorResponse = { + status: number; + data: { + type: string; + created_at: number; + last_used_at: number | null; + total_code_count?: number; + unused_code_count?: number; + }[]; +}; + export const load: PageServerLoad = async (event) => { if (!event.locals.user) { return redirect(302, '/'); } - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/'); } - let res = await fetch(`${endpoint}/auth/user/`, { + let res = await fetch(`${endpoint}/auth/user-metadata/`, { headers: { - Cookie: event.cookies.get('auth') || '' + Cookie: `sessionid=${sessionId}` } }); let user = (await res.json()) as User; - if (!res.ok) { + let emailFetch = await fetch(`${endpoint}/_allauth/browser/v1/account/email`, { + headers: { + Cookie: `sessionid=${sessionId}` + } + }); + let emailResponse = (await emailFetch.json()) as { + status: number; + data: { email: string; verified: boolean; primary: boolean }[]; + }; + let emails = emailResponse.data; + if (!res.ok || !emailFetch.ok) { return redirect(302, '/'); } + let mfaAuthenticatorFetch = await fetch( + `${endpoint}/_allauth/browser/v1/account/authenticators`, + { + headers: { + Cookie: `sessionid=${sessionId}` + } + } + ); + let mfaAuthenticatorResponse = (await mfaAuthenticatorFetch.json()) as MFAAuthenticatorResponse; + let authenticators = (mfaAuthenticatorResponse.data.length > 0) as boolean; + return { props: { - user + user, + emails, + authenticators } }; }; @@ -34,7 +70,8 @@ export const actions: Actions = { if (!event.locals.user) { return redirect(302, '/'); } - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/'); } @@ -47,9 +84,9 @@ export const actions: Actions = { let profile_pic = formData.get('profile_pic') as File | null | undefined; let public_profile = formData.get('public_profile') as string | null | undefined | boolean; - const resCurrent = await fetch(`${endpoint}/auth/user/`, { + const resCurrent = await fetch(`${endpoint}/auth/user-metadata/`, { headers: { - Cookie: event.cookies.get('auth') || '' + Cookie: `sessionid=${sessionId}` } }); @@ -57,12 +94,12 @@ export const actions: Actions = { return fail(resCurrent.status, await resCurrent.json()); } + // Gets the boolean value of the public_profile input if (public_profile === 'on') { public_profile = true; } else { public_profile = false; } - console.log(public_profile); let currentUser = (await resCurrent.json()) as User; @@ -80,6 +117,7 @@ export const actions: Actions = { } let formDataToSend = new FormData(); + if (username) { formDataToSend.append('username', username); } @@ -94,10 +132,13 @@ export const actions: Actions = { } formDataToSend.append('public_profile', public_profile.toString()); - let res = await fetch(`${endpoint}/auth/user/`, { + let csrfToken = await fetchCSRFToken(); + + let res = await fetch(`${endpoint}/auth/update-user/`, { method: 'PATCH', headers: { - Cookie: event.cookies.get('auth') || '' + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken }, body: formDataToSend }); @@ -105,47 +146,53 @@ export const actions: Actions = { let response = await res.json(); if (!res.ok) { - // change the first key in the response to 'message' for the fail function - response = { message: Object.values(response)[0] }; return fail(res.status, response); } return { success: true }; } catch (error) { console.error('Error:', error); - return { error: 'An error occurred while processing your request.' }; + return { error: 'settings.generic_error' }; } }, changePassword: async (event) => { if (!event.locals.user) { return redirect(302, '/'); } - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/'); } - console.log('changePassword'); + const formData = await event.request.formData(); const password1 = formData.get('password1') as string | null | undefined; const password2 = formData.get('password2') as string | null | undefined; + const current_password = formData.get('current_password') as string | null | undefined; if (password1 !== password2) { - return fail(400, { message: 'Passwords do not match' }); + return fail(400, { message: 'settings.password_does_not_match' }); + } + if (!current_password) { + return fail(400, { message: 'settings.password_is_required' }); } - let res = await fetch(`${endpoint}/auth/password/change/`, { + let csrfToken = await fetchCSRFToken(); + + let res = await fetch(`${endpoint}/_allauth/browser/v1/account/password/change`, { method: 'POST', headers: { - Cookie: event.cookies.get('auth') || '', + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json' }, body: JSON.stringify({ - new_password1: password1, - new_password2: password2 + current_password, + new_password: password1 }) }); if (!res.ok) { - return fail(res.status, await res.json()); + return fail(res.status, { message: 'settings.error_change_password' }); } return { success: true }; }, @@ -153,19 +200,22 @@ export const actions: Actions = { if (!event.locals.user) { return redirect(302, '/'); } - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/'); } const formData = await event.request.formData(); const new_email = formData.get('new_email') as string | null | undefined; if (!new_email) { - return fail(400, { message: 'Email is required' }); + return fail(400, { message: 'auth.email_required' }); } else { + let csrfToken = await fetchCSRFToken(); let res = await fetch(`${endpoint}/auth/change-email/`, { method: 'POST', headers: { - Cookie: event.cookies.get('auth') || '', - 'Content-Type': 'application/json' + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken }, body: JSON.stringify({ new_email diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 6ec3221..2ad898b 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -6,13 +6,20 @@ import { onMount } from 'svelte'; import { browser } from '$app/environment'; import { t } from 'svelte-i18n'; + import TotpModal from '$lib/components/TOTPModal.svelte'; export let data; let user: User; + let emails: typeof data.props.emails; if (data.user) { user = data.user; + emails = data.props.emails; } + let new_email: string = ''; + + let isMFAModalOpen: boolean = false; + onMount(async () => { if (browser) { const queryParams = new URLSearchParams($page.url.search); @@ -34,16 +41,6 @@ } } - // async function exportAdventures() { - // const url = await exportData(); - - // const a = document.createElement('a'); - // a.href = url; - // a.download = 'adventure-log-export.json'; - // a.click(); - // URL.revokeObjectURL(url); - // } - async function checkVisitedRegions() { let res = await fetch('/api/reverse-geocode/mark_visited_region/', { method: 'POST', @@ -58,8 +55,105 @@ addToast('error', $t('adventures.error_updating_regions')); } } + + async function removeEmail(email: { email: any; verified?: boolean; primary?: boolean }) { + let res = await fetch('/_allauth/browser/v1/account/email/', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email: email.email }) + }); + if (res.ok) { + addToast('success', $t('settings.email_removed')); + emails = emails.filter((e) => e.email !== email.email); + } else { + addToast('error', $t('settings.email_removed_error')); + } + } + + async function verifyEmail(email: { email: any; verified?: boolean; primary?: boolean }) { + let res = await fetch('/_allauth/browser/v1/account/email/', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email: email.email }) + }); + if (res.ok) { + addToast('success', $t('settings.verify_email_success')); + } else { + addToast('error', $t('settings.verify_email_error')); + } + } + + async function addEmail() { + let res = await fetch('/_allauth/browser/v1/account/email/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email: new_email }) + }); + if (res.ok) { + addToast('success', $t('settings.email_added')); + emails = [...emails, { email: new_email, verified: false, primary: false }]; + new_email = ''; + } else { + let error = await res.json(); + let error_code = error.errors[0].code; + addToast('error', $t(`settings.${error_code}`) || $t('settings.generic_error')); + } + } + + async function primaryEmail(email: { email: any; verified?: boolean; primary?: boolean }) { + let res = await fetch('/_allauth/browser/v1/account/email/', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ email: email.email, primary: true }) + }); + if (res.ok) { + addToast('success', $t('settings.email_set_primary')); + // remove primary from all other emails and set this one as primary + emails = emails.map((e) => { + if (e.email === email.email) { + e.primary = true; + } else { + e.primary = false; + } + return e; + }); + } else { + addToast('error', $t('settings.email_set_primary_error')); + } + } + + async function disableMfa() { + const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', { + method: 'DELETE' + }); + if (res.ok) { + addToast('success', $t('settings.mfa_disabled')); + data.props.authenticators = false; + } else { + if (res.status == 401) { + addToast('error', $t('settings.reset_session_error')); + } + addToast('error', $t('settings.generic_error')); + } + } +{#if isMFAModalOpen} + (isMFAModalOpen = false)} + bind:is_enabled={data.props.authenticators} + /> +{/if} +

    {$t('settings.settings_page')}

    {$t('settings.account_settings')}

    @@ -95,14 +189,6 @@ id="last_name" class="block mb-2 input input-bordered w-full max-w-xs" />
    - - {$page.form?.message} + {$t($page.form.message)}
    {/if}

    {$t('settings.password_change')}

    -
    + + +
    - +
    + +

    {$t('settings.email_change')}

    -
    -
    - - -
    - - + +
    +
    + {#each emails as email} +

    + {email.email} + {#if email.verified} +

    {$t('settings.verified')}
    + {:else} +
    {$t('settings.not_verified')}
    + {/if} + {#if email.primary} +
    {$t('settings.primary')}
    + {/if} + {#if !email.verified} + + {/if} + {#if !email.primary} + + {/if} + +

    + {/each} + {#if emails.length === 0} +

    {$t('settings.no_emai_set')}

    + {/if} +
    +
    + +
    + +
    + +
    +
    + +
    +

    {$t('settings.mfa_page_title')}

    + +
    +
    + {#if !data.props.authenticators} +

    {$t('settings.mfa_not_enabled')}

    + + {:else} + + {/if} +
    +
    +

    {$t('adventures.visited_region_check')} @@ -189,22 +332,15 @@

    {$t('adventures.visited_region_check_desc')}

    +

    {$t('adventures.update_visited_regions_disclaimer')}

    + -

    {$t('adventures.update_visited_regions_disclaimer')}

    - For Debug Use: Server PK={user.pk} | Date Joined: {user.date_joined - ? new Date(user.date_joined).toDateString() - : ''} | Staff user: {user.is_staff}For Debug Use: UUID={user.uuid} | Staff user: {user.is_staff} diff --git a/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts b/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts deleted file mode 100644 index 9aeca23..0000000 --- a/frontend/src/routes/settings/forgot-password/confirm/+page.server.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { fail, redirect, type Actions } from '@sveltejs/kit'; -import type { PageServerLoad } from './$types'; -const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; -const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; - -export const load = (async (event) => { - const token = event.url.searchParams.get('token'); - const uid = event.url.searchParams.get('uid'); - - return { - props: { - token, - uid - } - }; -}) satisfies PageServerLoad; - -export const actions: Actions = { - reset: async (event) => { - const formData = await event.request.formData(); - - const new_password1 = formData.get('new_password1') as string; - const new_password2 = formData.get('new_password2') as string; - const token = formData.get('token') as string; - const uid = formData.get('uid') as string; - - if (!new_password1 || !new_password2) { - return fail(400, { message: 'settings.password_is_required' }); - } - - if (new_password1 !== new_password2) { - return fail(400, { message: 'settings.password_does_not_match' }); - } - - if (!token || !uid) { - return redirect(302, '/settings/forgot-password'); - } else { - let response = await fetch(`${serverEndpoint}/auth/password/reset/confirm/`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - token: token, - uid: uid, - new_password1, - new_password2 - }) - }); - if (!response.ok) { - return fail(response.status, { message: 'settings.invalid_token' }); - } else { - return redirect(302, '/login'); - } - } - } -}; diff --git a/frontend/src/routes/settings/forgot-password/confirm/+page.svelte b/frontend/src/routes/settings/forgot-password/confirm/+page.svelte deleted file mode 100644 index 06bc156..0000000 --- a/frontend/src/routes/settings/forgot-password/confirm/+page.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - -

    {$t('settings.change_password')}

    - -{#if data.props.token && data.props.uid} -

    {$t('settings.login_redir')}

    - -{:else} -
    -
    -

    {$t('settings.token_required')}

    - - -
    -
    -{/if} - - - Password Reset Confirm - - diff --git a/frontend/src/routes/shared/+page.server.ts b/frontend/src/routes/shared/+page.server.ts index b40dbac..d5c1337 100644 --- a/frontend/src/routes/shared/+page.server.ts +++ b/frontend/src/routes/shared/+page.server.ts @@ -8,9 +8,10 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { + let sessionId = event.cookies.get('sessionid'); let res = await fetch(`${serverEndpoint}/api/collections/shared/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/src/routes/signup/+page.server.ts b/frontend/src/routes/signup/+page.server.ts index ad24805..4a1d857 100644 --- a/frontend/src/routes/signup/+page.server.ts +++ b/frontend/src/routes/signup/+page.server.ts @@ -41,22 +41,26 @@ export const actions: Actions = { if (!csrfTokenFetch.ok) { event.locals.user = null; - return fail(500, { message: 'Failed to fetch CSRF token' }); + return fail(500, { message: 'settings.csrf_failed' }); + } + + if (password1 !== password2) { + return fail(400, { message: 'settings.password_does_not_match' }); } const tokenPromise = await csrfTokenFetch.json(); const csrfToken = tokenPromise.csrfToken; - const loginFetch = await event.fetch(`${serverEndpoint}/auth/registration/`, { + const loginFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/signup`, { method: 'POST', headers: { 'X-CSRFToken': csrfToken, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Cookie: `csrftoken=${csrfToken}` }, body: JSON.stringify({ username: username, - password1: password1, - password2: password2, + password: password1, email: email, first_name, last_name @@ -65,31 +69,36 @@ export const actions: Actions = { const loginResponse = await loginFetch.json(); if (!loginFetch.ok) { - // get the value of the first key in the object - const firstKey = Object.keys(loginResponse)[0] || 'error'; - const error = - loginResponse[firstKey][0] || 'Failed to register user. Check your inputs and try again.'; - return fail(400, { - message: error - }); + return fail(loginFetch.status, { message: loginResponse.errors[0].code }); } else { - const token = loginResponse.access; - const tokenFormatted = `auth=${token}`; - const refreshToken = `${loginResponse.refresh}`; - event.cookies.set('auth', tokenFormatted, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - event.cookies.set('refresh', refreshToken, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year - path: '/' - }); + const setCookieHeader = loginFetch.headers.get('Set-Cookie'); - return redirect(302, '/'); + console.log('setCookieHeader:', setCookieHeader); + + if (setCookieHeader) { + // Regular expression to match sessionid cookie and its expiry + const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/; + const match = setCookieHeader.match(sessionIdRegex); + + if (match) { + const sessionId = match[1]; + const expiryString = match[2]; + const expiryDate = new Date(expiryString); + + console.log('Session ID:', sessionId); + console.log('Expiry Date:', expiryDate); + + // Set the sessionid cookie + event.cookies.set('sessionid', sessionId, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: true, + expires: expiryDate + }); + } + } + redirect(302, '/'); } } }; diff --git a/frontend/src/routes/signup/+page.svelte b/frontend/src/routes/signup/+page.svelte index fb3d7b0..6643e06 100644 --- a/frontend/src/routes/signup/+page.svelte +++ b/frontend/src/routes/signup/+page.svelte @@ -87,16 +87,14 @@
    {#if $page.form?.message} -
    {$page.form?.message}
    +
    {$t($page.form?.message)}
    {/if} {:else}
    diff --git a/frontend/src/routes/user/[uuid]/+page.server.ts b/frontend/src/routes/user/[uuid]/+page.server.ts index 882082f..aa374c3 100644 --- a/frontend/src/routes/user/[uuid]/+page.server.ts +++ b/frontend/src/routes/user/[uuid]/+page.server.ts @@ -4,7 +4,8 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/login'); } const uuid = event.params.uuid; @@ -13,7 +14,7 @@ export const load = (async (event) => { } let res = await fetch(`${serverEndpoint}/auth/user/${uuid}/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/src/routes/settings/forgot-password/+page.server.ts b/frontend/src/routes/user/reset-password/+page.server.ts similarity index 65% rename from frontend/src/routes/settings/forgot-password/+page.server.ts rename to frontend/src/routes/user/reset-password/+page.server.ts index 82fc304..f91db59 100644 --- a/frontend/src/routes/settings/forgot-password/+page.server.ts +++ b/frontend/src/routes/user/reset-password/+page.server.ts @@ -1,3 +1,4 @@ +import { fetchCSRFToken } from '$lib/index.server'; import { fail, type Actions } from '@sveltejs/kit'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; @@ -13,10 +14,14 @@ export const actions: Actions = { return fail(400, { message: 'missing_email' }); } - let res = await fetch(`${endpoint}/auth/password/reset/`, { + let csrfToken = await fetchCSRFToken(); + + let res = await fetch(`${endpoint}/_allauth/browser/v1/auth/password/request`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}` }, body: JSON.stringify({ email @@ -25,10 +30,7 @@ export const actions: Actions = { if (!res.ok) { let message = await res.json(); - - const key = Object.keys(message)[0]; - - return fail(res.status, { message: message[key] }); + return fail(res.status, message); } return { success: true }; } diff --git a/frontend/src/routes/settings/forgot-password/+page.svelte b/frontend/src/routes/user/reset-password/+page.svelte similarity index 100% rename from frontend/src/routes/settings/forgot-password/+page.svelte rename to frontend/src/routes/user/reset-password/+page.svelte diff --git a/frontend/src/routes/user/reset-password/[key]/+page.server.ts b/frontend/src/routes/user/reset-password/[key]/+page.server.ts new file mode 100644 index 0000000..2db51f6 --- /dev/null +++ b/frontend/src/routes/user/reset-password/[key]/+page.server.ts @@ -0,0 +1,55 @@ +import { fail, redirect } from '@sveltejs/kit'; +import { fetchCSRFToken } from '$lib/index.server'; +import type { PageServerLoad, Actions } from './$types'; + +export const load = (async ({ params }) => { + const key = params.key; + if (!key) { + throw redirect(302, '/'); + } + return { key }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + default: async (event) => { + const formData = await event.request.formData(); + const password = formData.get('password'); + const confirm_password = formData.get('confirm_password'); + const key = event.params.key; + + if (!password || !confirm_password) { + return fail(400, { message: 'auth.both_passwords_required' }); + } + + if (password !== confirm_password) { + return fail(400, { message: 'settings.password_does_not_match' }); + } + + const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; + const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + const csrfToken = await fetchCSRFToken(); + + const response = await event.fetch( + `${serverEndpoint}/_allauth/browser/v1/auth/password/reset`, + { + headers: { + 'Content-Type': 'application/json', + Cookie: `csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken + }, + method: 'POST', + credentials: 'include', + body: JSON.stringify({ key: key, password: password }) + } + ); + + if (response.status !== 401) { + const error_message = await response.json(); + console.error(error_message); + console.log(response); + return fail(response.status, { message: 'auth.reset_failed' }); + } + + return redirect(302, '/login'); + } +}; diff --git a/frontend/src/routes/user/reset-password/[key]/+page.svelte b/frontend/src/routes/user/reset-password/[key]/+page.svelte new file mode 100644 index 0000000..7e837a4 --- /dev/null +++ b/frontend/src/routes/user/reset-password/[key]/+page.svelte @@ -0,0 +1,53 @@ + + +

    {$t('settings.change_password')}

    + +
    +
    + + +
    + +
    + + +
    + + + + {#if $page.form?.message} +
    + {$t($page.form?.message)} +
    + {/if} +
    + + + Password Reset Confirm + + diff --git a/frontend/src/routes/user/verify-email/[key]/+page.server.ts b/frontend/src/routes/user/verify-email/[key]/+page.server.ts new file mode 100644 index 0000000..d9b641b --- /dev/null +++ b/frontend/src/routes/user/verify-email/[key]/+page.server.ts @@ -0,0 +1,34 @@ +import { fetchCSRFToken } from '$lib/index.server'; +import type { PageServerLoad } from './$types'; + +export const load = (async (event) => { + // get key from route params + const key = event.params.key; + if (!key) { + return { status: 404 }; + } + const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; + const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; + const csrfToken = await fetchCSRFToken(); + + let verifyFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/email/verify`, { + headers: { + Cookie: `csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken + }, + method: 'POST', + credentials: 'include', + + body: JSON.stringify({ key: key }) + }); + if (verifyFetch.ok || verifyFetch.status == 401) { + return { + verified: true + }; + } else { + let error_message = await verifyFetch.json(); + console.error(error_message); + console.error('Failed to verify email'); + return { status: 404 }; + } +}) satisfies PageServerLoad; diff --git a/frontend/src/routes/user/verify-email/[key]/+page.svelte b/frontend/src/routes/user/verify-email/[key]/+page.svelte new file mode 100644 index 0000000..f025ad0 --- /dev/null +++ b/frontend/src/routes/user/verify-email/[key]/+page.svelte @@ -0,0 +1,14 @@ + + +{#if data.verified} +

    {$t('settings.email_verified')}

    +

    {$t('settings.email_verified_success')}

    +{:else} +

    {$t('settings.email_verified_error')}

    +

    {$t('settings.email_verified_erorr_desc')}

    +{/if} diff --git a/frontend/src/routes/users/+page.server.ts b/frontend/src/routes/users/+page.server.ts index 4aa6573..3fe2574 100644 --- a/frontend/src/routes/users/+page.server.ts +++ b/frontend/src/routes/users/+page.server.ts @@ -4,13 +4,14 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/login'); } const res = await fetch(`${serverEndpoint}/auth/users/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/src/routes/worldtravel/+page.server.ts b/frontend/src/routes/worldtravel/+page.server.ts index 4fea381..ec696bf 100644 --- a/frontend/src/routes/worldtravel/+page.server.ts +++ b/frontend/src/routes/worldtravel/+page.server.ts @@ -2,6 +2,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { Country } from '$lib/types'; import { redirect, type Actions } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; +import { fetchCSRFToken } from '$lib/index.server'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -9,11 +10,12 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { - const res = await fetch(`${endpoint}/api/countries/`, { + const res = await event.fetch(`${endpoint}/api/countries/`, { method: 'GET', headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' }); if (!res.ok) { console.error('Failed to fetch countries'); @@ -27,8 +29,6 @@ export const load = (async (event) => { }; } } - - return {}; }) satisfies PageServerLoad; export const actions: Actions = { @@ -41,15 +41,20 @@ export const actions: Actions = { }; } - if (!event.locals.user || !event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + + if (!event.locals.user || !sessionId) { return redirect(302, '/login'); } + let csrfToken = await fetchCSRFToken(); + const res = await fetch(`${endpoint}/api/visitedregion/`, { method: 'POST', headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken }, body: JSON.stringify({ region: body.regionId }) }); @@ -75,15 +80,20 @@ export const actions: Actions = { const visitId = body.visitId as number; - if (!event.locals.user || !event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + + if (!event.locals.user || !sessionId) { return redirect(302, '/login'); } + let csrfToken = await fetchCSRFToken(); + const res = await fetch(`${endpoint}/api/visitedregion/${visitId}/`, { method: 'DELETE', headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken } }); diff --git a/frontend/src/routes/worldtravel/[id]/+page.server.ts b/frontend/src/routes/worldtravel/[id]/+page.server.ts index b13ad43..2b00f3c 100644 --- a/frontend/src/routes/worldtravel/[id]/+page.server.ts +++ b/frontend/src/routes/worldtravel/[id]/+page.server.ts @@ -12,10 +12,16 @@ export const load = (async (event) => { let visitedRegions: VisitedRegion[] = []; let country: Country; + let sessionId = event.cookies.get('sessionid'); + + if (!sessionId) { + return redirect(302, '/login'); + } + let res = await fetch(`${endpoint}/api/${id}/regions/`, { method: 'GET', headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { @@ -28,7 +34,7 @@ export const load = (async (event) => { res = await fetch(`${endpoint}/api/${id}/visits/`, { method: 'GET', headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { @@ -41,7 +47,7 @@ export const load = (async (event) => { res = await fetch(`${endpoint}/api/countries/${regions[0].country}/`, { method: 'GET', headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index aea4f15..cac804b 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -100,6 +100,33 @@ export default { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' + }, + northernLights: { + primary: '#479bb3', // Primary color + 'primary-focus': '#81A1C1', // Primary color - focused + 'primary-content': '#ECEFF4', // Foreground content color to use on primary color + + secondary: '#5E81AC', // Secondary color + 'secondary-focus': '#4C566A', // Secondary color - focused + 'secondary-content': '#ECEFF4', // Foreground content color to use on secondary color + + accent: '#B48EAD', // Accent color + 'accent-focus': '#A3BE8C', // Accent color - focused + 'accent-content': '#ECEFF4', // Foreground content color to use on accent color + + neutral: '#4C566A', // Neutral color + 'neutral-focus': '#3B4252', // Neutral color - focused + 'neutral-content': '#D8DEE9', // Foreground content color to use on neutral color + + 'base-100': '#2E3440', // Base color of page, used for blank backgrounds + 'base-200': '#3B4252', // Base color, a little lighter + 'base-300': '#434C5E', // Base color, even more lighter + 'base-content': '#ECEFF4', // Foreground content color to use on base color + + info: '#88C0D0', // Info + success: '#A3BE8C', // Success + warning: '#D08770', // Warning + error: '#BF616A' // Error } } ] diff --git a/proxy/Dockerfile.nginx b/proxy/Dockerfile.nginx deleted file mode 100644 index 4c49d2e..0000000 --- a/proxy/Dockerfile.nginx +++ /dev/null @@ -1,4 +0,0 @@ -FROM nginx:alpine - -RUN rm /etc/nginx/conf.d/default.conf -COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/proxy/nginx.conf b/proxy/nginx.conf deleted file mode 100644 index 67f5f0d..0000000 --- a/proxy/nginx.conf +++ /dev/null @@ -1,8 +0,0 @@ -server { - listen 80; - server_name localhost; - - location /media/ { - alias /app/media/; - } -} \ No newline at end of file