1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-19 21:09:37 +02:00

Initial migration to new session based auth system with AllAuth

This commit is contained in:
Sean Morley 2024-11-29 14:41:13 -05:00
parent 7defdac3a8
commit 9bc20be70e
24 changed files with 313 additions and 773 deletions

View file

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

View file

@ -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':

View file

@ -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'
SESSION_SAVE_EVERY_REQUEST = True

View file

@ -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/<uuid:user_id>/', 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)

View file

@ -1,13 +0,0 @@
{% extends "account/email/base_message.txt" %}
{% load i18n %}
{% block content %}{% autoescape off %}{% blocktrans %}You're receiving this email because you or someone else has requested a password reset for your user account.
It can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %}
{{ frontend_url }}/settings/forgot-password/confirm?token={{ temp_key }}&uid={{ user_pk }}
{% if username %}
{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}{% endif %}{% endautoescape %}{% endblock content %}

View file

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

View file

@ -1,3 +1,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)

View file

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

View file

@ -1,55 +1,17 @@
from allauth.account.utils import (filter_users_by_email, user_pk_to_url_str, user_username)
from 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

View file

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

View file

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

View file

@ -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;
event.cookies.delete('sessionid', { path: '/' });
return await resolve(event);
}
const refreshFetch = await event.fetch(`${serverEndpoint}/auth/token/refresh/`, {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ refresh: refreshCookie })
});
if (refreshFetch.ok) {
const refresh = await refreshFetch.json();
event.cookies.set('auth', 'auth=' + refresh.access, {
httpOnly: true,
sameSite: 'lax',
expires: new Date(Date.now() + 60 * 60 * 1000), // 60 minutes
path: '/'
});
userFetch = await event.fetch(`${serverEndpoint}/auth/user/`, {
headers: {
'X-CSRFToken': csrfToken,
Cookie: `auth=${refresh.access}`
}
});
}
}
}
if (userFetch.ok) {
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);

View file

@ -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) {

View file

@ -444,6 +444,7 @@
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(adventure)
});
let data = await res.json();

View file

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

View file

@ -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) {

View file

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

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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