1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-20 13:29:37 +02:00

Auth Migration, Calendar and Other Misc. Fixes

This commit is contained in:
Sean Morley 2024-12-15 12:13:56 -05:00 committed by GitHub
commit 148568fca4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 3267 additions and 2379 deletions

View file

@ -8,7 +8,6 @@
- **[Demo](https://demo.adventurelog.app)** - **[Demo](https://demo.adventurelog.app)**
- **[Join the AdventureLog Community Discord Server](https://discord.gg/wRbQ9Egr8C)** - **[Join the AdventureLog Community Discord Server](https://discord.gg/wRbQ9Egr8C)**
# Table of Contents # Table of Contents
- [Installation](#installation) - [Installation](#installation)
@ -60,7 +59,7 @@ Here is a summary of the configuration options available in the `docker-compose.
### Backend Container (server) ### Backend Container (server)
| Name | Required | Description | Default Value | | Name | Required | Description | Default Value |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `PGHOST` | Yes | Databse host. | db | | `PGHOST` | Yes | Databse host. | db |
| `PGDATABASE` | Yes | Database. | database | | `PGDATABASE` | Yes | Database. | database |
| `PGUSER` | Yes | Database user. | adventure | | `PGUSER` | Yes | Database user. | adventure |
@ -68,9 +67,9 @@ Here is a summary of the configuration options available in the `docker-compose.
| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin | | `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin |
| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin | | `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin |
| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com | | `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' | | `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 | | `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' | | `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 ## Running the Containers

View file

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

View file

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

View file

@ -3,6 +3,11 @@ from django.contrib import admin
from django.utils.html import mark_safe from django.utils.html import mark_safe
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category
from worldtravel.models import Country, Region, VisitedRegion 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): class AdventureAdmin(admin.ModelAdmin):
@ -54,9 +59,9 @@ from users.models import CustomUser
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):
model = CustomUser model = CustomUser
list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display'] list_display = ['username', 'is_staff', 'is_active', 'image_display']
readonly_fields = ('uuid',) readonly_fields = ('uuid',)
search_fields = ('username', 'email') search_fields = ('username',)
fieldsets = UserAdmin.fieldsets + ( fieldsets = UserAdmin.fieldsets + (
(None, {'fields': ('profile_pic', 'uuid', 'public_profile')}), (None, {'fields': ('profile_pic', 'uuid', 'public_profile')}),
) )

View file

@ -11,3 +11,13 @@ class AppVersionMiddleware:
response['X-AdventureLog-Version'] = '1.0.0' response['X-AdventureLog-Version'] = '1.0.0'
return response 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

View file

@ -1,6 +1,6 @@
from django.urls import include, path from django.urls import include, path
from rest_framework.routers import DefaultRouter 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 = DefaultRouter()
router.register(r'adventures', AdventureViewSet, basename='adventures') 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'images', AdventureImageViewSet, basename='images')
router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode') router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode')
router.register(r'categories', CategoryViewSet, basename='categories') router.register(r'categories', CategoryViewSet, basename='categories')
router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar')
urlpatterns = [ urlpatterns = [

View file

@ -17,6 +17,9 @@ from rest_framework.pagination import PageNumberPagination
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import status from rest_framework import status
from django.contrib.auth import get_user_model 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() User = get_user_model()
@ -73,6 +76,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
return queryset.order_by(ordering) return queryset.order_by(ordering)
def get_queryset(self): def get_queryset(self):
print(self.request.user)
# if the user is not authenticated return only public adventures for retrieve action # if the user is not authenticated return only public adventures for retrieve action
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
if self.action == 'retrieve': if self.action == 'retrieve':
@ -1202,3 +1206,56 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
new_region_count += 1 new_region_count += 1
new_regions[region.id] = region.name new_regions[region.id] = region.name
return Response({"new_regions": new_region_count, "regions": new_regions}) 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

View file

@ -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, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
from datetime import timedelta
from os import getenv from os import getenv
from pathlib import Path from pathlib import Path
# Load environment variables from .env file # Load environment variables from .env file
@ -35,8 +34,6 @@ DEBUG = getenv('DEBUG', 'True') == 'True'
# ] # ]
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = ( INSTALLED_APPS = (
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
@ -47,19 +44,19 @@ INSTALLED_APPS = (
'django.contrib.sites', 'django.contrib.sites',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'dj_rest_auth',
'allauth', 'allauth',
'allauth.account', 'allauth.account',
'dj_rest_auth.registration', 'allauth.mfa',
'allauth.headless',
'allauth.socialaccount', 'allauth.socialaccount',
'allauth.socialaccount.providers.facebook', # "widget_tweaks",
# "slippers",
'drf_yasg', 'drf_yasg',
'corsheaders', 'corsheaders',
'adventures', 'adventures',
'worldtravel', 'worldtravel',
'users', 'users',
'django.contrib.gis', 'django.contrib.gis',
) )
MIDDLEWARE = ( MIDDLEWARE = (
@ -83,7 +80,6 @@ CACHES = {
} }
} }
# For backwards compatibility for Django 1.8 # For backwards compatibility for Django 1.8
MIDDLEWARE_CLASSES = MIDDLEWARE MIDDLEWARE_CLASSES = MIDDLEWARE
@ -91,11 +87,6 @@ ROOT_URLCONF = 'main.urls'
# WSGI_APPLICATION = 'demo.wsgi.application' # WSGI_APPLICATION = 'demo.wsgi.application'
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
"REFRESH_TOKEN_LIFETIME": timedelta(days=365),
}
# Database # Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
@ -114,6 +105,7 @@ DATABASES = {
} }
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/ # https://docs.djangoproject.com/en/1.7/topics/i18n/
@ -127,6 +119,8 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/ # https://docs.djangoproject.com/en/1.7/howto/static-files/
@ -139,7 +133,15 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media' MEDIA_ROOT = BASE_DIR / 'media'
STATICFILES_DIRS = [BASE_DIR / 'static'] 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 = [ TEMPLATES = [
{ {
@ -157,32 +159,34 @@ TEMPLATES = [
}, },
] ]
REST_AUTH = { # Authentication settings
'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'
}
DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True' 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.') DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.')
STORAGES = { ALLAUTH_UI_THEME = "dark"
"staticfiles": { SILENCED_SYSTEM_CHECKS = ["slippers.E001"]
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
}
}
AUTH_USER_MODEL = 'users.CustomUser' 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') 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' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
SITE_ID = 1 SITE_ID = 1
ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_REQUIRED = True
@ -214,13 +218,8 @@ else:
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
'dj_rest_auth.jwt_auth.JWTCookieAuthentication'
), ),
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticated',
# ],
} }
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
@ -228,12 +227,11 @@ SWAGGER_SETTINGS = {
'LOGOUT_URL': 'logout', 'LOGOUT_URL': 'logout',
} }
# For demo purposes only. Use a white list in the real world. CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
CORS_ORIGIN_ALLOW_ALL = True
from os import getenv
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()] 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' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
LOGGING = { LOGGING = {
@ -260,6 +258,5 @@ LOGGING = {
}, },
}, },
} }
# https://github.com/dr5hn/countries-states-cities-database/tags # https://github.com/dr5hn/countries-states-cities-database/tags
COUNTRY_REGION_JSON_VERSION = 'v2.4' COUNTRY_REGION_JSON_VERSION = 'v2.4'

View file

@ -3,8 +3,7 @@ from django.contrib import admin
from django.views.generic import RedirectView, TemplateView from django.views.generic import RedirectView, TemplateView
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from adventures import urls as adventures from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView
from users.views import ChangeEmailView, IsRegistrationDisabled, PublicUserListView, PublicUserDetailView
from .views import get_csrf_token from .views import get_csrf_token
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
@ -19,56 +18,27 @@ schema_view = get_schema_view(
urlpatterns = [ urlpatterns = [
path('api/', include('adventures.urls')), path('api/', include('adventures.urls')),
path('api/', include('worldtravel.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/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
path('auth/users/', PublicUserListView.as_view(), name='public-user-list'), path('auth/users/', PublicUserListView.as_view(), name='public-user-list'),
path('auth/user/<uuid:user_id>/', PublicUserDetailView.as_view(), name='public-user-detail'), path('auth/user/<uuid:user_id>/', 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'), 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/$', path('', TemplateView.as_view(template_name='home.html')),
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<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[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')),
re_path(r'^admin/', admin.site.urls), re_path(r'^admin/', admin.site.urls),
re_path(r'^accounts/profile/$', RedirectView.as_view(url='/', re_path(r'^accounts/profile/$', RedirectView.as_view(url='/',
permanent=True), name='profile-redirect'), permanent=True), name='profile-redirect'),
re_path(r'^docs/$', schema_view.with_ui('swagger', re_path(r'^docs/$', schema_view.with_ui('swagger',
cache_timeout=0), name='api_docs'), cache_timeout=0), name='api_docs'),
# path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'), # 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) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -1,7 +1,5 @@
Django==5.0.8 Django==5.0.8
dj-rest-auth @ git+https://github.com/iMerica/dj-rest-auth.git@master
djangorestframework>=3.15.2 djangorestframework>=3.15.2
djangorestframework-simplejwt==5.3.1
django-allauth==0.63.3 django-allauth==0.63.3
drf-yasg==1.21.4 drf-yasg==1.21.4
django-cors-headers==4.4.0 django-cors-headers==4.4.0
@ -14,3 +12,9 @@ django-resized
django-geojson django-geojson
setuptools setuptools
gunicorn==23.0.0 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

View file

@ -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 %}

View file

@ -4,8 +4,8 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Django-dj-rest-auth demo" /> <meta name="description" content="AdventureLog Server" />
<meta name="author" content="iMerica, Inc." /> <meta name="author" content="Sean Morley" />
<title>AdventureLog API Server</title> <title>AdventureLog API Server</title>
@ -31,39 +31,6 @@
<body role="document"> <body role="document">
<div class="navbar navbar-inverse" role="navigation"> <div class="navbar navbar-inverse" role="navigation">
<div class="container"> <div class="container">
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"
>API endpoints <span class="caret"></span
></a>
<ul class="dropdown-menu" role="menu">
<!-- these pages don't require user token -->
<li><a href="{% url 'signup' %}">Signup</a></li>
<li>
<a href="{% url 'email-verification' %}">E-mail verification</a>
</li>
<li>
<a href="{% url 'resend-email-verification' %}"
>Resend E-mail verification</a
>
</li>
<li><a href="{% url 'login' %}">Login</a></li>
<li><a href="{% url 'password-reset' %}">Password Reset</a></li>
<li>
<a href="{% url 'password-reset-confirm' %}"
>Password Reset Confirm</a
>
</li>
<li class="divider"></li>
<!-- these pages require user token -->
<li><a href="{% url 'user-details' %}">User details</a></li>
<li><a href="{% url 'logout' %}">Logout</a></li>
<li><a href="{% url 'password-change' %}">Password change</a></li>
</ul>
</li>
</ul>
<div class="navbar-header"> <div class="navbar-header">
<button <button
type="button" type="button"
@ -80,20 +47,19 @@
</div> </div>
<div class="collapse navbar-collapse"> <div class="collapse navbar-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li class="active"><a href="/">Demo</a></li> <li class="active"><a href="/">Server Home</a></li>
<li> <li>
<a <a target="_blank" href="http://adventurelog.app"
target="_blank"
href="http://dj-rest-auth.readthedocs.org/en/latest/"
>Documentation</a >Documentation</a
> >
</li> </li>
<li> <li>
<a target="_blank" href="https://github.com/iMerica/dj-rest-auth" <a
>Source code</a target="_blank"
href="https://github.com/seanmorley15/AdventureLog"
>Source Code</a
> >
</li> </li>
<li><a target="_blank" href="{% url 'api_docs' %}">API Docs</a></li>
</ul> </ul>
</div> </div>
<!--/.nav-collapse --> <!--/.nav-collapse -->

View file

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>E-mail verification</h3><hr/>
{% include "fragments/email_verification_form.html" %}
</div>
{% endblock %}

View file

@ -4,7 +4,12 @@
<h1>AdventureLog API Server</h1> <h1>AdventureLog API Server</h1>
<p> <p>
<a class="btn btn-primary btn-lg" href="/admin" role="button">Admin Site</a> <a class="btn btn-primary btn-lg" href="/admin" role="button">Admin Site</a>
<a class="btn btn-secondary btn-lg" href="/docs" role="button">API Docs</a> <a
class="btn btn-secondary btn-lg"
href="/accounts/password/change"
role="button"
>Account Managment</a
>
</p> </p>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>Login</h3><hr/>
{% include "fragments/login_form.html" %}
</div>
{% endblock %}

View file

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>Logout</h3><hr/>
{% include "fragments/logout_form.html" %}
</div>
{% endblock %}

View file

@ -1,39 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="form-group">
<label for="token" class="col-sm-2 control-label">User Token</label>
<div class="col-sm-4">
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
<p class="help-block">Token received after login</p>
</div>
</div>
</div>
<div class="row">
<h3>Update User Details</h3><hr/>
{% include "fragments/password_change_form.html" %}
</div>
{% endblock %}
{% block script %}
<script type="text/javascript">
$().ready(function(){
$('form button[type=submit]').click(function(){
var token = $('input[name=token]').val();
var form = $('form');
$.ajax({
url: form.attr('action'),
data: $('form').serialize(),
type: "POST",
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);}
}).fail(function(data){error_response(data);})
.done(function(data){susccess_response(data);});
return false;
});
});
</script>
{% endblock %}

View file

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>Password reset</h3><hr/>
{% include "fragments/password_reset_form.html" %}
</div>
{% endblock %}

View file

@ -1,26 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>Password reset confirmation</h3><hr/>
{% include "fragments/password_reset_confirm_form.html" %}
</div>
{% endblock %}
{% block script %}
<script type="text/javascript">
var url_elements = window.location.pathname.split('/');
if (url_elements.length == 6){
var uid = url_elements[url_elements.length - 3];
if (uid !== undefined){
$('input[name=uid]').val(uid);
}
var token = url_elements[url_elements.length - 2];
if (token !== undefined){
$('input[name=token]').val(token);
}
}
</script>
{% endblock %}

View file

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>Resend E-mail verification</h3><hr/>
{% include "fragments/resend_email_verification_form.html" %}
</div>
{% endblock %}

View file

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>Signup</h3><hr/>
{% include "fragments/signup_form.html" %}
</div>
{% endblock %}

View file

@ -1,58 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<h3>Retrieve User Details</h3><hr/>
<div class="form-group">
<label for="token" class="col-sm-2 control-label">User Token</label>
<div class="col-sm-4">
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
<p class="help-block">Token received after login</p>
</div>
<button id="get-user-details" class="btn btn-primary">GET user details</button>
</div>
</div>
<div class="row">
<h3>Update User Details</h3><hr/>
{% include "fragments/user_details_form.html" %}
</div>
{% endblock %}
{% block script %}
<script type="text/javascript">
$().ready(function(){
$('#get-user-details').click(function(){
var token = $('input[name=token]').val();
$.ajax({
url: "{% url 'rest_user_details' %}",
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);},
type: "GET",
success: function(data) {
$('input[name=username]').val(data.username);
$('input[name=email]').val(data.email);
$('input[name=first_name]').val(data.first_name);
$('input[name=last_name]').val(data.last_name);
}
});
return false;
});
$('form button[type=submit]').click(function(){
var token = $('input[name=token]').val();
var form = $('form');
$.ajax({
url: form.attr('action'),
data: $('form').serialize(),
type: "PUT",
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);}
}).fail(function(data){error_response(data);})
.done(function(data){susccess_response(data);});
return false;
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,10 @@
from allauth.account.adapter import DefaultAccountAdapter
from django.conf import settings
class NoNewUsersAccountAdapter(DefaultAccountAdapter):
"""
Disable new user registration.
"""
def is_open_for_signup(self, request):
is_disabled = getattr(settings, 'DISABLE_REGISTRATION', False)
return not is_disabled

View file

@ -1,3 +1,13 @@
from django.contrib import admin from django.contrib import admin
from allauth.account.decorators import secure_admin_login
from django.contrib.sessions.models import Session
# Register your models here admin.autodiscover()
admin.site.login = secure_admin_login(admin.site.login)
class SessionAdmin(admin.ModelAdmin):
def _session_data(self, obj):
return obj.get_decoded()
list_display = ['session_key', '_session_data', 'expire_date']
admin.site.register(Session, SessionAdmin)

View file

@ -0,0 +1,17 @@
from django import forms
class CustomSignupForm(forms.Form):
first_name = forms.CharField(max_length=30, required=True)
last_name = forms.CharField(max_length=30, required=True)
def signup(self, request, user):
# Delay the import to avoid circular import
from allauth.account.forms import SignupForm
# No need to call super() from CustomSignupForm; use the SignupForm directly if needed
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
# Save the user instance
user.save()
return user

View file

@ -1,55 +1,17 @@
from allauth.account.utils import (filter_users_by_email, user_pk_to_url_str, user_username) from django import forms
from allauth.utils import build_absolute_uri
from allauth.account.adapter import get_adapter
from allauth.account.forms import default_token_generator
from allauth.account import app_settings
from django.conf import settings
from allauth.account.forms import ResetPasswordForm as AllAuthPasswordResetForm class CustomSignupForm(forms.Form):
first_name = forms.CharField(max_length=30, required=True)
last_name = forms.CharField(max_length=30, required=True)
class CustomAllAuthPasswordResetForm(AllAuthPasswordResetForm): def signup(self, request, user):
# Delay the import to avoid circular import
def clean_email(self): from allauth.account.forms import SignupForm
"""
Invalid email should not raise error, as this would leak users
for unit test: test_password_reset_with_invalid_email
"""
email = self.cleaned_data["email"]
email = get_adapter().clean_email(email)
self.users = filter_users_by_email(email, is_active=True)
return self.cleaned_data["email"]
def save(self, request, **kwargs):
email = self.cleaned_data['email']
token_generator = kwargs.get('token_generator', default_token_generator)
for user in self.users:
temp_key = token_generator.make_token(user)
path = f"custom_password_reset_url/{user_pk_to_url_str(user)}/{temp_key}/"
url = build_absolute_uri(request, path)
frontend_url = settings.FRONTEND_URL
# remove ' from frontend_url
frontend_url = frontend_url.replace("'", "")
#Values which are passed to password_reset_key_message.txt
context = {
"frontend_url": frontend_url,
"user": user,
"password_reset_url": url,
"request": request,
"path": path,
"temp_key": temp_key,
'user_pk': user_pk_to_url_str(user),
}
if app_settings.AUTHENTICATION_METHOD != app_settings.AuthenticationMethod.EMAIL:
context['username'] = user_username(user)
get_adapter(request).send_mail(
'account/email/password_reset_key', email, context
)
return self.cleaned_data['email']
# No need to call super() from CustomSignupForm; use the SignupForm directly if needed
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
# Save the user instance
user.save()
return user

View file

@ -1,26 +1,14 @@
from rest_framework import serializers from rest_framework import serializers
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from adventures.models import Adventure, Collection from adventures.models import Collection
from users.forms import CustomAllAuthPasswordResetForm
from dj_rest_auth.serializers import PasswordResetSerializer
from rest_framework.exceptions import PermissionDenied
User = get_user_model() User = get_user_model()
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
try:
from allauth.account import app_settings as allauth_account_settings
from allauth.account.adapter import get_adapter
from allauth.account.utils import setup_user_email
from allauth.socialaccount.models import EmailAddress
from allauth.utils import get_username_max_length
except ImportError:
raise ImportError('allauth needs to be added to INSTALLED_APPS.')
class ChangeEmailSerializer(serializers.Serializer): class ChangeEmailSerializer(serializers.Serializer):
new_email = serializers.EmailField(required=True) new_email = serializers.EmailField(required=True)
@ -32,104 +20,20 @@ class ChangeEmailSerializer(serializers.Serializer):
return value return value
class RegisterSerializer(serializers.Serializer):
username = serializers.CharField(
max_length=get_username_max_length(),
min_length=allauth_account_settings.USERNAME_MIN_LENGTH,
required=allauth_account_settings.USERNAME_REQUIRED,
)
email = serializers.EmailField(required=allauth_account_settings.EMAIL_REQUIRED)
password1 = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True)
first_name = serializers.CharField(required=False)
last_name = serializers.CharField(required=False)
def validate_username(self, username):
username = get_adapter().clean_username(username)
return username
def validate_email(self, email):
email = get_adapter().clean_email(email)
if allauth_account_settings.UNIQUE_EMAIL:
if email and EmailAddress.objects.is_verified(email):
raise serializers.ValidationError(
_('A user is already registered with this e-mail address.'),
)
return email
def validate_password1(self, password):
return get_adapter().clean_password(password)
def validate(self, data):
if data['password1'] != data['password2']:
raise serializers.ValidationError(_("The two password fields didn't match."))
# check if a user with the same email already exists
if User.objects.filter(email=data['email']).exists():
raise serializers.ValidationError("This email is already in use.")
return data
def custom_signup(self, request, user):
pass
def get_cleaned_data(self):
return {
'username': self.validated_data.get('username', ''),
'password1': self.validated_data.get('password1', ''),
'email': self.validated_data.get('email', ''),
'first_name': self.validated_data.get('first_name', ''),
'last_name': self.validated_data.get('last_name', ''),
}
def save(self, request):
# Check if registration is disabled
if getattr(settings, 'DISABLE_REGISTRATION', False):
raise PermissionDenied("Registration is currently disabled.")
# If registration is not disabled, proceed with the original logic
adapter = get_adapter()
user = adapter.new_user(request)
self.cleaned_data = self.get_cleaned_data()
user = adapter.save_user(request, user, self, commit=False)
if "password1" in self.cleaned_data:
try:
adapter.clean_password(self.cleaned_data['password1'], user=user)
except DjangoValidationError as exc:
raise serializers.ValidationError(
detail=serializers.as_serializer_error(exc)
)
user.save()
self.custom_signup(request, user)
setup_user_email(request, user, [])
return user
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
UserModel = get_user_model() UserModel = get_user_model()
from dj_rest_auth.serializers import UserDetailsSerializer # from dj_rest_auth.serializers import UserDetailsSerializer
from .models import CustomUser from .models import CustomUser
from rest_framework import serializers from rest_framework import serializers
from django.conf import settings from django.conf import settings
import os import os
# class AdventureSerializer(serializers.ModelSerializer):
# image = serializers.SerializerMethodField()
# class Meta:
# model = Adventure
# fields = ['id', 'user_id', 'type', 'name', 'location', 'activity_types', 'description',
# 'rating', 'link', 'image', 'date', 'trip_id', 'is_public', 'longitude', 'latitude']
# def get_image(self, obj):
# if obj.image:
# public_url = os.environ.get('PUBLIC_URL', '')
# return f'{public_url}/media/{obj.image.name}'
# return None
class UserDetailsSerializer(serializers.ModelSerializer): class UserDetailsSerializer(serializers.ModelSerializer):
""" """
User model w/o password User model w/o password
@ -163,9 +67,9 @@ class UserDetailsSerializer(serializers.ModelSerializer):
if hasattr(UserModel, 'public_profile'): if hasattr(UserModel, 'public_profile'):
extra_fields.append('public_profile') extra_fields.append('public_profile')
class Meta(UserDetailsSerializer.Meta): class Meta:
model = CustomUser model = CustomUser
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile') fields = ('profile_pic', 'uuid', 'public_profile', 'email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
model = UserModel model = UserModel
fields = ('pk', *extra_fields) fields = ('pk', *extra_fields)
@ -192,6 +96,7 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
class Meta(UserDetailsSerializer.Meta): class Meta(UserDetailsSerializer.Meta):
model = CustomUser model = CustomUser
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile') fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid',)
def to_representation(self, instance): def to_representation(self, instance):
representation = super().to_representation(instance) representation = super().to_representation(instance)
@ -203,13 +108,3 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
representation['profile_pic'] = f"{public_url}/media/{instance.profile_pic.name}" representation['profile_pic'] = f"{public_url}/media/{instance.profile_pic.name}"
del representation['pk'] # remove the pk field from the response del representation['pk'] # remove the pk field from the response
return representation return representation
class MyPasswordResetSerializer(PasswordResetSerializer):
def validate_email(self, value):
# use the custom reset form
self.reset_form = CustomAllAuthPasswordResetForm(data=self.initial_data)
if not self.reset_form.is_valid():
raise serializers.ValidationError(self.reset_form.errors)
return value

View file

@ -83,3 +83,41 @@ class PublicUserDetailView(APIView):
user.email = None user.email = None
serializer = PublicUserSerializer(user) serializer = PublicUserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
class UserMetadataView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
responses={
200: openapi.Response('User metadata'),
400: 'Bad Request'
},
operation_description="Get user metadata."
)
def get(self, request):
user = request.user
serializer = PublicUserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK)
class UpdateUserMetadataView(APIView):
"""
Update user metadata using fields from the PublicUserSerializer.
Using patch opposed to put allows for partial updates, covers the case where it checks the username and says it's already taken. Duplicate uesrname values should not be included in the request to avoid this.
"""
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
request_body=PublicUserSerializer,
responses={
200: openapi.Response('User metadata updated'),
400: 'Bad Request'
},
operation_description="Update user metadata."
)
def patch(self, request):
user = request.user
serializer = PublicUserSerializer(user, data=request.data, partial=True, context={'request': request})
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View file

@ -1,3 +1,5 @@
from django.contrib import admin from django.contrib import admin
from allauth.account.decorators import secure_admin_login
# Register your models here. admin.autodiscover()
admin.site.login = secure_admin_login(admin.site.login)

View file

@ -38,10 +38,10 @@ services:
- DJANGO_ADMIN_USERNAME=admin - DJANGO_ADMIN_USERNAME=admin
- DJANGO_ADMIN_PASSWORD=admin - DJANGO_ADMIN_PASSWORD=admin
- DJANGO_ADMIN_EMAIL=admin@example.com - DJANGO_ADMIN_EMAIL=admin@example.com
- PUBLIC_URL='http://localhost:8016' # Match the outward port, used for the creation of image urls - PUBLIC_URL=http://localhost:8016 # Match the outward port, used for the creation of image urls
- CSRF_TRUSTED_ORIGINS=http://localhost:8016 # Comma separated list of trusted origins for CSRF - CSRF_TRUSTED_ORIGINS=http://localhost:8016,http://localhost:8015 # Comma separated list of trusted origins for CSRF
- DEBUG=False - DEBUG=False
- FRONTEND_URL='http://localhost:8015' # Used for email generation. This should be the url of the frontend - FRONTEND_URL=http://localhost:8015 # Used for email generation. This should be the url of the frontend
ports: ports:
- "8016:80" - "8016:80"
depends_on: depends_on:

View file

@ -32,7 +32,7 @@ Here is a summary of the configuration options available in the `docker-compose.
### Backend Container (server) ### Backend Container (server)
| Name | Required | Description | Default Value | | Name | Required | Description | Default Value |
| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `PGHOST` | Yes | Databse host. | db | | `PGHOST` | Yes | Databse host. | db |
| `PGDATABASE` | Yes | Database. | database | | `PGDATABASE` | Yes | Database. | database |
| `PGUSER` | Yes | Database user. | adventure | | `PGUSER` | Yes | Database user. | adventure |
@ -40,9 +40,9 @@ Here is a summary of the configuration options available in the `docker-compose.
| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin | | `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin |
| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin | | `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin |
| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com | | `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' | | `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 | | `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 publically accessible url to the **frontend** container. This link should be accessable for all users. Used for email generation. | 'http://localhost:8015' | | `FRONTEND_URL` | Yes | This is the publically accessible url to the **frontend** container. This link should be accessable for all users. Used for email generation. | http://localhost:8015 |
## Running the Containers ## Running the Containers

View file

@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View file

@ -12,14 +12,18 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@event-calendar/core": "^3.7.1",
"@event-calendar/day-grid": "^3.7.1",
"@event-calendar/time-grid": "^3.7.1",
"@iconify-json/mdi": "^1.1.67", "@iconify-json/mdi": "^1.1.67",
"@sveltejs/adapter-auto": "^3.2.2", "@sveltejs/adapter-auto": "^3.2.2",
"@sveltejs/adapter-node": "^5.2.0", "@sveltejs/adapter-node": "^5.2.0",
"@sveltejs/adapter-vercel": "^5.4.1", "@sveltejs/adapter-vercel": "^5.4.1",
"@sveltejs/kit": "^2.5.17", "@sveltejs/kit": "^2.8.3",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@types/node": "^22.5.4", "@types/node": "^22.5.4",
"@types/qrcode": "^1.5.5",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"daisyui": "^4.12.6", "daisyui": "^4.12.6",
"postcss": "^8.4.38", "postcss": "^8.4.38",
@ -36,6 +40,8 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@lukulent/svelte-umami": "^0.0.3", "@lukulent/svelte-umami": "^0.0.3",
"emoji-picker-element": "^1.26.0",
"qrcode": "^1.5.4",
"svelte-i18n": "^4.0.1", "svelte-i18n": "^4.0.1",
"svelte-maplibre": "^0.9.8" "svelte-maplibre": "^0.9.8"
} }

257
frontend/pnpm-lock.yaml generated
View file

@ -11,6 +11,12 @@ importers:
'@lukulent/svelte-umami': '@lukulent/svelte-umami':
specifier: ^0.0.3 specifier: ^0.0.3
version: 0.0.3(svelte@4.2.19) version: 0.0.3(svelte@4.2.19)
emoji-picker-element:
specifier: ^1.26.0
version: 1.26.0
qrcode:
specifier: ^1.5.4
version: 1.5.4
svelte-i18n: svelte-i18n:
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1(svelte@4.2.19) version: 4.0.1(svelte@4.2.19)
@ -18,21 +24,30 @@ importers:
specifier: ^0.9.8 specifier: ^0.9.8
version: 0.9.8(svelte@4.2.19) version: 0.9.8(svelte@4.2.19)
devDependencies: devDependencies:
'@event-calendar/core':
specifier: ^3.7.1
version: 3.7.1
'@event-calendar/day-grid':
specifier: ^3.7.1
version: 3.7.1
'@event-calendar/time-grid':
specifier: ^3.7.1
version: 3.7.1
'@iconify-json/mdi': '@iconify-json/mdi':
specifier: ^1.1.67 specifier: ^1.1.67
version: 1.1.67 version: 1.1.67
'@sveltejs/adapter-auto': '@sveltejs/adapter-auto':
specifier: ^3.2.2 specifier: ^3.2.2
version: 3.2.2(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))) version: 3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
'@sveltejs/adapter-node': '@sveltejs/adapter-node':
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.2.0(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))) version: 5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
'@sveltejs/adapter-vercel': '@sveltejs/adapter-vercel':
specifier: ^5.4.1 specifier: ^5.4.1
version: 5.4.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))) version: 5.4.1(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.5.17 specifier: ^2.8.3
version: 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)) version: 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^3.1.1 specifier: ^3.1.1
version: 3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)) version: 3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
@ -42,6 +57,9 @@ importers:
'@types/node': '@types/node':
specifier: ^22.5.4 specifier: ^22.5.4
version: 22.5.4 version: 22.5.4
'@types/qrcode':
specifier: ^1.5.5
version: 1.5.5
autoprefixer: autoprefixer:
specifier: ^10.4.19 specifier: ^10.4.19
version: 10.4.19(postcss@8.4.38) version: 10.4.19(postcss@8.4.38)
@ -374,6 +392,15 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@event-calendar/core@3.7.1':
resolution: {integrity: sha512-S5D4arG7b47uhXmcT/rC7FT3UO9+KB+QhDuhfOzDgKCpAFlEBU1wt1UoHmPTbGy3J+yVMR+rmcresYUvM44+pA==}
'@event-calendar/day-grid@3.7.1':
resolution: {integrity: sha512-kwmadkhUxtQDv+0azMkePrmilFp5dljWLHsluHl1uepfJa1yXlrvFy3GMFnYuPo2Gva0MV+HnU/GMqVG8vIcWw==}
'@event-calendar/time-grid@3.7.1':
resolution: {integrity: sha512-kPC4+XhFcSoNSnYG0TSQeGylpvrbFF1g+cTcFFIW6qH3wPIeBBCo0fRuD4Tr5/q4ewZQ5lNrCkZXOpZxHJxOfw==}
'@formatjs/ecma402-abstract@2.2.1': '@formatjs/ecma402-abstract@2.2.1':
resolution: {integrity: sha512-O4ywpkdJybrjFc9zyL8qK5aklleIAi5O4nYhBVJaOFtCkNrnU+lKFeJOFC48zpsZQmR8Aok2V79hGpHnzbmFpg==} resolution: {integrity: sha512-O4ywpkdJybrjFc9zyL8qK5aklleIAi5O4nYhBVJaOFtCkNrnU+lKFeJOFC48zpsZQmR8Aok2V79hGpHnzbmFpg==}
@ -615,12 +642,12 @@ packages:
peerDependencies: peerDependencies:
'@sveltejs/kit': ^2.4.0 '@sveltejs/kit': ^2.4.0
'@sveltejs/kit@2.5.17': '@sveltejs/kit@2.8.3':
resolution: {integrity: sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==} resolution: {integrity: sha512-DVBVwugfzzn0SxKA+eAmKqcZ7aHZROCHxH7/pyrOi+HLtQ721eEsctGb9MkhEuqj6q/9S/OFYdn37vdxzFPdvw==}
engines: {node: '>=18.13'} engines: {node: '>=18.13'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
'@sveltejs/vite-plugin-svelte': ^3.0.0 '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1
svelte: ^4.0.0 || ^5.0.0-next.0 svelte: ^4.0.0 || ^5.0.0-next.0
vite: ^5.0.3 vite: ^5.0.3
@ -677,6 +704,9 @@ packages:
'@types/pug@2.0.10': '@types/pug@2.0.10':
resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==}
'@types/qrcode@1.5.5':
resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@ -813,8 +843,12 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
caniuse-lite@1.0.30001636: camelcase@5.3.1:
resolution: {integrity: sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==} resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001688:
resolution: {integrity: sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==}
chokidar@3.6.0: chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
@ -828,6 +862,9 @@ packages:
resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
code-red@1.0.4: code-red@1.0.4:
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
@ -907,6 +944,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
deepmerge@4.3.1: deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -926,12 +967,15 @@ packages:
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
engines: {node: '>=8'} engines: {node: '>=8'}
devalue@5.0.0: devalue@5.1.1:
resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==}
didyoumean@1.2.2: didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dijkstrajs@1.0.3:
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
dlv@1.1.3: dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
@ -944,6 +988,9 @@ packages:
electron-to-chromium@1.4.810: electron-to-chromium@1.4.810:
resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==} resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==}
emoji-picker-element@1.26.0:
resolution: {integrity: sha512-IcffFc+LNymYScmMuxOJooZulOCOACGc1Xvj+s7XeKqpc+0EoZfWrV9o4rBjEiuM7XjsgcEjD+m5DHg0aIfnnA==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@ -1032,6 +1079,10 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'} engines: {node: '>=8'}
find-up@4.1.0:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
find-up@5.0.0: find-up@5.0.0:
resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1066,6 +1117,10 @@ packages:
geojson-vt@4.0.2: geojson-vt@4.0.2:
resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-stream@6.0.1: get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1261,6 +1316,10 @@ packages:
locate-character@3.0.0: locate-character@3.0.0:
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
locate-path@6.0.0: locate-path@6.0.0:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1433,14 +1492,26 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'} engines: {node: '>=6'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
p-limit@3.1.0: p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-locate@4.1.0:
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
engines: {node: '>=8'}
p-locate@5.0.0: p-locate@5.0.0:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
package-json-from-dist@1.0.0: package-json-from-dist@1.0.0:
resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==}
@ -1501,6 +1572,10 @@ packages:
pmtiles@3.0.6: pmtiles@3.0.6:
resolution: {integrity: sha512-IdeMETd5lBIDVTLul1HFl0Q7l4KLJjzdxgcp+sN7pYvbipaV7o/0u0HiV06kaFCD0IGEN8KtUHyFZpY30WMflw==} resolution: {integrity: sha512-IdeMETd5lBIDVTLul1HFl0Q7l4KLJjzdxgcp+sN7pYvbipaV7o/0u0HiV06kaFCD0IGEN8KtUHyFZpY30WMflw==}
pngjs@5.0.0:
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
engines: {node: '>=10.13.0'}
postcss-import@15.1.0: postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
@ -1567,6 +1642,11 @@ packages:
protocol-buffers-schema@3.6.0: protocol-buffers-schema@3.6.0:
resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==}
qrcode@1.5.4:
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
engines: {node: '>=10.13.0'}
hasBin: true
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -1584,6 +1664,13 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -1668,9 +1755,9 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'} engines: {node: '>=14'}
sirv@2.0.4: sirv@3.0.0:
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==}
engines: {node: '>= 10'} engines: {node: '>=18'}
sorcery@0.11.1: sorcery@0.11.1:
resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==}
@ -1974,6 +2061,9 @@ packages:
whatwg-url@5.0.0: whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which@1.3.1: which@1.3.1:
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
hasBin: true hasBin: true
@ -1986,6 +2076,10 @@ packages:
wide-align@1.1.5: wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -1997,6 +2091,9 @@ packages:
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
y18n@4.0.3:
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
yallist@4.0.0: yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
@ -2005,6 +2102,14 @@ packages:
engines: {node: '>= 14'} engines: {node: '>= 14'}
hasBin: true hasBin: true
yargs-parser@18.1.3:
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
engines: {node: '>=6'}
yargs@15.4.1:
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
engines: {node: '>=8'}
yocto-queue@0.1.0: yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2167,6 +2272,20 @@ snapshots:
'@esbuild/win32-x64@0.21.5': '@esbuild/win32-x64@0.21.5':
optional: true optional: true
'@event-calendar/core@3.7.1':
dependencies:
svelte: 4.2.19
'@event-calendar/day-grid@3.7.1':
dependencies:
'@event-calendar/core': 3.7.1
svelte: 4.2.19
'@event-calendar/time-grid@3.7.1':
dependencies:
'@event-calendar/core': 3.7.1
svelte: 4.2.19
'@formatjs/ecma402-abstract@2.2.1': '@formatjs/ecma402-abstract@2.2.1':
dependencies: dependencies:
'@formatjs/fast-memoize': 2.2.2 '@formatjs/fast-memoize': 2.2.2
@ -2398,34 +2517,34 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.24.0': '@rollup/rollup-win32-x64-msvc@4.24.0':
optional: true optional: true
'@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))': '@sveltejs/adapter-auto@3.2.2(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
dependencies: dependencies:
'@sveltejs/kit': 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)) '@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
import-meta-resolve: 4.1.0 import-meta-resolve: 4.1.0
'@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))': '@sveltejs/adapter-node@5.2.0(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
dependencies: dependencies:
'@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0) '@rollup/plugin-commonjs': 26.0.1(rollup@4.24.0)
'@rollup/plugin-json': 6.1.0(rollup@4.24.0) '@rollup/plugin-json': 6.1.0(rollup@4.24.0)
'@rollup/plugin-node-resolve': 15.2.3(rollup@4.24.0) '@rollup/plugin-node-resolve': 15.2.3(rollup@4.24.0)
'@sveltejs/kit': 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)) '@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
rollup: 4.24.0 rollup: 4.24.0
'@sveltejs/adapter-vercel@5.4.1(@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))': '@sveltejs/adapter-vercel@5.4.1(@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))':
dependencies: dependencies:
'@sveltejs/kit': 2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)) '@sveltejs/kit': 2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
'@vercel/nft': 0.27.2 '@vercel/nft': 0.27.2
esbuild: 0.21.5 esbuild: 0.21.5
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- supports-color - supports-color
'@sveltejs/kit@2.5.17(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))': '@sveltejs/kit@2.8.3(@sveltejs/vite-plugin-svelte@3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)))(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)) '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4))
'@types/cookie': 0.6.0 '@types/cookie': 0.6.0
cookie: 0.6.0 cookie: 0.6.0
devalue: 5.0.0 devalue: 5.1.1
esm-env: 1.0.0 esm-env: 1.0.0
import-meta-resolve: 4.1.0 import-meta-resolve: 4.1.0
kleur: 4.1.5 kleur: 4.1.5
@ -2433,7 +2552,7 @@ snapshots:
mrmime: 2.0.0 mrmime: 2.0.0
sade: 1.8.1 sade: 1.8.1
set-cookie-parser: 2.6.0 set-cookie-parser: 2.6.0
sirv: 2.0.4 sirv: 3.0.0
svelte: 4.2.19 svelte: 4.2.19
tiny-glob: 0.2.9 tiny-glob: 0.2.9
vite: 5.3.6(@types/node@22.5.4) vite: 5.3.6(@types/node@22.5.4)
@ -2501,6 +2620,10 @@ snapshots:
'@types/pug@2.0.10': {} '@types/pug@2.0.10': {}
'@types/qrcode@1.5.5':
dependencies:
'@types/node': 22.5.4
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
'@types/supercluster@7.1.3': '@types/supercluster@7.1.3':
@ -2578,7 +2701,7 @@ snapshots:
autoprefixer@10.4.19(postcss@8.4.38): autoprefixer@10.4.19(postcss@8.4.38):
dependencies: dependencies:
browserslist: 4.23.1 browserslist: 4.23.1
caniuse-lite: 1.0.30001636 caniuse-lite: 1.0.30001688
fraction.js: 4.3.7 fraction.js: 4.3.7
normalize-range: 0.1.2 normalize-range: 0.1.2
picocolors: 1.0.1 picocolors: 1.0.1
@ -2612,7 +2735,7 @@ snapshots:
browserslist@4.23.1: browserslist@4.23.1:
dependencies: dependencies:
caniuse-lite: 1.0.30001636 caniuse-lite: 1.0.30001688
electron-to-chromium: 1.4.810 electron-to-chromium: 1.4.810
node-releases: 2.0.14 node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.1) update-browserslist-db: 1.0.16(browserslist@4.23.1)
@ -2636,7 +2759,9 @@ snapshots:
camelcase-css@2.0.1: {} camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001636: {} camelcase@5.3.1: {}
caniuse-lite@1.0.30001688: {}
chokidar@3.6.0: chokidar@3.6.0:
dependencies: dependencies:
@ -2660,6 +2785,12 @@ snapshots:
memoizee: 0.4.17 memoizee: 0.4.17
timers-ext: 0.1.8 timers-ext: 0.1.8
cliui@6.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
code-red@1.0.4: code-red@1.0.4:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
@ -2734,6 +2865,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
decamelize@1.2.0: {}
deepmerge@4.3.1: {} deepmerge@4.3.1: {}
delegates@1.0.0: {} delegates@1.0.0: {}
@ -2744,10 +2877,12 @@ snapshots:
detect-libc@2.0.3: {} detect-libc@2.0.3: {}
devalue@5.0.0: {} devalue@5.1.1: {}
didyoumean@1.2.2: {} didyoumean@1.2.2: {}
dijkstrajs@1.0.3: {}
dlv@1.1.3: {} dlv@1.1.3: {}
earcut@2.2.4: {} earcut@2.2.4: {}
@ -2756,6 +2891,8 @@ snapshots:
electron-to-chromium@1.4.810: {} electron-to-chromium@1.4.810: {}
emoji-picker-element@1.26.0: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}
@ -2908,6 +3045,11 @@ snapshots:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
find-up@4.1.0:
dependencies:
locate-path: 5.0.0
path-exists: 4.0.0
find-up@5.0.0: find-up@5.0.0:
dependencies: dependencies:
locate-path: 6.0.0 locate-path: 6.0.0
@ -2945,6 +3087,8 @@ snapshots:
geojson-vt@4.0.2: {} geojson-vt@4.0.2: {}
get-caller-file@2.0.5: {}
get-stream@6.0.1: {} get-stream@6.0.1: {}
get-value@2.0.6: {} get-value@2.0.6: {}
@ -3116,6 +3260,10 @@ snapshots:
locate-character@3.0.0: {} locate-character@3.0.0: {}
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
locate-path@6.0.0: locate-path@6.0.0:
dependencies: dependencies:
p-locate: 5.0.0 p-locate: 5.0.0
@ -3289,14 +3437,24 @@ snapshots:
dependencies: dependencies:
mimic-fn: 2.1.0 mimic-fn: 2.1.0
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
p-limit@3.1.0: p-limit@3.1.0:
dependencies: dependencies:
yocto-queue: 0.1.0 yocto-queue: 0.1.0
p-locate@4.1.0:
dependencies:
p-limit: 2.3.0
p-locate@5.0.0: p-locate@5.0.0:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
p-try@2.2.0: {}
package-json-from-dist@1.0.0: {} package-json-from-dist@1.0.0: {}
parent-module@1.0.1: parent-module@1.0.1:
@ -3350,6 +3508,8 @@ snapshots:
'@types/leaflet': 1.9.12 '@types/leaflet': 1.9.12
fflate: 0.8.2 fflate: 0.8.2
pngjs@5.0.0: {}
postcss-import@15.1.0(postcss@8.4.38): postcss-import@15.1.0(postcss@8.4.38):
dependencies: dependencies:
postcss: 8.4.38 postcss: 8.4.38
@ -3409,6 +3569,12 @@ snapshots:
protocol-buffers-schema@3.6.0: {} protocol-buffers-schema@3.6.0: {}
qrcode@1.5.4:
dependencies:
dijkstrajs: 1.0.3
pngjs: 5.0.0
yargs: 15.4.1
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
quickselect@2.0.0: {} quickselect@2.0.0: {}
@ -3427,6 +3593,10 @@ snapshots:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
require-directory@2.1.1: {}
require-main-filename@2.0.0: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve-from@5.0.0: {} resolve-from@5.0.0: {}
@ -3517,7 +3687,7 @@ snapshots:
signal-exit@4.1.0: {} signal-exit@4.1.0: {}
sirv@2.0.4: sirv@3.0.0:
dependencies: dependencies:
'@polka/url': 1.0.0-next.25 '@polka/url': 1.0.0-next.25
mrmime: 2.0.0 mrmime: 2.0.0
@ -3823,6 +3993,8 @@ snapshots:
tr46: 0.0.3 tr46: 0.0.3
webidl-conversions: 3.0.1 webidl-conversions: 3.0.1
which-module@2.0.1: {}
which@1.3.1: which@1.3.1:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
@ -3835,6 +4007,12 @@ snapshots:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
@ -3849,8 +4027,29 @@ snapshots:
wrappy@1.0.2: {} wrappy@1.0.2: {}
y18n@4.0.3: {}
yallist@4.0.0: {} yallist@4.0.0: {}
yaml@2.4.5: {} yaml@2.4.5: {}
yargs-parser@18.1.3:
dependencies:
camelcase: 5.3.1
decamelize: 1.2.0
yargs@15.4.1:
dependencies:
cliui: 6.0.0
decamelize: 1.2.0
find-up: 4.1.0
get-caller-file: 2.0.5
require-directory: 2.1.1
require-main-filename: 2.0.0
set-blocking: 2.0.0
string-width: 4.2.3
which-module: 2.0.1
y18n: 4.0.3
yargs-parser: 18.1.3
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}

View file

@ -1,95 +1,65 @@
import type { Handle } from '@sveltejs/kit'; import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks'; import { sequence } from '@sveltejs/kit/hooks';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server';
export const authHook: Handle = async ({ event, resolve }) => { export const authHook: Handle = async ({ event, resolve }) => {
try { try {
let authCookie = event.cookies.get('auth'); let sessionid = event.cookies.get('sessionid');
let refreshCookie = event.cookies.get('refresh');
if (!authCookie && !refreshCookie) { if (!sessionid) {
event.locals.user = null; event.locals.user = null;
return await resolve(event); return await resolve(event);
} }
if (!authCookie && refreshCookie) {
event.locals.user = null;
const token = await tryRefreshToken(event.cookies.get('refresh') || '');
if (token) {
authCookie = token;
event.cookies.set('auth', authCookie, {
httpOnly: true,
sameSite: 'lax',
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
path: '/'
});
} else {
return await resolve(event);
}
}
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
let userFetch = await event.fetch(`${serverEndpoint}/auth/user/`, { const cookie = event.request.headers.get('cookie') || '';
let userFetch = await event.fetch(`${serverEndpoint}/auth/user-metadata/`, {
headers: { headers: {
Cookie: `${authCookie}` cookie
} }
}); });
if (!userFetch.ok) { if (!userFetch.ok) {
console.log('Refreshing token');
const refreshCookie = event.cookies.get('refresh');
if (refreshCookie) {
const csrfToken = await fetchCSRFToken();
if (!csrfToken) {
console.error('Failed to fetch CSRF token');
event.locals.user = null; event.locals.user = null;
event.cookies.delete('sessionid', { path: '/' });
return await resolve(event); return await resolve(event);
} }
const refreshFetch = await event.fetch(`${serverEndpoint}/auth/token/refresh/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh: refreshCookie })
});
if (refreshFetch.ok) {
const refresh = await refreshFetch.json();
event.cookies.set('auth', 'auth=' + refresh.access, {
httpOnly: true,
sameSite: 'lax',
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
path: '/'
});
userFetch = await event.fetch(`${serverEndpoint}/auth/user/`, {
headers: {
'X-CSRFToken': csrfToken,
Cookie: `auth=${refresh.access}`
}
});
}
}
}
if (userFetch.ok) { if (userFetch.ok) {
const user = await userFetch.json(); const user = await userFetch.json();
event.locals.user = user; event.locals.user = user;
const setCookieHeader = userFetch.headers.get('Set-Cookie');
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);
// Set the sessionid cookie
event.cookies.set('sessionid', sessionId, {
path: '/',
httpOnly: true,
sameSite: 'lax',
secure: true,
expires: expiryDate
});
}
}
} else { } else {
event.locals.user = null; event.locals.user = null;
event.cookies.delete('auth', { path: '/' }); event.cookies.delete('sessionid', { path: '/' });
event.cookies.delete('refresh', { path: '/' });
} }
} catch (error) { } catch (error) {
console.error('Error in authHook:', error); console.error('Error in authHook:', error);
event.locals.user = null; event.locals.user = null;
event.cookies.delete('auth', { path: '/' }); event.cookies.delete('sessionid', { path: '/' });
event.cookies.delete('refresh', { path: '/' });
} }
return await resolve(event); return await resolve(event);

View file

@ -18,6 +18,7 @@
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}); });
console.log(res);
let data = await res.json(); let data = await res.json();
console.log('ACTIVITIES' + data.activities); console.log('ACTIVITIES' + data.activities);
if (data && data.activities) { if (data && data.activities) {

View file

@ -19,9 +19,10 @@
import CardCarousel from './CardCarousel.svelte'; import CardCarousel from './CardCarousel.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let type: string; export let type: string | null = null;
export let user: User | null; export let user: User | null;
export let collection: Collection | null = null; export let collection: Collection | null = null;
export let readOnly: boolean = false;
let isCollectionModalOpen: boolean = false; let isCollectionModalOpen: boolean = false;
let isWarningModalOpen: boolean = false; let isWarningModalOpen: boolean = false;
@ -164,8 +165,10 @@
{/each} {/each}
</ul> </ul>
{/if} {/if}
{#if !readOnly}
<div class="card-actions justify-end mt-2"> <div class="card-actions justify-end mt-2">
<!-- action options dropdown --> <!-- action options dropdown -->
{#if type != 'link'} {#if type != 'link'}
{#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))} {#if adventure.user_id == user?.uuid || (collection && user && collection.shared_with.includes(user.uuid))}
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
@ -196,7 +199,9 @@
> >
{/if} {/if}
{#if !adventure.collection} {#if !adventure.collection}
<button class="btn btn-neutral mb-2" on:click={() => (isCollectionModalOpen = true)} <button
class="btn btn-neutral mb-2"
on:click={() => (isCollectionModalOpen = true)}
><Plus class="w-6 h-6" />{$t('adventures.add_to_collection')}</button ><Plus class="w-6 h-6" />{$t('adventures.add_to_collection')}</button
> >
{/if} {/if}
@ -212,7 +217,8 @@
{:else} {:else}
<button <button
class="btn btn-neutral-200 mb-2" class="btn btn-neutral-200 mb-2"
on:click={() => goto(`/adventures/${adventure.id}`)}><Launch class="w-6 h-6" /></button on:click={() => goto(`/adventures/${adventure.id}`)}
><Launch class="w-6 h-6" /></button
> >
{/if} {/if}
{/if} {/if}
@ -220,5 +226,6 @@
<button class="btn btn-primary" on:click={link}><Link class="w-6 h-6" /></button> <button class="btn btn-primary" on:click={link}><Link class="w-6 h-6" /></button>
{/if} {/if}
</div> </div>
{/if}
</div> </div>
</div> </div>

View file

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { deserialize } from '$app/forms';
import type { Adventure, User } from '$lib/types'; import type { Adventure, User } from '$lib/types';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -59,7 +58,7 @@
{/if} {/if}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center"> <div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each adventures as adventure} {#each adventures as adventure}
<AdventureCard user={user ?? null} type="link" {adventure} on:link={add} /> <AdventureCard {user} type="link" {adventure} on:link={add} />
{/each} {/each}
{#if adventures.length === 0 && !isLoading} {#if adventures.length === 0 && !isLoading}
<p class="text-center text-lg"> <p class="text-center text-lg">

View file

@ -14,7 +14,12 @@
num_adventures: 0 num_adventures: 0
}; };
let isOpen = false; let isOpen: boolean = false;
let isEmojiPickerVisible: boolean = false;
function toggleEmojiPicker() {
isEmojiPickerVisible = !isEmojiPickerVisible;
}
function toggleDropdown() { function toggleDropdown() {
isOpen = !isOpen; isOpen = !isOpen;
@ -31,6 +36,10 @@
selectCategory(new_category); selectCategory(new_category);
} }
function handleEmojiSelect(event: CustomEvent) {
new_category.icon = event.detail.unicode;
}
// Close dropdown when clicking outside // Close dropdown when clicking outside
let dropdownRef: HTMLDivElement; let dropdownRef: HTMLDivElement;
@ -46,6 +55,9 @@
document.removeEventListener('click', handleClickOutside); document.removeEventListener('click', handleClickOutside);
}; };
}); });
onMount(async () => {
await import('emoji-picker-element');
});
</script> </script>
<div class="mt-2 relative" bind:this={dropdownRef}> <div class="mt-2 relative" bind:this={dropdownRef}>
@ -59,6 +71,7 @@
<div class="absolute z-10 w-full mt-1 bg-base-300 rounded shadow-lg p-2"> <div class="absolute z-10 w-full mt-1 bg-base-300 rounded shadow-lg p-2">
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input <input
type="text" type="text"
@ -72,10 +85,22 @@
class="input input-bordered w-full max-w-xs" class="input input-bordered w-full max-w-xs"
bind:value={new_category.icon} bind:value={new_category.icon}
/> />
<button on:click={custom_category} type="button" class="btn btn-primary" <button on:click={toggleEmojiPicker} type="button" class="btn btn-secondary">
>{$t('adventures.add')}</button {isEmojiPickerVisible ? $t('adventures.hide') : $t('adventures.hide')}
> {$t('adventures.emoji_picker')}
</button>
<button on:click={custom_category} type="button" class="btn btn-primary">
{$t('adventures.add')}
</button>
</div> </div>
{#if isEmojiPickerVisible}
<div class="mt-2">
<emoji-picker on:emoji-click={handleEmojiSelect}></emoji-picker>
</div>
{/if}
</div>
<div class="flex flex-wrap gap-2 mt-2"> <div class="flex flex-wrap gap-2 mt-2">
<!-- Sort the categories dynamically before rendering --> <!-- Sort the categories dynamically before rendering -->
{#each categories {#each categories

View file

@ -5,14 +5,10 @@
import type { SubmitFunction } from '@sveltejs/kit'; import type { SubmitFunction } from '@sveltejs/kit';
import DotsHorizontal from '~icons/mdi/dots-horizontal'; import DotsHorizontal from '~icons/mdi/dots-horizontal';
import WeatherSunny from '~icons/mdi/weather-sunny'; import Calendar from '~icons/mdi/calendar';
import WeatherNight from '~icons/mdi/weather-night';
import Forest from '~icons/mdi/forest';
import Water from '~icons/mdi/water';
import AboutModal from './AboutModal.svelte'; import AboutModal from './AboutModal.svelte';
import AccountMultiple from '~icons/mdi/account-multiple'; import AccountMultiple from '~icons/mdi/account-multiple';
import Avatar from './Avatar.svelte'; import Avatar from './Avatar.svelte';
import PaletteOutline from '~icons/mdi/palette-outline';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { t, locale, locales } from 'svelte-i18n'; import { t, locale, locales } from 'svelte-i18n';
import { themes } from '$lib'; import { themes } from '$lib';
@ -91,6 +87,9 @@
<li> <li>
<button on:click={() => goto('/map')}>{$t('navbar.map')}</button> <button on:click={() => goto('/map')}>{$t('navbar.map')}</button>
</li> </li>
<li>
<button on:click={() => goto('/calendar')}>{$t('navbar.calendar')}</button>
</li>
<li> <li>
<button on:click={() => goto('/users')}>{$t('navbar.users')}</button> <button on:click={() => goto('/users')}>{$t('navbar.users')}</button>
</li> </li>
@ -157,6 +156,9 @@
<li> <li>
<button class="btn btn-neutral" on:click={() => goto('/map')}>{$t('navbar.map')}</button> <button class="btn btn-neutral" on:click={() => goto('/map')}>{$t('navbar.map')}</button>
</li> </li>
<li>
<button class="btn btn-neutral" on:click={() => goto('/calendar')}><Calendar /></button>
</li>
<li> <li>
<button class="btn btn-neutral" on:click={() => goto('/users')} <button class="btn btn-neutral" on:click={() => goto('/users')}
><AccountMultiple /></button ><AccountMultiple /></button

View file

@ -246,7 +246,7 @@
</div> </div>
</div> </div>
<button type="submit" class="btn btn-primary mr-4 mt-4">{$t('transportation.edit')}</button> <button type="submit" class="btn btn-primary mr-4 mt-4">{$t('collection.create')}</button>
<!-- if there is a button in form, it will close the modal --> <!-- if there is a button in form, it will close the modal -->
<button class="btn mt-4" on:click={close}>{$t('about.close')}</button> <button class="btn mt-4" on:click={close}>{$t('about.close')}</button>
</form> </form>

View file

@ -0,0 +1,184 @@
<script lang="ts">
import { addToast } from '$lib/toasts';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { onMount } from 'svelte';
let modal: HTMLDialogElement;
// @ts-ignore
import QRCode from 'qrcode';
import { t } from 'svelte-i18n';
import type { User } from '$lib/types';
export let user: User | null = null;
let secret: string | null = null;
let qrCodeDataUrl: string | null = null;
let totpUrl: string | null = null;
let first_code: string = '';
let recovery_codes: string[] = [];
export let is_enabled: boolean;
let reauthError: boolean = false;
onMount(() => {
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
if (modal) {
modal.showModal();
}
fetchSetupInfo();
console.log(secret);
});
async function generateQRCode(secret: string | null) {
try {
if (secret) {
qrCodeDataUrl = await QRCode.toDataURL(secret);
}
} catch (error) {
console.error('Error generating QR code:', error);
}
}
async function fetchSetupInfo() {
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
method: 'GET'
});
const data = await res.json();
if (res.status == 404) {
secret = data.meta.secret;
totpUrl = `otpauth://totp/AdventureLog:${user?.username}?secret=${secret}&issuer=AdventureLog`;
generateQRCode(totpUrl);
} else if (res.ok) {
close();
} else {
addToast('error', $t('settings.generic_error'));
}
}
async function sendTotp() {
const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code: first_code
}),
credentials: 'include'
});
console.log(res);
if (res.ok) {
addToast('success', $t('settings.mfa_enabled'));
is_enabled = true;
getRecoveryCodes();
} else {
if (res.status == 401) {
reauthError = true;
}
addToast('error', $t('settings.generic_error'));
}
}
async function getRecoveryCodes() {
console.log('getting recovery codes');
const res = await fetch('/_allauth/browser/v1/account/authenticators/recovery-codes', {
method: 'GET'
});
if (res.ok) {
let data = await res.json();
recovery_codes = data.data.unused_codes;
} else {
addToast('error', $t('settings.generic_error'));
}
}
function close() {
dispatch('close');
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
dispatch('close');
}
}
function copyToClipboard(copyText: string | null) {
if (copyText) {
navigator.clipboard.writeText(copyText).then(
() => {
addToast('success', $t('adventures.copied_to_clipboard'));
},
() => {
addToast('error', $t('adventures.copy_failed'));
}
);
}
}
</script>
<dialog id="my_modal_1" class="modal">
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
<h3 class="font-bold text-lg">{$t('settings.enable_mfa')}</h3>
{#if qrCodeDataUrl}
<div class="mb-4 flex items-center justify-center mt-2">
<img src={qrCodeDataUrl} alt="QR Code" class="w-64 h-64" />
</div>
{/if}
<div class="flex items-center justify-center mb-6">
{#if secret}
<div class="flex items-center">
<input
type="text"
placeholder={secret}
class="input input-bordered w-full max-w-xs"
readonly
/>
<button class="btn btn-primary ml-2" on:click={() => copyToClipboard(secret)}
>{$t('settings.copy')}</button
>
</div>
{/if}
</div>
<input
type="text"
placeholder={$t('settings.authenticator_code')}
class="input input-bordered w-full max-w-xs"
bind:value={first_code}
/>
<div class="recovery-codes-container">
{#if recovery_codes.length > 0}
<h3 class="mt-4 text-center font-bold text-lg">{$t('settings.recovery_codes')}</h3>
<p class="text-center text-lg mb-2">
{$t('settings.recovery_codes_desc')}
</p>
<button
class="btn btn-primary ml-2"
on:click={() => copyToClipboard(recovery_codes.join(', '))}>{$t('settings.copy')}</button
>
{/if}
<div class="recovery-codes-grid flex flex-wrap">
{#each recovery_codes as code}
<div
class="recovery-code-item flex items-center justify-center m-2 w-full sm:w-1/2 md:w-1/3 lg:w-1/4"
>
<input type="text" value={code} class="input input-bordered w-full" readonly />
</div>
{/each}
</div>
</div>
{#if reauthError}
<div class="alert alert-error mt-4">
{$t('settings.reset_session_error')}
</div>
{/if}
{#if !is_enabled}
<button class="btn btn-primary mt-4" on:click={sendTotp}>{$t('settings.enable_mfa')}</button>
{/if}
<button class="btn btn-primary mt-4" on:click={close}>{$t('about.close')}</button>
</div>
</dialog>

View file

@ -10,27 +10,3 @@ export const fetchCSRFToken = async () => {
return null; 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: '/'
// });
}
};

View file

@ -313,5 +313,6 @@ export let themes = [
{ name: 'forest', label: 'Forest' }, { name: 'forest', label: 'Forest' },
{ name: 'aqua', label: 'Aqua' }, { name: 'aqua', label: 'Aqua' },
{ name: 'aestheticLight', label: 'Aesthetic Light' }, { name: 'aestheticLight', label: 'Aesthetic Light' },
{ name: 'aestheticDark', label: 'Aesthetic Dark' } { name: 'aestheticDark', label: 'Aesthetic Dark' },
{ name: 'northernLights', label: 'Northern Lights' }
]; ];

View file

@ -188,7 +188,14 @@
"add_a_tag": "Fügen Sie ein Tag hinzu", "add_a_tag": "Fügen Sie ein Tag hinzu",
"tags": "Schlagworte", "tags": "Schlagworte",
"set_to_pin": "Auf „Anpinnen“ setzen", "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": { "home": {
"desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit",
@ -226,14 +233,16 @@
"light": "Licht", "light": "Licht",
"night": "Nacht", "night": "Nacht",
"aestheticDark": "Ästhetisches Dunkel", "aestheticDark": "Ästhetisches Dunkel",
"aestheticLight": "Ästhetisches Licht" "aestheticLight": "Ästhetisches Licht",
"northernLights": "Nordlicht"
}, },
"users": "Benutzer", "users": "Benutzer",
"worldtravel": "Weltreisen", "worldtravel": "Weltreisen",
"my_tags": "Meine Tags", "my_tags": "Meine Tags",
"tag": "Etikett", "tag": "Etikett",
"language_selection": "Sprache", "language_selection": "Sprache",
"support": "Unterstützung" "support": "Unterstützung",
"calendar": "Kalender"
}, },
"auth": { "auth": {
"confirm_password": "Passwort bestätigen", "confirm_password": "Passwort bestätigen",
@ -249,7 +258,11 @@
"username": "Benutzername", "username": "Benutzername",
"profile_picture": "Profilbild", "profile_picture": "Profilbild",
"public_profile": "Öffentliches Profil", "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": { "users": {
"no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden." "no_users_found": "Keine Benutzer mit öffentlichen Profilen gefunden."
@ -291,7 +304,50 @@
"about_this_background": "Über diesen Hintergrund", "about_this_background": "Über diesen Hintergrund",
"join_discord": "Treten Sie dem Discord bei", "join_discord": "Treten Sie dem Discord bei",
"join_discord_desc": "um Ihre eigenen Fotos zu teilen. \nVeröffentlichen Sie sie im", "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": { "checklist": {
"add_item": "Artikel hinzufügen", "add_item": "Artikel hinzufügen",
@ -411,5 +467,14 @@
"no_categories_found": "Keine Kategorien gefunden.", "no_categories_found": "Keine Kategorien gefunden.",
"select_category": "Kategorie auswählen", "select_category": "Kategorie auswählen",
"update_after_refresh": "Die Abenteuerkarten werden aktualisiert, sobald Sie die Seite aktualisieren." "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"
} }
} }

View file

@ -19,6 +19,7 @@
"discord": "Discord", "discord": "Discord",
"language_selection": "Language", "language_selection": "Language",
"support": "Support", "support": "Support",
"calendar": "Calendar",
"theme_selection": "Theme Selection", "theme_selection": "Theme Selection",
"themes": { "themes": {
"light": "Light", "light": "Light",
@ -27,7 +28,8 @@
"forest": "Forest", "forest": "Forest",
"aestheticLight": "Aesthetic Light", "aestheticLight": "Aesthetic Light",
"aestheticDark": "Aesthetic Dark", "aestheticDark": "Aesthetic Dark",
"aqua": "Aqua" "aqua": "Aqua",
"northernLights": "Northern Lights"
} }
}, },
"about": { "about": {
@ -206,9 +208,16 @@
"notes": "Notes", "notes": "Notes",
"checklists": "Checklists", "checklists": "Checklists",
"transportations": "Transportations", "transportations": "Transportations",
"adventure_calendar": "Adventure Calendar",
"day": "Day", "day": "Day",
"itineary_by_date": "Itinerary by Date", "itineary_by_date": "Itinerary by Date",
"nothing_planned": "Nothing planned for this day. Enjoy the journey!", "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", "days": "days",
"activities": { "activities": {
"general": "General 🌍", "general": "General 🌍",
@ -260,7 +269,11 @@
"registration_disabled": "Registration is currently disabled.", "registration_disabled": "Registration is currently disabled.",
"profile_picture": "Profile Picture", "profile_picture": "Profile Picture",
"public_profile": "Public Profile", "public_profile": "Public Profile",
"public_tooltip": "With a public profile, users can share collections with you and view your profile on the users page." "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": { "users": {
"no_users_found": "No users found with public profiles." "no_users_found": "No users found with public profiles."
@ -291,7 +304,50 @@
"about_this_background": "About this background", "about_this_background": "About this background",
"photo_by": "Photo by", "photo_by": "Photo by",
"join_discord": "Join the Discord", "join_discord": "Join the Discord",
"join_discord_desc": "to share your own photos. Post them in the #travel-share channel." "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": {
"collection_created": "Collection created successfully!", "collection_created": "Collection created successfully!",
@ -411,5 +467,14 @@
"update_after_refresh": "The adventure cards will be updated once you refresh the page.", "update_after_refresh": "The adventure cards will be updated once you refresh the page.",
"select_category": "Select Category", "select_category": "Select Category",
"category_name": "Category Name" "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."
} }
} }

View file

@ -23,12 +23,14 @@
"forest": "Bosque", "forest": "Bosque",
"aqua": "Aqua", "aqua": "Aqua",
"aestheticDark": "Estética Oscura", "aestheticDark": "Estética Oscura",
"aestheticLight": "Luz estetica" "aestheticLight": "Luz estetica",
"northernLights": "Aurora boreal"
}, },
"my_tags": "Mis etiquetas", "my_tags": "Mis etiquetas",
"tag": "Etiqueta", "tag": "Etiqueta",
"language_selection": "Idioma", "language_selection": "Idioma",
"support": "Apoyo" "support": "Apoyo",
"calendar": "Calendario"
}, },
"about": { "about": {
"about": "Acerca de", "about": "Acerca de",
@ -233,7 +235,14 @@
"add_a_tag": "Agregar una etiqueta", "add_a_tag": "Agregar una etiqueta",
"tags": "Etiquetas", "tags": "Etiquetas",
"set_to_pin": "Establecer en Fijar", "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": { "worldtravel": {
"all": "Todo", "all": "Todo",
@ -260,7 +269,11 @@
"registration_disabled": "El registro está actualmente deshabilitado.", "registration_disabled": "El registro está actualmente deshabilitado.",
"profile_picture": "Foto de perfil", "profile_picture": "Foto de perfil",
"public_profile": "Perfil público", "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": { "users": {
"no_users_found": "No se encontraron usuarios con perfiles públicos." "no_users_found": "No se encontraron usuarios con perfiles públicos."
@ -291,7 +304,50 @@
"about_this_background": "Sobre este trasfondo", "about_this_background": "Sobre este trasfondo",
"join_discord": "Únete a la discordia", "join_discord": "Únete a la discordia",
"join_discord_desc": "para compartir tus propias fotos. \nPublicarlos en el", "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": { "checklist": {
"add_item": "Agregar artículo", "add_item": "Agregar artículo",
@ -411,5 +467,14 @@
"no_categories_found": "No se encontraron categorías.", "no_categories_found": "No se encontraron categorías.",
"select_category": "Seleccionar categoría", "select_category": "Seleccionar categoría",
"update_after_refresh": "Las tarjetas de aventuras se actualizarán una vez que actualices la página." "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"
} }
} }

View file

@ -188,7 +188,14 @@
"add_a_tag": "Ajouter une balise", "add_a_tag": "Ajouter une balise",
"tags": "Balises", "tags": "Balises",
"set_to_pin": "Définir sur Épingler", "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": { "home": {
"desc_1": "Découvrez, planifiez et explorez en toute simplicité", "desc_1": "Découvrez, planifiez et explorez en toute simplicité",
@ -226,14 +233,16 @@
"aqua": "Aqua", "aqua": "Aqua",
"dark": "Sombre", "dark": "Sombre",
"aestheticDark": "Esthétique sombre", "aestheticDark": "Esthétique sombre",
"aestheticLight": "Lumière esthétique" "aestheticLight": "Lumière esthétique",
"northernLights": "Aurores boréales"
}, },
"users": "Utilisateurs", "users": "Utilisateurs",
"worldtravel": "Voyage dans le monde", "worldtravel": "Voyage dans le monde",
"my_tags": "Mes balises", "my_tags": "Mes balises",
"tag": "Étiqueter", "tag": "Étiqueter",
"language_selection": "Langue", "language_selection": "Langue",
"support": "Soutien" "support": "Soutien",
"calendar": "Calendrier"
}, },
"auth": { "auth": {
"confirm_password": "Confirmez le mot de passe", "confirm_password": "Confirmez le mot de passe",
@ -249,7 +258,11 @@
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"profile_picture": "Photo de profil", "profile_picture": "Photo de profil",
"public_profile": "Profil public", "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": { "users": {
"no_users_found": "Aucun utilisateur trouvé avec des profils publics." "no_users_found": "Aucun utilisateur trouvé avec des profils publics."
@ -291,7 +304,50 @@
"about_this_background": "À propos de ce contexte", "about_this_background": "À propos de ce contexte",
"join_discord": "Rejoignez le Discord", "join_discord": "Rejoignez le Discord",
"join_discord_desc": "pour partager vos propres photos. \nPostez-les dans le", "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": { "checklist": {
"add_item": "Ajouter un article", "add_item": "Ajouter un article",
@ -411,5 +467,14 @@
"no_categories_found": "Aucune catégorie trouvée.", "no_categories_found": "Aucune catégorie trouvée.",
"select_category": "Sélectionnez une catégorie", "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." "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"
} }
} }

View file

@ -188,7 +188,14 @@
"add_a_tag": "Aggiungi un'etichetta", "add_a_tag": "Aggiungi un'etichetta",
"tags": "Tag", "tags": "Tag",
"set_to_pin": "Imposta su Blocca", "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": { "home": {
"desc_1": "Scopri, pianifica ed esplora con facilità", "desc_1": "Scopri, pianifica ed esplora con facilità",
@ -226,14 +233,16 @@
"light": "Leggero", "light": "Leggero",
"night": "Notte", "night": "Notte",
"aestheticDark": "Estetico scuro", "aestheticDark": "Estetico scuro",
"aestheticLight": "Luce estetica" "aestheticLight": "Luce estetica",
"northernLights": "Aurora boreale"
}, },
"users": "Utenti", "users": "Utenti",
"worldtravel": "Viaggio nel mondo", "worldtravel": "Viaggio nel mondo",
"my_tags": "I miei tag", "my_tags": "I miei tag",
"tag": "Etichetta", "tag": "Etichetta",
"language_selection": "Lingua", "language_selection": "Lingua",
"support": "Supporto" "support": "Supporto",
"calendar": "Calendario"
}, },
"auth": { "auth": {
"confirm_password": "Conferma password", "confirm_password": "Conferma password",
@ -249,7 +258,11 @@
"username": "Nome utente", "username": "Nome utente",
"profile_picture": "Immagine del profilo", "profile_picture": "Immagine del profilo",
"public_profile": "Profilo pubblico", "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": { "users": {
"no_users_found": "Nessun utente trovato con profili pubblici." "no_users_found": "Nessun utente trovato con profili pubblici."
@ -291,7 +304,50 @@
"about_this_background": "A proposito di questo contesto", "about_this_background": "A proposito di questo contesto",
"join_discord": "Unisciti alla Discordia", "join_discord": "Unisciti alla Discordia",
"join_discord_desc": "per condividere le tue foto. \nPubblicateli in", "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": { "checklist": {
"add_item": "Aggiungi articolo", "add_item": "Aggiungi articolo",
@ -411,5 +467,14 @@
"no_categories_found": "Nessuna categoria trovata.", "no_categories_found": "Nessuna categoria trovata.",
"select_category": "Seleziona Categoria", "select_category": "Seleziona Categoria",
"update_after_refresh": "Le carte avventura verranno aggiornate una volta aggiornata la pagina." "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"
} }
} }

View file

@ -1,13 +1,13 @@
{ {
"about": { "about": {
"about": "Over", "about": "Over",
"close": "Dichtbij", "close": "Sluiten",
"license": "Gelicentieerd onder de GPL-3.0-licentie.", "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_1": "Locatie zoeken en geocodering wordt verzorgd door",
"nominatim_2": "Hun gegevens zijn in licentie gegeven onder de ODbL-licentie.", "nominatim_2": "Hun gegevens zijn in licentie gegeven onder de ODbL-licentie.",
"oss_attributions": "Open source-attributies", "oss_attributions": "Open source gebruik",
"other_attributions": "Aanvullende toeschrijvingen zijn te vinden in het README-bestand.", "other_attributions": "Aanvullende vermeldingen zijn te vinden in het README-bestand.",
"source_code": "Broncode" "source_code": "Broncode"
}, },
"adventures": { "adventures": {
@ -33,38 +33,38 @@
"transportation": "Vervoer 🚗", "transportation": "Vervoer 🚗",
"volunteer_work": "Vrijwilligerswerk 🤝", "volunteer_work": "Vrijwilligerswerk 🤝",
"water_sports": "Watersport 🚤", "water_sports": "Watersport 🚤",
"wildlife": "Dieren in het wild 🦒" "wildlife": "Wilde dieren 🦒"
}, },
"add_to_collection": "Toevoegen aan collectie", "add_to_collection": "Toevoegen aan collectie",
"adventure": "Avontuur", "adventure": "Avontuur",
"adventure_delete_confirm": "Weet je zeker dat je dit avontuur wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.", "adventure_delete_confirm": "Weet je zeker dat je dit avontuur wilt verwijderen? \nDeze actie kan niet ongedaan worden gemaakt.",
"adventure_details": "Avontuurdetails", "adventure_details": "Avontuurdetails",
"adventure_type": "Avontuurtype", "adventure_type": "Avontuurtype",
"archive": "Archief", "archive": "Archiveer",
"archived": "Gearchiveerd", "archived": "Gearchiveerd",
"archived_collection_message": "Collectie succesvol gearchiveerd!", "archived_collection_message": "Collectie succesvol gearchiveerd!",
"archived_collections": "Gearchiveerde collecties", "archived_collections": "Gearchiveerde collecties",
"ascending": "Oplopend", "ascending": "Oplopend",
"cancel": "Annuleren", "cancel": "Annuleren",
"category_filter": "Categoriefilter", "category_filter": "Categoriefilter",
"clear": "Duidelijk", "clear": "Leeg maken",
"close_filters": "Sluit Filters", "close_filters": "Sluit filters",
"collection": "Verzameling", "collection": "Collectie",
"collection_adventures": "Inclusief collectie-avonturen", "collection_adventures": "Inclusief collectie-avonturen",
"collection_link_error": "Fout bij het koppelen van avontuur aan collectie", "collection_link_error": "Fout bij het koppelen van avontuur aan collectie",
"collection_link_success": "Avontuur succesvol gekoppeld 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!", "collection_remove_success": "Avontuur is succesvol uit de collectie verwijderd!",
"count_txt": "resultaten die overeenkomen met uw zoekopdracht", "count_txt": "resultaten die overeenkomen met uw zoekopdracht",
"create_new": "Maak nieuwe...", "create_new": "Maak nieuwe...",
"date": "Datum", "date": "Datum",
"delete": "Verwijderen", "delete": "Verwijderen",
"delete_collection": "Verzameling verwijderen", "delete_collection": "Collectie verwijderen",
"delete_collection_success": "Collectie succesvol verwijderd!", "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", "descending": "Aflopend",
"edit_adventure": "Avontuur bewerken", "edit_adventure": "Avontuur bewerken",
"edit_collection": "Verzameling bewerken", "edit_collection": "Collectie bewerken",
"filter": "Filter", "filter": "Filter",
"homepage": "Startpagina", "homepage": "Startpagina",
"latitude": "Breedte", "latitude": "Breedte",
@ -73,21 +73,21 @@
"name": "Naam", "name": "Naam",
"no_image_found": "Geen afbeelding gevonden", "no_image_found": "Geen afbeelding gevonden",
"not_found": "Avontuur niet 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_details": "Details openen",
"open_filters": "Filters openen", "open_filters": "Filters openen",
"order_by": "Bestel per", "order_by": "Sorteer op",
"order_direction": "Bestelrichting", "order_direction": "Sorteervolgorde",
"private": "Privé", "private": "Privé",
"public": "Openbaar", "public": "Openbaar",
"rating": "Beoordeling", "rating": "Beoordeling",
"remove_from_collection": "Verwijderen uit collectie", "remove_from_collection": "Verwijderen uit collectie",
"share": "Deel", "share": "Deel",
"sort": "Soort", "sort": "Sorteer",
"sources": "Bronnen", "sources": "Bronnen",
"unarchive": "Uit het archief halen", "unarchive": "Uit het archief halen",
"unarchived_collection_message": "Collectie is succesvol gedearchiveerd!", "unarchived_collection_message": "Collectie is succesvol gedearchiveerd!",
"updated": "Bijgewerkt", "updated": "Gewijzigd",
"visit": "Bezoek", "visit": "Bezoek",
"visits": "Bezoeken", "visits": "Bezoeken",
"adventure_delete_success": "Avontuur succesvol verwijderd!", "adventure_delete_success": "Avontuur succesvol verwijderd!",
@ -108,14 +108,14 @@
"activity_types": "Activiteitstypen", "activity_types": "Activiteitstypen",
"add": "Toevoegen", "add": "Toevoegen",
"add_an_activity": "Voeg een activiteit toe", "add_an_activity": "Voeg een activiteit toe",
"add_notes": "Voeg notities toe", "add_notes": "Voeg opmerkingen toe",
"adventure_create_error": "Kan geen avontuur creëren", "adventure_create_error": "Kan geen avontuur aanmaken",
"adventure_created": "Avontuur gecreëerd", "adventure_created": "Avontuur aangemaakt",
"adventure_update_error": "Kan avontuur niet updaten", "adventure_update_error": "Kan avontuur niet wijzigen",
"adventure_updated": "Avontuur bijgewerkt", "adventure_updated": "Avontuur gewijzigd",
"basic_information": "Basisinformatie", "basic_information": "Basisinformatie",
"category": "Categorie", "category": "Categorie",
"clear_map": "Duidelijke kaart", "clear_map": "Kaart leegmaken",
"copy_link": "Kopieer link", "copy_link": "Kopieer link",
"date_constrain": "Beperk u tot ophaaldata", "date_constrain": "Beperk u tot ophaaldata",
"description": "Beschrijving", "description": "Beschrijving",
@ -137,7 +137,7 @@
"no_start_date": "Voer een startdatum in", "no_start_date": "Voer een startdatum in",
"public_adventure": "Openbaar avontuur", "public_adventure": "Openbaar avontuur",
"remove": "Verwijderen", "remove": "Verwijderen",
"save_next": "Redden", "save_next": "Opslaan & Volgende",
"search_for_location": "Zoek een locatie", "search_for_location": "Zoek een locatie",
"search_results": "Zoekresultaten", "search_results": "Zoekresultaten",
"see_adventures": "Zie Avonturen", "see_adventures": "Zie Avonturen",
@ -152,27 +152,27 @@
"wikipedia": "Wikipedia", "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!", "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", "all": "Alle",
"error_updating_regions": "Fout bij updaten van regio's", "error_updating_regions": "Fout bij wijzigen van regio's",
"mark_visited": "Mark bezocht", "mark_visited": "Markeer bezocht",
"my_adventures": "Mijn avonturen", "my_adventures": "Mijn avonturen",
"no_adventures_found": "Geen avonturen gevonden", "no_adventures_found": "Geen avonturen gevonden",
"no_collections_found": "Er zijn geen collecties gevonden waar dit avontuur aan kan worden toegevoegd.", "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.", "no_linkable_adventures": "Er zijn geen avonturen gevonden die aan deze collectie kunnen worden gekoppeld.",
"not_visited": "Niet bezocht", "not_visited": "Niet bezocht",
"regions_updated": "regio's bijgewerkt", "regions_updated": "regio's bijgewerkt",
"update_visited_regions": "Update bezochte regio's", "update_visited_regions": "Werk bezochte regio's bij",
"update_visited_regions_disclaimer": "Dit kan even duren, afhankelijk van het aantal avonturen dat je hebt bezocht.", "update_visited_regions_disclaimer": "Dit kan even duren, afhankelijk van het aantal avondturen dat je hebt beleefd.",
"visited_region_check": "Regiocheck bezocht", "visited_region_check": "Check bezochte regio's",
"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.", "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...", "add_new": "Nieuw toevoegen...",
"checklist": "Controlelijst", "checklist": "Controlelijst",
"checklists": "Controlelijsten", "checklists": "Controlelijsten",
"collection_archived": "Deze collectie is gearchiveerd.", "collection_archived": "Deze collectie is gearchiveerd.",
"collection_completed": "Je hebt deze verzameling voltooid!", "collection_completed": "Je hebt deze collectie voltooid!",
"collection_stats": "Verzamelstatistieken", "collection_stats": "Collectiestatistieken",
"days": "dagen", "days": "dagen",
"itineary_by_date": "Reisplan op datum", "itineary_by_date": "Reisplan op datum",
"keep_exploring": "Blijf verkennen!", "keep_exploring": "Blijf ontdekken!",
"link_new": "Nieuwe link...", "link_new": "Nieuwe link...",
"linked_adventures": "Gekoppelde avonturen", "linked_adventures": "Gekoppelde avonturen",
"links": "Koppelingen", "links": "Koppelingen",
@ -182,13 +182,20 @@
"notes": "Opmerkingen", "notes": "Opmerkingen",
"nothing_planned": "Niets gepland voor deze dag. \nGeniet van de reis!", "nothing_planned": "Niets gepland voor deze dag. \nGeniet van de reis!",
"transportation": "Vervoer", "transportation": "Vervoer",
"transportations": "Transporten", "transportations": "Vervoer",
"visit_link": "Bezoek Link", "visit_link": "Bezoek Link",
"day": "Dag", "day": "Dag",
"add_a_tag": "Voeg een label toe", "add_a_tag": "Voeg een label toe",
"tags": "Labels", "tags": "Labels",
"set_to_pin": "Stel in op Vastzetten", "set_to_pin": "Stel in op pin",
"category_fetch_error": "Fout bij ophalen van categorieën" "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": { "home": {
"desc_1": "Ontdek, plan en verken met gemak", "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.", "feature_3_desc": "Bekijk uw reizen over de hele wereld met een interactieve kaart en ontdek nieuwe bestemmingen.",
"go_to": "Ga naar AdventureLog", "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_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" "key_features": "Belangrijkste kenmerken"
}, },
"navbar": { "navbar": {
"about": "Over AdventureLog", "about": "Over AdventureLog",
"adventures": "Avonturen", "adventures": "Avonturen",
"collections": "Collecties", "collections": "Collecties",
"discord": "Meningsverschil", "discord": "discord",
"documentation": "Documentatie", "documentation": "Documentatie",
"greeting": "Hoi", "greeting": "Hoi",
"logout": "Uitloggen", "logout": "Uitloggen",
@ -218,7 +225,7 @@
"search": "Zoekopdracht", "search": "Zoekopdracht",
"settings": "Instellingen", "settings": "Instellingen",
"shared_with_me": "Gedeeld met mij", "shared_with_me": "Gedeeld met mij",
"theme_selection": "Thema Selectie", "theme_selection": "Thema selectie",
"themes": { "themes": {
"aqua": "Aqua", "aqua": "Aqua",
"dark": "Donker", "dark": "Donker",
@ -226,14 +233,16 @@
"light": "Licht", "light": "Licht",
"night": "Nacht", "night": "Nacht",
"aestheticDark": "Esthetisch donker", "aestheticDark": "Esthetisch donker",
"aestheticLight": "Esthetisch licht" "aestheticLight": "Esthetisch licht",
"northernLights": "Noorderlicht"
}, },
"users": "Gebruikers", "users": "Gebruikers",
"worldtravel": "Wereldreizen", "worldtravel": "Wereldreizen",
"my_tags": "Mijn tags", "my_tags": "Mijn labels",
"tag": "Label", "tag": "Label",
"language_selection": "Taal", "language_selection": "Taal",
"support": "Steun" "support": "Steun",
"calendar": "Kalender"
}, },
"auth": { "auth": {
"confirm_password": "Bevestig wachtwoord", "confirm_password": "Bevestig wachtwoord",
@ -249,7 +258,11 @@
"username": "Gebruikersnaam", "username": "Gebruikersnaam",
"profile_picture": "Profielfoto", "profile_picture": "Profielfoto",
"public_profile": "Openbaar profiel", "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": { "users": {
"no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen." "no_users_found": "Er zijn geen gebruikers gevonden met openbare profielen."
@ -266,18 +279,18 @@
"partially_visited": "Gedeeltelijk bezocht" "partially_visited": "Gedeeltelijk bezocht"
}, },
"settings": { "settings": {
"account_settings": "Gebruikersaccountinstellingen", "account_settings": "Gebruikersaccount instellingen",
"confirm_new_password": "Bevestig nieuw wachtwoord", "confirm_new_password": "Bevestig nieuw wachtwoord",
"current_email": "Huidige e-mail", "current_email": "Huidig e-mailadres",
"email_change": "Wijzig e-mailadres", "email_change": "Wijzig e-mailadres",
"new_email": "Nieuwe e-mail", "new_email": "Nieuw e-mailadres",
"new_password": "Nieuw wachtwoord", "new_password": "Nieuw wachtwoord",
"no_email_set": "Geen e-mailadres ingesteld", "no_email_set": "Geen e-mailadres ingesteld",
"password_change": "Wachtwoord wijzigen", "password_change": "Wachtwoord wijzigen",
"settings_page": "Instellingenpagina", "settings_page": "Instellingenpagina",
"update": "Update", "update": "Wijzig",
"update_error": "Fout bij updaten van instellingen", "update_error": "Fout bij wijzigen van instellingen",
"update_success": "Instellingen succesvol bijgewerkt!", "update_success": "Instellingen succesvol gewijzigd!",
"change_password": "Wachtwoord wijzigen", "change_password": "Wachtwoord wijzigen",
"invalid_token": "Token is ongeldig of verlopen", "invalid_token": "Token is ongeldig of verlopen",
"login_redir": "Vervolgens wordt u doorgestuurd naar de inlogpagina.", "login_redir": "Vervolgens wordt u doorgestuurd naar de inlogpagina.",
@ -289,54 +302,97 @@
"submit": "Indienen", "submit": "Indienen",
"token_required": "Token en UID zijn vereist voor het opnieuw instellen van het wachtwoord.", "token_required": "Token en UID zijn vereist voor het opnieuw instellen van het wachtwoord.",
"about_this_background": "Over deze achtergrond", "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", "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": { "checklist": {
"add_item": "Artikel toevoegen", "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_deleted": "Controlelijst succesvol verwijderd!",
"checklist_editor": "Controlelijst-editor", "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", "editing_checklist": "Controlelijst bewerken",
"failed_to_save": "Kan checklist niet opslaan", "failed_to_save": "Kan controlelijst niet opslaan",
"item": "Item", "item": "Artikel",
"item_already_exists": "Artikel bestaat al", "item_already_exists": "Artikel bestaat al",
"item_cannot_be_empty": "Artikel mag niet leeg zijn", "item_cannot_be_empty": "Artikel mag niet leeg zijn",
"items": "Artikelen", "items": "Artikelen",
"new_item": "Nieuw artikel", "new_item": "Nieuw artikel",
"save": "Redden" "save": "Opslaan"
}, },
"collection": { "collection": {
"collection_created": "Collectie succesvol aangemaakt!", "collection_created": "Collectie succesvol aangemaakt!",
"collection_edit_success": "Collectie succesvol bewerkt!", "collection_edit_success": "Collectie succesvol bewerkt!",
"create": "Creëren", "create": "Aanmaken",
"edit_collection": "Verzameling bewerken", "edit_collection": "Collectie bewerken",
"error_creating_collection": "Fout bij maken collectie", "error_creating_collection": "Fout bij aanmaken collectie",
"error_editing_collection": "Fout bij bewerken collectie", "error_editing_collection": "Fout bij bewerken collectie",
"new_collection": "Nieuwe collectie" "new_collection": "Nieuwe collectie"
}, },
"notes": { "notes": {
"add_a_link": "Voeg een link toe", "add_a_link": "Voeg een link toe",
"content": "Inhoud", "content": "Inhoud",
"editing_note": "Notitie bewerken", "editing_note": "Opmerking bewerken",
"failed_to_save": "Kan notitie niet opslaan", "failed_to_save": "Kan opmerking niet opslaan",
"note_delete_error": "Fout bij verwijderen van notitie", "note_delete_error": "Fout bij verwijderen van opmerking",
"note_deleted": "Opmerking succesvol verwijderd!", "note_deleted": "Opmerking succesvol verwijderd!",
"note_editor": "Notitie-editor", "note_editor": "Opmerking-editor",
"note_public": "Deze notitie is openbaar omdat deze zich in een openbare collectie bevindt.", "note_public": "Deze opmerking is openbaar omdat deze zich in een openbare collectie bevindt.",
"open": "Open", "open": "Open",
"save": "Redden", "save": "Opslaan",
"invalid_url": "Ongeldige URL" "invalid_url": "Ongeldige URL"
}, },
"transportation": { "transportation": {
"date_and_time": "Datum", "date_and_time": "Datum",
"date_time": "Startdatum", "date_time": "Startdatum",
"edit": "Bewerking", "edit": "Bewerking",
"edit_transportation": "Transport bewerken", "edit_transportation": "Vervoer bewerken",
"end_date_time": "Einddatum", "end_date_time": "Einddatum",
"error_editing_transportation": "Fout bij bewerken van transport", "error_editing_transportation": "Fout bij bewerken van vervoer",
"flight_number": "Vluchtnummer", "flight_number": "Vluchtnummer",
"from_location": "Van locatie", "from_location": "Van locatie",
"modes": { "modes": {
@ -350,15 +406,15 @@
"walking": "Lopen" "walking": "Lopen"
}, },
"to_location": "Naar locatie", "to_location": "Naar locatie",
"transportation_edit_success": "Transport succesvol bewerkt!", "transportation_edit_success": "Vervoer succesvol bewerkt!",
"type": "Type", "type": "Type",
"new_transportation": "Nieuw transport", "new_transportation": "Nieuw vervoer",
"provide_start_date": "Geef een startdatum op", "provide_start_date": "Geef een startdatum op",
"start": "Begin", "start": "Begin",
"transport_type": "Transporttype", "transport_type": "Vervoerstype",
"transportation_added": "Transport succesvol toegevoegd!", "transportation_added": "Vervoer succesvol toegevoegd!",
"transportation_delete_error": "Fout bij verwijderen transport", "transportation_delete_error": "Fout bij verwijderen vervoer",
"transportation_deleted": "Transport succesvol verwijderd!" "transportation_deleted": "Vervoer succesvol verwijderd!"
}, },
"search": { "search": {
"adventurelog_results": "AdventureLog-resultaten", "adventurelog_results": "AdventureLog-resultaten",
@ -367,9 +423,9 @@
}, },
"map": { "map": {
"add_adventure": "Voeg nieuw avontuur toe", "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", "adventure_map": "Avonturenkaart",
"clear_marker": "Duidelijke markering", "clear_marker": "Verwijder markeerpunt",
"map_options": "Kaartopties", "map_options": "Kaartopties",
"show_visited_regions": "Toon bezochte regio's", "show_visited_regions": "Toon bezochte regio's",
"view_details": "Details bekijken" "view_details": "Details bekijken"
@ -388,7 +444,7 @@
"share": { "share": {
"no_users_shared": "Er zijn geen gebruikers gedeeld", "no_users_shared": "Er zijn geen gebruikers gedeeld",
"not_shared_with": "Niet gedeeld met", "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": "Gedeeld",
"shared_with": "Gedeeld met", "shared_with": "Gedeeld met",
"unshared": "Niet gedeeld", "unshared": "Niet gedeeld",
@ -406,10 +462,19 @@
"categories": { "categories": {
"category_name": "Categorienaam", "category_name": "Categorienaam",
"edit_category": "Categorie bewerken", "edit_category": "Categorie bewerken",
"icon": "Icon", "icon": "Ikoon",
"manage_categories": "Beheer categorieën", "manage_categories": "Beheer categorieën",
"no_categories_found": "Geen categorieën gevonden.", "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." "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"
} }
} }

View file

@ -27,8 +27,10 @@
"forest": "Leśny", "forest": "Leśny",
"aestheticLight": "Estetyczny Jasny", "aestheticLight": "Estetyczny Jasny",
"aestheticDark": "Estetyczny Ciemny", "aestheticDark": "Estetyczny Ciemny",
"aqua": "Aqua" "aqua": "Aqua",
} "northernLights": "Zorza Polarna"
},
"calendar": "Kalendarz"
}, },
"about": { "about": {
"about": "O aplikacji", "about": "O aplikacji",
@ -233,7 +235,14 @@
"spiritual_journeys": "Podróże duchowe 🧘‍♀️", "spiritual_journeys": "Podróże duchowe 🧘‍♀️",
"volunteer_work": "Praca wolontariacka 🤝", "volunteer_work": "Praca wolontariacka 🤝",
"other": "Inne" "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": { "worldtravel": {
"country_list": "Lista krajów", "country_list": "Lista krajów",
@ -260,7 +269,11 @@
"registration_disabled": "Rejestracja jest obecnie wyłączona.", "registration_disabled": "Rejestracja jest obecnie wyłączona.",
"profile_picture": "Zdjęcie profilowe", "profile_picture": "Zdjęcie profilowe",
"public_profile": "Publiczny profil", "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": { "users": {
"no_users_found": "Nie znaleziono użytkowników z publicznymi profilami." "no_users_found": "Nie znaleziono użytkowników z publicznymi profilami."
@ -291,7 +304,50 @@
"about_this_background": "O tym tle", "about_this_background": "O tym tle",
"photo_by": "Zdjęcie autorstwa", "photo_by": "Zdjęcie autorstwa",
"join_discord": "Dołącz do Discorda", "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": {
"collection_created": "Kolekcja została pomyślnie utworzona!", "collection_created": "Kolekcja została pomyślnie utworzona!",
@ -411,5 +467,14 @@
"update_after_refresh": "Karty podróży zostaną zaktualizowane po odświeżeniu strony.", "update_after_refresh": "Karty podróży zostaną zaktualizowane po odświeżeniu strony.",
"select_category": "Wybierz kategorię", "select_category": "Wybierz kategorię",
"category_name": "Nazwa kategorii" "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"
} }
} }

View file

@ -188,7 +188,14 @@
"add_a_tag": "Lägg till en tagg", "add_a_tag": "Lägg till en tagg",
"tags": "Taggar", "tags": "Taggar",
"set_to_pin": "Ställ in på Pin", "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": { "home": {
"desc_1": "Upptäck, planera och utforska med lätthet", "desc_1": "Upptäck, planera och utforska med lätthet",
@ -226,14 +233,16 @@
"light": "Ljus", "light": "Ljus",
"night": "Natt", "night": "Natt",
"aestheticDark": "Estetisk mörk", "aestheticDark": "Estetisk mörk",
"aestheticLight": "Estetiskt ljus" "aestheticLight": "Estetiskt ljus",
"northernLights": "Norrsken"
}, },
"users": "Användare", "users": "Användare",
"worldtravel": "Världsresor", "worldtravel": "Världsresor",
"my_tags": "Mina taggar", "my_tags": "Mina taggar",
"tag": "Märka", "tag": "Märka",
"language_selection": "Språk", "language_selection": "Språk",
"support": "Stöd" "support": "Stöd",
"calendar": "Kalender"
}, },
"worldtravel": { "worldtravel": {
"all": "Alla", "all": "Alla",
@ -260,7 +269,11 @@
"username": "Användarnamn", "username": "Användarnamn",
"public_tooltip": "Med en offentlig profil kan användare dela samlingar med dig och se din profil på användarsidan.", "public_tooltip": "Med en offentlig profil kan användare dela samlingar med dig och se din profil på användarsidan.",
"profile_picture": "Profilbild", "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": { "users": {
"no_users_found": "Inga användare hittades med offentliga profiler." "no_users_found": "Inga användare hittades med offentliga profiler."
@ -291,7 +304,50 @@
"about_this_background": "Om denna bakgrund", "about_this_background": "Om denna bakgrund",
"join_discord": "Gå med i Discord", "join_discord": "Gå med i Discord",
"join_discord_desc": "för att dela dina egna foton. \nLägg upp dem i", "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": { "checklist": {
"add_item": "Lägg till objekt", "add_item": "Lägg till objekt",
@ -411,5 +467,14 @@
"no_categories_found": "Inga kategorier hittades.", "no_categories_found": "Inga kategorier hittades.",
"select_category": "Välj Kategori", "select_category": "Välj Kategori",
"update_after_refresh": "Äventyrskorten kommer att uppdateras när du uppdaterar sidan." "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"
} }
} }

View file

@ -188,7 +188,14 @@
"add_a_tag": "添加标签", "add_a_tag": "添加标签",
"tags": "标签", "tags": "标签",
"set_to_pin": "设置为固定", "set_to_pin": "设置为固定",
"category_fetch_error": "获取类别时出错" "category_fetch_error": "获取类别时出错",
"copied_to_clipboard": "已复制到剪贴板!",
"copy_failed": "复制失败",
"adventure_calendar": "冒险日历",
"emoji_picker": "表情符号选择器",
"hide": "隐藏",
"show": "展示",
"download_calendar": "下载日历"
}, },
"home": { "home": {
"desc_1": "轻松发现、规划和探索", "desc_1": "轻松发现、规划和探索",
@ -226,14 +233,16 @@
"light": "光", "light": "光",
"night": "夜晚", "night": "夜晚",
"aestheticDark": "审美黑暗", "aestheticDark": "审美黑暗",
"aestheticLight": "美学之光" "aestheticLight": "美学之光",
"northernLights": "北极光"
}, },
"users": "用户", "users": "用户",
"worldtravel": "环球旅行", "worldtravel": "环球旅行",
"my_tags": "我的标签", "my_tags": "我的标签",
"tag": "标签", "tag": "标签",
"language_selection": "语言", "language_selection": "语言",
"support": "支持" "support": "支持",
"calendar": "日历"
}, },
"auth": { "auth": {
"forgot_password": "忘记密码?", "forgot_password": "忘记密码?",
@ -249,7 +258,11 @@
"registration_disabled": "目前已禁用注册。", "registration_disabled": "目前已禁用注册。",
"profile_picture": "个人资料图片", "profile_picture": "个人资料图片",
"public_profile": "公开资料", "public_profile": "公开资料",
"public_tooltip": "通过公开个人资料,用户可以与您共享收藏并在用户页面上查看您的个人资料。" "public_tooltip": "通过公开个人资料,用户可以与您共享收藏并在用户页面上查看您的个人资料。",
"email_required": "电子邮件为必填项",
"both_passwords_required": "两个密码都需要",
"new_password": "新密码",
"reset_failed": "重置密码失败"
}, },
"worldtravel": { "worldtravel": {
"all": "全部", "all": "全部",
@ -291,7 +304,50 @@
"about_this_background": "关于这个背景", "about_this_background": "关于这个背景",
"join_discord": "加入不和谐", "join_discord": "加入不和谐",
"join_discord_desc": "分享您自己的照片。\n将它们张贴在", "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": { "checklist": {
"add_item": "添加项目", "add_item": "添加项目",
@ -411,5 +467,14 @@
"no_categories_found": "未找到类别。", "no_categories_found": "未找到类别。",
"select_category": "选择类别", "select_category": "选择类别",
"update_after_refresh": "刷新页面后,冒险卡将更新。" "update_after_refresh": "刷新页面后,冒险卡将更新。"
},
"dashboard": {
"add_some": "为什么不开始计划你的下一次冒险呢?\n您可以通过单击下面的按钮添加新的冒险。",
"countries_visited": "访问国家",
"no_recent_adventures": "最近没有冒险吗?",
"recent_adventures": "最近的冒险",
"total_adventures": "全面冒险",
"total_visited_regions": "总访问地区",
"welcome_back": "欢迎回来"
} }
} }

View file

@ -1,9 +1,17 @@
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import { redirect, type Actions } from '@sveltejs/kit'; import { redirect, type Actions } from '@sveltejs/kit';
import { themes } from '$lib'; import { themes } from '$lib';
import { fetchCSRFToken } from '$lib/index.server';
import type { PageServerLoad } from './$types';
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 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 = { export const actions: Actions = {
setTheme: async ({ url, cookies }) => { setTheme: async ({ url, cookies }) => {
const theme = url.searchParams.get('theme'); const theme = url.searchParams.get('theme');
@ -16,23 +24,24 @@ export const actions: Actions = {
}); });
} }
}, },
logout: async ({ cookies }: { cookies: any }) => { logout: async (event) => {
const cookie = cookies.get('auth') || null; let sessionId = event.cookies.get('sessionid');
let csrfToken = await fetchCSRFToken();
if (!cookie) { if (!sessionId) {
return; return;
} }
const res = await fetch(`${serverEndpoint}/auth/logout/`, { const res = await fetch(`${serverEndpoint}/_allauth/browser/v1/auth/session`, {
method: 'POST', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: cookies.get('auth') Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
} 'X-CSRFToken': csrfToken
},
credentials: 'include'
}); });
if (res.ok) { if (res.status == 401) {
cookies.delete('auth', { path: '/', secure: false });
cookies.delete('refresh', { path: '/', secure: false });
return redirect(302, '/login'); return redirect(302, '/login');
} else { } else {
return redirect(302, '/'); return redirect(302, '/');

View file

@ -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 });
}
}

View file

@ -1,5 +1,6 @@
import { redirect, type Actions } from '@sveltejs/kit'; import { redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { fetchCSRFToken } from '$lib/index.server';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
@ -7,13 +8,16 @@ export const load = (async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/login'); return redirect(302, '/login');
} }
let csrfToken = await fetchCSRFToken();
let allActivities: string[] = []; let allActivities: string[] = [];
let res = await fetch(`${endpoint}/api/activity-types/types/`, { let res = await event.fetch(`${endpoint}/api/activity-types/types/`, {
headers: { headers: {
'Content-Type': 'application/json', 'X-CSRFToken': csrfToken,
Cookie: `${event.cookies.get('auth')}` Cookie: `csrftoken=${csrfToken}`
} },
credentials: 'include'
}); });
console.log(res);
let data = await res.json(); let data = await res.json();
if (data) { if (data) {
allActivities = data; allActivities = data;
@ -27,13 +31,16 @@ export const load = (async (event) => {
export const actions: Actions = { export const actions: Actions = {
getActivities: async (event) => { getActivities: async (event) => {
let csrfToken = await fetchCSRFToken();
let allActivities: string[] = []; let allActivities: string[] = [];
let res = await fetch(`${endpoint}/api/activity-types/types/`, { let res = await fetch(`${endpoint}/api/activity-types/types/`, {
headers: { headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `${event.cookies.get('auth')}` Cookie: `csrftoken=${csrfToken}`
} }
}); });
console.log(res);
let data = await res.json(); let data = await res.json();
if (data) { if (data) {
allActivities = data; allActivities = data;

View file

@ -1,15 +1,19 @@
import { json } from '@sveltejs/kit'; 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 PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const POST: RequestHandler = async (event) => { export const POST: RequestHandler = async (event) => {
let allActivities: string[] = []; 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: { headers: {
'Content-Type': 'application/json', 'X-CSRFToken': csrfToken,
Cookie: `${event.cookies.get('auth')}` Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`
} },
credentials: 'include'
}); });
let data = await res.json(); let data = await res.json();
if (data) { if (data) {

View file

@ -4,8 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { Adventure } from '$lib/types'; import type { Adventure } from '$lib/types';
import type { Actions } from '@sveltejs/kit'; import type { Actions } 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'; 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 page = event.url.searchParams.get('page') || '1';
const is_visited = event.url.searchParams.get('is_visited') || 'all'; 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}`, `${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: { 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; }) satisfies PageServerLoad;
export const actions: Actions = { 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) => { image: async (event) => {
let formData = await event.request.formData(); let formData = await event.request.formData();
let csrfToken = await fetchCSRFToken();
let sessionId = event.cookies.get('sessionid');
let res = await fetch(`${serverEndpoint}/api/images/`, { let res = await fetch(`${serverEndpoint}/api/images/`, {
method: 'POST', method: 'POST',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`,
'X-CSRFToken': csrfToken
}, },
body: formData body: formData
}); });

View file

@ -226,7 +226,6 @@
{#each adventures as adventure} {#each adventures as adventure}
<AdventureCard <AdventureCard
user={data.user} user={data.user}
type={adventure.type}
{adventure} {adventure}
on:delete={deleteAdventure} on:delete={deleteAdventure}
on:edit={editAdventure} on:edit={editAdventure}

View file

@ -7,8 +7,9 @@ export const load = (async (event) => {
const id = event.params as { id: string }; const id = event.params as { id: string };
let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, { let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${event.cookies.get('sessionid')}`
} },
credentials: 'include'
}); });
if (!request.ok) { if (!request.ok) {
console.error('Failed to fetch adventure ' + id.id); console.error('Failed to fetch adventure ' + id.id);
@ -24,8 +25,9 @@ export const load = (async (event) => {
if (adventure.collection) { if (adventure.collection) {
let res2 = await fetch(`${endpoint}/api/collections/${adventure.collection}/`, { let res2 = await fetch(`${endpoint}/api/collections/${adventure.collection}/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${event.cookies.get('sessionid')}`
} },
credentials: 'include'
}); });
collection = await res2.json(); collection = await res2.json();
} }
@ -39,8 +41,8 @@ export const load = (async (event) => {
} }
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
import type { Actions } from '@sveltejs/kit'; import { redirect, 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'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
@ -50,29 +52,7 @@ export const actions: Actions = {
const adventureId = id.id; const adventureId = id.id;
if (!event.locals.user) { if (!event.locals.user) {
const refresh = event.cookies.get('refresh'); return redirect(302, '/login');
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) { if (!adventureId) {
return { return {
@ -81,12 +61,15 @@ export const actions: Actions = {
}; };
} }
let csrfToken = await fetchCSRFToken();
let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}`, { let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}`, Cookie: `sessionid=${event.cookies.get('sessionid')}; csrftoken=${csrfToken}`,
'Content-Type': 'application/json' 'X-CSRFToken': csrfToken
} },
credentials: 'include'
}); });
console.log(res); console.log(res);
if (!res.ok) { if (!res.ok) {

View file

@ -1,69 +1,77 @@
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
import { fetchCSRFToken } from '$lib/index.server';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
/** @type {import('./$types').RequestHandler} */ /** @type {import('./$types').RequestHandler} */
export async function GET({ url, params, request, fetch, cookies }) { export async function GET(event) {
// add the param format = json to the url or add additional if anothre param is already present const { url, params, request, fetch, cookies } = event;
if (url.search) { const searchParam = url.search ? `${url.search}&format=json` : '?format=json';
url.search = url.search + '&format=json'; return handleRequest(url, params, request, fetch, cookies, searchParam);
} else {
url.search = '?format=json';
}
return handleRequest(url, params, request, fetch, cookies);
} }
/** @type {import('./$types').RequestHandler} */ /** @type {import('./$types').RequestHandler} */
export async function POST({ url, params, request, fetch, cookies }) { 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 }) { 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 }) { 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 }) { 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( async function handleRequest(
url: any, url: any,
params: any, params: any,
request: any, request: any,
fetch: any, fetch: any,
cookies: any, cookies: any,
searchParam: string,
requreTrailingSlash: boolean | undefined = false requreTrailingSlash: boolean | undefined = false
) { ) {
const path = params.path; 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('/')) { if (requreTrailingSlash && !targetUrl.endsWith('/')) {
targetUrl += '/'; 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 headers = new Headers(request.headers);
const authCookie = cookies.get('auth'); const csrfToken = await fetchCSRFToken();
if (!csrfToken) {
if (authCookie) { return json({ error: 'CSRF token is missing or invalid' }, { status: 400 });
headers.set('Cookie', `${authCookie}`);
} }
try { try {
const response = await fetch(targetUrl, { const response = await fetch(targetUrl, {
method: request.method, method: request.method,
headers: headers, headers: {
body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined ...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) { if (response.status === 204) {
// For 204 No Content, return a response with no body
return new Response(null, { return new Response(null, {
status: 204, status: 204,
headers: response.headers headers: response.headers
@ -71,10 +79,13 @@ async function handleRequest(
} }
const responseData = await response.text(); 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, { return new Response(responseData, {
status: response.status, status: response.status,
headers: response.headers headers: cleanHeaders
}); });
} catch (error) { } catch (error) {
console.error('Error forwarding request:', error); console.error('Error forwarding request:', error);

View file

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

View file

@ -0,0 +1,37 @@
<script lang="ts">
import type { PageData } from './$types';
// @ts-ignore
import Calendar from '@event-calendar/core';
// @ts-ignore
import TimeGrid from '@event-calendar/time-grid';
// @ts-ignore
import DayGrid from '@event-calendar/day-grid';
import { t } from 'svelte-i18n';
export let data: PageData;
let adventures = data.props.adventures;
let dates = data.props.dates;
let icsCalendar = data.props.ics_calendar;
// turn the ics calendar into a data URL
let icsCalendarDataUrl = URL.createObjectURL(new Blob([icsCalendar], { type: 'text/calendar' }));
let plugins = [TimeGrid, DayGrid];
let options = {
view: 'dayGridMonth',
events: [...dates]
};
</script>
<h1 class="text-center text-2xl font-bold">{$t('adventures.adventure_calendar')}</h1>
<Calendar {plugins} {options} />
<!-- download calendar -->
<div class="flex items-center justify-center mt-4">
<a href={icsCalendarDataUrl} download="adventures.ics" class="btn btn-primary"
>Download Calendar</a
>
</div>

View file

@ -4,7 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { Adventure, Collection } from '$lib/types'; import type { Adventure, Collection } from '$lib/types';
import type { Actions, RequestEvent } from '@sveltejs/kit'; import type { Actions, RequestEvent } from '@sveltejs/kit';
import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; import { fetchCSRFToken } from '$lib/index.server';
import { checkLink } from '$lib'; import { checkLink } from '$lib';
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
@ -17,10 +17,12 @@ export const load = (async (event) => {
let previous = null; let previous = null;
let count = 0; let count = 0;
let adventures: Adventure[] = []; let adventures: Adventure[] = [];
let sessionId = event.cookies.get('sessionid');
let initialFetch = await fetch(`${serverEndpoint}/api/collections/?order_by=updated_at`, { let initialFetch = await fetch(`${serverEndpoint}/api/collections/?order_by=updated_at`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} },
credentials: 'include'
}); });
if (!initialFetch.ok) { if (!initialFetch.ok) {
console.error('Failed to fetch visited adventures'); console.error('Failed to fetch visited adventures');
@ -72,34 +74,9 @@ export const actions: Actions = {
formDataToSend.append('start_date', start_date || ''); formDataToSend.append('start_date', start_date || '');
formDataToSend.append('end_date', end_date || ''); formDataToSend.append('end_date', end_date || '');
formDataToSend.append('link', link || ''); formDataToSend.append('link', link || '');
let auth = event.cookies.get('auth'); let sessionid = event.cookies.get('sessionid');
if (!auth) { if (!sessionid) {
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 { return {
status: 401, status: 401,
body: { message: 'Unauthorized' } body: { message: 'Unauthorized' }
@ -119,7 +96,7 @@ export const actions: Actions = {
method: 'POST', method: 'POST',
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
Cookie: auth Cookie: `sessionid=${sessionid}; csrftoken=${csrfToken}`
}, },
body: formDataToSend body: formDataToSend
}); });
@ -175,34 +152,9 @@ export const actions: Actions = {
formDataToSend.append('end_date', end_date || ''); formDataToSend.append('end_date', end_date || '');
formDataToSend.append('link', link || ''); formDataToSend.append('link', link || '');
let auth = event.cookies.get('auth'); let sessionId = event.cookies.get('sessionid');
if (!auth) { if (!sessionId) {
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 { return {
status: 401, status: 401,
body: { message: 'Unauthorized' } body: { message: 'Unauthorized' }
@ -222,9 +174,10 @@ export const actions: Actions = {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
Cookie: auth Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`
}, },
body: formDataToSend body: formDataToSend,
credentials: 'include'
}); });
if (!res.ok) { if (!res.ok) {
@ -241,6 +194,10 @@ export const actions: Actions = {
}, },
get: async (event) => { get: async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return {
status: 401,
body: { message: 'Unauthorized' }
};
} }
const formData = await event.request.formData(); const formData = await event.request.formData();
@ -263,19 +220,20 @@ export const actions: Actions = {
let previous = null; let previous = null;
let count = 0; let count = 0;
let visitedFetch = await fetch( let collectionsFetch = await fetch(
`${serverEndpoint}/api/collections/?order_by=${order_by}&order_direction=${order_direction}`, `${serverEndpoint}/api/collections/?order_by=${order_by}&order_direction=${order_direction}`,
{ {
headers: { 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'); console.error('Failed to fetch visited adventures');
return redirect(302, '/login'); return redirect(302, '/login');
} else { } else {
let res = await visitedFetch.json(); let res = await collectionsFetch.json();
let visited = res.results as Adventure[]; let visited = res.results as Adventure[];
next = res.next; next = res.next;
previous = res.previous; previous = res.previous;
@ -332,15 +290,16 @@ export const actions: Actions = {
} }
const fullUrl = `${serverEndpoint}${url}`; const fullUrl = `${serverEndpoint}${url}`;
console.log(fullUrl);
console.log(serverEndpoint); let sessionId = event.cookies.get('sessionid');
try { try {
const response = await fetch(fullUrl, { const response = await fetch(fullUrl, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} },
credentials: 'include'
}); });
if (!response.ok) { if (!response.ok) {

View file

@ -6,9 +6,10 @@ const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => { export const load = (async (event) => {
const id = event.params as { id: string }; const id = event.params as { id: string };
let sessionid = event.cookies.get('sessionid');
let request = await fetch(`${endpoint}/api/collections/${id.id}/`, { let request = await fetch(`${endpoint}/api/collections/${id.id}/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionid}`
} }
}); });
if (!request.ok) { if (!request.ok) {
@ -30,7 +31,7 @@ export const load = (async (event) => {
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
import type { Actions } from '@sveltejs/kit'; 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'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
@ -39,31 +40,6 @@ export const actions: Actions = {
const id = event.params as { id: string }; const id = event.params as { id: string };
const adventureId = id.id; 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) { if (!adventureId) {
return { return {
status: 400, 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}`, { let res = await fetch(`${serverEndpoint}/api/collections/${event.params.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}`, Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json',
} 'X-CSRFToken': csrfToken
},
credentials: 'include'
}); });
console.log(res);
if (!res.ok) { if (!res.ok) {
return { return {
status: res.status, status: res.status,

View file

@ -398,7 +398,6 @@
user={data.user} user={data.user}
on:edit={editAdventure} on:edit={editAdventure}
on:delete={deleteAdventure} on:delete={deleteAdventure}
type={adventure.type}
{adventure} {adventure}
{collection} {collection}
/> />
@ -521,7 +520,6 @@
user={data.user} user={data.user}
on:edit={editAdventure} on:edit={editAdventure}
on:delete={deleteAdventure} on:delete={deleteAdventure}
type={adventure.type}
{adventure} {adventure}
/> />
{/each} {/each}

View file

@ -8,13 +8,11 @@ export const load = (async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/login'); return redirect(302, '/login');
} else { } else {
let next = null; let sessionId = event.cookies.get('sessionid');
let previous = null;
let count = 0;
let adventures: Adventure[] = []; let adventures: Adventure[] = [];
let initialFetch = await fetch(`${serverEndpoint}/api/collections/archived/`, { let initialFetch = await fetch(`${serverEndpoint}/api/collections/archived/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
if (!initialFetch.ok) { if (!initialFetch.ok) {

View file

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

View file

@ -0,0 +1,102 @@
<script lang="ts">
import AdventureCard from '$lib/components/AdventureCard.svelte';
import type { PageData } from './$types';
import { t } from 'svelte-i18n';
export let data: PageData;
const user = data.user;
const recentAdventures = data.props.adventures;
const stats = data.props.stats;
</script>
<div class="container mx-auto p-4">
<!-- Welcome Message -->
<div class="mb-8">
<h1 class="text-4xl font-extrabold">{$t('dashboard.welcome_back')}, {user?.first_name}!</h1>
</div>
<!-- Stats -->
<div class="stats shadow mb-8 w-full bg-neutral">
<div class="stat">
<div class="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-8 h-8 stroke-current"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"
></path></svg
>
</div>
<div class="stat-title text-neutral-content">{$t('dashboard.countries_visited')}</div>
<div class="stat-value text-primary">{stats.country_count}</div>
</div>
<div class="stat">
<div class="stat-figure text-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-8 h-8 stroke-current"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path></svg
>
</div>
<div class="stat-title text-neutral-content">{$t('dashboard.total_adventures')}</div>
<div class="stat-value text-secondary">{stats.adventure_count}</div>
</div>
<div class="stat">
<div class="stat-figure text-success">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-8 h-8 stroke-current"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"
></path></svg
>
</div>
<div class="stat-title text-neutral-content">{$t('dashboard.total_visited_regions')}</div>
<div class="stat-value text-success">{stats.visited_region_count}</div>
</div>
</div>
<!-- Recent Adventures -->
{#if recentAdventures.length > 0}
<h2 class="text-3xl font-semibold mb-4">{$t('dashboard.recent_adventures')}</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
{#each recentAdventures as adventure}
<AdventureCard {adventure} user={data.user} readOnly />
{/each}
</div>
{/if}
<!-- Inspiration if there are no recent adventures -->
{#if recentAdventures.length === 0}
<div class="flex flex-col items-center justify-center bg-neutral shadow p-8 mb-8 rounded-lg">
<h2 class="text-3xl font-semibold mb-4">{$t('dashboard.no_recent_adventures')}</h2>
<p class="text-lg text-center">
{$t('dashboard.add_some')}
</p>
<a href="/adventures" class="btn btn-primary mt-4">{$t('map.add_adventure')}</a>
</div>
{/if}
</div>
<svelte:head>
<title>Dashboard | AdventureLog</title>
<meta name="description" content="Home dashboard for AdventureLog." />
</svelte:head>

View file

@ -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 { getRandomBackground, getRandomQuote } from '$lib';
import { fetchCSRFToken } from '$lib/index.server';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
export const load: PageServerLoad = async (event) => { export const load: PageServerLoad = async (event) => {
@ -24,65 +25,99 @@ export const actions: Actions = {
default: async (event) => { default: async (event) => {
const formData = await event.request.formData(); const formData = await event.request.formData();
const formUsername = formData.get('username'); const formUsername = formData.get('username');
const formPassword = formData.get('password'); const username = formUsername?.toString().toLowerCase();
let username = formUsername?.toString().toLocaleLowerCase();
const password = formData.get('password'); const password = formData.get('password');
const totp = formData.get('totp');
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
const csrfTokenFetch = await event.fetch(`${serverEndpoint}/csrf/`); const csrfToken = await fetchCSRFToken();
if (!csrfTokenFetch.ok) { // Initial login attempt
console.error('Failed to fetch CSRF token'); const loginFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/login`, {
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/`, {
method: 'POST', method: 'POST',
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
'Content-Type': 'application/json' 'Content-Type': 'application/json',
Cookie: `csrftoken=${csrfToken}`
}, },
body: JSON.stringify({ body: JSON.stringify({ username, password }),
username, credentials: 'include'
password
})
});
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, '/'); 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<RouteParams, '/login'>, 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 '';
}

View file

@ -51,12 +51,21 @@
id="password" id="password"
class="block input input-bordered w-full max-w-xs" class="block input input-bordered w-full max-w-xs"
/><br /> /><br />
{#if $page.form?.mfa_required}
<label for="password">TOTP</label>
<input
type="password"
name="totp"
id="totp"
class="block input input-bordered w-full max-w-xs"
/><br />
{/if}
<button class="py-2 px-4 btn btn-primary mr-2">{$t('auth.login')}</button> <button class="py-2 px-4 btn btn-primary mr-2">{$t('auth.login')}</button>
<div class="flex justify-between mt-4"> <div class="flex justify-between mt-4">
<p><a href="/signup" class="underline">{$t('auth.signup')}</a></p> <p><a href="/signup" class="underline">{$t('auth.signup')}</a></p>
<p> <p>
<a href="/settings/forgot-password" class="underline">{$t('auth.forgot_password')}</a> <a href="/user/reset-password" class="underline">{$t('auth.forgot_password')}</a>
</p> </p>
</div> </div>
</form> </form>
@ -64,7 +73,7 @@
{#if ($page.form?.message && $page.form?.message.length > 1) || $page.form?.type === 'error'} {#if ($page.form?.message && $page.form?.message.length > 1) || $page.form?.type === 'error'}
<div class="text-center text-error mt-4"> <div class="text-center text-error mt-4">
{$page.form.message || $t('auth.login_error')} {$t($page.form.message) || $t('auth.login_error')}
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -8,15 +8,16 @@ export const load = (async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/login'); return redirect(302, '/login');
} else { } else {
let sessionId = event.cookies.get('sessionid');
let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, { let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
let visitedRegionsFetch = await fetch(`${endpoint}/api/visitedregion/`, { let visitedRegionsFetch = await fetch(`${endpoint}/api/visitedregion/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });

View file

@ -4,15 +4,16 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
export const load: PageServerLoad = async (event: RequestEvent) => { export const load: PageServerLoad = async (event: RequestEvent) => {
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 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'); return redirect(302, '/login');
} }
let sessionId = event.cookies.get('sessionid');
let stats = null; let stats = null;
let res = await event.fetch(`${endpoint}/api/stats/counts/`, { let res = await event.fetch(`${endpoint}/api/stats/counts/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
if (!res.ok) { if (!res.ok) {

View file

@ -14,12 +14,14 @@ export const load = (async (event) => {
return { data: [] }; return { data: [] };
} }
let sessionId = event.cookies.get('sessionid');
let res = await fetch( let res = await fetch(
`${serverEndpoint}/api/adventures/search/?query=${query}&property=${property}`, `${serverEndpoint}/api/adventures/search/?query=${query}&property=${property}`,
{ {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
} }
); );

View file

@ -155,7 +155,6 @@
{#each myAdventures as adventure} {#each myAdventures as adventure}
<AdventureCard <AdventureCard
user={data.user} user={data.user}
type={adventure.type}
{adventure} {adventure}
on:delete={deleteAdventure} on:delete={deleteAdventure}
on:edit={editAdventure} on:edit={editAdventure}
@ -168,13 +167,7 @@
<h2 class="text-center font-bold text-2xl mb-4">{$t('search.public_adventures')}</h2> <h2 class="text-center font-bold text-2xl mb-4">{$t('search.public_adventures')}</h2>
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center"> <div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each publicAdventures as adventure} {#each publicAdventures as adventure}
<AdventureCard <AdventureCard user={null} {adventure} on:delete={deleteAdventure} on:edit={editAdventure} />
user={null}
type={adventure.type}
{adventure}
on:delete={deleteAdventure}
on:edit={editAdventure}
/>
{/each} {/each}
</div> </div>
{/if} {/if}

View file

@ -2,29 +2,65 @@ import { fail, redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from '../$types'; import type { PageServerLoad } from '../$types';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { User } from '$lib/types'; import type { User } from '$lib/types';
import { fetchCSRFToken } from '$lib/index.server';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; 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) => { export const load: PageServerLoad = async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/'); return redirect(302, '/');
} }
if (!event.cookies.get('auth')) { let sessionId = event.cookies.get('sessionid');
if (!sessionId) {
return redirect(302, '/'); return redirect(302, '/');
} }
let res = await fetch(`${endpoint}/auth/user/`, { let res = await fetch(`${endpoint}/auth/user-metadata/`, {
headers: { headers: {
Cookie: event.cookies.get('auth') || '' Cookie: `sessionid=${sessionId}`
} }
}); });
let user = (await res.json()) as User; 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, '/'); 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 { return {
props: { props: {
user user,
emails,
authenticators
} }
}; };
}; };
@ -34,7 +70,8 @@ export const actions: Actions = {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/'); return redirect(302, '/');
} }
if (!event.cookies.get('auth')) { let sessionId = event.cookies.get('sessionid');
if (!sessionId) {
return redirect(302, '/'); return redirect(302, '/');
} }
@ -47,9 +84,9 @@ export const actions: Actions = {
let profile_pic = formData.get('profile_pic') as File | null | undefined; let profile_pic = formData.get('profile_pic') as File | null | undefined;
let public_profile = formData.get('public_profile') as string | null | undefined | boolean; 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: { headers: {
Cookie: event.cookies.get('auth') || '' Cookie: `sessionid=${sessionId}`
} }
}); });
@ -57,12 +94,12 @@ export const actions: Actions = {
return fail(resCurrent.status, await resCurrent.json()); return fail(resCurrent.status, await resCurrent.json());
} }
// Gets the boolean value of the public_profile input
if (public_profile === 'on') { if (public_profile === 'on') {
public_profile = true; public_profile = true;
} else { } else {
public_profile = false; public_profile = false;
} }
console.log(public_profile);
let currentUser = (await resCurrent.json()) as User; let currentUser = (await resCurrent.json()) as User;
@ -80,6 +117,7 @@ export const actions: Actions = {
} }
let formDataToSend = new FormData(); let formDataToSend = new FormData();
if (username) { if (username) {
formDataToSend.append('username', username); formDataToSend.append('username', username);
} }
@ -94,10 +132,13 @@ export const actions: Actions = {
} }
formDataToSend.append('public_profile', public_profile.toString()); 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', method: 'PATCH',
headers: { headers: {
Cookie: event.cookies.get('auth') || '' Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'X-CSRFToken': csrfToken
}, },
body: formDataToSend body: formDataToSend
}); });
@ -105,47 +146,53 @@ export const actions: Actions = {
let response = await res.json(); let response = await res.json();
if (!res.ok) { 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 fail(res.status, response);
} }
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
return { error: 'An error occurred while processing your request.' }; return { error: 'settings.generic_error' };
} }
}, },
changePassword: async (event) => { changePassword: async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/'); return redirect(302, '/');
} }
if (!event.cookies.get('auth')) { let sessionId = event.cookies.get('sessionid');
if (!sessionId) {
return redirect(302, '/'); return redirect(302, '/');
} }
console.log('changePassword');
const formData = await event.request.formData(); const formData = await event.request.formData();
const password1 = formData.get('password1') as string | null | undefined; const password1 = formData.get('password1') as string | null | undefined;
const password2 = formData.get('password2') 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) { 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', method: 'POST',
headers: { headers: {
Cookie: event.cookies.get('auth') || '', Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
new_password1: password1, current_password,
new_password2: password2 new_password: password1
}) })
}); });
if (!res.ok) { if (!res.ok) {
return fail(res.status, await res.json()); return fail(res.status, { message: 'settings.error_change_password' });
} }
return { success: true }; return { success: true };
}, },
@ -153,19 +200,22 @@ export const actions: Actions = {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/'); return redirect(302, '/');
} }
if (!event.cookies.get('auth')) { let sessionId = event.cookies.get('sessionid');
if (!sessionId) {
return redirect(302, '/'); return redirect(302, '/');
} }
const formData = await event.request.formData(); const formData = await event.request.formData();
const new_email = formData.get('new_email') as string | null | undefined; const new_email = formData.get('new_email') as string | null | undefined;
if (!new_email) { if (!new_email) {
return fail(400, { message: 'Email is required' }); return fail(400, { message: 'auth.email_required' });
} else { } else {
let csrfToken = await fetchCSRFToken();
let res = await fetch(`${endpoint}/auth/change-email/`, { let res = await fetch(`${endpoint}/auth/change-email/`, {
method: 'POST', method: 'POST',
headers: { headers: {
Cookie: event.cookies.get('auth') || '', Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}, },
body: JSON.stringify({ body: JSON.stringify({
new_email new_email

View file

@ -6,13 +6,20 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import TotpModal from '$lib/components/TOTPModal.svelte';
export let data; export let data;
let user: User; let user: User;
let emails: typeof data.props.emails;
if (data.user) { if (data.user) {
user = data.user; user = data.user;
emails = data.props.emails;
} }
let new_email: string = '';
let isMFAModalOpen: boolean = false;
onMount(async () => { onMount(async () => {
if (browser) { if (browser) {
const queryParams = new URLSearchParams($page.url.search); 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() { async function checkVisitedRegions() {
let res = await fetch('/api/reverse-geocode/mark_visited_region/', { let res = await fetch('/api/reverse-geocode/mark_visited_region/', {
method: 'POST', method: 'POST',
@ -58,8 +55,105 @@
addToast('error', $t('adventures.error_updating_regions')); 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'));
}
}
</script> </script>
{#if isMFAModalOpen}
<TotpModal
user={data.user}
on:close={() => (isMFAModalOpen = false)}
bind:is_enabled={data.props.authenticators}
/>
{/if}
<h1 class="text-center font-extrabold text-4xl mb-6">{$t('settings.settings_page')}</h1> <h1 class="text-center font-extrabold text-4xl mb-6">{$t('settings.settings_page')}</h1>
<h1 class="text-center font-extrabold text-xl">{$t('settings.account_settings')}</h1> <h1 class="text-center font-extrabold text-xl">{$t('settings.account_settings')}</h1>
@ -95,14 +189,6 @@
id="last_name" id="last_name"
class="block mb-2 input input-bordered w-full max-w-xs" class="block mb-2 input input-bordered w-full max-w-xs"
/><br /> /><br />
<!-- <label for="first_name">Email</label>
<input
type="email"
bind:value={user.email}
name="email"
id="email"
class="block mb-2 input input-bordered w-full max-w-xs"
/><br /> -->
<label for="profilePicture">{$t('auth.profile_picture')}</label> <label for="profilePicture">{$t('auth.profile_picture')}</label>
<input <input
type="file" type="file"
@ -131,13 +217,21 @@
{#if $page.form?.message} {#if $page.form?.message}
<div class="text-center text-error mt-4"> <div class="text-center text-error mt-4">
{$page.form?.message} {$t($page.form.message)}
</div> </div>
{/if} {/if}
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.password_change')}</h1> <h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.password_change')}</h1>
<div class="flex justify-center"> <div class="flex justify-center">
<form action="?/changePassword" method="post" class="w-full max-w-xs"> <form action="?/changePassword" method="post" class="w-full max-w-xs" use:enhance>
<input
type="password"
name="current_password"
placeholder={$t('settings.current_password')}
id="current_password"
class="block mb-2 input input-bordered w-full max-w-xs"
/>
<br />
<input <input
type="password" type="password"
name="password1" name="password1"
@ -153,35 +247,84 @@
placeholder={$t('settings.confirm_new_password')} placeholder={$t('settings.confirm_new_password')}
class="block mb-2 input input-bordered w-full max-w-xs" class="block mb-2 input input-bordered w-full max-w-xs"
/> />
<div class="tooltip tooltip-warning" data-tip={$t('settings.password_change_lopout_warning')}>
<button class="py-2 px-4 btn btn-primary mt-2">{$t('settings.password_change')}</button> <button class="py-2 px-4 btn btn-primary mt-2">{$t('settings.password_change')}</button>
</div>
<br /> <br />
</form> </form>
</div> </div>
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.email_change')}</h1> <h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.email_change')}</h1>
<div class="flex justify-center">
<form action="?/changeEmail" method="post" class="w-full max-w-xs"> <div class="flex justify-center mb-4">
<label for="current_email">{$t('settings.current_email')}</label> <div>
<input {#each emails as email}
type="email" <p class="mb-2">
name="current_email" {email.email}
placeholder={user.email || $t('settings.no_email_set')} {#if email.verified}
id="current_email" <div class="badge badge-success">{$t('settings.verified')}</div>
readonly {:else}
class="block mb-2 input input-bordered w-full max-w-xs" <div class="badge badge-error">{$t('settings.not_verified')}</div>
/> {/if}
<br /> {#if email.primary}
<div class="badge badge-primary">{$t('settings.primary')}</div>
{/if}
{#if !email.verified}
<button class="btn btn-sm btn-secondary ml-2" on:click={() => verifyEmail(email)}
>{$t('settings.verify')}</button
>
{/if}
{#if !email.primary}
<button class="btn btn-sm btn-secondary ml-2" on:click={() => primaryEmail(email)}
>{$t('settings.make_primary')}</button
>
{/if}
<button class="btn btn-sm btn-warning ml-2" on:click={() => removeEmail(email)}
>{$t('adventures.remove')}</button
>
</p>
{/each}
{#if emails.length === 0}
<p>{$t('settings.no_emai_set')}</p>
{/if}
</div>
</div>
<div class="flex justify-center mt-4">
<form class="w-full max-w-xs" on:submit={addEmail}>
<div class="mb-4">
<input <input
type="email" type="email"
name="new_email" name="new_email"
placeholder={$t('settings.new_email')} placeholder={$t('settings.new_email')}
bind:value={new_email}
id="new_email" id="new_email"
class="block mb-2 input input-bordered w-full max-w-xs" class="block mb-2 input input-bordered w-full max-w-xs"
/> />
<button class="py-2 px-4 btn btn-primary mt-2">{$t('settings.email_change')}</button> </div>
<div>
<button class="py-2 px-4 mb-4 btn btn-primary">{$t('settings.email_change')}</button>
</div>
</form> </form>
</div> </div>
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">{$t('settings.mfa_page_title')}</h1>
<div class="flex justify-center mb-4">
<div>
{#if !data.props.authenticators}
<p>{$t('settings.mfa_not_enabled')}</p>
<button class="btn btn-primary mt-2" on:click={() => (isMFAModalOpen = true)}
>{$t('settings.enable_mfa')}</button
>
{:else}
<button class="btn btn-warning mt-2" on:click={disableMfa}
>{$t('settings.disable_mfa')}</button
>
{/if}
</div>
</div>
<div class="flex flex-col items-center mt-4"> <div class="flex flex-col items-center mt-4">
<h1 class="text-center font-extrabold text-xl mt-4 mb-2"> <h1 class="text-center font-extrabold text-xl mt-4 mb-2">
{$t('adventures.visited_region_check')} {$t('adventures.visited_region_check')}
@ -189,22 +332,15 @@
<p> <p>
{$t('adventures.visited_region_check_desc')} {$t('adventures.visited_region_check_desc')}
</p> </p>
<p>{$t('adventures.update_visited_regions_disclaimer')}</p>
<button class="btn btn-neutral mt-2 mb-2" on:click={checkVisitedRegions} <button class="btn btn-neutral mt-2 mb-2" on:click={checkVisitedRegions}
>{$t('adventures.update_visited_regions')}</button >{$t('adventures.update_visited_regions')}</button
> >
<p>{$t('adventures.update_visited_regions_disclaimer')}</p>
</div> </div>
<!--
<div class="flex flex-col items-center mt-4">
<h1 class="text-center font-extrabold text-xl mt-4 mb-2">Data Export</h1>
<button class="btn btn-neutral mb-4" on:click={exportAdventures}> Export to JSON </button>
<p>This may take a few seconds...</p>
</div> -->
<small class="text-center" <small class="text-center"
><b>For Debug Use:</b> Server PK={user.pk} | Date Joined: {user.date_joined ><b>For Debug Use:</b> UUID={user.uuid} | Staff user: {user.is_staff}</small
? new Date(user.date_joined).toDateString()
: ''} | Staff user: {user.is_staff}</small
> >
<svelte:head> <svelte:head>

View file

@ -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');
}
}
}
};

View file

@ -1,64 +0,0 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { PageData } from './$types';
import { t } from 'svelte-i18n';
export let data: PageData;
</script>
<h1 class="text-center font-bold text-4xl mb-4">{$t('settings.change_password')}</h1>
{#if data.props.token && data.props.uid}
<p class="text-center">{$t('settings.login_redir')}</p>
<div
class="modal-action items-center"
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
>
<form action="?/reset" method="post" use:enhance>
<input type="hidden" name="uid" value={data.props.uid} />
<input type="hidden" name="token" value={data.props.token} />
<div class="mb-2 w-full">
<input
type="password"
class="input input-bordered w-full"
id="new_password1"
name="new_password1"
placeholder={$t('settings.new_password')}
/>
</div>
<div class="mb-2 w-full">
<input
type="password"
class="input input-bordered w-full"
id="new_password2"
name="new_password2"
placeholder={$t('settings.confirm_new_password')}
/>
</div>
<button type="submit" class="btn btn-primary w-full">{$t('settings.submit')}</button>
{#if $page.form?.message}
<div class="text-center text-error mt-4">
{$t($page.form?.message)}
</div>
{/if}
</form>
</div>
{:else}
<div class="flex justify-center">
<div class="items-center justify-center">
<p class="text-center">{$t('settings.token_required')}</p>
<button class="btn btn-neutral" on:click={() => goto('/settings/forgot-password')}>
{$t('settings.reset_password')}
</button>
</div>
</div>
{/if}
<svelte:head>
<title>Password Reset Confirm</title>
<meta name="description" content="Confirm your password reset and make a new password." />
</svelte:head>

View file

@ -8,9 +8,10 @@ export const load = (async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/login'); return redirect(302, '/login');
} else { } else {
let sessionId = event.cookies.get('sessionid');
let res = await fetch(`${serverEndpoint}/api/collections/shared/`, { let res = await fetch(`${serverEndpoint}/api/collections/shared/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
if (!res.ok) { if (!res.ok) {

View file

@ -41,22 +41,26 @@ export const actions: Actions = {
if (!csrfTokenFetch.ok) { if (!csrfTokenFetch.ok) {
event.locals.user = null; 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 tokenPromise = await csrfTokenFetch.json();
const csrfToken = tokenPromise.csrfToken; 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', method: 'POST',
headers: { headers: {
'X-CSRFToken': csrfToken, 'X-CSRFToken': csrfToken,
'Content-Type': 'application/json' 'Content-Type': 'application/json',
Cookie: `csrftoken=${csrfToken}`
}, },
body: JSON.stringify({ body: JSON.stringify({
username: username, username: username,
password1: password1, password: password1,
password2: password2,
email: email, email: email,
first_name, first_name,
last_name last_name
@ -65,31 +69,36 @@ export const actions: Actions = {
const loginResponse = await loginFetch.json(); const loginResponse = await loginFetch.json();
if (!loginFetch.ok) { if (!loginFetch.ok) {
// get the value of the first key in the object return fail(loginFetch.status, { message: loginResponse.errors[0].code });
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
});
} else { } else {
const token = loginResponse.access; const setCookieHeader = loginFetch.headers.get('Set-Cookie');
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: '/'
});
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, '/');
} }
} }
}; };

View file

@ -87,16 +87,14 @@
<div class="flex justify-between mt-4"> <div class="flex justify-between mt-4">
<p><a href="/login" class="underline">{$t('auth.login')}</a></p> <p><a href="/login" class="underline">{$t('auth.login')}</a></p>
<p> <p>
<a href="/settings/forgot-password" class="underline" <a href="/user/reset-password" class="underline">{$t('auth.forgot_password')}</a>
>{$t('auth.forgot_password')}</a
>
</p> </p>
</div> </div>
</form> </form>
</div> </div>
{#if $page.form?.message} {#if $page.form?.message}
<div class="text-center text-error mt-4">{$page.form?.message}</div> <div class="text-center text-error mt-4">{$t($page.form?.message)}</div>
{/if} {/if}
{:else} {:else}
<div class="flex justify-center"> <div class="flex justify-center">

View file

@ -4,7 +4,8 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => { export const load = (async (event) => {
if (!event.cookies.get('auth')) { let sessionId = event.cookies.get('sessionid');
if (!sessionId) {
return redirect(302, '/login'); return redirect(302, '/login');
} }
const uuid = event.params.uuid; const uuid = event.params.uuid;
@ -13,7 +14,7 @@ export const load = (async (event) => {
} }
let res = await fetch(`${serverEndpoint}/auth/user/${uuid}/`, { let res = await fetch(`${serverEndpoint}/auth/user/${uuid}/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
if (!res.ok) { if (!res.ok) {

View file

@ -1,3 +1,4 @@
import { fetchCSRFToken } from '$lib/index.server';
import { fail, type Actions } from '@sveltejs/kit'; import { fail, type Actions } from '@sveltejs/kit';
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
@ -13,10 +14,14 @@ export const actions: Actions = {
return fail(400, { message: 'missing_email' }); 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
Cookie: `csrftoken=${csrfToken}`
}, },
body: JSON.stringify({ body: JSON.stringify({
email email
@ -25,10 +30,7 @@ export const actions: Actions = {
if (!res.ok) { if (!res.ok) {
let message = await res.json(); let message = await res.json();
return fail(res.status, message);
const key = Object.keys(message)[0];
return fail(res.status, { message: message[key] });
} }
return { success: true }; return { success: true };
} }

View file

@ -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');
}
};

View file

@ -0,0 +1,53 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import type { PageData } from '../../../$types';
import { t } from 'svelte-i18n';
export let data: PageData;
</script>
<h1 class="text-center font-bold text-4xl mb-4">{$t('settings.change_password')}</h1>
<form method="POST" use:enhance class="flex flex-col items-center justify-center space-y-4">
<div class="w-full max-w-xs">
<label for="password" class="label">
<span class="label-text">{$t('auth.new_password')}</span>
</label>
<input
type="password"
id="password"
name="password"
required
class="input input-bordered w-full"
/>
</div>
<div class="w-full max-w-xs">
<label for="confirm_password" class="label">
<span class="label-text">{$t('auth.confirm_password')}</span>
</label>
<input
type="password"
id="confirm_password"
name="confirm_password"
required
class="input input-bordered w-full"
/>
</div>
<button type="submit" class="btn btn-primary">
{$t('settings.reset_password')}
</button>
{#if $page.form?.message}
<div class="text-error">
{$t($page.form?.message)}
</div>
{/if}
</form>
<svelte:head>
<title>Password Reset Confirm</title>
<meta name="description" content="Confirm your password reset and make a new password." />
</svelte:head>

View file

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

View file

@ -0,0 +1,14 @@
<script lang="ts">
import type { PageData } from '../$types';
import { t } from 'svelte-i18n';
export let data: PageData;
</script>
{#if data.verified}
<h1>{$t('settings.email_verified')}</h1>
<p>{$t('settings.email_verified_success')}</p>
{:else}
<h1>{$t('settings.email_verified_error')}</h1>
<p>{$t('settings.email_verified_erorr_desc')}</p>
{/if}

View file

@ -4,13 +4,14 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
export const load = (async (event) => { export const load = (async (event) => {
if (!event.cookies.get('auth')) { let sessionId = event.cookies.get('sessionid');
if (!sessionId) {
return redirect(302, '/login'); return redirect(302, '/login');
} }
const res = await fetch(`${serverEndpoint}/auth/users/`, { const res = await fetch(`${serverEndpoint}/auth/users/`, {
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
if (!res.ok) { if (!res.ok) {

View file

@ -2,6 +2,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
import type { Country } from '$lib/types'; import type { Country } from '$lib/types';
import { redirect, type Actions } from '@sveltejs/kit'; import { redirect, type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { fetchCSRFToken } from '$lib/index.server';
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
@ -9,11 +10,12 @@ export const load = (async (event) => {
if (!event.locals.user) { if (!event.locals.user) {
return redirect(302, '/login'); return redirect(302, '/login');
} else { } else {
const res = await fetch(`${endpoint}/api/countries/`, { const res = await event.fetch(`${endpoint}/api/countries/`, {
method: 'GET', method: 'GET',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${event.cookies.get('sessionid')}`
} },
credentials: 'include'
}); });
if (!res.ok) { if (!res.ok) {
console.error('Failed to fetch countries'); console.error('Failed to fetch countries');
@ -27,8 +29,6 @@ export const load = (async (event) => {
}; };
} }
} }
return {};
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
export const actions: Actions = { 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'); return redirect(302, '/login');
} }
let csrfToken = await fetchCSRFToken();
const res = await fetch(`${endpoint}/api/visitedregion/`, { const res = await fetch(`${endpoint}/api/visitedregion/`, {
method: 'POST', method: 'POST',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}`, Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}, },
body: JSON.stringify({ region: body.regionId }) body: JSON.stringify({ region: body.regionId })
}); });
@ -75,15 +80,20 @@ export const actions: Actions = {
const visitId = body.visitId as number; 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'); return redirect(302, '/login');
} }
let csrfToken = await fetchCSRFToken();
const res = await fetch(`${endpoint}/api/visitedregion/${visitId}/`, { const res = await fetch(`${endpoint}/api/visitedregion/${visitId}/`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}`, Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
} }
}); });

View file

@ -12,10 +12,16 @@ export const load = (async (event) => {
let visitedRegions: VisitedRegion[] = []; let visitedRegions: VisitedRegion[] = [];
let country: Country; let country: Country;
let sessionId = event.cookies.get('sessionid');
if (!sessionId) {
return redirect(302, '/login');
}
let res = await fetch(`${endpoint}/api/${id}/regions/`, { let res = await fetch(`${endpoint}/api/${id}/regions/`, {
method: 'GET', method: 'GET',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
if (!res.ok) { if (!res.ok) {
@ -28,7 +34,7 @@ export const load = (async (event) => {
res = await fetch(`${endpoint}/api/${id}/visits/`, { res = await fetch(`${endpoint}/api/${id}/visits/`, {
method: 'GET', method: 'GET',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
if (!res.ok) { if (!res.ok) {
@ -41,7 +47,7 @@ export const load = (async (event) => {
res = await fetch(`${endpoint}/api/countries/${regions[0].country}/`, { res = await fetch(`${endpoint}/api/countries/${regions[0].country}/`, {
method: 'GET', method: 'GET',
headers: { headers: {
Cookie: `${event.cookies.get('auth')}` Cookie: `sessionid=${sessionId}`
} }
}); });
if (!res.ok) { if (!res.ok) {

View file

@ -100,6 +100,33 @@ export default {
fontFamily: fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace' '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
} }
} }
] ]

View file

@ -1,4 +0,0 @@
FROM nginx:alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d

View file

@ -1,8 +0,0 @@
server {
listen 80;
server_name localhost;
location /media/ {
alias /app/media/;
}
}