Merge branch 'login_view' into 'master'
Auth: login and logout views See merge request !1
This commit is contained in:
commit
02167a9faa
|
@ -1,3 +1,4 @@
|
||||||
SECRET_KEY='$omeR@nd0mSecr3tKeyWith4V3ryL0ng$tring!?'
|
SECRET_KEY='$omeR@nd0mSecr3tKeyWith4V3ryL0ng$tring!?'
|
||||||
DEBUG=True
|
DEBUG=True
|
||||||
ALLOWED_HOSTS=.localhost,127.0.0.1
|
ALLOWED_HOSTS=.localhost,127.0.0.1
|
||||||
|
API_BASE_URL = 'https://api.examplea.org/'
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import requests
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.urls.exceptions import NoReverseMatch
|
||||||
|
|
||||||
|
DOMAINS_PATH = 'domains/'
|
||||||
|
TOKEN_PATH = '/api-token-auth/'
|
||||||
|
|
||||||
|
API_PATHS = {
|
||||||
|
# auth
|
||||||
|
'token-auth': '/api-token-auth/',
|
||||||
|
'my-account': 'accounts/',
|
||||||
|
|
||||||
|
# services
|
||||||
|
'domain-list': 'domains/',
|
||||||
|
# ... TODO (@slamora) complete list of backend URLs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Orchestra(object):
|
||||||
|
def __init__(self, *args, username=None, password=None, **kwargs):
|
||||||
|
self.base_url = kwargs.pop('base_url', settings.API_BASE_URL)
|
||||||
|
self.username = username
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.auth_token = kwargs.pop("auth_token", None)
|
||||||
|
|
||||||
|
if self.auth_token is None:
|
||||||
|
self.auth_token = self.authenticate(self.username, password)
|
||||||
|
|
||||||
|
def build_absolute_uri(self, path_name):
|
||||||
|
path = API_PATHS.get(path_name, None)
|
||||||
|
if path is None:
|
||||||
|
raise NoReverseMatch(
|
||||||
|
"Not found API path name '{}'".format(path_name))
|
||||||
|
|
||||||
|
return urllib.parse.urljoin(self.base_url, path)
|
||||||
|
|
||||||
|
def authenticate(self, username, password):
|
||||||
|
url = self.build_absolute_uri('token-auth')
|
||||||
|
response = self.session.post(
|
||||||
|
url,
|
||||||
|
data={"username": username, "password": password},
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json().get("token", None)
|
||||||
|
|
||||||
|
def request(self, verb, resource, raise_exception=True):
|
||||||
|
assert verb in ["HEAD", "GET", "POST", "PATCH", "PUT", "DELETE"]
|
||||||
|
url = self.build_absolute_uri(resource)
|
||||||
|
|
||||||
|
verb = getattr(self.session, verb.lower())
|
||||||
|
response = verb(url, headers={"Authorization": "Token {}".format(
|
||||||
|
self.auth_token)}, allow_redirects=False)
|
||||||
|
|
||||||
|
if raise_exception:
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
status = response.status_code
|
||||||
|
output = response.json()
|
||||||
|
|
||||||
|
return status, output
|
||||||
|
|
||||||
|
def retrieve_domains(self):
|
||||||
|
status, output = self.request("GET", 'domain-list')
|
||||||
|
return output
|
||||||
|
|
||||||
|
def retreve_profile(self):
|
||||||
|
_, output = self.request("GET", 'my-account')
|
||||||
|
return output
|
||||||
|
|
||||||
|
def verify_credentials(self):
|
||||||
|
"""
|
||||||
|
Returns:
|
||||||
|
A user profile info if the
|
||||||
|
credentials are valid, None otherwise.
|
||||||
|
"""
|
||||||
|
status, output = self.request("GET", 'my-account', raise_exception=False)
|
||||||
|
|
||||||
|
if status < 400:
|
||||||
|
return output
|
||||||
|
|
||||||
|
return None
|
|
@ -0,0 +1,38 @@
|
||||||
|
from django.middleware.csrf import rotate_token
|
||||||
|
from django.utils.crypto import constant_time_compare
|
||||||
|
|
||||||
|
SESSION_KEY_TOKEN = '_auth_token'
|
||||||
|
SESSION_KEY_USERNAME = '_auth_username'
|
||||||
|
|
||||||
|
|
||||||
|
def login(request, username, token):
|
||||||
|
"""
|
||||||
|
Persist a user id and a backend in the request. This way a user doesn't
|
||||||
|
have to reauthenticate on every request. Note that data set during
|
||||||
|
the anonymous session is retained when the user logs in.
|
||||||
|
"""
|
||||||
|
if SESSION_KEY_TOKEN in request.session:
|
||||||
|
if request.session[SESSION_KEY_USERNAME] != username:
|
||||||
|
# To avoid reusing another user's session, create a new, empty
|
||||||
|
# session if the existing session corresponds to a different
|
||||||
|
# authenticated user.
|
||||||
|
request.session.flush()
|
||||||
|
else:
|
||||||
|
request.session.cycle_key()
|
||||||
|
|
||||||
|
request.session[SESSION_KEY_TOKEN] = token
|
||||||
|
request.session[SESSION_KEY_USERNAME] = username
|
||||||
|
# if hasattr(request, 'user'):
|
||||||
|
# request.user = user
|
||||||
|
rotate_token(request)
|
||||||
|
|
||||||
|
|
||||||
|
def logout(request):
|
||||||
|
"""
|
||||||
|
Remove the authenticated user's ID from the request and flush their session
|
||||||
|
data.
|
||||||
|
"""
|
||||||
|
request.session.flush()
|
||||||
|
# if hasattr(request, 'user'):
|
||||||
|
# from django.contrib.auth.models import AnonymousUser
|
||||||
|
# request.user = AnonymousUser()
|
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
|
||||||
|
class LoginForm(AuthenticationForm):
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
username = self.cleaned_data.get('username')
|
||||||
|
password = self.cleaned_data.get('password')
|
||||||
|
|
||||||
|
if username is not None and password:
|
||||||
|
orchestra = api.Orchestra(username=username, password=password)
|
||||||
|
|
||||||
|
if orchestra.auth_token is None:
|
||||||
|
raise self.get_invalid_login_error()
|
||||||
|
else:
|
||||||
|
self.username = username
|
||||||
|
self.token = orchestra.auth_token
|
||||||
|
|
||||||
|
return self.cleaned_data
|
|
@ -1,6 +1,8 @@
|
||||||
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.views.generic.base import ContextMixin
|
from django.views.generic.base import ContextMixin
|
||||||
|
|
||||||
from . import get_version
|
from . import get_version
|
||||||
|
from .auth import SESSION_KEY_TOKEN
|
||||||
|
|
||||||
|
|
||||||
class CustomContextMixin(ContextMixin):
|
class CustomContextMixin(ContextMixin):
|
||||||
|
@ -12,3 +14,20 @@ class CustomContextMixin(ContextMixin):
|
||||||
})
|
})
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class UserTokenRequiredMixin(UserPassesTestMixin):
|
||||||
|
def test_func(self):
|
||||||
|
"""Check that the user has an authorized token."""
|
||||||
|
token = self.request.session.get(SESSION_KEY_TOKEN, None)
|
||||||
|
if token is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# initialize orchestra api orm
|
||||||
|
self.orchestra = api.Orchestra(auth_token=token)
|
||||||
|
|
||||||
|
# verify if the token is valid
|
||||||
|
if self.orchestra.verify_credentials() is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
</li>
|
</li>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="#logout">{% trans 'Log out' %}</a>
|
<a class="nav-link" href="{% url 'musician:logout' %}">{% trans 'Log out' %}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
<h1>Domains and websites</h1>
|
<h1>Domains and websites</h1>
|
||||||
<p>Little description of what to be expected...</p>
|
<p>Little description of what to be expected...</p>
|
||||||
|
|
||||||
{% for i in "123"|make_list %}
|
{% for domain in domains %}
|
||||||
<div class="row border mt-4">
|
<div class="row border mt-4">
|
||||||
<div class="col-12 bg-light">
|
<div class="col-12 bg-light">
|
||||||
<h3>domain.com</h3>
|
<h3>{{ domain.name }}</h3>
|
||||||
</div>
|
</div>
|
||||||
{% for service in "123"|make_list %}
|
{% for service in "123"|make_list %}
|
||||||
<div class="card" style="width: 18rem;">
|
<div class="card" style="width: 18rem;">
|
||||||
|
|
|
@ -4,7 +4,6 @@ URL routes definition.
|
||||||
|
|
||||||
Describe the paths where the views are accesible.
|
Describe the paths where the views are accesible.
|
||||||
"""
|
"""
|
||||||
from django.contrib.auth import views as auth_views
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
@ -13,8 +12,7 @@ from . import views
|
||||||
app_name = 'musician'
|
app_name = 'musician'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('auth/login/', auth_views.LoginView.as_view(template_name='auth/login.html',
|
path('auth/login/', views.LoginView.as_view(), name='login'),
|
||||||
extra_context={'version': '0.1'}), name='login'),
|
path('auth/logout/', views.LogoutView.as_view(), name='logout'),
|
||||||
# path('auth/logout/', views.LogoutView.as_view(), name='logout'),
|
|
||||||
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,9 +1,80 @@
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views.generic.base import TemplateView
|
from django.urls import reverse_lazy
|
||||||
|
from django.views.generic.base import RedirectView, TemplateView
|
||||||
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
from .mixins import CustomContextMixin
|
from . import api, get_version
|
||||||
|
from .auth import login as auth_login
|
||||||
|
from .auth import logout as auth_logout
|
||||||
|
from .forms import LoginForm
|
||||||
|
from .mixins import CustomContextMixin, UserTokenRequiredMixin
|
||||||
|
|
||||||
|
|
||||||
class DashboardView(CustomContextMixin, TemplateView): ## TODO LoginRequiredMixin
|
class DashboardView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
template_name = "musician/dashboard.html"
|
template_name = "musician/dashboard.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
# TODO retrieve all data needed from orchestra
|
||||||
|
raw_domains = self.orchestra.retrieve_domains()
|
||||||
|
|
||||||
|
context.update({
|
||||||
|
'domains': raw_domains
|
||||||
|
})
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class MailView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
template_name = "musician/mail.html"
|
||||||
|
|
||||||
|
|
||||||
|
class MailingListsView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
template_name = "musician/mailinglists.html"
|
||||||
|
|
||||||
|
|
||||||
|
class DatabasesView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
template_name = "musician/databases.html"
|
||||||
|
|
||||||
|
|
||||||
|
class SaasView(CustomContextMixin, UserTokenRequiredMixin, TemplateView):
|
||||||
|
template_name = "musician/saas.html"
|
||||||
|
|
||||||
|
|
||||||
|
class LoginView(FormView):
|
||||||
|
template_name = 'auth/login.html'
|
||||||
|
form_class = LoginForm
|
||||||
|
success_url = reverse_lazy('musician:dashboard')
|
||||||
|
extra_context = {'version': get_version()}
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs['request'] = self.request
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
"""Security check complete. Log the user in."""
|
||||||
|
auth_login(self.request, form.username, form.token)
|
||||||
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|
||||||
|
|
||||||
|
class LogoutView(RedirectView):
|
||||||
|
"""
|
||||||
|
Log out the user.
|
||||||
|
"""
|
||||||
|
permanent = False
|
||||||
|
pattern_name = 'musician:login'
|
||||||
|
|
||||||
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Logs out the user.
|
||||||
|
"""
|
||||||
|
auth_logout(self.request)
|
||||||
|
return super().get_redirect_url(*args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
"""Logout may be done via POST."""
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
|
@ -2,4 +2,5 @@ django==2.2
|
||||||
python-decouple==3.1
|
python-decouple==3.1
|
||||||
django-extensions
|
django-extensions
|
||||||
dj_database_url==0.5.0
|
dj_database_url==0.5.0
|
||||||
|
requests==2.22.0
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,14 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
LOGIN_URL = '/auth/login/'
|
||||||
|
|
||||||
|
# Sessions
|
||||||
|
# https://docs.djangoproject.com/en/2.2/topics/http/sessions/#configuring-sessions
|
||||||
|
|
||||||
|
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
|
||||||
|
|
||||||
|
# SESSION_COOKIE_SECURE = True
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
# https://docs.djangoproject.com/en/2.2/topics/i18n/
|
||||||
|
@ -135,3 +143,7 @@ USE_TZ = True
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
STATIC_ROOT = config('STATIC_ROOT')
|
STATIC_ROOT = config('STATIC_ROOT')
|
||||||
|
|
||||||
|
# Backend API configuration
|
||||||
|
|
||||||
|
API_BASE_URL = config('API_BASE_URL')
|
||||||
|
|
Loading…
Reference in New Issue