Skip to content

Commit

Permalink
Add NGO Hub login for admins
Browse files Browse the repository at this point in the history
  • Loading branch information
tudoramariei committed Sep 18, 2024
1 parent db8a05a commit 6fdbf6a
Show file tree
Hide file tree
Showing 11 changed files with 285 additions and 19 deletions.
15 changes: 12 additions & 3 deletions backend/donations/views/account_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _

from redirectioneaza.common.messaging import send_email
Expand Down Expand Up @@ -74,14 +75,22 @@ class LoginView(BaseAccountView):
template_name = "account/login.html"

def get(self, request, *args, **kwargs):
context = {"title": "Contul meu"}

# if the user is logged in just redirect
# if the user is logged in, then redirect
if request.user.is_authenticated:
if request.user.has_perm("users.can_view_old_dashboard"):
return redirect(reverse("admin-index"))
return redirect(reverse("contul-meu"))

signup_text: str = _("sign up")
signup_link: str = request.build_absolute_uri(reverse("signup"))
signup_url: str = f'<a href="{signup_link}">{signup_text}</a>'

# noinspection DjangoSafeString
context = {
"title": "Contul meu",
"signup_url": mark_safe(signup_url),
}

return render(request, self.template_name, context)

def post(self, request: HttpRequest):
Expand Down
70 changes: 70 additions & 0 deletions backend/redirectioneaza/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import environ
import sentry_sdk
from cryptography.fernet import Fernet
from django.urls import reverse_lazy
from django.templatetags.static import static
from django.utils import timezone
from localflavor.ro.ro_counties import COUNTIES_CHOICES
Expand Down Expand Up @@ -62,6 +63,7 @@
DATABASE_PORT=(str, "3306"),
# site settings
APEX_DOMAIN=(str, "redirectioneaza.ro"),
BASE_WEBSITE=(str, "https://redirectioneaza.ro"),
SITE_TITLE=(str, "redirectioneaza.ro"),
DONATIONS_LIMIT_DAY=(int, 25),
DONATIONS_LIMIT_MONTH=(int, 5),
Expand Down Expand Up @@ -124,6 +126,17 @@
AWS_SES_CONFIGURATION_SET_NAME=(str, None),
AWS_SES_AUTO_THROTTLE=(float, 0.5),
AWS_SES_REGION_ENDPOINT=(str, ""),
# Cognito
AWS_COGNITO_DOMAIN=(str, ""),
AWS_COGNITO_CLIENT_ID=(str, ""),
AWS_COGNITO_CLIENT_SECRET=(str, ""),
# NGO Hub
NGOHUB_HOME_HOST=(str, "ngohub.ro"),
NGOHUB_APP_HOST=(str, "app-staging.ngohub.ro"),
NGOHUB_API_HOST=(str, "api-staging.ngohub.ro"),
NGOHUB_API_ACCOUNT=(str, ""),
NGOHUB_API_KEY=(str, ""),
UPDATE_ORGANIZATION_METHOD=(str, "async"),
# sentry
SENTRY_DSN=(str, ""),
SENTRY_TRACES_SAMPLE_RATE=(float, 0),
Expand Down Expand Up @@ -158,6 +171,7 @@

ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
APEX_DOMAIN = env.str("APEX_DOMAIN")
BASE_WEBSITE = env.str("BASE_WEBSITE")

CSRF_HEADER_NAME = "HTTP_X_XSRF_TOKEN"
CSRF_COOKIE_NAME = "XSRF-TOKEN"
Expand Down Expand Up @@ -233,6 +247,11 @@
"storages",
"django_q",
"django_recaptcha",
# authentication
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.amazon_cognito",
# custom apps:
"donations",
"partners",
Expand All @@ -254,6 +273,12 @@
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"partners.middleware.PartnerDomainMiddleware",
"allauth.account.middleware.AccountMiddleware",
]

AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend", # this is the default
"allauth.account.auth_backends.AuthenticationBackend",
]

ROOT_URLCONF = "redirectioneaza.urls"
Expand Down Expand Up @@ -683,3 +708,48 @@

# Feature flags
ENABLE_FLAG_CONTACT = env.bool("ENABLE_FLAG_CONTACT", False)


LOGIN_URL = reverse_lazy("login")
LOGIN_REDIRECT_URL = reverse_lazy("home")
LOGOUT_REDIRECT_URL = reverse_lazy("home")

# Django Allauth settings
SOCIALACCOUNT_PROVIDERS = {
"amazon_cognito": {
"DOMAIN": "https://" + env.str("AWS_COGNITO_DOMAIN"),
"EMAIL_AUTHENTICATION": True, # TODO
"VERIFIED_EMAIL": True, # TODO
"APPS": [
{
"client_id": env.str("AWS_COGNITO_CLIENT_ID"),
"secret": env.str("AWS_COGNITO_CLIENT_SECRET"),
},
],
}
}

# Django Allauth Social Login adapter
SOCIALACCOUNT_ADAPTER = "redirectioneaza.social_adapters.UserOrgAdapter"

# Django Allauth allow only social logins
SOCIALACCOUNT_ONLY = False
SOCIALACCOUNT_ENABLED = True
ACCOUNT_EMAIL_VERIFICATION = "none"

# NGO Hub settings
NGOHUB_HOME_HOST = env("NGOHUB_HOME_HOST")
NGOHUB_HOME_BASE = f"https://{env('NGOHUB_HOME_HOST')}/"
NGOHUB_APP_BASE = f"https://{env('NGOHUB_APP_HOST')}/"
NGOHUB_API_HOST = env("NGOHUB_API_HOST")
NGOHUB_API_BASE = f"https://{NGOHUB_API_HOST}/"
NGOHUB_API_ACCOUNT = env("NGOHUB_API_ACCOUNT")
NGOHUB_API_KEY = env("NGOHUB_API_KEY")

# NGO Hub user roles
NGOHUB_ROLE_SUPER_ADMIN = "super-admin"
NGOHUB_ROLE_NGO_ADMIN = "admin"
NGOHUB_ROLE_NGO_EMPLOYEE = "employee"

# Configurations for the NGO Hub integration
UPDATE_ORGANIZATION_METHOD = env("UPDATE_ORGANIZATION_METHOD")
110 changes: 110 additions & 0 deletions backend/redirectioneaza/social_adapters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import logging
from typing import Dict

from allauth.core.exceptions import ImmediateHttpResponse
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from allauth.socialaccount.models import SocialLogin
from allauth.socialaccount.signals import social_account_added
from django.conf import settings
from django.contrib.auth.models import Group
from django.dispatch import receiver
from django.http import HttpRequest
from django.shortcuts import redirect
from django.urls import reverse
from ngohub import NGOHub
from ngohub.exceptions import HubHTTPException

from users.groups_management import RESTRICTED_ADMIN

logger = logging.getLogger(__name__)


@receiver(social_account_added)
def handle_new_login(sociallogin: SocialLogin, **kwargs) -> None:
"""
Handler for the social-account-added signal, which is sent for the initial login of a new User.
We must create a User, an Organization and schedule its data update from NGO Hub.
"""

common_user_init(sociallogin=sociallogin)


class UserOrgAdapter(DefaultSocialAccountAdapter):
"""
Authentication adapter which makes sure that each new `User` also has an `NGO`
"""

def save_user(self, request: HttpRequest, sociallogin: SocialLogin, form=None) -> settings.AUTH_USER_MODEL:
"""
Besides the default user creation, also mark this user as coming from NGO Hub,
create a blank Organization for them, and schedule a data update from NGO Hub.
"""

user: settings.AUTH_USER_MODEL = super().save_user(request, sociallogin, form)
user.is_ngohub_user = True
user.save()

try:
common_user_init(sociallogin=sociallogin)
except Exception as e:
user.delete()
return

return user


def common_user_init(sociallogin: SocialLogin) -> settings.AUTH_USER_MODEL:
user = sociallogin.user
if user.is_superuser:
return user

token: str = sociallogin.token.token

hub = NGOHub(settings.NGOHUB_API_HOST)

try:
user_profile: Dict = hub.get_profile(user_token=token)
except HubHTTPException:
user_profile = {}

user_role: str = user_profile.get("role", "")

# Check the user role from NGO Hub
if user_role == settings.NGOHUB_ROLE_SUPER_ADMIN:
# A super admin from NGO Hub will become a Django admin
user.first_name = user_profile.get("name", "")
user.save()

user.groups.add(Group.objects.get(name=RESTRICTED_ADMIN))

return None

# TODO: Implement the following roles when we have the create/update organization tasks
# elif user_role == settings.NGOHUB_ROLE_NGO_ADMIN:
# if not hub.check_user_organization_has_application(ngo_token=token, login_link=settings.BASE_WEBSITE):
# if user.is_active:
# user.is_active = False
# user.save()
#
# raise ImmediateHttpResponse(redirect(reverse("error-app-missing")))
# elif not user.is_active:
# user.is_active = True
# user.save()
#
# # Add the user to the NGO group
# user.groups.add(Group.objects.get(name=NGO_ADMIN))
#
# org = Ngo.objects.filter(user=user).first()
# if not org:
# org = create_blank_org(user)
#
# return org
#
# elif user_role == settings.NGOHUB_ROLE_NGO_EMPLOYEE:
# # Employees cannot have organizations
# raise ImmediateHttpResponse(redirect(reverse("error-user-role")))

else:
# Unknown user role
raise ImmediateHttpResponse(redirect(reverse("error-unknown-user-role")))
11 changes: 10 additions & 1 deletion backend/redirectioneaza/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin, admin as django_admin
from django.urls import path, re_path
from django.urls import include, path, re_path, reverse
from django.views.generic import RedirectView

from donations.views.account_management import (
Expand Down Expand Up @@ -140,6 +140,15 @@
re_path(r"^(?P<ngo_url>[\w-]+)/semnatura/", FormSignature.as_view(), name="ngo-twopercent-signature"),
re_path(r"^(?P<ngo_url>[\w-]+)/succes/", DonationSucces.as_view(), name="ngo-twopercent-success"),
re_path(r"^(?P<ngo_url>[\w-]+)/$", TwoPercentHandler.as_view(), name="twopercent"),
# Skip the login provider selector page and redirect to Cognito
path(
"allauth/login/",
RedirectView.as_view(
url=f'/allauth{reverse("amazon_cognito_login", urlconf="allauth.urls")}',
permanent=True,
),
),
path("allauth/", include("allauth.urls")),
]
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Expand Down
21 changes: 13 additions & 8 deletions backend/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.11
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --output-file=requirements-dev.txt --strip-extras requirements-dev.in
Expand All @@ -12,11 +12,11 @@ black==24.8.0
# via -r requirements-dev.in
blessed==1.20.0
# via -r requirements.txt
boto3==1.35.17
boto3==1.35.20
# via
# -r requirements.txt
# django-ses
botocore==1.35.17
botocore==1.35.20
# via
# -r requirements.txt
# boto3
Expand Down Expand Up @@ -57,13 +57,16 @@ cssselect2==0.7.0
django==5.1.1
# via
# -r requirements.txt
# django-allauth
# django-localflavor
# django-picklefield
# django-q2
# django-recaptcha
# django-ses
# django-storages
# django-unfold
django-allauth==64.2.1
# via -r requirements.txt
django-environ==0.11.2
# via -r requirements.txt
django-ipware==7.0.1
Expand All @@ -82,7 +85,7 @@ django-ses==4.1.1
# via -r requirements.txt
django-storages==1.14.4
# via -r requirements.txt
django-unfold==0.38.0
django-unfold==0.39.0
# via -r requirements.txt
execnet==2.1.1
# via pytest-xdist
Expand All @@ -96,7 +99,7 @@ greenlet==3.1.0
# gevent
gunicorn==23.0.0
# via -r requirements.txt
idna==3.8
idna==3.10
# via
# -r requirements.txt
# requests
Expand All @@ -119,6 +122,8 @@ markupsafe==2.1.5
# jinja2
mypy-extensions==1.0.0
# via black
ngohub==0.0.6
# via -r requirements.txt
packaging==24.1
# via
# -r requirements.txt
Expand All @@ -134,7 +139,7 @@ pillow==10.4.0
# reportlab
pip-tools==7.4.1
# via -r requirements-dev.in
platformdirs==4.3.2
platformdirs==4.3.3
# via black
pluggy==1.5.0
# via pytest
Expand Down Expand Up @@ -190,7 +195,7 @@ reportlab==4.2.2
# svglib
requests==2.32.3
# via -r requirements.txt
ruff==0.6.4
ruff==0.6.5
# via -r requirements-dev.in
s3transfer==0.10.2
# via
Expand All @@ -214,7 +219,7 @@ tinycss2==1.3.0
# -r requirements.txt
# cssselect2
# svglib
urllib3==2.2.2
urllib3==2.2.3
# via
# -r requirements.txt
# botocore
Expand Down
6 changes: 5 additions & 1 deletion backend/requirements.in
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
django~=5.1.1
django-environ~=0.11.2
django-ipware~=7.0.1
django-unfold~=0.38.0
django-unfold~=0.39.0
django-allauth~=64.2.1

# captcha
django-recaptcha~=4.0.0

# NGO Hub integration
ngohub~=0.0.6

# encrypting data
cryptography~=43.0.1

Expand Down
Loading

0 comments on commit 6fdbf6a

Please sign in to comment.