Commit 04796779 authored by David Haynes's avatar David Haynes 🙆
Browse files

Merge branch '187-react-auth-flow' into go-three

parents ccceb178 c6226e59
Pipeline #3253 passed with stage
in 53 seconds
...@@ -16,8 +16,8 @@ django-redis-cache = "==1.7.1" ...@@ -16,8 +16,8 @@ django-redis-cache = "==1.7.1"
hashids = "==1.2.0" hashids = "==1.2.0"
django-cas-client = "*" django-cas-client = "*"
requests = "*" requests = "*"
mysqlclient = "*"
djangorestframework = "*" djangorestframework = "*"
mysqlclient = "*"
[requires] [requires]
python_version = "3.7" python_version = "3.7"
...@@ -18,10 +18,10 @@ ...@@ -18,10 +18,10 @@
"default": { "default": {
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", "sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" "sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
], ],
"version": "==2018.4.16" "version": "==2018.10.15"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
...@@ -32,11 +32,11 @@ ...@@ -32,11 +32,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:0c5b65847d00845ee404bbc0b4a85686f15eb3001ffddda3db4e9baa265bf136", "sha256:25df265e1fdb74f7e7305a1de620a84681bcc9c05e84a3ed97e4a1a63024f18d",
"sha256:68aeea369a8130259354b6ba1fa9babe0c5ee6bced505dea4afcd00f765ae38b" "sha256:d6d94554abc82ca37e447c3d28958f5ac39bd7d4adaa285543ae97fb1129fd69"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.8" "version": "==2.0.9"
}, },
"django-cas-client": { "django-cas-client": {
"hashes": [ "hashes": [
...@@ -71,11 +71,11 @@ ...@@ -71,11 +71,11 @@
}, },
"djangorestframework": { "djangorestframework": {
"hashes": [ "hashes": [
"sha256:b6714c3e4b0f8d524f193c91ecf5f5450092c2145439ac2769711f7eba89a9d9", "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136",
"sha256:c375e4f95a3a64fccac412e36fb42ba36881e52313ec021ef410b40f67cddca4" "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.8.2" "version": "==3.9.0"
}, },
"hashids": { "hashids": {
"hashes": [ "hashes": [
...@@ -114,28 +114,27 @@ ...@@ -114,28 +114,27 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.19.1" "version": "==2.20.0"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" "sha256:8819bba37a02d143296a4d032373c4dd4aca11f6d4c9973335ca75f9c8475f59"
], ],
"markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version < '4' and python_version >= '2.6' and python_version != '3.1.*'", "version": "==1.24"
"version": "==1.23"
} }
}, },
"develop": { "develop": {
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:a48b57ede295c3188ef5c84273bc2a8eadc46e4cbb001eae0d49fb5d1fabbb19", "sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be",
"sha256:d066cdeec5faeb51a4be5010da612680653d844b57afd86a5c8315f2f801b4cc" "sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d"
], ],
"version": "==2.0.2" "version": "==2.0.4"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
...@@ -180,7 +179,6 @@ ...@@ -180,7 +179,6 @@
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
], ],
"markers": "python_version != '3.2.*' and python_version != '3.1.*' and python_version != '3.0.*' and python_version != '3.3.*' and python_version >= '2.7'",
"version": "==4.3.4" "version": "==4.3.4"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
...@@ -226,19 +224,19 @@ ...@@ -226,19 +224,19 @@
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
"sha256:0edfec21270725c5aa8e8d8d06ef5666f766e0e748ed2f1ab23624727303b935", "sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec",
"sha256:4cadcaa4f1fb19123d4baa758d9fbe6286c5b3aa513af6ea42a2d51d405db205" "sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.1.0" "version": "==2.1.1"
}, },
"pylint-django": { "pylint-django": {
"hashes": [ "hashes": [
"sha256:5c5a20c443b4e70fdc8c47e42cff8ce79c953954e918f8e559f6e1d05a971585", "sha256:5dc5f85caef2c5f9e61622b9cbd89d94edd3dcf546939b2974d18de4fa90d676",
"sha256:70f2b5397aa2468373fcf87d64a700b359050e905e56e2dbaf954e6edb04c593" "sha256:bf313f10b68ed915a34f0f475cc9ff8c7f574a95302beb48b79c5993f7efd84c"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0" "version": "==2.0.2"
}, },
"pylint-plugin-utils": { "pylint-plugin-utils": {
"hashes": [ "hashes": [
...@@ -253,42 +251,6 @@ ...@@ -253,42 +251,6 @@
], ],
"version": "==1.11.0" "version": "==1.11.0"
}, },
"typed-ast": {
"hashes": [
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
],
"version": "==1.1.0"
},
"typing": {
"hashes": [
"sha256:3a887b021a77b292e151afb75323dea88a7bc1b3dfa92176cff8e44c8b68bddf",
"sha256:b2c689d54e1144bbcfd191b0832980a21c2dbcf7b5ff7a66248a60c90e951eb8",
"sha256:d400a9344254803a2368533e4533a4200d21eb7b6b729c173bc38201a74db3f2"
],
"version": "==3.6.4"
},
"wrapt": { "wrapt": {
"hashes": [ "hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
......
import React from "react";
import { Button } from "reactstrap";
class AuthButton extends React.Component {
constructor(props) {
super(props);
this.state = { is_auth: false };
}
componentDidMount() {
this.setState(() => {
return { is_auth: window.django.user.is_authenticated == "True" };
});
}
render() {
return (
<div>
{this.state.is_auth ? (
<Button color="info" href="/auth/logout">
Logout
</Button>
) : (
<Button color="info" href="/auth/login">
Login
</Button>
)}
</div>
);
}
}
export default AuthButton;
// Apply Global Masonstrap styling
import "masonstrap/build/css/masonstrap.min.css";
import "masonstrap/build/js/masonstrap.min.js";
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import AuthButton from "./AuthButton.jsx";
ReactDOM.render( ReactDOM.render(
<div> <div>
<p>Hello Go 3 with React!</p> <AuthButton />
</div>, </div>,
document.getElementById("root") document.getElementById("root")
); );
{% load static %}
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#006633" /> <meta name="theme-color" content="#006633" />
<meta name="description" content="University-branded URL shortening" /> <meta name="description" content="University-branded URL shortening" />
<title>Welcome &bull; SRCT Go</title> <title>Welcome &bull; SRCT Go</title>
</head> </head>
<body>
<!-- React injects itself here -->
<div id="root"></div>
</body>
<body> <script>
<!-- React injects itself here --> window.django = {
<div id="root"></div> logout: "{% url "cas_logout" %}",
</body> user: {
username: "{{ request.user.username }}",
full_name: "{{ request.user.get_full_name }}",
last_login: "{{ request.user.last_login }}",
is_authenticated: "{{ request.user.is_authenticated }}"
}
};
</script>
<script src="static/main.js"></script> <script src="static/main.js"></script>
</html> </html>
\ No newline at end of file
...@@ -15,8 +15,6 @@ from django.db.models.signals import post_save ...@@ -15,8 +15,6 @@ from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
# Other Imports # Other Imports
from hashids import Hashids from hashids import Hashids
...@@ -27,62 +25,39 @@ from rest_framework.authtoken.models import Token ...@@ -27,62 +25,39 @@ from rest_framework.authtoken.models import Token
# Note: the Hashids library already implements several restrictions oncharacter # Note: the Hashids library already implements several restrictions oncharacter
# placement, including repeating or incrementing numbers, or placing curse word # placement, including repeating or incrementing numbers, or placing curse word
# characters adjacent to one another. # characters adjacent to one another.
SIMILAR_CHARS = set(['b', 'G', '6', 'g', 'q', 'l', SIMILAR_CHARS = set(["b", "G", "6", "g", "q", "l", "1", "I", "S", "5", "O", "0"])
'1', 'I', 'S', '5', 'O', '0'])
ALPHANUMERICS = set(string.ascii_letters + string.digits) ALPHANUMERICS = set(string.ascii_letters + string.digits)
LINK_CHARS = ''.join(ALPHANUMERICS - SIMILAR_CHARS) LINK_CHARS = "".join(ALPHANUMERICS - SIMILAR_CHARS)
HASHIDS = Hashids(salt="srct.gmu.edu", alphabet=(LINK_CHARS))
HASHIDS = Hashids(
salt="srct.gmu.edu", alphabet=(LINK_CHARS)
)
class RegisteredUser(models.Model): class RegisteredUser(models.Model):
""" """
Wrapper model for the built in User model which stores data pertaining to Wrapper model for the built in User model which stores data pertaining to
the registration / approval / blocked status of a django user. the registration / approval / blocked status of a django user.
""" """
user = models.OneToOneField( user = models.OneToOneField(
User, User, on_delete="cascade", verbose_name="Django User Object"
on_delete="cascade",
verbose_name="Django User Object"
) )
full_name = models.CharField( full_name = models.CharField("Full Name", max_length=100, default="")
"Full Name",
max_length=100,
default="",
)
organization = models.CharField( organization = models.CharField("Organization", max_length=100, default="")
"Organization",
max_length=100,
default="",
)
description = models.TextField( description = models.TextField("Signup Description", blank=True, default="")
"Signup Description",
blank=True,
default="",
)
registered = models.BooleanField( registered = models.BooleanField("Registration Status", default=False)
"Registration Status",
default=False,
)
approved = models.BooleanField( approved = models.BooleanField("Approval Status", default=False)
"Approval Status",
default=False,
)
blocked = models.BooleanField( blocked = models.BooleanField("Blocked Status", default=False)
"Blocked Status",
default=False,
)
def __str__(self): def __str__(self):
return f"<RegisteredUser: {self.user} - Approval Status: {self.approved}>" return f"<RegisteredUser: {self.user} - Approval Status: {self.approved}>"
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def handle_reguser_creation(sender, instance, created, **kwargs): def handle_reguser_creation(sender, instance, created, **kwargs):
""" """
...@@ -92,38 +67,29 @@ def handle_reguser_creation(sender, instance, created, **kwargs): ...@@ -92,38 +67,29 @@ def handle_reguser_creation(sender, instance, created, **kwargs):
if created: if created:
RegisteredUser.objects.create(user=instance, full_name=instance.get_full_name()) RegisteredUser.objects.create(user=instance, full_name=instance.get_full_name())
@receiver(post_save, sender=settings.AUTH_USER_MODEL) @receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs): def create_auth_token(sender, instance=None, created=False, **kwargs):
if created: if created:
token = Token.objects.create(user=instance) Token.objects.create(user=instance)
print(token.key)
class URL(models.Model): class URL(models.Model):
""" """
The representation of a stored URL redirection rule. Each URL has The representation of a stored URL redirection rule. Each URL has
attributes that are used for analytic purposes. attributes that are used for analytic purposes.
""" """
owner = models.ForeignKey( owner = models.ForeignKey(
RegisteredUser, RegisteredUser, on_delete="cascade", verbose_name="RegisteredUser Owner"
on_delete="cascade",
verbose_name="RegisteredUser Owner"
) )
date_created = models.DateTimeField( date_created = models.DateTimeField("Go Link Creation Date", default=timezone.now)
"Go Link Creation Date",
default=timezone.now,
)
date_expires = models.DateTimeField( date_expires = models.DateTimeField("Go Link Expiry Date", blank=True, null=True)
"Go Link Expiry Date",
blank=True,
null=True,
)
destination = models.URLField( destination = models.URLField(
"Go Link Destination URL", "Go Link Destination URL", max_length=1000, default="https://go.gmu.edu"
max_length=1000,
default="https://go.gmu.edu",
) )
# Note: min_length cannot exist on a model so it is enforced in forms.py # Note: min_length cannot exist on a model so it is enforced in forms.py
...@@ -143,7 +109,7 @@ class URL(models.Model): ...@@ -143,7 +109,7 @@ class URL(models.Model):
return f"<Owner: {self.owner.user} - Destination URL: {self.destination}>" return f"<Owner: {self.owner.user} - Destination URL: {self.destination}>"
class Meta: class Meta:
ordering = ['short'] ordering = ["short"]
@staticmethod @staticmethod
def generate_valid_short(): def generate_valid_short():
......
...@@ -8,9 +8,12 @@ from .models import URL, RegisteredUser ...@@ -8,9 +8,12 @@ from .models import URL, RegisteredUser
# Third Party Imports # Third Party Imports
from rest_framework import serializers from rest_framework import serializers
from rest_framework.authtoken.models import Token
class URLSerializer(serializers.HyperlinkedModelSerializer): class URLSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = URL model = URL
lookup_field = 'short' lookup_field = "short"
fields = ('destination', 'short', 'date_expires') fields = ("destination", "short", "date_expires")
...@@ -17,7 +17,7 @@ from cas import views as cas_views ...@@ -17,7 +17,7 @@ from cas import views as cas_views
from rest_framework import routers from rest_framework import routers
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register(r'golinks', views.URLViewSet, base_name="golinks") router.register(r"golinks", views.URLViewSet, base_name="golinks")
# This function attempts to import an admin module in each installed # This function attempts to import an admin module in each installed
# application. Such modules are expected to register models with the admin. # application. Such modules are expected to register models with the admin.
...@@ -26,25 +26,21 @@ admin.autodiscover() ...@@ -26,25 +26,21 @@ admin.autodiscover()
urlpatterns = [ urlpatterns = [
# Root API URL # Root API URL
path("api/", include(router.urls)), path("api/", include(router.urls)),
# Authentication URLs # Authentication URLs
path('auth/login/', cas_views.login, name='cas_login'), path("auth/login/", cas_views.login, name="cas_login"),
path('auth/logout/', cas_views.logout, {'next_page': '/'}, name='cas_logout'), path("auth/logout/", cas_views.logout, name="cas_logout"),
# /admin - Administrator interface. # /admin - Administrator interface.
path('admin/', admin.site.urls, name='go_admin'), path("admin/", admin.site.urls, name="go_admin"),
path('auth/', include('rest_framework.urls')) path("auth/", include("rest_framework.urls")),
path("auth/token/", views.CustomAuthToken.as_view())
# # /view/<short> - View URL data. Cached for 15 minutes
# # /view/<short> - View URL data. Cached for 15 minutes # re_path(r'^view/(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$',
# re_path(r'^view/(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$', # cache_page(60 * 15)(go.views.view), name='view'),
# cache_page(60 * 15)(go.views.view), name='view'), # re_path(r'^view/(?P<short>[-\w]+)$',
# re_path(r'^view/(?P<short>[-\w]+)$', # cache_page(60 * 15)(go.views.view), name='view'),
# cache_page(60 * 15)(go.views.view), name='view'), # # Redirection regex.
# re_path(r'^(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$',
# # Redirection regex. # go.views.redirection, name='redirection'),
# re_path(r'^(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$', # re_path(r'^(?P<short>[-\w]+)$',
# go.views.redirection, name='redirection'), # go.views.redirection, name='redirection'),
# re_path(r'^(?P<short>[-\w]+)$',
# go.views.redirection, name='redirection'),
] ]
...@@ -4,53 +4,58 @@ go/views.py ...@@ -4,53 +4,58 @@ go/views.py
The functions that handle a request to a given URL. Get some data, manipulate The functions that handle a request to a given URL. Get some data, manipulate
it, and return a rendered template. it, and return a rendered template.
""" """
# Python stdlib imports
from datetime import timedelta
# Django Imports
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied # ValidationError
from django.core.mail import EmailMessage, send_mail
from django.http import HttpResponseServerError # Http404
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
# Other imports
from ratelimit.decorators import ratelimit
# App Imports
from .forms import EditForm, SignupForm, URLForm
from .models import URL, RegisteredUser
from django.contrib.auth.models import User, Group
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework import permissions from rest_framework import permissions
from rest_framework.authentication import TokenAuthentication, SessionAuthentication
from .serializers import URLSerializer from .serializers import URLSerializer