Skip to content

Commit

Permalink
Merge branch 'main' of github.com:velis74/django-project-base
Browse files Browse the repository at this point in the history
  • Loading branch information
KlemenSpruk committed Oct 13, 2023
2 parents 8c090e7 + 639645d commit ee06707
Show file tree
Hide file tree
Showing 28 changed files with 971 additions and 66 deletions.
63 changes: 51 additions & 12 deletions django_project_base/account/rest/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@
from typing import Optional

import swapper
from django.contrib.auth import get_user_model, update_session_auth_hash
from django.contrib.auth import get_user_model, login, update_session_auth_hash
from django.contrib.auth.models import AnonymousUser
from django.core.cache import cache
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import validate_email
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse, OpenApiTypes
from dynamicforms import fields as df_fields, serializers as df_serializers, viewsets as df_viewsets
from dynamicforms.action import Actions
from rest_framework import fields, serializers, status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.permissions import IsAdminUser, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_registration.api.views import change_password, logout, register, verify_registration
from rest_registration.api.views import change_password, logout, register
from rest_registration.settings import registration_settings
from social_django.models import UserSocialAuth

from django_project_base.account.rest.reset_password import ResetPasswordSerializer
Expand Down Expand Up @@ -179,15 +184,7 @@ class SuperUserChangePasswordSerializer(ChangePasswordSerializer):
return response


class VerifyRegistrationSerializer(serializers.Serializer):
user_id = fields.CharField(required=True)
timestamp = fields.IntegerField(required=True)
signature = fields.CharField(required=True)


class VerifyRegistrationViewSet(viewsets.ViewSet):
serializer_class = VerifyRegistrationSerializer()

@extend_schema(
description="Verify registration with signature. The endpoint will generate an e-mail with a confirmation URL "
"which the user should GET to confirm the account.",
Expand All @@ -199,11 +196,53 @@ class VerifyRegistrationViewSet(viewsets.ViewSet):
@action(
detail=False,
methods=["post"],
url_path="verify_registration",
url_path="verify-registration",
url_name="verify-registration",
)
def verify_registration(self, request: Request) -> Response:
return verify_registration(request._request)
if (
(flow_id := request.COOKIES.get("register-flow"))
and (code := cache.get(flow_id))
and (req_code := request.data.get("code"))
and code == req_code
and len(code)
and len(req_code)
and (user := cache.get(code))
):
user.is_active = True
user.save(update_fields=["is_active"])
login(request, user, backend="django_project_base.base.auth_backends.UsersCachingBackend")
response = Response()
response.delete_cookie("register-flow")
return response
raise ValidationError(dict(code=[_("Code invalid")]))

@action(
detail=False,
methods=["post"],
url_path="verify-registration-email-change",
url_name="verify-registration-email-change",
)
def verify_registration_change_email(self, request: Request) -> Response:
if (
(flow_id := request.COOKIES.get("register-flow"))
and (code := cache.get(flow_id))
and len(code)
and (user := cache.get(code))
):
email = request.data.get("email")
if not email:
raise ValidationError(dict(email=[_("Email invalid")]))
try:
validate_email(email)
except DjangoValidationError:
raise ValidationError(dict(email=[_("Email invalid")]))
user.email = email
user.save(update_fields=["email"])
if registration_settings.REGISTER_VERIFICATION_ENABLED:
registration_settings.REGISTER_VERIFICATION_EMAIL_SENDER(request=request, user=user)
return Response()
raise PermissionDenied


class AbstractRegisterSerializer(df_serializers.Serializer):
Expand Down
1 change: 1 addition & 0 deletions django_project_base/account/rest/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,5 @@ def create(self, request: Request, *args, **kwargs) -> Response:
and (user := get_user_model().objects.filter(pk=user_id).first())
):
UserLoginEvent(user=user).trigger(payload=request)
response.delete_cookie("register-flow")
return response
24 changes: 19 additions & 5 deletions django_project_base/account/rest/profile.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import uuid
from random import randrange

import django
Expand All @@ -11,6 +12,7 @@
from django.db.models import ForeignKey, Model, QuerySet
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse
from dynamicforms import fields
Expand All @@ -36,7 +38,10 @@
from django_project_base.account.rest.project_profiles_utils import get_project_members
from django_project_base.base.event import UserRegisteredEvent
from django_project_base.constants import NOTIFY_NEW_USER_SETTING_NAME
from django_project_base.notifications.email_notification import EMailNotification, EMailNotificationWithListOfEmails
from django_project_base.notifications.email_notification import (
EMailNotificationWithListOfEmails,
SystemEMailNotification,
)
from django_project_base.notifications.models import DjangoProjectBaseMessage
from django_project_base.permissions import BasePermissions
from django_project_base.rest.project import ProjectSerializer, ProjectViewSet
Expand Down Expand Up @@ -327,7 +332,17 @@ def list(self, request, *args, **kwargs):
permission_classes=[],
)
def register_account(self, request: Request, **kwargs):
return Response(ProfileRegisterSerializer(None, context=self.get_serializer_context()).data)
register_flow_identifier = str(uuid.uuid4())
response = Response(ProfileRegisterSerializer(None, context=self.get_serializer_context()).data)
response.set_cookie(
"register-flow",
register_flow_identifier,
max_age=settings.CONFIRMATION_CODE_TIMEOUT,
httponly=True,
samesite="Strict",
)
cache.set(register_flow_identifier, get_random_string(length=6), timeout=settings.CONFIRMATION_CODE_TIMEOUT)
return response

@extend_schema(
description="Registering new account",
Expand All @@ -342,7 +357,7 @@ def register_account(self, request: Request, **kwargs):
def create_new_account(self, request: Request, **kwargs):
# set default values
request.data["date_joined"] = datetime.datetime.now()
request.data["is_active"] = True
request.data["is_active"] = False

# call serializer to do the data processing drf way - hijack
serializer = ProfileRegisterSerializer(
Expand Down Expand Up @@ -592,7 +607,7 @@ def create(self, request, *args, **kwargs):
.first()
) and sett.python_value:
recipients = [response.data[get_pk_name(get_user_model())]]
EMailNotification(
SystemEMailNotification(
message=DjangoProjectBaseMessage(
subject=_("Your account was created for you"),
body=render_to_string(
Expand All @@ -604,7 +619,6 @@ def create(self, request, *args, **kwargs):
footer="",
content_type=DjangoProjectBaseMessage.HTML,
),
raw_recipents=recipients,
project=project.slug,
recipients=recipients,
user=self.request.user.pk,
Expand Down
30 changes: 30 additions & 0 deletions django_project_base/account/service/register_user_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext as __
from natural.date import compress
from rest_framework.request import Request

from django_project_base.notifications.email_notification import SystemEMailNotification
from django_project_base.notifications.models import DjangoProjectBaseMessage


def send_register_verification_email_notification(
request: Request,
user,
) -> None:
if (flow_id := request.COOKIES.get("register-flow")) and (code := cache.get(flow_id)):
SystemEMailNotification(
message=DjangoProjectBaseMessage(
subject=f"{__('Account confirmation for')} {request.META['HTTP_HOST']}",
body=f"{__('You or someone acting as you registered an account at')} "
f"{request.META['HTTP_HOST']}. "
f"\n\n{__('Your verification code is')}: "
f"{code} \n\n {__('Code is valid for')} {compress(settings.CONFIRMATION_CODE_TIMEOUT)}.\n\n"
f"{__('If this was not you or it was unintentional, you may safely ignore this message.')}",
footer="",
content_type=DjangoProjectBaseMessage.PLAIN_TEXT,
),
recipients=[user.pk],
user=user.pk,
).send()
cache.set(code, user, timeout=settings.CONFIRMATION_CODE_TIMEOUT)
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
from rest_registration.utils.users import get_user_verification_id

from django_project_base.account.constants import RESET_USER_PASSWORD_VERIFICATION_CODE
from django_project_base.notifications.base.enums import NotificationLevel, NotificationType as NotificationTypeDPB
from django_project_base.notifications.email_notification import EMailNotification
from django_project_base.notifications.email_notification import SystemEMailNotification
from django_project_base.notifications.models import DjangoProjectBaseMessage


Expand All @@ -31,7 +30,7 @@ def send_reset_password_verification_email(request: Request, user, send=False) -
code = get_random_string(length=6)
cache.set(code_ck, code, timeout=settings.CONFIRMATION_CODE_TIMEOUT)

EMailNotification(
SystemEMailNotification(
message=DjangoProjectBaseMessage(
subject=f"{__('Password recovery for')} {request.META['HTTP_HOST']}",
body=f"{__('You or someone acting as you requested a password reset for your account at')} "
Expand All @@ -42,11 +41,6 @@ def send_reset_password_verification_email(request: Request, user, send=False) -
footer="",
content_type=DjangoProjectBaseMessage.PLAIN_TEXT,
),
raw_recipents=[user.pk],
project=None,
persist=True,
level=NotificationLevel.INFO,
type=NotificationTypeDPB.STANDARD,
recipients=[user.pk],
user=request.user.pk,
).send()
Expand Down
120 changes: 113 additions & 7 deletions django_project_base/base/event.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import copy
import datetime
from abc import ABC, abstractmethod

import swapper
from django.conf import settings
from django.shortcuts import get_object_or_404
from rest_registration.settings import registration_settings

from django_project_base.constants import EMAIL_SENDER_ID_SETTING_NAME
from django_project_base.constants import EMAIL_SENDER_ID_SETTING_NAME, SMS_SENDER_ID_SETTING_NAME


class UserModel:
Expand Down Expand Up @@ -44,16 +46,43 @@ def trigger_changed(self, old_state=None, new_state=None, payload=None, **kwargs
super().trigger_changed(old_state, new_state, payload, **kwargs)

if new_state.name == EMAIL_SENDER_ID_SETTING_NAME:
# TODO: THIS IS NOLY FOR AWS FOR NOW
# TODO: THIS IS ONLY FOR AWS FOR NOW
from django_project_base.aws.ses import AwsSes

if not old_state:
AwsSes.add_sender_email(new_state.python_value)
return
if old_state.python_value != new_state.python_value:
AwsSes.remove_sender_email(old_state.python_value) if old_state.python_value else None
AwsSes.add_sender_email(new_state.python_value)
return
elif (old_state.python_value != new_state.python_value) or (
new_state.python_value
and new_state.pending_value
and new_state.python_pending_value != new_state.python_value
):
AwsSes.add_sender_email(new_state.pending_value)

project_settings_manager = swapper.load_model("django_project_base", "ProjectSettings").objects
for sender in set(AwsSes.list_sender_emails()) - (
set(
project_settings_manager.objects.filter(name=EMAIL_SENDER_ID_SETTING_NAME).values_list(
"value", flat=True
)
)
| set(
project_settings_manager.objects.filter(name=EMAIL_SENDER_ID_SETTING_NAME).values_list(
"pending_value", flat=True
)
)
):
AwsSes.remove_sender_email(sender)


class SmsSenderChangedEvent(ProjectSettingChangedEvent):
def trigger(self, payload=None, **kwargs):
super().trigger(payload, **kwargs)

def trigger_changed(self, old_state=None, new_state=None, payload=None, **kwargs):
super().trigger_changed(old_state, new_state, payload, **kwargs)

if new_state.name == SMS_SENDER_ID_SETTING_NAME:
pass


class UserInviteFoundEvent(BaseEvent):
Expand Down Expand Up @@ -98,6 +127,8 @@ def trigger(self, payload=None, **kwargs):
UserInviteFoundEvent(self.user).trigger(payload=invite, request=payload)
return
payload.session.pop("invite-pk", None)
if registration_settings.REGISTER_VERIFICATION_ENABLED:
registration_settings.REGISTER_VERIFICATION_EMAIL_SENDER(request=payload, user=self.user)


class UserLoginEvent(BaseEvent):
Expand All @@ -114,3 +145,78 @@ def trigger(self, payload=None, **kwargs):
UserInviteFoundEvent(self.user).trigger(payload=invite, request=payload)
return
payload.session.pop("invite-pk", None)


class ProjectSettingConfirmedEvent(BaseEvent):
def trigger_changed(self, old_state=None, new_state=None, payload=None, **kwargs):
super().trigger_changed(old_state=old_state, new_state=new_state, payload=payload, **kwargs)

def trigger(self, payload=None, **kwargs):
super().trigger(payload, **kwargs)
if not payload:
return
from django_project_base.aws.ses import AwsSes

def confirm(item):
item.value = copy.copy(item.python_pending_value)
item.pending_value = None
item.save(update_fields=["value", "pending_value"])

# not self.user Event is trigerred from management command
if payload.name == EMAIL_SENDER_ID_SETTING_NAME and (
payload.python_pending_value in AwsSes.list_verified_sender_emails() or not self.user
):
confirm(payload)
if payload.name == SMS_SENDER_ID_SETTING_NAME:
if not self.user:
confirm(payload)


class ProjectSettingActionRequiredEvent(BaseEvent):
def trigger_changed(self, old_state=None, new_state=None, payload=None, **kwargs):
super().trigger_changed(old_state=old_state, new_state=new_state, payload=payload, **kwargs)

def trigger(self, payload=None, **kwargs):
super().trigger(payload, **kwargs)
if not payload:
return

# if to := getattr(settings, "ADMINS", getattr(settings, "MANAGERS", [])):
# # TODO: SEND THIS AS SYSTEM MSG WHEN PR IS MERGED
# EMailNotificationWithListOfEmails(
# message=DjangoProjectBaseMessage(
# subject=gettext"Project settings action required"),
# body=f"{gettext('Action required for setting')} {payload.name} in project {payload.project.name}",
# footer="",
# content_type=DjangoProjectBaseMessage.HTML,
# ),
# recipients=to,
# project=None,
# user=None,
# ).send()


class ProjectSettingPendingResetEvent(BaseEvent):
def trigger_changed(self, old_state=None, new_state=None, payload=None, **kwargs):
super().trigger_changed(old_state=old_state, new_state=new_state, payload=payload, **kwargs)

def trigger(self, payload=None, **kwargs):
super().trigger(payload, **kwargs)
if not payload:
return
from django_project_base.aws.ses import AwsSes

pending_value = copy.copy(payload.python_pending_value)
payload.pending_value = None
payload.save(update_fields=["pending_value"])

if payload.name == EMAIL_SENDER_ID_SETTING_NAME:
if pending_value in AwsSes.list_sender_emails():
AwsSes.remove_sender_email(pending_value)
AwsSes.add_sender_email(payload.python_value)
if payload.python_value not in AwsSes.list_verified_sender_emails():
payload.action_required = True
payload.save(update_fields=["action_required"])
if payload.name == SMS_SENDER_ID_SETTING_NAME:
payload.action_required = True
payload.save(update_fields=["action_required"])
Loading

0 comments on commit ee06707

Please sign in to comment.