Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Login views to Django #6

Open
wants to merge 4 commits into
base: serve-react-with-django
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,4 @@ app/babel.config.json
app/webpack-stats.json
app/webpack.prod.config.js
app/yarn.lock
app/react/dist/*
11 changes: 11 additions & 0 deletions app/core/templates/react.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,21 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
{{ css_render|safe }} {{ partials.google_analytics|default:''|safe }}
<script type="text/javascript">
var LOGIN_USER = "{{ LOGIN_USER | default:'' }}";
var LOGIN_PW = "{{ LOGIN_PW | default:'' }}";
var ACTION_LOGIN = "{{ ACTION_LOGIN | default:'' }}";
var ACTION_REDIRECT = "{{ ACTION_REDIRECT | default:'' }}";
var BYPASS = "{{ BYPASS | default:'' }}";
var LOGIN_LINKS = "{{ LOGIN_LINKS | default:'' }}";
var ERR_LOGIN = "{{ ERR_LOGIN | default:'' }}";
var CONTEXT = "{{ CONTEXT | default:'' }}";
</script>
</head>
<body class="{{ page_type|default:'' }}">
<div id="app"></div>
<div id="modal"></div>
<!-- {% csrf_token %} -->
{% render_bundle bundle_name %} {% render_bundle 'commons' %}
</body>
</html>
71 changes: 70 additions & 1 deletion app/core/views/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError
from django.shortcuts import redirect, render
from django.views.generic import View
from django.contrib.auth import logout, login, authenticate
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
from django.utils.decorators import method_decorator
import logging


Expand Down Expand Up @@ -35,7 +39,7 @@ def help(request):
return render(request, "react.html", context)


def handler404(request):
def handler404(request, exception):
# Log the 404 URL
logger = logging.getLogger(__name__)
logger.warning("404 URL: %s", request.path)
Expand All @@ -51,3 +55,68 @@ def handler404(request):

def handler500(request):
return "ADSFASDF"

def create_context(title, bundle_name, js_group_name, css_group_name):
# Get the theme override, if any.
theme_overrides = get_theme_overrides()

# If a theme override exists, use its JS and CSS.
# Otherwise, use the default JS and CSS.
if theme_overrides:
js_group = ["react", theme_overrides[0][1]["js"]]
css_group = theme_overrides[0][1]["css"]
else:
js_group = ["react", js_group_name]
css_group = css_group_name

context = {"title": title, "bundle_name": bundle_name, "css_group": css_group}

return context

def user_get(request):
"""
API Endpoint to get the current user.
"""
if request.user.is_authenticated:
user = request.user
return HttpResponse(user, content_type="application/json")
else:
return HttpResponse('Not logged in')

class LoginView(View):
"""
Build a non-SAML login page for users to authenticate.
"""
def get(self, *args, **kwargs): # pylint: disable=unused-argument
if self.request.user.is_authenticated:
return redirect("home page")
else:
context = create_context("Login", "login", "login", "login")
return render(self.request, "react.html", context)

def post(self, *args, **kwargs):
username = self.request.POST.get("username")
password = self.request.POST.get("password")
# Check the authentication backends for the user
user = authenticate(username=username, password=password)
if user is not None:
login(self.request, user)
return redirect("home page")
else:
context = create_context("Login", "login", "login", "login")
# Add an error message to the context
context["ERR_LOGIN"] = "Invalid username or password"
return render(self.request, "react.html", context)

class LogoutView(View):
"""
Log the user out of the application.
"""

def get(self, *args, **kwargs): # pylint: disable=unused-argument
logout(self.request)
return redirect("home page")

def post(self, *args, **kwargs):
logout(self.request)
return redirect("home page")
2 changes: 2 additions & 0 deletions app/materia/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
STATIC_URL=/static/
SAML_ENABLED=True
27 changes: 24 additions & 3 deletions app/materia/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/

Expand All @@ -41,6 +40,7 @@
# apps
"core",
"webpack_loader",
"materia_ucfauth.apps.UCFAuthConfig",
]

MIDDLEWARE = [
Expand All @@ -55,6 +55,11 @@
"whitenoise.middleware.WhiteNoiseMiddleware",
]

AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'materia_ucfauth.authentication.saml_backends.SAMLServiceProviderBackend'
]

ROOT_URLCONF = "materia.urls"

TEMPLATES = [
Expand Down Expand Up @@ -109,6 +114,20 @@
},
]

# SECURITY
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_NAME = "materia_sessionid"
CSRF_COOKIE_NAME = "materia_csrftoken"
CSRF_COOKIE_HTTPONLY = False
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = "DENY"

# SAML CONFIG
LOGIN_URL = "/saml2/login/"
LOGIN_REDIRECT_URL = "/"
LOGOUT_URL = "/saml2/logout/"
SAML_ENABLED = os.environ.get("SAML_ENABLED", "False") == "True"
SAML_FOLDER = os.path.join(BASE_DIR, "materia_ucfauth", "authentication", "saml")

# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
Expand All @@ -125,17 +144,19 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/

STATIC_URL = "/"
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "public"),
]
MEDIA_URL = "/media/"

WEBPACK_LOADER = {
"DEFAULT": {
"CACHE": False, # Allow for cache in prod since files won't be changing.
"BUNDLE_DIR_NAME": "static/",
"STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.json"),
'POLL_INTERVAL': 0.1,
}
}

Expand Down Expand Up @@ -165,4 +186,4 @@
"level": "ERROR", # change to DEBUG to see all queries
},
},
}
}
17 changes: 17 additions & 0 deletions app/materia/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,28 @@
from django.urls import include, path

from core.views import main as core_views
from django.conf import settings
from materia_ucfauth.authentication.saml_views import SamlLoginView, SamlLogoutView

urlpatterns = [
path("", core_views.index, name="home page"),
path("help/", core_views.help, name="help"),
path("api/json/user_get", core_views.user_get, name="user_get"),
]

# Register SAML login routes
if getattr(settings, "SAML_ENABLED", False) == True:
urlpatterns += [
path("login/", SamlLoginView.as_view(), name="saml_login"),
path("logout/", SamlLogoutView.as_view(), name="saml_logout"),
path(r'saml2/', include('materia_ucfauth.authentication.saml_urls')),
]
else:
# Register default login routes
urlpatterns += [
path("login/", core_views.LoginView.as_view(), name="login"),
path("logout/", core_views.LogoutView.as_view(), name="logout"),
]

handler404 = "core.views.main.handler404"
handler500 = "core.views.main.handler500"
5 changes: 5 additions & 0 deletions app/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ Django==5.0.1
mysqlclient==2.2.4
setuptools==69.0.3
sqlparse==0.4.4
python3-saml==1.16.0 # https://github.com/onelogin/python3-saml
uWSGI==2.0.23
wheel==0.42.0
pytz==2024.1
django-webpack-loader==0.7.0
whitenoise
# Pinning version of lxml and xmlsec due to segfault issue with python3-saml
# https://github.com/SAML-Toolkits/python3-saml/issues/389
lxml == 4.9.3 # https://github.com/lxml/lxml
xmlsec==1.3.13
4 changes: 3 additions & 1 deletion materia-app.node-python.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ COPY ./public /var/www/html/public
COPY ./babel.config.json /var/www/html/babel.config.json
COPY ./webpack.prod.config.js /var/www/html/webpack.prod.config.js


WORKDIR /var/www/html
RUN yarn install

Expand All @@ -42,6 +41,9 @@ RUN apt-get update && apt-get install -y \
python3-dev \
default-libmysqlclient-dev

# Install MateriaUCFAuth
RUN pip install --user /var/www/html/packages/materia-ucfauth-0.1.tar.gz

RUN pip install -r /var/www/html/requirements.txt

USER www-data
Expand Down
8 changes: 7 additions & 1 deletion materia-app.python.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ RUN apt-get update
RUN pip install --upgrade pip

# Install some dependencies necessary for supporting the SAML library
RUN apt-get install -y --no-install-recommends libxmlsec1-dev pkg-config
# 1. libffi-dev is required for ??
# 2. libxml2-dev and libxmlsec1-dev are required for xmlsec, which is required for python3-saml
# 3. libxml2-dev is required for lxml
RUN apt-get install -y --no-install-recommends \
pkg-config \
libffi-dev \
libxml2-dev=2.9.4+dfsg1-6.1ubuntu1.9 libxmlsec1-dev=1.2.25-1build1

# Install uwsgi now because it takes a little while
RUN pip install uwsgi
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,6 @@
"lines": 43
}
}
}
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
12 changes: 7 additions & 5 deletions src/components/login-page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'
import Header from './header'
import Summary from './widget-summary'
import './login-page.scss'
import {csrfToken} from '../util/csrf-token'

const LoginPage = () => {

Expand Down Expand Up @@ -29,7 +30,7 @@ const LoginPage = () => {

// If there is no redirect query in the url but there is a hash, it will redirect to my-widgets#hash
// Otherwise, it adds it onto the end of the redirect query
actionRedirect += (window.location.hash ? window.location.hash : '')
actionRedirect += (window.location.hash ? window.location.hash : '')

setState({
loginUser: window.LOGIN_USER,
Expand All @@ -43,7 +44,7 @@ const LoginPage = () => {
widgetName: window.WIDGET_NAME != undefined ? window.WIDGET_NAME : null,
isPreview: window.IS_PREVIEW != undefined ? window.IS_PREVIEW : null,
loginLinks: links,
errContent: window.ERR_LOGIN != undefined ? <div className='error'><p>{`${window.ERR_LOGIN}`}</p></div> : '',
errContent: window.ERR_LOGIN != undefined && window.ERR_LOGIN != '' ? <div className='error'><p>{`${window.ERR_LOGIN}`}</p></div> : '',
noticeContent: window.NOTICE_LOGIN != undefined ? <div className='error'><p>{`${window.NOTICE_LOGIN}`}</p></div> : ''
})
})
Expand All @@ -63,13 +64,13 @@ const LoginPage = () => {

let detailContent = <></>
if (!state.context || state.context == 'login') {
detailContent =
detailContent =
<div className="login_context detail">
<h2 className="context-header">Log In to Your Account</h2>
<span className="subtitle">{`Using your ${state.loginUser} and ${state.loginPw} to access your Widgets.`}</span>
</div>
} else if (state.context && state.context == 'widget') {
detailContent =
detailContent =
<div className="login_context detail">
<h2 className="context-header">Log in to play this widget</h2>
<span className="subtitle">{`Using your ${state.loginUser} and ${state.loginPw} to access your Widgets.`}</span>
Expand All @@ -88,6 +89,7 @@ const LoginPage = () => {
{ state.errContent }
{ state.noticeContent }
<form method="post" action={`${state.actionLogin}?redirect=${state.actionRedirect}`} className='form-content'>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<ul>
<li>
<input type="text" name="username" id="username" placeholder={state.loginUser} tabIndex="1" autoComplete="username" />
Expand All @@ -99,7 +101,7 @@ const LoginPage = () => {
<button type="submit" tabIndex="3" className="action_button">Login</button>
</li>
</ul>
{ state.bypass ?
{ state.bypass ?
<ul className="help_links">
{ state.loginLinks }
<li><a href="/help">Help</a></li>
Expand Down
Loading