mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-23 14:59:36 +02:00
Auth Migration, Calendar and Other Misc. Fixes
This commit is contained in:
commit
148568fca4
95 changed files with 3267 additions and 2379 deletions
|
@ -3,6 +3,11 @@ from django.contrib import admin
|
|||
from django.utils.html import mark_safe
|
||||
from .models import Adventure, Checklist, ChecklistItem, Collection, Transportation, Note, AdventureImage, Visit, Category
|
||||
from worldtravel.models import Country, Region, VisitedRegion
|
||||
from allauth.account.decorators import secure_admin_login
|
||||
|
||||
admin.autodiscover()
|
||||
admin.site.login = secure_admin_login(admin.site.login)
|
||||
|
||||
|
||||
|
||||
class AdventureAdmin(admin.ModelAdmin):
|
||||
|
@ -54,9 +59,9 @@ from users.models import CustomUser
|
|||
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
model = CustomUser
|
||||
list_display = ['username', 'email', 'is_staff', 'is_active', 'image_display']
|
||||
list_display = ['username', 'is_staff', 'is_active', 'image_display']
|
||||
readonly_fields = ('uuid',)
|
||||
search_fields = ('username', 'email')
|
||||
search_fields = ('username',)
|
||||
fieldsets = UserAdmin.fieldsets + (
|
||||
(None, {'fields': ('profile_pic', 'uuid', 'public_profile')}),
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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 = [
|
||||
|
|
|
@ -17,6 +17,9 @@ from rest_framework.pagination import PageNumberPagination
|
|||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from django.contrib.auth import get_user_model
|
||||
from icalendar import Calendar, Event, vText, vCalAddress
|
||||
from django.http import HttpResponse
|
||||
from datetime import datetime
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -73,6 +76,7 @@ class AdventureViewSet(viewsets.ModelViewSet):
|
|||
return queryset.order_by(ordering)
|
||||
|
||||
def get_queryset(self):
|
||||
print(self.request.user)
|
||||
# if the user is not authenticated return only public adventures for retrieve action
|
||||
if not self.request.user.is_authenticated:
|
||||
if self.action == 'retrieve':
|
||||
|
@ -1201,4 +1205,57 @@ class ReverseGeocodeViewSet(viewsets.ViewSet):
|
|||
visited_region.save()
|
||||
new_region_count += 1
|
||||
new_regions[region.id] = region.name
|
||||
return Response({"new_regions": new_region_count, "regions": new_regions})
|
||||
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
|
||||
|
|
|
@ -11,7 +11,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
|
|||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from datetime import timedelta
|
||||
from os import getenv
|
||||
from pathlib import Path
|
||||
# Load environment variables from .env file
|
||||
|
@ -35,8 +34,6 @@ DEBUG = getenv('DEBUG', 'True') == 'True'
|
|||
# ]
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
|
@ -47,19 +44,19 @@ INSTALLED_APPS = (
|
|||
'django.contrib.sites',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'dj_rest_auth',
|
||||
'allauth',
|
||||
'allauth.account',
|
||||
'dj_rest_auth.registration',
|
||||
'allauth.mfa',
|
||||
'allauth.headless',
|
||||
'allauth.socialaccount',
|
||||
'allauth.socialaccount.providers.facebook',
|
||||
# "widget_tweaks",
|
||||
# "slippers",
|
||||
'drf_yasg',
|
||||
'corsheaders',
|
||||
'adventures',
|
||||
'worldtravel',
|
||||
'users',
|
||||
'django.contrib.gis',
|
||||
|
||||
)
|
||||
|
||||
MIDDLEWARE = (
|
||||
|
@ -83,7 +80,6 @@ CACHES = {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
# For backwards compatibility for Django 1.8
|
||||
MIDDLEWARE_CLASSES = MIDDLEWARE
|
||||
|
||||
|
@ -91,11 +87,6 @@ ROOT_URLCONF = 'main.urls'
|
|||
|
||||
# WSGI_APPLICATION = 'demo.wsgi.application'
|
||||
|
||||
SIMPLE_JWT = {
|
||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
|
||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=365),
|
||||
}
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
|
||||
|
@ -114,6 +105,7 @@ DATABASES = {
|
|||
}
|
||||
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||
|
||||
|
@ -127,6 +119,8 @@ USE_L10N = True
|
|||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
|
||||
|
@ -139,7 +133,15 @@ MEDIA_URL = '/media/'
|
|||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
STATICFILES_DIRS = [BASE_DIR / 'static']
|
||||
|
||||
# TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')]
|
||||
STORAGES = {
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
|
@ -157,32 +159,34 @@ TEMPLATES = [
|
|||
},
|
||||
]
|
||||
|
||||
REST_AUTH = {
|
||||
'SESSION_LOGIN': True,
|
||||
'USE_JWT': True,
|
||||
'JWT_AUTH_COOKIE': 'auth',
|
||||
'JWT_AUTH_HTTPONLY': False,
|
||||
'REGISTER_SERIALIZER': 'users.serializers.RegisterSerializer',
|
||||
'USER_DETAILS_SERIALIZER': 'users.serializers.CustomUserDetailsSerializer',
|
||||
'PASSWORD_RESET_SERIALIZER': 'users.serializers.MyPasswordResetSerializer'
|
||||
}
|
||||
# Authentication settings
|
||||
|
||||
DISABLE_REGISTRATION = getenv('DISABLE_REGISTRATION', 'False') == 'True'
|
||||
DISABLE_REGISTRATION_MESSAGE = getenv('DISABLE_REGISTRATION_MESSAGE', 'Registration is disabled. Please contact the administrator if you need an account.')
|
||||
|
||||
STORAGES = {
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
}
|
||||
}
|
||||
ALLAUTH_UI_THEME = "dark"
|
||||
SILENCED_SYSTEM_CHECKS = ["slippers.E001"]
|
||||
|
||||
AUTH_USER_MODEL = 'users.CustomUser'
|
||||
|
||||
ACCOUNT_ADAPTER = 'users.adapters.NoNewUsersAccountAdapter'
|
||||
|
||||
ACCOUNT_SIGNUP_FORM_CLASS = 'users.form_overrides.CustomSignupForm'
|
||||
|
||||
SESSION_SAVE_EVERY_REQUEST = True
|
||||
|
||||
FRONTEND_URL = getenv('FRONTEND_URL', 'http://localhost:3000')
|
||||
|
||||
HEADLESS_FRONTEND_URLS = {
|
||||
"account_confirm_email": f"{FRONTEND_URL}/user/verify-email/{{key}}",
|
||||
"account_reset_password": f"{FRONTEND_URL}/user/reset-password",
|
||||
"account_reset_password_from_key": f"{FRONTEND_URL}/user/reset-password/{{key}}",
|
||||
"account_signup": f"{FRONTEND_URL}/signup",
|
||||
# Fallback in case the state containing the `next` URL is lost and the handshake
|
||||
# with the third-party provider fails.
|
||||
"socialaccount_login_error": f"{FRONTEND_URL}/account/provider/callback",
|
||||
}
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
SITE_ID = 1
|
||||
ACCOUNT_EMAIL_REQUIRED = True
|
||||
|
@ -214,13 +218,8 @@ else:
|
|||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'dj_rest_auth.jwt_auth.JWTCookieAuthentication'
|
||||
),
|
||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
||||
# 'DEFAULT_PERMISSION_CLASSES': [
|
||||
# 'rest_framework.permissions.IsAuthenticated',
|
||||
# ],
|
||||
}
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
|
@ -228,12 +227,11 @@ SWAGGER_SETTINGS = {
|
|||
'LOGOUT_URL': 'logout',
|
||||
}
|
||||
|
||||
# For demo purposes only. Use a white list in the real world.
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
|
||||
|
||||
from os import getenv
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [origin.strip() for origin in getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost').split(',') if origin.strip()]
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
LOGGING = {
|
||||
|
@ -260,6 +258,5 @@ LOGGING = {
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
# https://github.com/dr5hn/countries-states-cities-database/tags
|
||||
COUNTRY_REGION_JSON_VERSION = 'v2.4'
|
|
@ -3,8 +3,7 @@ from django.contrib import admin
|
|||
from django.views.generic import RedirectView, TemplateView
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from adventures import urls as adventures
|
||||
from users.views import ChangeEmailView, IsRegistrationDisabled, PublicUserListView, PublicUserDetailView
|
||||
from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView
|
||||
from .views import get_csrf_token
|
||||
from drf_yasg.views import get_schema_view
|
||||
|
||||
|
@ -19,56 +18,27 @@ schema_view = get_schema_view(
|
|||
urlpatterns = [
|
||||
path('api/', include('adventures.urls')),
|
||||
path('api/', include('worldtravel.urls')),
|
||||
path("_allauth/", include("allauth.headless.urls")),
|
||||
|
||||
path('auth/change-email/', ChangeEmailView.as_view(), name='change_email'),
|
||||
path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'),
|
||||
path('auth/users/', PublicUserListView.as_view(), name='public-user-list'),
|
||||
path('auth/user/<uuid:user_id>/', PublicUserDetailView.as_view(), name='public-user-detail'),
|
||||
path('auth/update-user/', UpdateUserMetadataView.as_view(), name='update-user-metadata'),
|
||||
|
||||
path('auth/user-metadata/', UserMetadataView.as_view(), name='user-metadata'),
|
||||
|
||||
path('csrf/', get_csrf_token, name='get_csrf_token'),
|
||||
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<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,32})/$',
|
||||
TemplateView.as_view(template_name="password_reset_confirm.html"),
|
||||
name='password_reset_confirm'),
|
||||
|
||||
re_path(r'^auth/', include('dj_rest_auth.urls')),
|
||||
re_path(r'^auth/registration/',
|
||||
include('dj_rest_auth.registration.urls')),
|
||||
re_path(r'^account/', include('allauth.urls')),
|
||||
|
||||
path('', TemplateView.as_view(template_name='home.html')),
|
||||
|
||||
re_path(r'^admin/', admin.site.urls),
|
||||
re_path(r'^accounts/profile/$', RedirectView.as_view(url='/',
|
||||
permanent=True), name='profile-redirect'),
|
||||
re_path(r'^docs/$', schema_view.with_ui('swagger',
|
||||
cache_timeout=0), name='api_docs'),
|
||||
# path('auth/account-confirm-email/', VerifyEmailView.as_view(), name='account_email_verification_sent'),
|
||||
path("accounts/", include("allauth.urls")),
|
||||
|
||||
# Include the API endpoints:
|
||||
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
Django==5.0.8
|
||||
dj-rest-auth @ git+https://github.com/iMerica/dj-rest-auth.git@master
|
||||
djangorestframework>=3.15.2
|
||||
djangorestframework-simplejwt==5.3.1
|
||||
django-allauth==0.63.3
|
||||
drf-yasg==1.21.4
|
||||
django-cors-headers==4.4.0
|
||||
|
@ -13,4 +11,10 @@ whitenoise
|
|||
django-resized
|
||||
django-geojson
|
||||
setuptools
|
||||
gunicorn==23.0.0
|
||||
gunicorn==23.0.0
|
||||
qrcode==8.0
|
||||
# slippers==0.6.2
|
||||
# django-allauth-ui==1.5.1
|
||||
# django-widget-tweaks==1.5.0
|
||||
django-ical==1.9.2
|
||||
icalendar==6.1.0
|
|
@ -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 %}
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Django-dj-rest-auth demo" />
|
||||
<meta name="author" content="iMerica, Inc." />
|
||||
<meta name="description" content="AdventureLog Server" />
|
||||
<meta name="author" content="Sean Morley" />
|
||||
|
||||
<title>AdventureLog API Server</title>
|
||||
|
||||
|
@ -31,39 +31,6 @@
|
|||
<body role="document">
|
||||
<div class="navbar navbar-inverse" role="navigation">
|
||||
<div class="container">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"
|
||||
>API endpoints <span class="caret"></span
|
||||
></a>
|
||||
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<!-- these pages don't require user token -->
|
||||
<li><a href="{% url 'signup' %}">Signup</a></li>
|
||||
<li>
|
||||
<a href="{% url 'email-verification' %}">E-mail verification</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'resend-email-verification' %}"
|
||||
>Resend E-mail verification</a
|
||||
>
|
||||
</li>
|
||||
<li><a href="{% url 'login' %}">Login</a></li>
|
||||
<li><a href="{% url 'password-reset' %}">Password Reset</a></li>
|
||||
<li>
|
||||
<a href="{% url 'password-reset-confirm' %}"
|
||||
>Password Reset Confirm</a
|
||||
>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<!-- these pages require user token -->
|
||||
<li><a href="{% url 'user-details' %}">User details</a></li>
|
||||
<li><a href="{% url 'logout' %}">Logout</a></li>
|
||||
<li><a href="{% url 'password-change' %}">Password change</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="navbar-header">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -80,20 +47,19 @@
|
|||
</div>
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="active"><a href="/">Demo</a></li>
|
||||
<li class="active"><a href="/">Server Home</a></li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="http://dj-rest-auth.readthedocs.org/en/latest/"
|
||||
<a target="_blank" href="http://adventurelog.app"
|
||||
>Documentation</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a target="_blank" href="https://github.com/iMerica/dj-rest-auth"
|
||||
>Source code</a
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/seanmorley15/AdventureLog"
|
||||
>Source Code</a
|
||||
>
|
||||
</li>
|
||||
<li><a target="_blank" href="{% url 'api_docs' %}">API Docs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--/.nav-collapse -->
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>E-mail verification</h3><hr/>
|
||||
{% include "fragments/email_verification_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -4,7 +4,12 @@
|
|||
<h1>AdventureLog API Server</h1>
|
||||
<p>
|
||||
<a class="btn btn-primary btn-lg" href="/admin" role="button">Admin Site</a>
|
||||
<a class="btn btn-secondary btn-lg" href="/docs" role="button">API Docs</a>
|
||||
<a
|
||||
class="btn btn-secondary btn-lg"
|
||||
href="/accounts/password/change"
|
||||
role="button"
|
||||
>Account Managment</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Login</h3><hr/>
|
||||
{% include "fragments/login_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Logout</h3><hr/>
|
||||
{% include "fragments/logout_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,39 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<label for="token" class="col-sm-2 control-label">User Token</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
|
||||
<p class="help-block">Token received after login</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h3>Update User Details</h3><hr/>
|
||||
{% include "fragments/password_change_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script type="text/javascript">
|
||||
$().ready(function(){
|
||||
$('form button[type=submit]').click(function(){
|
||||
var token = $('input[name=token]').val();
|
||||
var form = $('form');
|
||||
$.ajax({
|
||||
url: form.attr('action'),
|
||||
data: $('form').serialize(),
|
||||
type: "POST",
|
||||
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);}
|
||||
}).fail(function(data){error_response(data);})
|
||||
.done(function(data){susccess_response(data);});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Password reset</h3><hr/>
|
||||
{% include "fragments/password_reset_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,26 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Password reset confirmation</h3><hr/>
|
||||
{% include "fragments/password_reset_confirm_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% block script %}
|
||||
<script type="text/javascript">
|
||||
var url_elements = window.location.pathname.split('/');
|
||||
if (url_elements.length == 6){
|
||||
var uid = url_elements[url_elements.length - 3];
|
||||
if (uid !== undefined){
|
||||
$('input[name=uid]').val(uid);
|
||||
}
|
||||
var token = url_elements[url_elements.length - 2];
|
||||
if (token !== undefined){
|
||||
$('input[name=token]').val(token);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Resend E-mail verification</h3><hr/>
|
||||
{% include "fragments/resend_email_verification_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h3>Signup</h3><hr/>
|
||||
{% include "fragments/signup_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,58 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h3>Retrieve User Details</h3><hr/>
|
||||
<div class="form-group">
|
||||
<label for="token" class="col-sm-2 control-label">User Token</label>
|
||||
<div class="col-sm-4">
|
||||
<input name="token" type="text" class="form-control" id="token" placeholder="Token">
|
||||
<p class="help-block">Token received after login</p>
|
||||
</div>
|
||||
<button id="get-user-details" class="btn btn-primary">GET user details</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h3>Update User Details</h3><hr/>
|
||||
{% include "fragments/user_details_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script type="text/javascript">
|
||||
$().ready(function(){
|
||||
$('#get-user-details').click(function(){
|
||||
var token = $('input[name=token]').val();
|
||||
|
||||
$.ajax({
|
||||
url: "{% url 'rest_user_details' %}",
|
||||
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);},
|
||||
type: "GET",
|
||||
success: function(data) {
|
||||
$('input[name=username]').val(data.username);
|
||||
$('input[name=email]').val(data.email);
|
||||
$('input[name=first_name]').val(data.first_name);
|
||||
$('input[name=last_name]').val(data.last_name);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('form button[type=submit]').click(function(){
|
||||
var token = $('input[name=token]').val();
|
||||
var form = $('form');
|
||||
$.ajax({
|
||||
url: form.attr('action'),
|
||||
data: $('form').serialize(),
|
||||
type: "PUT",
|
||||
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Token '+token);}
|
||||
}).fail(function(data){error_response(data);})
|
||||
.done(function(data){susccess_response(data);});
|
||||
return false;
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
10
backend/server/users/adapters.py
Normal file
10
backend/server/users/adapters.py
Normal 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
|
|
@ -1,3 +1,13 @@
|
|||
from django.contrib import admin
|
||||
from allauth.account.decorators import secure_admin_login
|
||||
from django.contrib.sessions.models import Session
|
||||
|
||||
# Register your models here
|
||||
admin.autodiscover()
|
||||
admin.site.login = secure_admin_login(admin.site.login)
|
||||
|
||||
class SessionAdmin(admin.ModelAdmin):
|
||||
def _session_data(self, obj):
|
||||
return obj.get_decoded()
|
||||
list_display = ['session_key', '_session_data', 'expire_date']
|
||||
|
||||
admin.site.register(Session, SessionAdmin)
|
17
backend/server/users/form_overrides.py
Normal file
17
backend/server/users/form_overrides.py
Normal 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
|
|
@ -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
|
|
@ -1,26 +1,14 @@
|
|||
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 dj_rest_auth.serializers import PasswordResetSerializer
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from adventures.models import Collection
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
try:
|
||||
from allauth.account import app_settings as allauth_account_settings
|
||||
from allauth.account.adapter import get_adapter
|
||||
from allauth.account.utils import setup_user_email
|
||||
from allauth.socialaccount.models import EmailAddress
|
||||
from allauth.utils import get_username_max_length
|
||||
except ImportError:
|
||||
raise ImportError('allauth needs to be added to INSTALLED_APPS.')
|
||||
|
||||
class ChangeEmailSerializer(serializers.Serializer):
|
||||
new_email = serializers.EmailField(required=True)
|
||||
|
@ -32,104 +20,20 @@ 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
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
UserModel = get_user_model()
|
||||
from dj_rest_auth.serializers import UserDetailsSerializer
|
||||
# from dj_rest_auth.serializers import UserDetailsSerializer
|
||||
from .models import CustomUser
|
||||
|
||||
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
|
||||
|
@ -163,9 +67,9 @@ class UserDetailsSerializer(serializers.ModelSerializer):
|
|||
if hasattr(UserModel, 'public_profile'):
|
||||
extra_fields.append('public_profile')
|
||||
|
||||
class Meta(UserDetailsSerializer.Meta):
|
||||
class Meta:
|
||||
model = CustomUser
|
||||
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
|
||||
fields = ('profile_pic', 'uuid', 'public_profile', 'email', 'date_joined', 'is_staff', 'is_superuser', 'is_active', 'pk')
|
||||
|
||||
model = UserModel
|
||||
fields = ('pk', *extra_fields)
|
||||
|
@ -192,6 +96,7 @@ class CustomUserDetailsSerializer(UserDetailsSerializer):
|
|||
class Meta(UserDetailsSerializer.Meta):
|
||||
model = CustomUser
|
||||
fields = UserDetailsSerializer.Meta.fields + ('profile_pic', 'uuid', 'public_profile')
|
||||
read_only_fields = UserDetailsSerializer.Meta.read_only_fields + ('uuid',)
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
|
@ -203,13 +108,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
|
|
@ -83,3 +83,41 @@ 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)
|
||||
|
||||
class UpdateUserMetadataView(APIView):
|
||||
"""
|
||||
Update user metadata using fields from the PublicUserSerializer.
|
||||
Using patch opposed to put allows for partial updates, covers the case where it checks the username and says it's already taken. Duplicate uesrname values should not be included in the request to avoid this.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=PublicUserSerializer,
|
||||
responses={
|
||||
200: openapi.Response('User metadata updated'),
|
||||
400: 'Bad Request'
|
||||
},
|
||||
operation_description="Update user metadata."
|
||||
)
|
||||
def patch(self, request):
|
||||
user = request.user
|
||||
serializer = PublicUserSerializer(user, data=request.data, partial=True, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue