From d7baf6961e9197b9c9ad65a51e27b3a8f42b660b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:06:57 +0000 Subject: [PATCH 01/26] Bump @sveltejs/kit Bumps the npm_and_yarn group with 1 update in the /frontend directory: [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit). Updates `@sveltejs/kit` from 2.5.17 to 2.8.3 - [Release notes](https://github.com/sveltejs/kit/releases) - [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md) - [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.8.3/packages/kit) --- updated-dependencies: - dependency-name: "@sveltejs/kit" dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 48 ++++++++++++++++++++--------------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b9aa1f4..e41ba33 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "@sveltejs/adapter-auto": "^3.2.2", "@sveltejs/adapter-node": "^5.2.0", "@sveltejs/adapter-vercel": "^5.4.1", - "@sveltejs/kit": "^2.5.17", + "@sveltejs/kit": "^2.8.3", "@sveltejs/vite-plugin-svelte": "^3.1.1", "@tailwindcss/typography": "^0.5.13", "@types/node": "^22.5.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index d545c4c..4fa546f 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -23,16 +23,16 @@ importers: version: 1.1.67 '@sveltejs/adapter-auto': 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': 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': 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': - specifier: ^2.5.17 - 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)) + specifier: ^2.8.3 + 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': specifier: ^3.1.1 version: 3.1.1(svelte@4.2.19)(vite@5.3.6(@types/node@22.5.4)) @@ -615,12 +615,12 @@ packages: peerDependencies: '@sveltejs/kit': ^2.4.0 - '@sveltejs/kit@2.5.17': - resolution: {integrity: sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==} + '@sveltejs/kit@2.8.3': + resolution: {integrity: sha512-DVBVwugfzzn0SxKA+eAmKqcZ7aHZROCHxH7/pyrOi+HLtQ721eEsctGb9MkhEuqj6q/9S/OFYdn37vdxzFPdvw==} engines: {node: '>=18.13'} hasBin: true 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 vite: ^5.0.3 @@ -926,8 +926,8 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} - devalue@5.0.0: - resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==} + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1668,9 +1668,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - sirv@2.0.4: - resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} - engines: {node: '>= 10'} + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + engines: {node: '>=18'} sorcery@0.11.1: resolution: {integrity: sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==} @@ -2398,34 +2398,34 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.24.0': 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: - '@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 - '@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: '@rollup/plugin-commonjs': 26.0.1(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) - '@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 - '@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: - '@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 esbuild: 0.21.5 transitivePeerDependencies: - encoding - 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: '@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 cookie: 0.6.0 - devalue: 5.0.0 + devalue: 5.1.1 esm-env: 1.0.0 import-meta-resolve: 4.1.0 kleur: 4.1.5 @@ -2433,7 +2433,7 @@ snapshots: mrmime: 2.0.0 sade: 1.8.1 set-cookie-parser: 2.6.0 - sirv: 2.0.4 + sirv: 3.0.0 svelte: 4.2.19 tiny-glob: 0.2.9 vite: 5.3.6(@types/node@22.5.4) @@ -2744,7 +2744,7 @@ snapshots: detect-libc@2.0.3: {} - devalue@5.0.0: {} + devalue@5.1.1: {} didyoumean@1.2.2: {} @@ -3517,7 +3517,7 @@ snapshots: signal-exit@4.1.0: {} - sirv@2.0.4: + sirv@3.0.0: dependencies: '@polka/url': 1.0.0-next.25 mrmime: 2.0.0 From 9bc20be70e237b53cccd8a96e38cedd1adb9e303 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 29 Nov 2024 14:41:13 -0500 Subject: [PATCH 02/26] Initial migration to new session based auth system with AllAuth --- backend/server/adventures/middleware.py | 10 + backend/server/adventures/views.py | 1 + backend/server/main/settings.py | 37 +- backend/server/main/urls.py | 12 +- .../email/password_reset_key_message.txt | 13 - backend/server/users/adapters.py | 10 + backend/server/users/admin.py | 9 +- backend/server/users/form_overrides.py | 17 + backend/server/users/forms.py | 66 +--- backend/server/users/serializers.py | 98 +---- backend/server/users/views.py | 15 + frontend/src/hooks.server.ts | 104 ++--- .../lib/components/ActivityComplete.svelte | 1 + .../src/lib/components/AdventureModal.svelte | 1 + .../src/routes/activities/+page.server.ts | 17 +- frontend/src/routes/activities/+server.ts | 14 +- .../src/routes/adventures/+page.server.ts | 374 +----------------- .../routes/adventures/[id]/+page.server.ts | 47 +-- frontend/src/routes/api/[...path]/+server.ts | 52 +-- .../src/routes/collections/+page.server.ts | 37 +- frontend/src/routes/login/+page.server.ts | 68 ++-- .../settings/forgot-password/+page.server.ts | 14 +- frontend/src/routes/signup/+page.server.ts | 63 +-- .../src/routes/worldtravel/+page.server.ts | 6 +- 24 files changed, 313 insertions(+), 773 deletions(-) delete mode 100644 backend/server/templates/account/email/password_reset_key_message.txt create mode 100644 backend/server/users/adapters.py create mode 100644 backend/server/users/form_overrides.py diff --git a/backend/server/adventures/middleware.py b/backend/server/adventures/middleware.py index af54b68..3cd9713 100644 --- a/backend/server/adventures/middleware.py +++ b/backend/server/adventures/middleware.py @@ -11,3 +11,13 @@ class AppVersionMiddleware: response['X-AdventureLog-Version'] = '1.0.0' return response + +# make a middlewra that prints all of the request cookies +class PrintCookiesMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + print(request.COOKIES) + response = self.get_response(request) + return response \ No newline at end of file diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 348c142..808b9aa 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -73,6 +73,7 @@ class AdventureViewSet(viewsets.ModelViewSet): return queryset.order_by(ordering) def get_queryset(self): + print(self.request.user) # if the user is not authenticated return only public adventures for retrieve action if not self.request.user.is_authenticated: if self.action == 'retrieve': diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 44aa5d4..14d3983 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -47,10 +47,11 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'rest_framework', 'rest_framework.authtoken', - 'dj_rest_auth', + # 'dj_rest_auth', 'allauth', 'allauth.account', - 'dj_rest_auth.registration', + 'allauth.headless', + # 'dj_rest_auth.registration', 'allauth.socialaccount', 'allauth.socialaccount.providers.facebook', 'drf_yasg', @@ -113,6 +114,7 @@ DATABASES = { } } +ACCOUNT_SIGNUP_FORM_CLASS = 'users.form_overrides.CustomSignupForm' # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ @@ -157,16 +159,6 @@ TEMPLATES = [ }, ] -REST_AUTH = { - 'SESSION_LOGIN': True, - 'USE_JWT': True, - 'JWT_AUTH_COOKIE': 'auth', - 'JWT_AUTH_HTTPONLY': False, - 'REGISTER_SERIALIZER': 'users.serializers.RegisterSerializer', - 'USER_DETAILS_SERIALIZER': 'users.serializers.CustomUserDetailsSerializer', - 'PASSWORD_RESET_SERIALIZER': 'users.serializers.MyPasswordResetSerializer' -} - 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.') @@ -181,8 +173,16 @@ STORAGES = { AUTH_USER_MODEL = 'users.CustomUser' +ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter' + FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000') +# HEADLESS_FRONTEND_URLS = { +# "account_confirm_email": "https://app.project.org/account/verify-email/{key}", +# "account_reset_password_from_key": "https://app.org/account/password/reset/key/{key}", +# "account_signup": "https://app.org/account/signup", +# } + EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' SITE_ID = 1 ACCOUNT_EMAIL_REQUIRED = True @@ -228,12 +228,14 @@ SWAGGER_SETTINGS = { 'LOGOUT_URL': 'logout', } -# For demo purposes only. Use a white list in the real world. -CORS_ORIGIN_ALLOW_ALL = True - from os import getenv + +CORS_ALLOWED_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' LOGGING = { @@ -260,6 +262,7 @@ LOGGING = { }, }, } - # https://github.com/dr5hn/countries-states-cities-database/tags -COUNTRY_REGION_JSON_VERSION = 'v2.4' \ No newline at end of file +COUNTRY_REGION_JSON_VERSION = 'v2.4' + +SESSION_SAVE_EVERY_REQUEST = True \ No newline at end of file diff --git a/backend/server/main/urls.py b/backend/server/main/urls.py index fe098bd..fa3456d 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -4,7 +4,7 @@ from django.views.generic import RedirectView, TemplateView from django.conf import settings from django.conf.urls.static import static from adventures import urls as adventures -from users.views import ChangeEmailView, IsRegistrationDisabled, PublicUserListView, PublicUserDetailView +from users.views import ChangeEmailView, IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView from .views import get_csrf_token from drf_yasg.views import get_schema_view @@ -25,6 +25,10 @@ urlpatterns = [ path('auth/users/', PublicUserListView.as_view(), name='public-user-list'), path('auth/user//', PublicUserDetailView.as_view(), name='public-user-detail'), + path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'), + + + path('csrf/', get_csrf_token, name='get_csrf_token'), re_path(r'^$', TemplateView.as_view( template_name="home.html"), name='home'), @@ -64,11 +68,15 @@ urlpatterns = [ 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'^account/', include('allauth.urls')), re_path(r'^admin/', admin.site.urls), re_path(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'), re_path(r'^docs/$', schema_view.with_ui('swagger', cache_timeout=0), name='api_docs'), # path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'), + path("accounts/", include("allauth.urls")), + + # Include the API endpoints: + path("_allauth/", include("allauth.headless.urls")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/server/templates/account/email/password_reset_key_message.txt b/backend/server/templates/account/email/password_reset_key_message.txt deleted file mode 100644 index 42473bf..0000000 --- a/backend/server/templates/account/email/password_reset_key_message.txt +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "account/email/base_message.txt" %} -{% load i18n %} - -{% block content %}{% autoescape off %}{% blocktrans %}You're receiving this email because you or someone else has requested a password reset for your user account. - -It can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %} - -{{ frontend_url }}/settings/forgot-password/confirm?token={{ temp_key }}&uid={{ user_pk }} - -{% if username %} - - -{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}{% endif %}{% endautoescape %}{% endblock content %} \ No newline at end of file diff --git a/backend/server/users/adapters.py b/backend/server/users/adapters.py new file mode 100644 index 0000000..cf0435f --- /dev/null +++ b/backend/server/users/adapters.py @@ -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 \ No newline at end of file diff --git a/backend/server/users/admin.py b/backend/server/users/admin.py index 66d2600..db75154 100644 --- a/backend/server/users/admin.py +++ b/backend/server/users/admin.py @@ -1,3 +1,10 @@ from django.contrib import admin -# Register your models here +from django.contrib.sessions.models import Session + +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) \ No newline at end of file diff --git a/backend/server/users/form_overrides.py b/backend/server/users/form_overrides.py new file mode 100644 index 0000000..266bfd0 --- /dev/null +++ b/backend/server/users/form_overrides.py @@ -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 \ No newline at end of file diff --git a/backend/server/users/forms.py b/backend/server/users/forms.py index 2bb5454..266bfd0 100644 --- a/backend/server/users/forms.py +++ b/backend/server/users/forms.py @@ -1,55 +1,17 @@ -from allauth.account.utils import (filter_users_by_email, user_pk_to_url_str, user_username) -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 django import forms -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 clean_email(self): - """ - 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'] - + 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 \ No newline at end of file diff --git a/backend/server/users/serializers.py b/backend/server/users/serializers.py index 25f5cc0..f0cd764 100644 --- a/backend/server/users/serializers.py +++ b/backend/server/users/serializers.py @@ -1,10 +1,8 @@ from rest_framework import serializers from django.contrib.auth import get_user_model -from adventures.models import Adventure, Collection -from users.forms import CustomAllAuthPasswordResetForm +from adventures.models import Collection from dj_rest_auth.serializers import PasswordResetSerializer -from rest_framework.exceptions import PermissionDenied User = get_user_model() @@ -32,77 +30,7 @@ class ChangeEmailSerializer(serializers.Serializer): 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.contrib.auth import get_user_model @@ -116,20 +44,6 @@ from rest_framework import serializers from django.conf import settings 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): """ User model w/o password @@ -203,13 +117,3 @@ class CustomUserDetailsSerializer(UserDetailsSerializer): representation['profile_pic'] = f"{public_url}/media/{instance.profile_pic.name}" del representation['pk'] # remove the pk field from the response 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 \ No newline at end of file diff --git a/backend/server/users/views.py b/backend/server/users/views.py index 5300d8c..ae959ac 100644 --- a/backend/server/users/views.py +++ b/backend/server/users/views.py @@ -83,3 +83,18 @@ class PublicUserDetailView(APIView): user.email = None serializer = PublicUserSerializer(user) 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) \ No newline at end of file diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts index 0c1f991..cbd152f 100644 --- a/frontend/src/hooks.server.ts +++ b/frontend/src/hooks.server.ts @@ -1,95 +1,75 @@ import type { Handle } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; -import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; export const authHook: Handle = async ({ event, resolve }) => { try { - let authCookie = event.cookies.get('auth'); - let refreshCookie = event.cookies.get('refresh'); + let sessionid = event.cookies.get('sessionid'); + console.log('sessionid:', sessionid); - if (!authCookie && !refreshCookie) { + if (!sessionid) { + console.log('No sessionid cookie'); event.locals.user = null; 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); - } - } + // print all cookies in the request + console.log('Cookies:', event.request.headers.get('cookie')); 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: { - Cookie: `${authCookie}` + cookie } }); 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; - 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}` - } - }); - } - } + event.locals.user = null; + event.cookies.delete('sessionid', { path: '/' }); + return await resolve(event); } if (userFetch.ok) { const user = await userFetch.json(); event.locals.user = user; + const setCookieHeader = userFetch.headers.get('Set-Cookie'); + + 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 + }); + } + } } else { event.locals.user = null; - event.cookies.delete('auth', { path: '/' }); - event.cookies.delete('refresh', { path: '/' }); + event.cookies.delete('sessionid', { path: '/' }); } } catch (error) { console.error('Error in authHook:', error); event.locals.user = null; - event.cookies.delete('auth', { path: '/' }); - event.cookies.delete('refresh', { path: '/' }); + event.cookies.delete('sessionid', { path: '/' }); } return await resolve(event); diff --git a/frontend/src/lib/components/ActivityComplete.svelte b/frontend/src/lib/components/ActivityComplete.svelte index 0636127..ef39c52 100644 --- a/frontend/src/lib/components/ActivityComplete.svelte +++ b/frontend/src/lib/components/ActivityComplete.svelte @@ -18,6 +18,7 @@ 'Content-Type': 'application/json' } }); + console.log(res); let data = await res.json(); console.log('ACTIVITIES' + data.activities); if (data && data.activities) { diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index c0dc277..3d64599 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -444,6 +444,7 @@ headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify(adventure) }); let data = await res.json(); diff --git a/frontend/src/routes/activities/+page.server.ts b/frontend/src/routes/activities/+page.server.ts index 4407eda..238e6b4 100644 --- a/frontend/src/routes/activities/+page.server.ts +++ b/frontend/src/routes/activities/+page.server.ts @@ -1,5 +1,6 @@ import { redirect, type Actions } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; +import { fetchCSRFToken } from '$lib/index.server'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -7,13 +8,16 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } + let csrfToken = await fetchCSRFToken(); let allActivities: string[] = []; - let res = await fetch(`${endpoint}/api/activity-types/types/`, { + let res = await event.fetch(`${endpoint}/api/activity-types/types/`, { headers: { - 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` - } + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}` + }, + credentials: 'include' }); + console.log(res); let data = await res.json(); if (data) { allActivities = data; @@ -27,13 +31,16 @@ export const load = (async (event) => { export const actions: Actions = { getActivities: async (event) => { + let csrfToken = await fetchCSRFToken(); let allActivities: string[] = []; let res = await fetch(`${endpoint}/api/activity-types/types/`, { headers: { + 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` + Cookie: `csrftoken=${csrfToken}` } }); + console.log(res); let data = await res.json(); if (data) { allActivities = data; diff --git a/frontend/src/routes/activities/+server.ts b/frontend/src/routes/activities/+server.ts index ebb4252..c5143dc 100644 --- a/frontend/src/routes/activities/+server.ts +++ b/frontend/src/routes/activities/+server.ts @@ -1,15 +1,19 @@ import { json } from '@sveltejs/kit'; -import type { RequestHandler } from '../data/$types'; +import type { RequestHandler } from '@sveltejs/kit'; +import { fetchCSRFToken } from '$lib/index.server'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const POST: RequestHandler = async (event) => { let allActivities: string[] = []; - let res = await fetch(`${endpoint}/api/activity-types/types/`, { + let csrfToken = await fetchCSRFToken(); + let sessionId = event.cookies.get('sessionid'); + let res = await event.fetch(`${endpoint}/api/activity-types/types/`, { headers: { - 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` - } + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}` + }, + credentials: 'include' }); let data = await res.json(); if (data) { diff --git a/frontend/src/routes/adventures/+page.server.ts b/frontend/src/routes/adventures/+page.server.ts index e0a0f12..da69fdf 100644 --- a/frontend/src/routes/adventures/+page.server.ts +++ b/frontend/src/routes/adventures/+page.server.ts @@ -4,8 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { Adventure } from '$lib/types'; import type { Actions } from '@sveltejs/kit'; -import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; -import { checkLink } from '$lib'; +import { fetchCSRFToken } from '$lib/index.server'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -29,12 +28,13 @@ export const load = (async (event) => { const page = event.url.searchParams.get('page') || '1'; const is_visited = event.url.searchParams.get('is_visited') || 'all'; - let initialFetch = await fetch( + let initialFetch = await event.fetch( `${serverEndpoint}/api/adventures/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' } ); @@ -61,371 +61,15 @@ export const load = (async (event) => { }) satisfies PageServerLoad; export const actions: Actions = { - create: async (event) => { - const formData = await event.request.formData(); - - const type = formData.get('type') as string; - const name = formData.get('name') as string; - const location = formData.get('location') as string | null; - let date = (formData.get('date') as string | null) ?? null; - const description = formData.get('description') as string | null; - const activity_types = formData.get('activity_types') - ? (formData.get('activity_types') as string).split(',') - : null; - const rating = formData.get('rating') ? Number(formData.get('rating')) : null; - let link = formData.get('link') as string | null; - let latitude = formData.get('latitude') as string | null; - let longitude = formData.get('longitude') as string | null; - let collection = formData.get('collection') as string | null; - let is_public = formData.get('is_public') as string | null | boolean; - - if (is_public) { - is_public = true; - } else { - is_public = false; - } - - // check if latitude and longitude are valid - if (latitude && longitude) { - if (isNaN(Number(latitude)) || isNaN(Number(longitude))) { - return { - status: 400, - body: { error: 'Invalid latitude or longitude' } - }; - } - } - - // round latitude and longitude to 6 decimal places - if (latitude) { - latitude = Number(latitude).toFixed(6); - } - if (longitude) { - longitude = Number(longitude).toFixed(6); - } - - const image = formData.get('image') as File; - - if (!type || !name) { - return { - status: 400, - body: { error: 'Missing required fields' } - }; - } - - if (date == null || date == '') { - date = null; - } - - if (link) { - link = checkLink(link); - } - - const formDataToSend = new FormData(); - formDataToSend.append('type', type); - formDataToSend.append('name', name); - formDataToSend.append('location', location || ''); - formDataToSend.append('date', date || ''); - formDataToSend.append('description', description || ''); - formDataToSend.append('latitude', latitude || ''); - formDataToSend.append('longitude', longitude || ''); - formDataToSend.append('is_public', is_public.toString()); - - if (!isNaN(Number(collection))) { - if (collection !== null) { - formDataToSend.append('collection', collection); - } - } - - if (activity_types) { - // Filter out empty and duplicate activity types, then trim each activity type - const cleanedActivityTypes = Array.from( - new Set( - activity_types - .map((activity_type) => activity_type.trim()) - .filter((activity_type) => activity_type !== '' && activity_type !== ',') - ) - ); - - // Append each cleaned activity type to formDataToSend - cleanedActivityTypes.forEach((activity_type) => { - formDataToSend.append('activity_types', activity_type); - }); - } - formDataToSend.append('rating', rating ? rating.toString() : ''); - formDataToSend.append('link', link || ''); - // formDataToSend.append('image', image); - - // log each key-value pair in the FormData - for (let pair of formDataToSend.entries()) { - console.log(pair[0] + ', ' + pair[1]); - } - - let auth = event.cookies.get('auth'); - - if (!auth) { - const refresh = event.cookies.get('refresh'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - - if (!auth) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - - const csrfToken = await fetchCSRFToken(); - - if (!csrfToken) { - return { - status: 500, - body: { message: 'Failed to fetch CSRF token' } - }; - } - - const res = await fetch(`${serverEndpoint}/api/adventures/`, { - method: 'POST', - headers: { - 'X-CSRFToken': csrfToken, - Cookie: auth - }, - body: formDataToSend - }); - - let new_id = await res.json(); - - if (!res.ok) { - const errorBody = await res.json(); - return { - status: res.status, - body: { error: errorBody } - }; - } - - let id = new_id.id; - let user_id = new_id.user_id; - let image_url = new_id.image; - let link_url = new_id.link; - - if (image && image.size > 0) { - let imageForm = new FormData(); - imageForm.append('image', image); - imageForm.append('adventure', id); - let imageRes = await fetch(`${serverEndpoint}/api/images/`, { - method: 'POST', - headers: { - Cookie: `${event.cookies.get('auth')}` - }, - body: imageForm - }); - let data = await imageRes.json(); - console.log(data); - } - - return { id, user_id, image_url, link }; - }, - edit: async (event) => { - const formData = await event.request.formData(); - - const adventureId = formData.get('adventureId') as string; - const type = formData.get('type') as string; - const name = formData.get('name') as string; - const location = formData.get('location') as string | null; - let date = (formData.get('date') as string | null) ?? null; - const description = formData.get('description') as string | null; - let activity_types = formData.get('activity_types') - ? (formData.get('activity_types') as string).split(',') - : null; - const rating = formData.get('rating') ? Number(formData.get('rating')) : null; - let link = formData.get('link') as string | null; - let latitude = formData.get('latitude') as string | null; - let longitude = formData.get('longitude') as string | null; - let is_public = formData.get('is_public') as string | null | boolean; - - if (is_public) { - is_public = true; - } else { - is_public = false; - } - - // check if latitude and longitude are valid - if (latitude && longitude) { - if (isNaN(Number(latitude)) || isNaN(Number(longitude))) { - return { - status: 400, - body: { error: 'Invalid latitude or longitude' } - }; - } - } - - // round latitude and longitude to 6 decimal places - if (latitude) { - latitude = Number(latitude).toFixed(6); - } - if (longitude) { - longitude = Number(longitude).toFixed(6); - } - - const image = formData.get('image') as File; - - // console.log(activity_types); - - if (!type || !name) { - return { - status: 400, - body: { error: 'Missing required fields' } - }; - } - - if (date == null || date == '') { - date = null; - } - - if (link) { - link = checkLink(link); - } - - const formDataToSend = new FormData(); - formDataToSend.append('type', type); - formDataToSend.append('name', name); - formDataToSend.append('location', location || ''); - formDataToSend.append('date', date || ''); - formDataToSend.append('description', description || ''); - formDataToSend.append('latitude', latitude || ''); - formDataToSend.append('longitude', longitude || ''); - formDataToSend.append('is_public', is_public.toString()); - - let csrfToken = await fetchCSRFToken(); - - if (activity_types) { - // Filter out empty and duplicate activity types, then trim each activity type - const cleanedActivityTypes = Array.from( - new Set( - activity_types - .map((activity_type) => activity_type.trim()) - .filter((activity_type) => activity_type !== '' && activity_type !== ',') - ) - ); - - // Append each cleaned activity type to formDataToSend - cleanedActivityTypes.forEach((activity_type) => { - formDataToSend.append('activity_types', activity_type); - }); - } else { - let res = await fetch(`${serverEndpoint}/api/adventures/${adventureId}/`, { - method: 'PATCH', - headers: { - Cookie: `${event.cookies.get('auth')}`, - 'X-CSRFToken': csrfToken, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ activity_types: [] }) - }); - if (!res.ok) { - const errorBody = await res.json(); - return { - status: res.status, - body: { error: errorBody } - }; - } - } - formDataToSend.append('rating', rating ? rating.toString() : ''); - formDataToSend.append('link', link || ''); - - if (image && image.size > 0) { - formDataToSend.append('image', image); - } - - let auth = event.cookies.get('auth'); - - if (!auth) { - const refresh = event.cookies.get('refresh'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - - if (!auth) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - - if (!csrfToken) { - return { - status: 500, - body: { message: 'Failed to fetch CSRF token' } - }; - } - - const res = await fetch(`${serverEndpoint}/api/adventures/${adventureId}/`, { - method: 'PATCH', - headers: { - 'X-CSRFToken': csrfToken, - Cookie: auth - }, - body: formDataToSend - }); - - if (!res.ok) { - const errorBody = await res.json(); - return { - status: res.status, - body: { error: errorBody } - }; - } - - let adventure = await res.json(); - - let image_url = adventure.image; - let link_url = adventure.link; - return { image_url, link_url }; - }, image: async (event) => { let formData = await event.request.formData(); + let csrfToken = await fetchCSRFToken(); + let sessionId = event.cookies.get('sessionid'); let res = await fetch(`${serverEndpoint}/api/images/`, { method: 'POST', headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `csrftoken=${csrfToken}; sessionid=${sessionId}`, + 'X-CSRFToken': csrfToken }, body: formData }); diff --git a/frontend/src/routes/adventures/[id]/+page.server.ts b/frontend/src/routes/adventures/[id]/+page.server.ts index aa01d2e..bba55aa 100644 --- a/frontend/src/routes/adventures/[id]/+page.server.ts +++ b/frontend/src/routes/adventures/[id]/+page.server.ts @@ -7,8 +7,9 @@ export const load = (async (event) => { const id = event.params as { id: string }; let request = await fetch(`${endpoint}/api/adventures/${id.id}/`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' }); if (!request.ok) { console.error('Failed to fetch adventure ' + id.id); @@ -24,8 +25,9 @@ export const load = (async (event) => { if (adventure.collection) { let res2 = await fetch(`${endpoint}/api/collections/${adventure.collection}/`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' }); collection = await res2.json(); } @@ -39,8 +41,8 @@ export const load = (async (event) => { } }) satisfies PageServerLoad; -import type { Actions } from '@sveltejs/kit'; -import { tryRefreshToken } from '$lib/index.server'; +import { redirect, type Actions } from '@sveltejs/kit'; +import { fetchCSRFToken } from '$lib/index.server'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -50,29 +52,7 @@ export const actions: Actions = { const adventureId = id.id; if (!event.locals.user) { - const refresh = event.cookies.get('refresh'); - let auth = event.cookies.get('auth'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } + return redirect(302, '/login'); } if (!adventureId) { return { @@ -81,12 +61,15 @@ export const actions: Actions = { }; } + let csrfToken = await fetchCSRFToken(); + let res = await fetch(`${serverEndpoint}/api/adventures/${event.params.id}`, { method: 'DELETE', headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' - } + Cookie: `sessionid=${event.cookies.get('sessionid')}; csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken + }, + credentials: 'include' }); console.log(res); if (!res.ok) { diff --git a/frontend/src/routes/api/[...path]/+server.ts b/frontend/src/routes/api/[...path]/+server.ts index 981debb..ef6bbbf 100644 --- a/frontend/src/routes/api/[...path]/+server.ts +++ b/frontend/src/routes/api/[...path]/+server.ts @@ -1,69 +1,77 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +import { fetchCSRFToken } from '$lib/index.server'; import { json } from '@sveltejs/kit'; /** @type {import('./$types').RequestHandler} */ -export async function GET({ url, params, request, fetch, cookies }) { - // add the param format = json to the url or add additional if anothre param is already present - if (url.search) { - url.search = url.search + '&format=json'; - } else { - url.search = '?format=json'; - } - return handleRequest(url, params, request, fetch, cookies); +export async function GET(event) { + const { url, params, request, fetch, cookies } = event; + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam); } /** @type {import('./$types').RequestHandler} */ export async function POST({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies, true); + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam, true); } export async function PATCH({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies, true); + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam, true); } export async function PUT({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies, true); + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam, true); } export async function DELETE({ url, params, request, fetch, cookies }) { - return handleRequest(url, params, request, fetch, cookies, true); + const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; + return handleRequest(url, params, request, fetch, cookies, searchParam, true); } -// Implement other HTTP methods as needed (PUT, DELETE, etc.) - async function handleRequest( url: any, params: any, request: any, fetch: any, cookies: any, + searchParam: string, requreTrailingSlash: boolean | undefined = false ) { const path = params.path; - let targetUrl = `${endpoint}/api/${path}${url.search}`; + let targetUrl = `${endpoint}/api/${path}`; + // Ensure the path ends with a trailing slash if (requreTrailingSlash && !targetUrl.endsWith('/')) { targetUrl += '/'; } + // Append query parameters to the path correctly + targetUrl += searchParam; // This will add ?format=json or &format=json to the URL + const headers = new Headers(request.headers); - const authCookie = cookies.get('auth'); - - if (authCookie) { - headers.set('Cookie', `${authCookie}`); + const csrfToken = await fetchCSRFToken(); + if (!csrfToken) { + return json({ error: 'CSRF token is missing or invalid' }, { status: 400 }); } try { const response = await fetch(targetUrl, { method: request.method, - headers: headers, - body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined + headers: { + ...Object.fromEntries(headers), + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}` + }, + body: + request.method !== 'GET' && request.method !== 'HEAD' ? await request.text() : undefined, + credentials: 'include' // This line ensures cookies are sent with the request }); if (response.status === 204) { - // For 204 No Content, return a response with no body return new Response(null, { status: 204, headers: response.headers diff --git a/frontend/src/routes/collections/+page.server.ts b/frontend/src/routes/collections/+page.server.ts index 9d2477f..e0cd851 100644 --- a/frontend/src/routes/collections/+page.server.ts +++ b/frontend/src/routes/collections/+page.server.ts @@ -17,10 +17,12 @@ export const load = (async (event) => { let previous = null; let count = 0; let adventures: Adventure[] = []; + let sessionId = event.cookies.get('sessionid'); let initialFetch = await fetch(`${serverEndpoint}/api/collections/?order_by=updated_at`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${sessionId}` + }, + credentials: 'include' }); if (!initialFetch.ok) { console.error('Failed to fetch visited adventures'); @@ -72,34 +74,9 @@ export const actions: Actions = { formDataToSend.append('start_date', start_date || ''); formDataToSend.append('end_date', end_date || ''); formDataToSend.append('link', link || ''); - let auth = event.cookies.get('auth'); + let sessionid = event.cookies.get('sessionid'); - if (!auth) { - const refresh = event.cookies.get('refresh'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - - if (!auth) { + if (!sessionid) { return { status: 401, body: { message: 'Unauthorized' } @@ -119,7 +96,7 @@ export const actions: Actions = { method: 'POST', headers: { 'X-CSRFToken': csrfToken, - Cookie: auth + Cookie: `sessionid=${sessionid}; csrftoken=${csrfToken}` }, body: formDataToSend }); diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index fcafe72..60b2dbe 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -2,6 +2,7 @@ import { fail, redirect } from '@sveltejs/kit'; import type { Actions, PageServerLoad } from './$types'; import { getRandomBackground, getRandomQuote } from '$lib'; +import { fetchCSRFToken } from '$lib/index.server'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; export const load: PageServerLoad = async (event) => { @@ -24,37 +25,29 @@ export const actions: Actions = { default: async (event) => { const formData = await event.request.formData(); const formUsername = formData.get('username'); - const formPassword = formData.get('password'); let username = formUsername?.toString().toLocaleLowerCase(); const password = formData.get('password'); const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; - const csrfTokenFetch = await event.fetch(`${serverEndpoint}/csrf/`); - if (!csrfTokenFetch.ok) { - console.error('Failed to fetch CSRF token'); - event.locals.user = null; - return fail(500, { - message: 'Failed to fetch CSRF token' - }); - } + const csrfToken = await fetchCSRFToken(); - const tokenPromise = await csrfTokenFetch.json(); - const csrfToken = tokenPromise.csrfToken; - - const loginFetch = await event.fetch(`${serverEndpoint}/auth/login/`, { + const loginFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/login`, { method: 'POST', headers: { 'X-CSRFToken': csrfToken, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Cookie: `csrftoken=${csrfToken}` }, body: JSON.stringify({ username, password - }) + }), + credentials: 'include' }); + const loginResponse = await loginFetch.json(); if (!loginFetch.ok) { // get the value of the first key in the object @@ -64,25 +57,34 @@ export const actions: Actions = { 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 - }); + const setCookieHeader = loginFetch.headers.get('Set-Cookie'); - return redirect(302, '/'); + console.log('setCookieHeader:', setCookieHeader); + + if (setCookieHeader) { + // Regular expression to match sessionid cookie and its expiry + const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/; + const match = setCookieHeader.match(sessionIdRegex); + + if (match) { + const sessionId = match[1]; + const expiryString = match[2]; + const expiryDate = new Date(expiryString); + + console.log('Session ID:', sessionId); + console.log('Expiry Date:', expiryDate); + + // Set the sessionid cookie + event.cookies.set('sessionid', sessionId, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: true, + expires: expiryDate + }); + } + } + redirect(302, '/'); } } }; diff --git a/frontend/src/routes/settings/forgot-password/+page.server.ts b/frontend/src/routes/settings/forgot-password/+page.server.ts index 82fc304..f91db59 100644 --- a/frontend/src/routes/settings/forgot-password/+page.server.ts +++ b/frontend/src/routes/settings/forgot-password/+page.server.ts @@ -1,3 +1,4 @@ +import { fetchCSRFToken } from '$lib/index.server'; import { fail, type Actions } from '@sveltejs/kit'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; @@ -13,10 +14,14 @@ export const actions: Actions = { return fail(400, { message: 'missing_email' }); } - let res = await fetch(`${endpoint}/auth/password/reset/`, { + let csrfToken = await fetchCSRFToken(); + + let res = await fetch(`${endpoint}/_allauth/browser/v1/auth/password/request`, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, + Cookie: `csrftoken=${csrfToken}` }, body: JSON.stringify({ email @@ -25,10 +30,7 @@ export const actions: Actions = { if (!res.ok) { let message = await res.json(); - - const key = Object.keys(message)[0]; - - return fail(res.status, { message: message[key] }); + return fail(res.status, message); } return { success: true }; } diff --git a/frontend/src/routes/signup/+page.server.ts b/frontend/src/routes/signup/+page.server.ts index ad24805..2541936 100644 --- a/frontend/src/routes/signup/+page.server.ts +++ b/frontend/src/routes/signup/+page.server.ts @@ -44,19 +44,23 @@ export const actions: Actions = { return fail(500, { message: 'Failed to fetch CSRF token' }); } + if (password1 !== password2) { + return fail(400, { message: 'Passwords do not match' }); + } + const tokenPromise = await csrfTokenFetch.json(); const csrfToken = tokenPromise.csrfToken; - const loginFetch = await event.fetch(`${serverEndpoint}/auth/registration/`, { + const loginFetch = await event.fetch(`${serverEndpoint}/_allauth/browser/v1/auth/signup`, { method: 'POST', headers: { 'X-CSRFToken': csrfToken, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Cookie: `csrftoken=${csrfToken}` }, body: JSON.stringify({ username: username, - password1: password1, - password2: password2, + password: password1, email: email, first_name, last_name @@ -65,31 +69,36 @@ export const actions: Actions = { const loginResponse = await loginFetch.json(); if (!loginFetch.ok) { - // get the value of the first key in the object - const firstKey = Object.keys(loginResponse)[0] || 'error'; - const error = - loginResponse[firstKey][0] || 'Failed to register user. Check your inputs and try again.'; - return fail(400, { - message: error - }); + return fail(loginFetch.status, loginResponse); } else { - const token = loginResponse.access; - const tokenFormatted = `auth=${token}`; - const refreshToken = `${loginResponse.refresh}`; - event.cookies.set('auth', tokenFormatted, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - event.cookies.set('refresh', refreshToken, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), // 1 year - path: '/' - }); + const setCookieHeader = loginFetch.headers.get('Set-Cookie'); - return redirect(302, '/'); + console.log('setCookieHeader:', setCookieHeader); + + if (setCookieHeader) { + // Regular expression to match sessionid cookie and its expiry + const sessionIdRegex = /sessionid=([^;]+).*?expires=([^;]+)/; + const match = setCookieHeader.match(sessionIdRegex); + + if (match) { + const sessionId = match[1]; + const expiryString = match[2]; + const expiryDate = new Date(expiryString); + + console.log('Session ID:', sessionId); + console.log('Expiry Date:', expiryDate); + + // Set the sessionid cookie + event.cookies.set('sessionid', sessionId, { + path: '/', + httpOnly: true, + sameSite: 'lax', + secure: true, + expires: expiryDate + }); + } + } + redirect(302, '/'); } } }; diff --git a/frontend/src/routes/worldtravel/+page.server.ts b/frontend/src/routes/worldtravel/+page.server.ts index 4fea381..fefd08d 100644 --- a/frontend/src/routes/worldtravel/+page.server.ts +++ b/frontend/src/routes/worldtravel/+page.server.ts @@ -9,11 +9,9 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { - const res = await fetch(`${endpoint}/api/countries/`, { + const res = await event.fetch(`${endpoint}/api/countries/`, { method: 'GET', - headers: { - Cookie: `${event.cookies.get('auth')}` - } + credentials: 'include' }); if (!res.ok) { console.error('Failed to fetch countries'); From f119e6fdc231afed71322b72ea9f08dd07bc0a47 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 29 Nov 2024 16:55:28 -0500 Subject: [PATCH 03/26] Refactor settings.py to remove unused authentication configurations and clean up middleware --- backend/server/main/settings.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 14d3983..d5f8e51 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -47,20 +47,16 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'rest_framework', 'rest_framework.authtoken', - # 'dj_rest_auth', 'allauth', 'allauth.account', 'allauth.headless', - # 'dj_rest_auth.registration', 'allauth.socialaccount', - 'allauth.socialaccount.providers.facebook', 'drf_yasg', 'corsheaders', 'adventures', 'worldtravel', 'users', 'django.contrib.gis', - ) MIDDLEWARE = ( @@ -84,7 +80,6 @@ CACHES = { } } - # For backwards compatibility for Django 1.8 MIDDLEWARE_CLASSES = MIDDLEWARE @@ -92,11 +87,6 @@ ROOT_URLCONF = 'main.urls' # WSGI_APPLICATION = 'demo.wsgi.application' -SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), - "REFRESH_TOKEN_LIFETIME": timedelta(days=365), -} - # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases @@ -141,8 +131,6 @@ MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' STATICFILES_DIRS = [BASE_DIR / 'static'] -# TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')] - TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -214,13 +202,8 @@ else: REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.TokenAuthentication', - 'dj_rest_auth.jwt_auth.JWTCookieAuthentication' ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', - # 'DEFAULT_PERMISSION_CLASSES': [ - # 'rest_framework.permissions.IsAuthenticated', - # ], } SWAGGER_SETTINGS = { From b86c7258e7efdb2f58a55dbf4c2777237419f95e Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 29 Nov 2024 17:51:32 -0500 Subject: [PATCH 04/26] Enhance admin security by integrating secure_admin_login from AllAuth and updating settings for new dependencies --- backend/server/adventures/admin.py | 5 +++++ backend/server/main/settings.py | 6 ++++++ backend/server/requirements.txt | 5 ++++- backend/server/users/admin.py | 5 ++++- backend/server/worldtravel/admin.py | 4 +++- 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 6160f60..1a8f7a8 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -3,6 +3,11 @@ from django.contrib import admin from django.utils.html import mark_safe from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category from worldtravel.models import Country, Region, VisitedRegion +from allauth.account.decorators import secure_admin_login + +admin.autodiscover() +admin.site.login = secure_admin_login(admin.site.login) + class AdventureAdmin(admin.ModelAdmin): diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index d5f8e51..77c3067 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -47,10 +47,13 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'rest_framework', 'rest_framework.authtoken', + "allauth_ui", 'allauth', 'allauth.account', 'allauth.headless', 'allauth.socialaccount', + "widget_tweaks", + "slippers", 'drf_yasg', 'corsheaders', 'adventures', @@ -119,6 +122,9 @@ USE_L10N = True USE_TZ = True +ALLAUTH_UI_THEME = "dark" +SILENCED_SYSTEM_CHECKS = ["slippers.E001"] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index 0458487..73b6e9a 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -13,4 +13,7 @@ whitenoise django-resized django-geojson setuptools -gunicorn==23.0.0 \ No newline at end of file +gunicorn==23.0.0 +slippers==0.6.2 +django-allauth-ui==1.5.1 +django-widget-tweaks==1.5.0 \ No newline at end of file diff --git a/backend/server/users/admin.py b/backend/server/users/admin.py index db75154..4418947 100644 --- a/backend/server/users/admin.py +++ b/backend/server/users/admin.py @@ -1,7 +1,10 @@ from django.contrib import admin - +from allauth.account.decorators import secure_admin_login from django.contrib.sessions.models import Session +admin.autodiscover() +admin.site.login = secure_admin_login(admin.site.login) + class SessionAdmin(admin.ModelAdmin): def _session_data(self, obj): return obj.get_decoded() diff --git a/backend/server/worldtravel/admin.py b/backend/server/worldtravel/admin.py index 8c38f3f..f0d74d6 100644 --- a/backend/server/worldtravel/admin.py +++ b/backend/server/worldtravel/admin.py @@ -1,3 +1,5 @@ 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) \ No newline at end of file From c65fcc255892b60f255f7fac8722b6802d201076 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 29 Nov 2024 18:20:51 -0500 Subject: [PATCH 05/26] Migrate to session based auth --- backend/server/main/settings.py | 1 + .../src/routes/collections/+page.server.ts | 58 +++++++------------ .../routes/collections/[id]/+page.server.ts | 50 ++++++---------- .../collections/archived/+page.server.ts | 6 +- frontend/src/routes/map/+page.server.ts | 5 +- frontend/src/routes/profile/+page.server.ts | 5 +- frontend/src/routes/search/+page.server.ts | 4 +- frontend/src/routes/settings/+page.server.ts | 33 +++++++---- frontend/src/routes/shared/+page.server.ts | 3 +- .../src/routes/user/[uuid]/+page.server.ts | 5 +- frontend/src/routes/users/+page.server.ts | 5 +- .../src/routes/worldtravel/+page.server.ts | 28 ++++++--- .../routes/worldtravel/[id]/+page.server.ts | 12 +++- 13 files changed, 111 insertions(+), 104 deletions(-) diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 77c3067..a185178 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -50,6 +50,7 @@ INSTALLED_APPS = ( "allauth_ui", 'allauth', 'allauth.account', + 'allauth.mfa', 'allauth.headless', 'allauth.socialaccount', "widget_tweaks", diff --git a/frontend/src/routes/collections/+page.server.ts b/frontend/src/routes/collections/+page.server.ts index e0cd851..8e2dac7 100644 --- a/frontend/src/routes/collections/+page.server.ts +++ b/frontend/src/routes/collections/+page.server.ts @@ -152,34 +152,9 @@ export const actions: Actions = { formDataToSend.append('end_date', end_date || ''); formDataToSend.append('link', link || ''); - let auth = event.cookies.get('auth'); + let sessionId = event.cookies.get('sessionid'); - if (!auth) { - const refresh = event.cookies.get('refresh'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } - - if (!auth) { + if (!sessionId) { return { status: 401, body: { message: 'Unauthorized' } @@ -199,9 +174,10 @@ export const actions: Actions = { method: 'PATCH', headers: { 'X-CSRFToken': csrfToken, - Cookie: auth + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}` }, - body: formDataToSend + body: formDataToSend, + credentials: 'include' }); if (!res.ok) { @@ -218,6 +194,10 @@ export const actions: Actions = { }, get: async (event) => { if (!event.locals.user) { + return { + status: 401, + body: { message: 'Unauthorized' } + }; } const formData = await event.request.formData(); @@ -240,19 +220,20 @@ export const actions: Actions = { let previous = null; let count = 0; - let visitedFetch = await fetch( + let collectionsFetch = await fetch( `${serverEndpoint}/api/collections/?order_by=${order_by}&order_direction=${order_direction}`, { headers: { - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, + credentials: 'include' } ); - if (!visitedFetch.ok) { + if (!collectionsFetch.ok) { console.error('Failed to fetch visited adventures'); return redirect(302, '/login'); } else { - let res = await visitedFetch.json(); + let res = await collectionsFetch.json(); let visited = res.results as Adventure[]; next = res.next; previous = res.previous; @@ -309,15 +290,16 @@ export const actions: Actions = { } const fullUrl = `${serverEndpoint}${url}`; - console.log(fullUrl); - console.log(serverEndpoint); + + let sessionId = event.cookies.get('sessionid'); try { const response = await fetch(fullUrl, { headers: { 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` - } + Cookie: `sessionid=${sessionId}` + }, + credentials: 'include' }); if (!response.ok) { diff --git a/frontend/src/routes/collections/[id]/+page.server.ts b/frontend/src/routes/collections/[id]/+page.server.ts index 80d7ef9..a7af726 100644 --- a/frontend/src/routes/collections/[id]/+page.server.ts +++ b/frontend/src/routes/collections/[id]/+page.server.ts @@ -6,9 +6,10 @@ const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { const id = event.params as { id: string }; + let sessionid = event.cookies.get('sessionid'); let request = await fetch(`${endpoint}/api/collections/${id.id}/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionid}` } }); if (!request.ok) { @@ -30,7 +31,7 @@ export const load = (async (event) => { }) satisfies PageServerLoad; import type { Actions } from '@sveltejs/kit'; -import { tryRefreshToken } from '$lib/index.server'; +import { fetchCSRFToken, tryRefreshToken } from '$lib/index.server'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -39,31 +40,6 @@ export const actions: Actions = { const id = event.params as { id: string }; const adventureId = id.id; - if (!event.locals.user) { - const refresh = event.cookies.get('refresh'); - let auth = event.cookies.get('auth'); - if (!refresh) { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - let res = await tryRefreshToken(refresh); - if (res) { - auth = res; - event.cookies.set('auth', auth, { - httpOnly: true, - sameSite: 'lax', - expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes - path: '/' - }); - } else { - return { - status: 401, - body: { message: 'Unauthorized' } - }; - } - } if (!adventureId) { return { status: 400, @@ -71,15 +47,27 @@ export const actions: Actions = { }; } + let sessionId = event.cookies.get('sessionid'); + + if (!sessionId) { + return { + status: 401, + error: new Error('Unauthorized') + }; + } + + let csrfToken = await fetchCSRFToken(); + let res = await fetch(`${serverEndpoint}/api/collections/${event.params.id}`, { method: 'DELETE', headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' - } + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + credentials: 'include' }); - console.log(res); if (!res.ok) { return { status: res.status, diff --git a/frontend/src/routes/collections/archived/+page.server.ts b/frontend/src/routes/collections/archived/+page.server.ts index c951a3a..7b0f8c5 100644 --- a/frontend/src/routes/collections/archived/+page.server.ts +++ b/frontend/src/routes/collections/archived/+page.server.ts @@ -8,13 +8,11 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { - let next = null; - let previous = null; - let count = 0; + let sessionId = event.cookies.get('sessionid'); let adventures: Adventure[] = []; let initialFetch = await fetch(`${serverEndpoint}/api/collections/archived/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!initialFetch.ok) { diff --git a/frontend/src/routes/map/+page.server.ts b/frontend/src/routes/map/+page.server.ts index 98d036c..96db704 100644 --- a/frontend/src/routes/map/+page.server.ts +++ b/frontend/src/routes/map/+page.server.ts @@ -8,15 +8,16 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { + let sessionId = event.cookies.get('sessionid'); let visitedFetch = await fetch(`${endpoint}/api/adventures/all/?include_collections=true`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); let visitedRegionsFetch = await fetch(`${endpoint}/api/visitedregion/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); diff --git a/frontend/src/routes/profile/+page.server.ts b/frontend/src/routes/profile/+page.server.ts index 2e4e39a..825a867 100644 --- a/frontend/src/routes/profile/+page.server.ts +++ b/frontend/src/routes/profile/+page.server.ts @@ -4,15 +4,16 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; export const load: PageServerLoad = async (event: RequestEvent) => { const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; - if (!event.locals.user || !event.cookies.get('auth')) { + if (!event.locals.user || !event.cookies.get('sessionid')) { return redirect(302, '/login'); } + let sessionId = event.cookies.get('sessionid'); let stats = null; let res = await event.fetch(`${endpoint}/api/stats/counts/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/src/routes/search/+page.server.ts b/frontend/src/routes/search/+page.server.ts index cf8c4f6..8ff7e23 100644 --- a/frontend/src/routes/search/+page.server.ts +++ b/frontend/src/routes/search/+page.server.ts @@ -14,12 +14,14 @@ export const load = (async (event) => { return { data: [] }; } + let sessionId = event.cookies.get('sessionid'); + let res = await fetch( `${serverEndpoint}/api/adventures/search/?query=${query}&property=${property}`, { headers: { 'Content-Type': 'application/json', - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } } ); diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts index 9b97697..0409ce7 100644 --- a/frontend/src/routes/settings/+page.server.ts +++ b/frontend/src/routes/settings/+page.server.ts @@ -2,18 +2,20 @@ import { fail, redirect, type Actions } from '@sveltejs/kit'; import type { PageServerLoad } from '../$types'; const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { User } from '$lib/types'; +import { fetchCSRFToken } from '$lib/index.server'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load: PageServerLoad = async (event) => { if (!event.locals.user) { return redirect(302, '/'); } - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/'); } let res = await fetch(`${endpoint}/auth/user/`, { headers: { - Cookie: event.cookies.get('auth') || '' + Cookie: `sessionid=${sessionId}` } }); let user = (await res.json()) as User; @@ -34,7 +36,8 @@ export const actions: Actions = { if (!event.locals.user) { return redirect(302, '/'); } - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/'); } @@ -49,7 +52,7 @@ export const actions: Actions = { const resCurrent = await fetch(`${endpoint}/auth/user/`, { headers: { - Cookie: event.cookies.get('auth') || '' + Cookie: `sessionid=${sessionId}` } }); @@ -94,10 +97,13 @@ export const actions: Actions = { } formDataToSend.append('public_profile', public_profile.toString()); + let csrfToken = await fetchCSRFToken(); + let res = await fetch(`${endpoint}/auth/user/`, { method: 'PATCH', headers: { - Cookie: event.cookies.get('auth') || '' + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken }, body: formDataToSend }); @@ -120,7 +126,8 @@ export const actions: Actions = { if (!event.locals.user) { return redirect(302, '/'); } - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/'); } console.log('changePassword'); @@ -133,10 +140,13 @@ export const actions: Actions = { return fail(400, { message: 'Passwords do not match' }); } + let csrfToken = await fetchCSRFToken(); + let res = await fetch(`${endpoint}/auth/password/change/`, { method: 'POST', headers: { - Cookie: event.cookies.get('auth') || '', + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'X-CSRFToken': csrfToken, 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -153,7 +163,8 @@ export const actions: Actions = { if (!event.locals.user) { return redirect(302, '/'); } - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/'); } const formData = await event.request.formData(); @@ -161,11 +172,13 @@ export const actions: Actions = { if (!new_email) { return fail(400, { message: 'Email is required' }); } else { + let csrfToken = await fetchCSRFToken(); let res = await fetch(`${endpoint}/auth/change-email/`, { method: 'POST', headers: { - Cookie: event.cookies.get('auth') || '', - 'Content-Type': 'application/json' + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken }, body: JSON.stringify({ new_email diff --git a/frontend/src/routes/shared/+page.server.ts b/frontend/src/routes/shared/+page.server.ts index b40dbac..d5c1337 100644 --- a/frontend/src/routes/shared/+page.server.ts +++ b/frontend/src/routes/shared/+page.server.ts @@ -8,9 +8,10 @@ export const load = (async (event) => { if (!event.locals.user) { return redirect(302, '/login'); } else { + let sessionId = event.cookies.get('sessionid'); let res = await fetch(`${serverEndpoint}/api/collections/shared/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/src/routes/user/[uuid]/+page.server.ts b/frontend/src/routes/user/[uuid]/+page.server.ts index 882082f..aa374c3 100644 --- a/frontend/src/routes/user/[uuid]/+page.server.ts +++ b/frontend/src/routes/user/[uuid]/+page.server.ts @@ -4,7 +4,8 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/login'); } const uuid = event.params.uuid; @@ -13,7 +14,7 @@ export const load = (async (event) => { } let res = await fetch(`${serverEndpoint}/auth/user/${uuid}/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/src/routes/users/+page.server.ts b/frontend/src/routes/users/+page.server.ts index 4aa6573..3fe2574 100644 --- a/frontend/src/routes/users/+page.server.ts +++ b/frontend/src/routes/users/+page.server.ts @@ -4,13 +4,14 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; export const load = (async (event) => { - if (!event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + if (!sessionId) { return redirect(302, '/login'); } const res = await fetch(`${serverEndpoint}/auth/users/`, { headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { diff --git a/frontend/src/routes/worldtravel/+page.server.ts b/frontend/src/routes/worldtravel/+page.server.ts index fefd08d..ec696bf 100644 --- a/frontend/src/routes/worldtravel/+page.server.ts +++ b/frontend/src/routes/worldtravel/+page.server.ts @@ -2,6 +2,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { Country } from '$lib/types'; import { redirect, type Actions } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; +import { fetchCSRFToken } from '$lib/index.server'; const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -11,6 +12,9 @@ export const load = (async (event) => { } else { const res = await event.fetch(`${endpoint}/api/countries/`, { method: 'GET', + headers: { + Cookie: `sessionid=${event.cookies.get('sessionid')}` + }, credentials: 'include' }); if (!res.ok) { @@ -25,8 +29,6 @@ export const load = (async (event) => { }; } } - - return {}; }) satisfies PageServerLoad; export const actions: Actions = { @@ -39,15 +41,20 @@ export const actions: Actions = { }; } - if (!event.locals.user || !event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + + if (!event.locals.user || !sessionId) { return redirect(302, '/login'); } + let csrfToken = await fetchCSRFToken(); + const res = await fetch(`${endpoint}/api/visitedregion/`, { method: 'POST', headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken }, body: JSON.stringify({ region: body.regionId }) }); @@ -73,15 +80,20 @@ export const actions: Actions = { const visitId = body.visitId as number; - if (!event.locals.user || !event.cookies.get('auth')) { + let sessionId = event.cookies.get('sessionid'); + + if (!event.locals.user || !sessionId) { return redirect(302, '/login'); } + let csrfToken = await fetchCSRFToken(); + const res = await fetch(`${endpoint}/api/visitedregion/${visitId}/`, { method: 'DELETE', headers: { - Cookie: `${event.cookies.get('auth')}`, - 'Content-Type': 'application/json' + Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken } }); diff --git a/frontend/src/routes/worldtravel/[id]/+page.server.ts b/frontend/src/routes/worldtravel/[id]/+page.server.ts index b13ad43..2b00f3c 100644 --- a/frontend/src/routes/worldtravel/[id]/+page.server.ts +++ b/frontend/src/routes/worldtravel/[id]/+page.server.ts @@ -12,10 +12,16 @@ export const load = (async (event) => { let visitedRegions: VisitedRegion[] = []; let country: Country; + let sessionId = event.cookies.get('sessionid'); + + if (!sessionId) { + return redirect(302, '/login'); + } + let res = await fetch(`${endpoint}/api/${id}/regions/`, { method: 'GET', headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { @@ -28,7 +34,7 @@ export const load = (async (event) => { res = await fetch(`${endpoint}/api/${id}/visits/`, { method: 'GET', headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { @@ -41,7 +47,7 @@ export const load = (async (event) => { res = await fetch(`${endpoint}/api/countries/${regions[0].country}/`, { method: 'GET', headers: { - Cookie: `${event.cookies.get('auth')}` + Cookie: `sessionid=${sessionId}` } }); if (!res.ok) { From 84566b8ec1f3330340f1644062f94589da736258 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 30 Nov 2024 10:24:27 -0500 Subject: [PATCH 06/26] User profile settings API and remove old Dj-Rest-Auth code --- backend/LICENSE | 27 - backend/README.md | 4 - backend/server/main/settings.py | 43 +- backend/server/main/urls.py | 54 +- backend/server/requirements.txt | 2 - backend/server/templates/base.html | 50 +- .../server/templates/email_verification.html | 8 - backend/server/templates/home.html | 7 +- backend/server/templates/login.html | 8 - backend/server/templates/logout.html | 8 - backend/server/templates/password_change.html | 39 - backend/server/templates/password_reset.html | 8 - .../templates/password_reset_confirm.html | 26 - .../templates/resend_email_verification.html | 8 - backend/server/templates/signup.html | 8 - backend/server/templates/user_details.html | 58 -- backend/server/users/serializers.py | 1 + backend/server/users/views.py | 25 +- frontend/README.md | 38 - frontend/src/locales/en.json | 828 +++++++++--------- frontend/src/routes/settings/+page.server.ts | 31 +- frontend/src/routes/settings/+page.svelte | 24 +- 22 files changed, 514 insertions(+), 791 deletions(-) delete mode 100644 backend/LICENSE delete mode 100644 backend/README.md delete mode 100644 backend/server/templates/email_verification.html delete mode 100644 backend/server/templates/login.html delete mode 100644 backend/server/templates/logout.html delete mode 100644 backend/server/templates/password_change.html delete mode 100644 backend/server/templates/password_reset.html delete mode 100644 backend/server/templates/password_reset_confirm.html delete mode 100644 backend/server/templates/resend_email_verification.html delete mode 100644 backend/server/templates/signup.html delete mode 100644 backend/server/templates/user_details.html delete mode 100644 frontend/README.md diff --git a/backend/LICENSE b/backend/LICENSE deleted file mode 100644 index 01c7bc7..0000000 --- a/backend/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -# Preface - -AdventureLog uses DjRestAuth, a Django REST Framework authentication backend for Django Rest Framework. DjRestAuth is licensed under the MIT License. - ---- - -## The MIT License (MIT) - -Copyright (c) 2014 iMerica https://github.com/iMerica/ - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 349b7be..0000000 --- a/backend/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# AdventureLog Django Backend -A demo of a possible AdventureLog 2.0 version using Django as the backend with a REST API. - -Based of django-rest-framework and dj-rest-auth. \ No newline at end of file diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index a185178..23d091f 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -11,7 +11,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os from dotenv import load_dotenv -from datetime import timedelta from os import getenv from pathlib import Path # Load environment variables from .env file @@ -35,8 +34,6 @@ DEBUG = getenv('DEBUG', 'True') == 'True' # ] ALLOWED_HOSTS = ['*'] -# Application definition - INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', @@ -50,7 +47,7 @@ INSTALLED_APPS = ( "allauth_ui", 'allauth', 'allauth.account', - 'allauth.mfa', + # 'allauth.mfa', 'allauth.headless', 'allauth.socialaccount', "widget_tweaks", @@ -108,7 +105,7 @@ DATABASES = { } } -ACCOUNT_SIGNUP_FORM_CLASS = 'users.form_overrides.CustomSignupForm' + # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ @@ -123,8 +120,7 @@ USE_L10N = True USE_TZ = True -ALLAUTH_UI_THEME = "dark" -SILENCED_SYSTEM_CHECKS = ["slippers.E001"] + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ @@ -138,6 +134,16 @@ MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' STATICFILES_DIRS = [BASE_DIR / 'static'] +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + } +} + + TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -154,22 +160,22 @@ TEMPLATES = [ }, ] +# Authentication settings + DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True' DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.') -STORAGES = { - "staticfiles": { - "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", - }, - "default": { - "BACKEND": "django.core.files.storage.FileSystemStorage", - } -} +ALLAUTH_UI_THEME = "dark" +SILENCED_SYSTEM_CHECKS = ["slippers.E001"] AUTH_USER_MODEL = 'users.CustomUser' ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter' +ACCOUNT_SIGNUP_FORM_CLASS = 'users.form_overrides.CustomSignupForm' + +SESSION_SAVE_EVERY_REQUEST = True + FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000') # HEADLESS_FRONTEND_URLS = { @@ -218,9 +224,6 @@ SWAGGER_SETTINGS = { 'LOGOUT_URL': 'logout', } -from os import getenv - - CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()] @@ -253,6 +256,4 @@ LOGGING = { }, } # https://github.com/dr5hn/countries-states-cities-database/tags -COUNTRY_REGION_JSON_VERSION = 'v2.4' - -SESSION_SAVE_EVERY_REQUEST = True \ No newline at end of file +COUNTRY_REGION_JSON_VERSION = 'v2.4' \ No newline at end of file diff --git a/backend/server/main/urls.py b/backend/server/main/urls.py index fa3456d..3e3c53f 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -3,8 +3,7 @@ from django.contrib import admin from django.views.generic import RedirectView, TemplateView from django.conf import settings from django.conf.urls.static import static -from adventures import urls as adventures -from users.views import ChangeEmailView, IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView +from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView from .views import get_csrf_token from drf_yasg.views import get_schema_view @@ -19,56 +18,19 @@ schema_view = get_schema_view( urlpatterns = [ path('api/', include('adventures.urls')), path('api/', include('worldtravel.urls')), + path("_allauth/", include("allauth.headless.urls")), - path('auth/change-email/', ChangeEmailView.as_view(), name='change_email'), path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'), path('auth/users/', PublicUserListView.as_view(), name='public-user-list'), path('auth/user//', PublicUserDetailView.as_view(), name='public-user-detail'), + path('auth/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'), - path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'), - - + path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'), path('csrf/', get_csrf_token, name='get_csrf_token'), - re_path(r'^$', TemplateView.as_view( - template_name="home.html"), name='home'), - re_path(r'^signup/$', TemplateView.as_view(template_name="signup.html"), - name='signup'), - re_path(r'^email-verification/$', - TemplateView.as_view(template_name="email_verification.html"), - name='email-verification'), - re_path(r'^login/$', TemplateView.as_view(template_name="login.html"), - name='login'), - re_path(r'^logout/$', TemplateView.as_view(template_name="logout.html"), - name='logout'), - re_path(r'^password-reset/$', - TemplateView.as_view(template_name="password_reset.html"), - name='password-reset'), - re_path(r'^password-reset/confirm/$', - TemplateView.as_view(template_name="password_reset_confirm.html"), - name='password-reset-confirm'), - - re_path(r'^user-details/$', - TemplateView.as_view(template_name="user_details.html"), - name='user-details'), - re_path(r'^password-change/$', - TemplateView.as_view(template_name="password_change.html"), - name='password-change'), - re_path(r'^resend-email-verification/$', - TemplateView.as_view( - template_name="resend_email_verification.html"), - name='resend-email-verification'), - - - # this url is used to generate email content - re_path(r'^password-reset/confirm/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$', - TemplateView.as_view(template_name="password_reset_confirm.html"), - name='password_reset_confirm'), - - re_path(r'^auth/', include('dj_rest_auth.urls')), - re_path(r'^auth/registration/', - include('dj_rest_auth.registration.urls')), -# re_path(r'^account/', include('allauth.urls')), + + path('', TemplateView.as_view(template_name='home.html')), + re_path(r'^admin/', admin.site.urls), re_path(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=True), name='profile-redirect'), @@ -78,5 +40,5 @@ urlpatterns = [ path("accounts/", include("allauth.urls")), # Include the API endpoints: - path("_allauth/", include("allauth.headless.urls")), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index 73b6e9a..0185310 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -1,7 +1,5 @@ Django==5.0.8 -dj-rest-auth @ git+https://github.com/iMerica/dj-rest-auth.git@master djangorestframework>=3.15.2 -djangorestframework-simplejwt==5.3.1 django-allauth==0.63.3 drf-yasg==1.21.4 django-cors-headers==4.4.0 diff --git a/backend/server/templates/base.html b/backend/server/templates/base.html index f8a7bd5..be712b7 100644 --- a/backend/server/templates/base.html +++ b/backend/server/templates/base.html @@ -4,8 +4,8 @@ - - + + AdventureLog API Server @@ -31,39 +31,6 @@ diff --git a/frontend/src/lib/components/AdventureLink.svelte b/frontend/src/lib/components/AdventureLink.svelte index db02e4e..9d7d094 100644 --- a/frontend/src/lib/components/AdventureLink.svelte +++ b/frontend/src/lib/components/AdventureLink.svelte @@ -1,5 +1,4 @@ + +

Adventure Calendar

+ + diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 495893a..1c9f986 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -1,27 +1,9 @@

{$t('settings.settings_page')}

@@ -160,17 +178,21 @@

{$t('settings.email_change')}

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

+ {email.email} + {email.verified ? '✅' : '❌'} + {email.primary ? '🔑' : ''} + +

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

No emails

+ {/if} +
+ +
+
+
- +
From 0272c6b076280633ec63519752b2ecdbebfad49a Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Wed, 11 Dec 2024 20:46:20 -0500 Subject: [PATCH 14/26] Refactor user admin settings and enhance email management functionality --- backend/server/adventures/admin.py | 4 +- .../src/routes/_allauth/[...path]/+server.ts | 8 +- frontend/src/routes/settings/+page.svelte | 90 +++++++++++++++---- 3 files changed, 78 insertions(+), 24 deletions(-) diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index 1a8f7a8..1beac0f 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -59,9 +59,9 @@ from users.models import CustomUser class CustomUserAdmin(UserAdmin): model = CustomUser - list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display'] + list_display = ['username', 'is_staff', 'is_active', 'image_display'] readonly_fields = ('uuid',) - search_fields = ('username', 'email') + search_fields = ('username',) fieldsets = UserAdmin.fieldsets + ( (None, {'fields': ('profile_pic', 'uuid', 'public_profile')}), ) diff --git a/frontend/src/routes/_allauth/[...path]/+server.ts b/frontend/src/routes/_allauth/[...path]/+server.ts index 9261320..92071cb 100644 --- a/frontend/src/routes/_allauth/[...path]/+server.ts +++ b/frontend/src/routes/_allauth/[...path]/+server.ts @@ -12,8 +12,8 @@ export async function GET(event) { /** @type {import('./$types').RequestHandler} */ export async function POST({ url, params, request, fetch, cookies }) { - const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; - return handleRequest(url, params, request, fetch, cookies, searchParam, true); + const searchParam = url.search ? `${url.search}` : ''; + return handleRequest(url, params, request, fetch, cookies, searchParam, false); } export async function PATCH({ url, params, request, fetch, cookies }) { @@ -22,8 +22,8 @@ export async function PATCH({ url, params, request, fetch, cookies }) { } export async function PUT({ url, params, request, fetch, cookies }) { - const searchParam = url.search ? `${url.search}&format=json` : '?format=json'; - return handleRequest(url, params, request, fetch, cookies, searchParam, true); + const searchParam = url.search ? `${url.search}` : ''; + return handleRequest(url, params, request, fetch, cookies, searchParam, false); } export async function DELETE({ url, params, request, fetch, cookies }) { diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 7b73e9b..6b55c8c 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -15,6 +15,8 @@ emails = data.props.emails; } + let new_email: string = ''; + onMount(async () => { if (browser) { const queryParams = new URLSearchParams($page.url.search); @@ -66,6 +68,38 @@ addToast('error', 'Error removing email'); } } + + 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', 'Email sent to verify'); + } else { + addToast('error', 'Error verifying email. Try again in a few minutes.'); + } + } + + 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', 'Email added'); + emails = [...emails, { email: new_email, verified: false, primary: false }]; + new_email = ''; + } else { + addToast('error', 'Error adding email'); + } + }

{$t('settings.settings_page')}

@@ -177,33 +211,52 @@

{$t('settings.email_change')}

-
+ +
{#each emails as email} -

+

{email.email} - {email.verified ? '✅' : '❌'} - {email.primary ? '🔑' : ''} - + {#if email.verified} +

Verified
+ {:else} +
Not Verified
+ {/if} + {#if email.primary} +
Primary
+ {/if} + + {#if !email.verified} + + {/if}

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

No emails

{/if}
+
-
- -
-
- -
+
+
+
+ +
+
+ +
+
@@ -213,10 +266,11 @@

{$t('adventures.visited_region_check_desc')}

+

{$t('adventures.update_visited_regions_disclaimer')}

+ -

{$t('adventures.update_visited_regions_disclaimer')}

- {$page.form?.message} + {$t($page.form.message)}
{/if} @@ -242,30 +234,30 @@

{email.email} {#if email.verified} -

Verified
+
{$t('settings.verified')}
{:else} -
Not Verified
+
{$t('settings.not_verified')}
{/if} {#if email.primary} -
Primary
+
{$t('settings.primary')}
{/if} {#if !email.verified} {$t('settings.verify')} {/if} {#if !email.primary} {$t('settings.make_primary')} {/if} {$t('adventures.remove')}

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

No emails

+

{$t('settings.no_emai_set')}

{/if} @@ -283,11 +275,26 @@ />
- +
+

Multi-factor Authentication Settings

+ +
+
+ {#if data.props.authenticators.length === 0} +

MFA not enabled

+ {/if} + {#each data.props.authenticators as authenticator} +

+ {authenticator.type} - {authenticator.created_at} +

+ {/each} +
+
+

{$t('adventures.visited_region_check')} @@ -301,12 +308,6 @@ >{$t('adventures.update_visited_regions')}

- For Debug Use: UUID={user.uuid} | Staff user: {user.is_staff} Date: Fri, 13 Dec 2024 10:48:18 -0500 Subject: [PATCH 19/26] Implement TOTP 2FA modal; add QR code generation and recovery codes management --- frontend/package.json | 2 + frontend/pnpm-lock.yaml | 159 +++++++++++++++ frontend/src/lib/components/TOTPModal.svelte | 191 +++++++++++++++++++ frontend/src/locales/en.json | 2 + frontend/src/routes/settings/+page.server.ts | 2 +- frontend/src/routes/settings/+page.svelte | 38 +++- 6 files changed, 387 insertions(+), 7 deletions(-) create mode 100644 frontend/src/lib/components/TOTPModal.svelte diff --git a/frontend/package.json b/frontend/package.json index be4a5bc..c4315d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@sveltejs/vite-plugin-svelte": "^3.1.1", "@tailwindcss/typography": "^0.5.13", "@types/node": "^22.5.4", + "@types/qrcode": "^1.5.5", "autoprefixer": "^10.4.19", "daisyui": "^4.12.6", "postcss": "^8.4.38", @@ -39,6 +40,7 @@ "type": "module", "dependencies": { "@lukulent/svelte-umami": "^0.0.3", + "qrcode": "^1.5.4", "svelte-i18n": "^4.0.1", "svelte-maplibre": "^0.9.8" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 7783f84..b9ce2ef 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@lukulent/svelte-umami': specifier: ^0.0.3 version: 0.0.3(svelte@4.2.19) + qrcode: + specifier: ^1.5.4 + version: 1.5.4 svelte-i18n: specifier: ^4.0.1 version: 4.0.1(svelte@4.2.19) @@ -51,6 +54,9 @@ importers: '@types/node': specifier: ^22.5.4 version: 22.5.4 + '@types/qrcode': + specifier: ^1.5.5 + version: 1.5.5 autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) @@ -695,6 +701,9 @@ packages: '@types/pug@2.0.10': resolution: {integrity: sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==} + '@types/qrcode@1.5.5': + resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -831,6 +840,10 @@ packages: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + caniuse-lite@1.0.30001688: resolution: {integrity: sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==} @@ -846,6 +859,9 @@ packages: resolution: {integrity: sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==} engines: {node: '>=0.10'} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} @@ -925,6 +941,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -950,6 +970,9 @@ packages: didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -1050,6 +1073,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1084,6 +1111,10 @@ packages: geojson-vt@4.0.2: 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: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -1279,6 +1310,10 @@ packages: locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1451,14 +1486,26 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 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: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} @@ -1519,6 +1566,10 @@ packages: pmtiles@3.0.6: 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: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -1585,6 +1636,11 @@ packages: protocol-buffers-schema@3.6.0: 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: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1602,6 +1658,13 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 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: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1992,6 +2055,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -2004,6 +2070,10 @@ packages: wide-align@1.1.5: 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: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2015,6 +2085,9 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -2023,6 +2096,14 @@ packages: engines: {node: '>= 14'} 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: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2533,6 +2614,10 @@ snapshots: '@types/pug@2.0.10': {} + '@types/qrcode@1.5.5': + dependencies: + '@types/node': 22.5.4 + '@types/resolve@1.20.2': {} '@types/supercluster@7.1.3': @@ -2668,6 +2753,8 @@ snapshots: camelcase-css@2.0.1: {} + camelcase@5.3.1: {} + caniuse-lite@1.0.30001688: {} chokidar@3.6.0: @@ -2692,6 +2779,12 @@ snapshots: memoizee: 0.4.17 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: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -2766,6 +2859,8 @@ snapshots: dependencies: ms: 2.1.2 + decamelize@1.2.0: {} + deepmerge@4.3.1: {} delegates@1.0.0: {} @@ -2780,6 +2875,8 @@ snapshots: didyoumean@1.2.2: {} + dijkstrajs@1.0.3: {} + dlv@1.1.3: {} earcut@2.2.4: {} @@ -2940,6 +3037,11 @@ snapshots: dependencies: 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: dependencies: locate-path: 6.0.0 @@ -2977,6 +3079,8 @@ snapshots: geojson-vt@4.0.2: {} + get-caller-file@2.0.5: {} + get-stream@6.0.1: {} get-value@2.0.6: {} @@ -3148,6 +3252,10 @@ snapshots: locate-character@3.0.0: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -3321,14 +3429,24 @@ snapshots: dependencies: mimic-fn: 2.1.0 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.0: {} parent-module@1.0.1: @@ -3382,6 +3500,8 @@ snapshots: '@types/leaflet': 1.9.12 fflate: 0.8.2 + pngjs@5.0.0: {} + postcss-import@15.1.0(postcss@8.4.38): dependencies: postcss: 8.4.38 @@ -3441,6 +3561,12 @@ snapshots: 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: {} quickselect@2.0.0: {} @@ -3459,6 +3585,10 @@ snapshots: dependencies: picomatch: 2.3.1 + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -3855,6 +3985,8 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-module@2.0.1: {} + which@1.3.1: dependencies: isexe: 2.0.0 @@ -3867,6 +3999,12 @@ snapshots: dependencies: 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: dependencies: ansi-styles: 4.3.0 @@ -3881,8 +4019,29 @@ snapshots: wrappy@1.0.2: {} + y18n@4.0.3: {} + yallist@4.0.0: {} 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: {} diff --git a/frontend/src/lib/components/TOTPModal.svelte b/frontend/src/lib/components/TOTPModal.svelte new file mode 100644 index 0000000..68b24bd --- /dev/null +++ b/frontend/src/lib/components/TOTPModal.svelte @@ -0,0 +1,191 @@ + + + + + + + diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 7c9af87..e49455b 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -211,6 +211,8 @@ "day": "Day", "itineary_by_date": "Itinerary by Date", "nothing_planned": "Nothing planned for this day. Enjoy the journey!", + "copied_to_clipboard": "Copied to clipboard!", + "copy_failed": "Copy failed", "days": "days", "activities": { "general": "General 🌍", diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts index 2337619..beb8432 100644 --- a/frontend/src/routes/settings/+page.server.ts +++ b/frontend/src/routes/settings/+page.server.ts @@ -54,7 +54,7 @@ export const load: PageServerLoad = async (event) => { } ); let mfaAuthenticatorResponse = (await mfaAuthenticatorFetch.json()) as MFAAuthenticatorResponse; - let authenticators = mfaAuthenticatorResponse.data; + let authenticators = (mfaAuthenticatorResponse.data.length > 0) as boolean; return { props: { diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index bb2d084..008a39d 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -6,6 +6,7 @@ import { onMount } from 'svelte'; import { browser } from '$app/environment'; import { t } from 'svelte-i18n'; + import TotpModal from '$lib/components/TOTPModal.svelte'; export let data; let user: User; @@ -17,6 +18,8 @@ let new_email: string = ''; + let is2FAModalOpen: boolean = false; + onMount(async () => { if (browser) { const queryParams = new URLSearchParams($page.url.search); @@ -124,8 +127,31 @@ 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', '2FA disabled'); + data.props.authenticators = false; + } else { + if (res.status == 401) { + addToast('error', 'Logout and back in to refresh your session and try again.'); + } + addToast('error', $t('settings.generic_error')); + } + } +{#if is2FAModalOpen} + (is2FAModalOpen = false)} + bind:is_enabled={data.props.authenticators} + /> +{/if} +

{$t('settings.settings_page')}

{$t('settings.account_settings')}

@@ -284,14 +310,14 @@
- {#if data.props.authenticators.length === 0} + {#if !data.props.authenticators}

MFA not enabled

+ + {:else} + {/if} - {#each data.props.authenticators as authenticator} -

- {authenticator.type} - {authenticator.created_at} -

- {/each}
From 9bf0849b920a08b7f4bf97dec4f673e4e807e55f Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Fri, 13 Dec 2024 20:21:44 -0500 Subject: [PATCH 20/26] Add multi-factor authentication (MFA) support; update localization and error handling --- backend/server/requirements.txt | 1 + frontend/src/lib/components/TOTPModal.svelte | 31 +++++-------- frontend/src/locales/de.json | 46 +++++++++++++++++-- frontend/src/locales/en.json | 20 +++++++- frontend/src/locales/es.json | 24 +++++++++- frontend/src/locales/fr.json | 46 +++++++++++++++++-- frontend/src/locales/it.json | 46 +++++++++++++++++-- frontend/src/locales/nl.json | 46 +++++++++++++++++-- frontend/src/locales/pl.json | 46 +++++++++++++++++-- frontend/src/locales/sv.json | 46 +++++++++++++++++-- frontend/src/locales/zh.json | 46 +++++++++++++++++-- frontend/src/routes/login/+page.server.ts | 6 +-- frontend/src/routes/login/+page.svelte | 2 +- frontend/src/routes/settings/+page.svelte | 22 +++++---- .../user/verify-email/[key]/+page.svelte | 9 ++-- 15 files changed, 369 insertions(+), 68 deletions(-) diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index 5e2d10a..3f6b406 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -12,6 +12,7 @@ django-resized django-geojson setuptools gunicorn==23.0.0 +qrcode==8.0 # slippers==0.6.2 # django-allauth-ui==1.5.1 # django-widget-tweaks==1.5.0 \ No newline at end of file diff --git a/frontend/src/lib/components/TOTPModal.svelte b/frontend/src/lib/components/TOTPModal.svelte index 68b24bd..77da717 100644 --- a/frontend/src/lib/components/TOTPModal.svelte +++ b/frontend/src/lib/components/TOTPModal.svelte @@ -53,18 +53,10 @@ } async function sendTotp() { - console.log('sending totp'); - - let sessionid = document.cookie - .split('; ') - .find((row) => row.startsWith('sessionid')) - ?.split('=')[1]; - const res = await fetch('/_allauth/browser/v1/account/authenticators/totp', { method: 'POST', headers: { - 'Content-Type': 'application/json', - Cookie: `sessionid=${sessionid}` + 'Content-Type': 'application/json' }, body: JSON.stringify({ code: first_code @@ -73,7 +65,7 @@ }); console.log(res); if (res.ok) { - addToast('success', '2FA enabled'); + addToast('success', $t('settings.mfa_enabled')); is_enabled = true; getRecoveryCodes(); } else { @@ -125,10 +117,10 @@
@@ -54,7 +51,7 @@ >
-
Total Adventures
+
{$t('dashboard.total_adventures')}
{stats.adventure_count}
@@ -72,32 +69,31 @@ >
-
Total Visited Regions
+
{$t('dashboard.total_visited_regions')}
{stats.visited_region_count}
-

Recent Adventures

-
- {#each recentAdventures as adventure} - - {/each} -
- - -
-
- Inspiration -
-
-

Get Inspired

-

"{inspirationQuote}"

-
- -
+ {#if recentAdventures.length > 0} +

{$t('dashboard.recent_adventures')}

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

{$t('dashboard.no_recent_adventures')}

+

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

+ {$t('map.add_adventure')} +
+ {/if}
diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 1b9dd1d..2ad898b 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -100,7 +100,9 @@ emails = [...emails, { email: new_email, verified: false, primary: false }]; new_email = ''; } else { - addToast('error', $t('settings.email_added_error')); + let error = await res.json(); + let error_code = error.errors[0].code; + addToast('error', $t(`settings.${error_code}`) || $t('settings.generic_error')); } } diff --git a/frontend/src/routes/signup/+page.server.ts b/frontend/src/routes/signup/+page.server.ts index 2541936..4a1d857 100644 --- a/frontend/src/routes/signup/+page.server.ts +++ b/frontend/src/routes/signup/+page.server.ts @@ -41,11 +41,11 @@ export const actions: Actions = { if (!csrfTokenFetch.ok) { event.locals.user = null; - return fail(500, { message: 'Failed to fetch CSRF token' }); + return fail(500, { message: 'settings.csrf_failed' }); } if (password1 !== password2) { - return fail(400, { message: 'Passwords do not match' }); + return fail(400, { message: 'settings.password_does_not_match' }); } const tokenPromise = await csrfTokenFetch.json(); @@ -69,7 +69,7 @@ export const actions: Actions = { const loginResponse = await loginFetch.json(); if (!loginFetch.ok) { - return fail(loginFetch.status, loginResponse); + return fail(loginFetch.status, { message: loginResponse.errors[0].code }); } else { const setCookieHeader = loginFetch.headers.get('Set-Cookie'); diff --git a/frontend/src/routes/signup/+page.svelte b/frontend/src/routes/signup/+page.svelte index 7278785..6643e06 100644 --- a/frontend/src/routes/signup/+page.svelte +++ b/frontend/src/routes/signup/+page.svelte @@ -94,7 +94,7 @@ {#if $page.form?.message} -
{$page.form?.message}
+
{$t($page.form?.message)}
{/if} {:else}
diff --git a/frontend/src/routes/user/verify-email/[key]/+page.server.ts b/frontend/src/routes/user/verify-email/[key]/+page.server.ts index 86f82ee..d9b641b 100644 --- a/frontend/src/routes/user/verify-email/[key]/+page.server.ts +++ b/frontend/src/routes/user/verify-email/[key]/+page.server.ts @@ -21,13 +21,14 @@ export const load = (async (event) => { body: JSON.stringify({ key: key }) }); - if (!verifyFetch.ok) { + 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 }; } - return { - verified: true - }; }) satisfies PageServerLoad; From c0fd91ef8da7e0d805cb4397780f335300fe6c4d Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 14 Dec 2024 11:30:34 -0500 Subject: [PATCH 22/26] Add emoji picker to category dropdown; implement toggle functionality and handle emoji selection --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 8 +++ .../lib/components/CategoryDropdown.svelte | 58 +++++++++++++------ 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index c4315d5..da9ceec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "type": "module", "dependencies": { "@lukulent/svelte-umami": "^0.0.3", + "emoji-picker-element": "^1.26.0", "qrcode": "^1.5.4", "svelte-i18n": "^4.0.1", "svelte-maplibre": "^0.9.8" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b9ce2ef..702ef84 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@lukulent/svelte-umami': specifier: ^0.0.3 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 @@ -985,6 +988,9 @@ packages: electron-to-chromium@1.4.810: resolution: {integrity: sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==} + emoji-picker-element@1.26.0: + resolution: {integrity: sha512-IcffFc+LNymYScmMuxOJooZulOCOACGc1Xvj+s7XeKqpc+0EoZfWrV9o4rBjEiuM7XjsgcEjD+m5DHg0aIfnnA==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2885,6 +2891,8 @@ snapshots: electron-to-chromium@1.4.810: {} + emoji-picker-element@1.26.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} diff --git a/frontend/src/lib/components/CategoryDropdown.svelte b/frontend/src/lib/components/CategoryDropdown.svelte index 8994767..e9c2631 100644 --- a/frontend/src/lib/components/CategoryDropdown.svelte +++ b/frontend/src/lib/components/CategoryDropdown.svelte @@ -14,7 +14,12 @@ num_adventures: 0 }; - let isOpen = false; + let isOpen: boolean = false; + let isEmojiPickerVisible: boolean = false; + + function toggleEmojiPicker() { + isEmojiPickerVisible = !isEmojiPickerVisible; + } function toggleDropdown() { isOpen = !isOpen; @@ -31,6 +36,10 @@ selectCategory(new_category); } + function handleEmojiSelect(event: CustomEvent) { + new_category.icon = event.detail.unicode; + } + // Close dropdown when clicking outside let dropdownRef: HTMLDivElement; @@ -46,6 +55,9 @@ document.removeEventListener('click', handleClickOutside); }; }); + onMount(async () => { + await import('emoji-picker-element'); + });
@@ -59,23 +71,35 @@
-
- - - +
+
+ + + + +
+ + {#if isEmojiPickerVisible} +
+ +
+ {/if}
+
{#each categories From 4839edde7bbfbf3e950711e752eca6f2f90f0f20 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 14 Dec 2024 13:36:15 -0500 Subject: [PATCH 23/26] Update password reset form and add more localization for error messages --- .../lib/components/CategoryDropdown.svelte | 3 +- frontend/src/locales/de.json | 10 +++++-- frontend/src/locales/en.json | 8 ++++- frontend/src/locales/es.json | 10 +++++-- frontend/src/locales/fr.json | 10 +++++-- frontend/src/locales/it.json | 10 +++++-- frontend/src/locales/nl.json | 10 +++++-- frontend/src/locales/pl.json | 10 +++++-- frontend/src/locales/sv.json | 10 +++++-- frontend/src/locales/zh.json | 10 +++++-- .../src/routes/collections/+page.server.ts | 2 +- .../routes/collections/[id]/+page.server.ts | 2 +- .../user/reset-password/[key]/+page.server.ts | 6 ++-- .../user/reset-password/[key]/+page.svelte | 30 +++++++++++-------- 14 files changed, 96 insertions(+), 35 deletions(-) diff --git a/frontend/src/lib/components/CategoryDropdown.svelte b/frontend/src/lib/components/CategoryDropdown.svelte index e9c2631..d509370 100644 --- a/frontend/src/lib/components/CategoryDropdown.svelte +++ b/frontend/src/lib/components/CategoryDropdown.svelte @@ -86,7 +86,8 @@ bind:value={new_category.icon} /> + + {#if $page.form?.message} -
- {$page.form?.message} +
+ {$t($page.form?.message)}
{/if} From 0c27f4b8a4e51cce01ba1f3af0f8d2702f37aeb3 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 14 Dec 2024 14:37:16 -0500 Subject: [PATCH 24/26] Add download adventures as ICS calendar --- backend/server/adventures/urls.py | 3 +- backend/server/adventures/views.py | 58 +++++++++++++++++++- backend/server/requirements.txt | 4 +- frontend/src/locales/de.json | 3 +- frontend/src/locales/en.json | 1 + frontend/src/locales/es.json | 3 +- frontend/src/locales/fr.json | 3 +- frontend/src/locales/it.json | 3 +- frontend/src/locales/nl.json | 3 +- frontend/src/locales/pl.json | 3 +- frontend/src/locales/sv.json | 3 +- frontend/src/locales/zh.json | 3 +- frontend/src/routes/calendar/+page.server.ts | 10 +++- frontend/src/routes/calendar/+page.svelte | 11 ++++ 14 files changed, 99 insertions(+), 12 deletions(-) diff --git a/backend/server/adventures/urls.py b/backend/server/adventures/urls.py index 9b7566d..d035522 100644 --- a/backend/server/adventures/urls.py +++ b/backend/server/adventures/urls.py @@ -1,6 +1,6 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet +from .views import AdventureViewSet, ChecklistViewSet, CollectionViewSet, NoteViewSet, StatsViewSet, GenerateDescription, ActivityTypesView, TransportationViewSet, AdventureImageViewSet, ReverseGeocodeViewSet, CategoryViewSet, IcsCalendarGeneratorViewSet router = DefaultRouter() router.register(r'adventures', AdventureViewSet, basename='adventures') @@ -14,6 +14,7 @@ router.register(r'checklists', ChecklistViewSet, basename='checklists') router.register(r'images', AdventureImageViewSet, basename='images') router.register(r'reverse-geocode', ReverseGeocodeViewSet, basename='reverse-geocode') router.register(r'categories', CategoryViewSet, basename='categories') +router.register(r'ics-calendar', IcsCalendarGeneratorViewSet, basename='ics-calendar') urlpatterns = [ diff --git a/backend/server/adventures/views.py b/backend/server/adventures/views.py index 808b9aa..acb03f0 100644 --- a/backend/server/adventures/views.py +++ b/backend/server/adventures/views.py @@ -17,6 +17,9 @@ from rest_framework.pagination import PageNumberPagination from django.shortcuts import get_object_or_404 from rest_framework import status from django.contrib.auth import get_user_model +from icalendar import Calendar, Event, vText, vCalAddress +from django.http import HttpResponse +from datetime import datetime User = get_user_model() @@ -1203,4 +1206,57 @@ class ReverseGeocodeViewSet(viewsets.ViewSet): visited_region.save() new_region_count += 1 new_regions[region.id] = region.name - return Response({"new_regions": new_region_count, "regions": new_regions}) \ No newline at end of file + return Response({"new_regions": new_region_count, "regions": new_regions}) + + +from django.http import HttpResponse +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from icalendar import Calendar, Event, vText, vCalAddress +from datetime import datetime, timedelta + +class IcsCalendarGeneratorViewSet(viewsets.ViewSet): + permission_classes = [IsAuthenticated] + + @action(detail=False, methods=['get']) + def generate(self, request): + adventures = Adventure.objects.filter(user_id=request.user) + serializer = AdventureSerializer(adventures, many=True) + user = request.user + name = f"{user.first_name} {user.last_name}" + print(serializer.data) + + cal = Calendar() + cal.add('prodid', '-//My Adventure Calendar//example.com//') + cal.add('version', '2.0') + + for adventure in serializer.data: + if adventure['visits']: + for visit in adventure['visits']: + event = Event() + event.add('summary', adventure['name']) + start_date = datetime.strptime(visit['start_date'], '%Y-%m-%d').date() + end_date = datetime.strptime(visit['end_date'], '%Y-%m-%d').date() + timedelta(days=1) if visit['end_date'] else start_date + timedelta(days=1) + event.add('dtstart', start_date) + event.add('dtend', end_date) + event.add('dtstamp', datetime.now()) + event.add('transp', 'TRANSPARENT') + event.add('class', 'PUBLIC') + event.add('created', datetime.now()) + event.add('last-modified', datetime.now()) + event.add('description', adventure['description']) + if adventure.get('location'): + event.add('location', adventure['location']) + if adventure.get('link'): + event.add('url', adventure['link']) + + organizer = vCalAddress(f'MAILTO:{user.email}') + organizer.params['cn'] = vText(name) + event.add('organizer', organizer) + + cal.add_component(event) + + response = HttpResponse(cal.to_ical(), content_type='text/calendar') + response['Content-Disposition'] = 'attachment; filename=adventures.ics' + return response diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index 3f6b406..bae189f 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -15,4 +15,6 @@ gunicorn==23.0.0 qrcode==8.0 # slippers==0.6.2 # django-allauth-ui==1.5.1 -# django-widget-tweaks==1.5.0 \ No newline at end of file +# django-widget-tweaks==1.5.0 +django-ical==1.9.2 +icalendar==6.1.0 \ No newline at end of file diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 53e620e..25e2b41 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -194,7 +194,8 @@ "adventure_calendar": "Abenteuerkalender", "emoji_picker": "Emoji-Picker", "hide": "Verstecken", - "show": "Zeigen" + "show": "Zeigen", + "download_calendar": "Kalender herunterladen" }, "home": { "desc_1": "Entdecken, planen und erkunden Sie mit Leichtigkeit", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index b7603b2..cd9b824 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -217,6 +217,7 @@ "show": "Show", "hide": "Hide", "emoji_picker": "Emoji Picker", + "download_calendar": "Download Calendar", "days": "days", "activities": { "general": "General 🌍", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 5283e35..42da49e 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -241,7 +241,8 @@ "adventure_calendar": "Calendario de aventuras", "emoji_picker": "Selector de emojis", "hide": "Esconder", - "show": "Espectáculo" + "show": "Espectáculo", + "download_calendar": "Descargar Calendario" }, "worldtravel": { "all": "Todo", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 1f58959..9a532de 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -194,7 +194,8 @@ "adventure_calendar": "Calendrier d'aventure", "emoji_picker": "Sélecteur d'émoticônes", "hide": "Cacher", - "show": "Montrer" + "show": "Montrer", + "download_calendar": "Télécharger le calendrier" }, "home": { "desc_1": "Découvrez, planifiez et explorez en toute simplicité", diff --git a/frontend/src/locales/it.json b/frontend/src/locales/it.json index 77b5498..fa0b1fb 100644 --- a/frontend/src/locales/it.json +++ b/frontend/src/locales/it.json @@ -194,7 +194,8 @@ "adventure_calendar": "Calendario delle avventure", "emoji_picker": "Selettore di emoji", "hide": "Nascondere", - "show": "Spettacolo" + "show": "Spettacolo", + "download_calendar": "Scarica Calendario" }, "home": { "desc_1": "Scopri, pianifica ed esplora con facilità", diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index 4ebd11b..90ac331 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -194,7 +194,8 @@ "adventure_calendar": "Avonturenkalender", "emoji_picker": "Emoji-kiezer", "hide": "Verbergen", - "show": "Show" + "show": "Show", + "download_calendar": "Agenda downloaden" }, "home": { "desc_1": "Ontdek, plan en verken met gemak", diff --git a/frontend/src/locales/pl.json b/frontend/src/locales/pl.json index 7e50cc8..db3e758 100644 --- a/frontend/src/locales/pl.json +++ b/frontend/src/locales/pl.json @@ -241,7 +241,8 @@ "adventure_calendar": "Kalendarz przygód", "emoji_picker": "Wybór emoji", "hide": "Ukrywać", - "show": "Pokazywać" + "show": "Pokazywać", + "download_calendar": "Pobierz Kalendarz" }, "worldtravel": { "country_list": "Lista krajów", diff --git a/frontend/src/locales/sv.json b/frontend/src/locales/sv.json index fe462f9..aae55d5 100644 --- a/frontend/src/locales/sv.json +++ b/frontend/src/locales/sv.json @@ -194,7 +194,8 @@ "adventure_calendar": "Äventyrskalender", "emoji_picker": "Emoji-väljare", "hide": "Dölja", - "show": "Visa" + "show": "Visa", + "download_calendar": "Ladda ner kalender" }, "home": { "desc_1": "Upptäck, planera och utforska med lätthet", diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json index 228f664..56cf01f 100644 --- a/frontend/src/locales/zh.json +++ b/frontend/src/locales/zh.json @@ -194,7 +194,8 @@ "adventure_calendar": "冒险日历", "emoji_picker": "表情符号选择器", "hide": "隐藏", - "show": "展示" + "show": "展示", + "download_calendar": "下载日历" }, "home": { "desc_1": "轻松发现、规划和探索", diff --git a/frontend/src/routes/calendar/+page.server.ts b/frontend/src/routes/calendar/+page.server.ts index 0305b93..6c9ede0 100644 --- a/frontend/src/routes/calendar/+page.server.ts +++ b/frontend/src/routes/calendar/+page.server.ts @@ -30,10 +30,18 @@ export const load = (async (event) => { }); }); + let icsFetch = await fetch(`${endpoint}/api/ics-calendar/generate`, { + headers: { + Cookie: `sessionid=${sessionId}` + } + }); + let ics_calendar = await icsFetch.text(); + return { props: { adventures, - dates + dates, + ics_calendar } }; }) satisfies PageServerLoad; diff --git a/frontend/src/routes/calendar/+page.svelte b/frontend/src/routes/calendar/+page.svelte index 3dc5b2c..476519d 100644 --- a/frontend/src/routes/calendar/+page.svelte +++ b/frontend/src/routes/calendar/+page.svelte @@ -14,6 +14,10 @@ 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', @@ -24,3 +28,10 @@

{$t('adventures.adventure_calendar')}

+ + + From c966b73eba4e1b934231945c1e1c723c1b74fbb2 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 14 Dec 2024 17:18:38 -0500 Subject: [PATCH 25/26] Fix formatting in README.md for better readability --- README.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9ff2821..3724be9 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,9 @@ [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/seanmorley15) -- **[Documentation](https://adventurelog.app)** -- **[Demo](https://demo.adventurelog.app)** -- **[Join the AdventureLog Community Discord Server](https://discord.gg/wRbQ9Egr8C)** - +- **[Documentation](https://adventurelog.app)** +- **[Demo](https://demo.adventurelog.app)** +- **[Join the AdventureLog Community Discord Server](https://discord.gg/wRbQ9Egr8C)** # Table of Contents @@ -59,18 +58,18 @@ Here is a summary of the configuration options available in the `docker-compose. ### Backend Container (server) -| Name | Required | Description | Default Value | -| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | -| `PGHOST` | Yes | Databse host. | db | -| `PGDATABASE` | Yes | Database. | database | -| `PGUSER` | Yes | Database user. | adventure | -| `PGPASSWORD` | Yes | Database password. | changeme123 | -| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin | -| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin | -| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com | -| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | 'http://localhost:8016' | -| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | http://localhost:8016 | -| `FRONTEND_URL` | Yes | This is the publicly accessible url to the **frontend** container. This link should be accessible for all users. Used for email generation. | 'http://localhost:8015' | +| Name | Required | Description | Default Value | +| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| `PGHOST` | Yes | Databse host. | db | +| `PGDATABASE` | Yes | Database. | database | +| `PGUSER` | Yes | Database user. | adventure | +| `PGPASSWORD` | Yes | Database password. | changeme123 | +| `DJANGO_ADMIN_USERNAME` | Yes | Default username. | admin | +| `DJANGO_ADMIN_PASSWORD` | Yes | Default password, change after inital login. | admin | +| `DJANGO_ADMIN_EMAIL` | Yes | Default user's email. | admin@example.com | +| `PUBLIC_URL` | Yes | This needs to match the outward port of the server and be accessible from where the app is used. It is used for the creation of image urls. | http://localhost:8016 | +| `CSRF_TRUSTED_ORIGINS` | Yes | Need to be changed to the orgins where you use your backend server and frontend. These values are comma seperated. | http://localhost:8016 | +| `FRONTEND_URL` | Yes | This is the publicly accessible url to the **frontend** container. This link should be accessible for all users. Used for email generation. | http://localhost:8015 | ## Running the Containers From 10dbafd1eeed5411216809c1b19477025893f421 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 14 Dec 2024 17:27:10 -0500 Subject: [PATCH 26/26] Update button label in NewTransportation component to Create --- frontend/src/lib/components/NewTransportation.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/components/NewTransportation.svelte b/frontend/src/lib/components/NewTransportation.svelte index f90fb19..e841105 100644 --- a/frontend/src/lib/components/NewTransportation.svelte +++ b/frontend/src/lib/components/NewTransportation.svelte @@ -246,7 +246,7 @@
- +