Skip to content

Commit

Permalink
Add login form page and logout page
Browse files Browse the repository at this point in the history
- Style login page and authorize page
- Style base page for django-allauth
  • Loading branch information
tnagorra committed Jan 15, 2025
1 parent c0f81af commit 77e3526
Show file tree
Hide file tree
Showing 9 changed files with 531 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.11.10
3.11
42 changes: 42 additions & 0 deletions api/forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

from .logger import logger
from .models import Action, ActionOrg, ActionType


Expand All @@ -11,3 +15,41 @@ class ActionForm(forms.ModelForm):
class Meta:
model = Action
fields = "__all__"


class LoginForm(forms.Form):
email = forms.CharField(label=_("email"), required=True)
password = forms.CharField(
label=_("password"),
widget=forms.PasswordInput(),
required=True,
)

# FIXME: We need to refactor this code
def get_user(self, username, password):
if "ifrc" in password.lower() or "redcross" in password.lower():
logger.warning("User should be warned to use a stronger password.")

if username is None or password is None:
raise ValidationError("Should not happen. Frontend prevents login without username/password")

user = authenticate(username=username, password=password)
if user is None and User.objects.filter(email=username).count() > 1:
users = User.objects.filter(email=username, is_active=True)
# FIXME: Use users.exists()
if users:
# We get the first one if there are still multiple available is_active:
user = authenticate(username=users[0].username, password=password)

return user

def clean(self):
cleaned_data = super().clean()
email = cleaned_data.get("email")
password = cleaned_data.get("password")
user = self.get_user(email, password)
if not user:
raise ValidationError("Invalid credentials.")

cleaned_data["user"] = user
return cleaned_data
68 changes: 68 additions & 0 deletions api/templates/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{% extends "oauth2_provider/base.html" %}
{% block title %}
IFRC GO | SSO Login
{% endblock %}
{% block css %}
<style>
form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}

form > div {
display: flex;
flex-direction: column;
}

form label {
text-transform: capitalize;
font-size: 0.875rem;
}

form input[type="password"],
form input[type="text"] {
border: unset;
background-color: rgba(0, 0, 0, 0.1);
padding: 0.5rem;
border-radius: 0.25rem;
}

.block-center {
display: flex;
flex-direction: column;
gap: 1rem;
}

</style>
{% endblock %}
{% block content %}
{% if request.user.is_authenticated %}
<div class="block-center">
<h2>GO SSO</h2>
<div>{% firstof request.user.get_full_name request.user.username %}</div>
<form method="post" action="{{ logout_url }}">
{% csrf_token %}
<div class="control-group">
<button class='btn-primary' type="submit">
Logout
</button>
</div>
</form>
</div>
{% else %}
<div class="block-center">
<h2>GO SSO Login</h2>
<form method="post">
{% csrf_token %}
{{ form.as_div }}
<div class="control-group">
<button class='btn-primary' type="submit">
Login
</button>
</div>
</form>
</div>
{% endif %}
{% endblock %}
127 changes: 127 additions & 0 deletions api/templates/oauth2_provider/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}{% endblock title %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
{% block css %}
{% endblock css %}

<style>
:root {
font-family: 'Poppins', sans-serif;
}
body, html {
margin: 0;
padding: 0;
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
}
p {
margin: 0;
}
* { box-sizing: border-box }

body {
background-color: #f0f0f0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.container {
display: flex;
flex-direction: column;
gap: 1.5rem;
align-items: center;
justify-content: center;
animation: slide-up .5s .3s ease-in forwards;
opacity: 0;
width: 100%;
max-width: 30rem;
}

.block-center {
width: 100%;
background-color: #ffffff;
padding: 1.5rem;
border-radius: 0.5rem;
box-shadow: 0 5px 3px -2px rgba(0, 0, 0, 0.2);
}

@keyframes slide-up {
0% {
opacity: 0;
transform: translateY(10px);
}

100% {
opacity: 1;
transform: translateY(0);
}
}

.block-center form {
display: flex;
flex-direction: column;
gap: 1rem;
}

ul {
margin: 0;
}

button,
input[type='submit'] {
border: unset;
line-height: 1;
padding: 0.5rem 1rem;
border-radius: 1.5rem;
border: 1px solid rgba(0, 0, 0, 0.1);
cursor: pointer;
width: fit-content;
}

.btn-primary {
background-color: #e04656;
color: #fff;
border-color: #e04656;
}

.control-group {
display: flex;
flex-direction: row;
justify-content: flex-end;
}

.control-group .controls {
display: flex;
gap: 0.5rem;
}

#go-logo {
height: 3rem;
}
</style>
</head>

<body>

<div class="container">
<img id="go-logo" alt="IFRC GO" src="{% static 'images/logo/go-logo-2020-6cdc2b0c.svg' %}" />
{% block content %}
{% endblock content %}
</div>
</body>
</html>
69 changes: 69 additions & 0 deletions api/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import json
from datetime import datetime, timedelta

from urllib.parse import urlparse
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme
from django.shortcuts import redirect
from django.contrib.auth import login, logout
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
Expand All @@ -18,7 +23,10 @@
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework.views import APIView
from django.views.generic.edit import FormView
from django.urls import reverse_lazy

from api.forms import LoginForm
from api.models import Country, District, Region
from api.serializers import (
AggregateByDtypeSerializer,
Expand Down Expand Up @@ -802,10 +810,12 @@ def get(self, request):
class GetAuthToken(APIView):
permission_classes = []

# FIXME: We need to refactor this block
def post(self, request):
username = request.data.get("username", None)
password = request.data.get("password", None)

# FIXME: Remove this
if "ifrc" in password.lower() or "redcross" in password.lower():
logger.warning("User should be warned to use a stronger password.")

Expand All @@ -816,6 +826,7 @@ def post(self, request):
user = authenticate(username=username, password=password)
if user is None and User.objects.filter(email=username).count() > 1:
users = User.objects.filter(email=username, is_active=True)
# FIXME: Use users.exists()
if users:
# We get the first one if there are still multiple available is_active:
user = authenticate(username=users[0].username, password=password)
Expand Down Expand Up @@ -994,3 +1005,61 @@ def get(self, request, *args, **kwargs):
class DummyExceptionError(View):
def get(self, request, *args, **kwargs):
raise Exception("Dev raised exception!")


class LoginFormView(FormView):
template_name = "login.html"
form_class = LoginForm

def is_safe_url(self, url):
# get_host already validates the given host, so no need to check it again
allowed_hosts = {self.request.get_host()} | set(settings.ALLOWED_HOSTS)

if "*" in allowed_hosts:
parsed_host = urlparse(url).netloc
allowed_host = {parsed_host} if parsed_host else None
return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_host)

return url_has_allowed_host_and_scheme(url, allowed_hosts=allowed_hosts)

def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
context_data["logout_url"] = reverse("go_logout")
return context_data

def form_valid(self, form):
user = form.cleaned_data["user"]

# Determining the client IP is not always straightforward:
clientIP = ""
# if 'REMOTE_ADDR' in request.META: clientIP += 'R' + request.META['REMOTE_ADDR']
# if 'HTTP_CLIENT_IP' in request.META: clientIP += 'C' + request.META['HTTP_CLIENT_IP']
# if 'HTTP_X_FORWARDED' in request.META: clientIP += 'x' + request.META['HTTP_X_FORWARDED']
# if 'HTTP_FORWARDED_FOR' in request.META: clientIP += 'F' + request.META['HTTP_FORWARDED_FOR']
# if 'HTTP_FORWARDED' in request.META: clientIP += 'f' + request.META['HTTP_FORWARDED']
if "HTTP_X_FORWARDED_FOR" in self.request.META:
clientIP += self.request.META["HTTP_X_FORWARDED_FOR"].split(",")[0]

logger.info(
"%s FROM %s: %s (%s) %s"
% (
user.username,
clientIP,
"ok" if user else "ERR",
self.request.META["HTTP_ACCEPT_LANGUAGE"] if "HTTP_ACCEPT_LANGUAGE" in self.request.META else "",
self.request.META["HTTP_USER_AGENT"] if "HTTP_USER_AGENT" in self.request.META else "",
)
)

login(self.request, user)

next_url = self.request.GET.get("next")
if next_url and self.is_safe_url(next_url):
return redirect(next_url)

return self.render_to_response(self.get_context_data(form=form))

def logout_user(request):
if request.method == 'POST' and request.user.is_authenticated:
logout(request)
return redirect(reverse(settings.LOGIN_URL))
5 changes: 5 additions & 0 deletions main/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,11 @@ def decode_base64(env_key, fallback_env_key):
AZURE_OPENAI_KEY = env("AZURE_OPENAI_KEY")
AZURE_OPENAI_DEPLOYMENT_NAME = env("AZURE_OPENAI_DEPLOYMENT_NAME")

# FIXME: Do not hard-code http protocol
LOGIN_REDIRECT_URL = f"http://{FRONTEND_URL}/permalink/login-callback"
LOGOUT_REDIRECT_URL = f"http://{FRONTEND_URL}/permalink/logout-callback"
LOGIN_URL = "go_login"

# django-oauth-toolkit configs
OIDC_RSA_PRIVATE_KEY = decode_base64("OIDC_RSA_PRIVATE_KEY_BASE64_ENCODED", "OIDC_RSA_PRIVATE_KEY")
OIDC_RSA_PUBLIC_KEY = decode_base64("OIDC_RSA_PUBLIC_KEY_BASE64_ENCODED", "OIDC_RSA_PUBLIC_KEY")
Expand Down
4 changes: 4 additions & 0 deletions main/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
ResendValidation,
ShowUsername,
UpdateSubscriptionPreferences,
LoginFormView,
logout_user,
)
from country_plan import drf_views as country_plan_views
from databank import views as data_bank_views
Expand Down Expand Up @@ -170,6 +172,8 @@
admin.site.site_title = "IFRC Go admin"

urlpatterns = [
path("login/", LoginFormView.as_view(), name="go_login"),
path("logout/", logout_user, name="go_logout"),
path("o/", include(oauth2_urls, namespace="oauth2_provider")),
# url(r"^api/v1/es_search/", EsPageSearch.as_view()),
url(r"^api/v1/search/", HayStackSearch.as_view()),
Expand Down
Loading

0 comments on commit 77e3526

Please sign in to comment.