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

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"
hashids = "==1.2.0"
django-cas-client = "*"
requests = "*"
mysqlclient = "*"
djangorestframework = "*"
mysqlclient = "*"
[requires]
python_version = "3.7"
......@@ -18,10 +18,10 @@
"default": {
"certifi": {
"hashes": [
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
"sha256:339dc09518b07e2fa7eda5450740925974815557727d6bd35d319c1524a04a4c",
"sha256:6d58c986d22b038c8c0df30d639f23a3e6d172a05c3583e766f4c0b785c0986a"
],
"version": "==2018.4.16"
"version": "==2018.10.15"
},
"chardet": {
"hashes": [
......@@ -32,11 +32,11 @@
},
"django": {
"hashes": [
"sha256:0c5b65847d00845ee404bbc0b4a85686f15eb3001ffddda3db4e9baa265bf136",
"sha256:68aeea369a8130259354b6ba1fa9babe0c5ee6bced505dea4afcd00f765ae38b"
"sha256:25df265e1fdb74f7e7305a1de620a84681bcc9c05e84a3ed97e4a1a63024f18d",
"sha256:d6d94554abc82ca37e447c3d28958f5ac39bd7d4adaa285543ae97fb1129fd69"
],
"index": "pypi",
"version": "==2.0.8"
"version": "==2.0.9"
},
"django-cas-client": {
"hashes": [
......@@ -71,11 +71,11 @@
},
"djangorestframework": {
"hashes": [
"sha256:b6714c3e4b0f8d524f193c91ecf5f5450092c2145439ac2769711f7eba89a9d9",
"sha256:c375e4f95a3a64fccac412e36fb42ba36881e52313ec021ef410b40f67cddca4"
"sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136",
"sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5"
],
"index": "pypi",
"version": "==3.8.2"
"version": "==3.9.0"
},
"hashids": {
"hashes": [
......@@ -114,28 +114,27 @@
},
"requests": {
"hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
"sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c",
"sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279"
],
"index": "pypi",
"version": "==2.19.1"
"version": "==2.20.0"
},
"urllib3": {
"hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
"sha256:41c3db2fc01e5b907288010dec72f9d0a74e37d6994e6eb56849f59fea2265ae",
"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.23"
"version": "==1.24"
}
},
"develop": {
"astroid": {
"hashes": [
"sha256:a48b57ede295c3188ef5c84273bc2a8eadc46e4cbb001eae0d49fb5d1fabbb19",
"sha256:d066cdeec5faeb51a4be5010da612680653d844b57afd86a5c8315f2f801b4cc"
"sha256:292fa429e69d60e4161e7612cb7cc8fa3609e2e309f80c224d93a76d5e7b58be",
"sha256:c7013d119ec95eb626f7a2011f0b63d0c9a095df9ad06d8507b37084eada1a8d"
],
"version": "==2.0.2"
"version": "==2.0.4"
},
"coverage": {
"hashes": [
......@@ -180,7 +179,6 @@
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"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"
},
"lazy-object-proxy": {
......@@ -226,19 +224,19 @@
},
"pylint": {
"hashes": [
"sha256:0edfec21270725c5aa8e8d8d06ef5666f766e0e748ed2f1ab23624727303b935",
"sha256:4cadcaa4f1fb19123d4baa758d9fbe6286c5b3aa513af6ea42a2d51d405db205"
"sha256:1d6d3622c94b4887115fe5204982eee66fdd8a951cf98635ee5caee6ec98c3ec",
"sha256:31142f764d2a7cd41df5196f9933b12b7ee55e73ef12204b648ad7e556c119fb"
],
"index": "pypi",
"version": "==2.1.0"
"version": "==2.1.1"
},
"pylint-django": {
"hashes": [
"sha256:5c5a20c443b4e70fdc8c47e42cff8ce79c953954e918f8e559f6e1d05a971585",
"sha256:70f2b5397aa2468373fcf87d64a700b359050e905e56e2dbaf954e6edb04c593"
"sha256:5dc5f85caef2c5f9e61622b9cbd89d94edd3dcf546939b2974d18de4fa90d676",
"sha256:bf313f10b68ed915a34f0f475cc9ff8c7f574a95302beb48b79c5993f7efd84c"
],
"index": "pypi",
"version": "==2.0"
"version": "==2.0.2"
},
"pylint-plugin-utils": {
"hashes": [
......@@ -253,42 +251,6 @@
],
"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": {
"hashes": [
"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 ReactDOM from "react-dom";
import AuthButton from "./AuthButton.jsx";
ReactDOM.render(
<div>
<p>Hello Go 3 with React!</p>
<AuthButton />
</div>,
document.getElementById("root")
);
{% load static %}
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#006633" />
<meta name="description" content="University-branded URL shortening" />
<title>Welcome &bull; SRCT Go</title>
</head>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#006633" />
<meta name="description" content="University-branded URL shortening" />
<title>Welcome &bull; SRCT Go</title>
</head>
<body>
<!-- React injects itself here -->
<div id="root"></div>
</body>
<body>
<!-- React injects itself here -->
<div id="root"></div>
</body>
<script>
window.django = {
logout: "{% url "cas_logout" %}",
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
from django.dispatch import receiver
from django.utils import timezone
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
# Other Imports
from hashids import Hashids
......@@ -27,62 +25,39 @@ from rest_framework.authtoken.models import Token
# Note: the Hashids library already implements several restrictions oncharacter
# placement, including repeating or incrementing numbers, or placing curse word
# characters adjacent to one another.
SIMILAR_CHARS = set(['b', 'G', '6', 'g', 'q', 'l',
'1', 'I', 'S', '5', 'O', '0'])
SIMILAR_CHARS = set(["b", "G", "6", "g", "q", "l", "1", "I", "S", "5", "O", "0"])
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):
"""
Wrapper model for the built in User model which stores data pertaining to
the registration / approval / blocked status of a django user.
"""
user = models.OneToOneField(
User,
on_delete="cascade",
verbose_name="Django User Object"
User, on_delete="cascade", verbose_name="Django User Object"
)
full_name = models.CharField(
"Full Name",
max_length=100,
default="",
)
full_name = models.CharField("Full Name", max_length=100, default="")
organization = models.CharField(
"Organization",
max_length=100,
default="",
)
organization = models.CharField("Organization", max_length=100, default="")
description = models.TextField(
"Signup Description",
blank=True,
default="",
)
description = models.TextField("Signup Description", blank=True, default="")
registered = models.BooleanField(
"Registration Status",
default=False,
)
registered = models.BooleanField("Registration Status", default=False)
approved = models.BooleanField(
"Approval Status",
default=False,
)
approved = models.BooleanField("Approval Status", default=False)
blocked = models.BooleanField(
"Blocked Status",
default=False,
)
blocked = models.BooleanField("Blocked Status", default=False)
def __str__(self):
return f"<RegisteredUser: {self.user} - Approval Status: {self.approved}>"
@receiver(post_save, sender=User)
def handle_reguser_creation(sender, instance, created, **kwargs):
"""
......@@ -92,38 +67,29 @@ def handle_reguser_creation(sender, instance, created, **kwargs):
if created:
RegisteredUser.objects.create(user=instance, full_name=instance.get_full_name())
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
token = Token.objects.create(user=instance)
print(token.key)
Token.objects.create(user=instance)
class URL(models.Model):
"""
The representation of a stored URL redirection rule. Each URL has
attributes that are used for analytic purposes.
"""
owner = models.ForeignKey(
RegisteredUser,
on_delete="cascade",
verbose_name="RegisteredUser Owner"
RegisteredUser, on_delete="cascade", verbose_name="RegisteredUser Owner"
)
date_created = models.DateTimeField(
"Go Link Creation Date",
default=timezone.now,
)
date_created = models.DateTimeField("Go Link Creation Date", default=timezone.now)
date_expires = models.DateTimeField(
"Go Link Expiry Date",
blank=True,
null=True,
)
date_expires = models.DateTimeField("Go Link Expiry Date", blank=True, null=True)
destination = models.URLField(
"Go Link Destination URL",
max_length=1000,
default="https://go.gmu.edu",
"Go Link Destination URL", max_length=1000, default="https://go.gmu.edu"
)
# Note: min_length cannot exist on a model so it is enforced in forms.py
......@@ -143,7 +109,7 @@ class URL(models.Model):
return f"<Owner: {self.owner.user} - Destination URL: {self.destination}>"
class Meta:
ordering = ['short']
ordering = ["short"]
@staticmethod
def generate_valid_short():
......
......@@ -8,9 +8,12 @@ from .models import URL, RegisteredUser
# Third Party Imports
from rest_framework import serializers
from rest_framework.authtoken.models import Token
class URLSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = URL
lookup_field = 'short'
fields = ('destination', 'short', 'date_expires')
lookup_field = "short"
fields = ("destination", "short", "date_expires")
......@@ -17,7 +17,7 @@ from cas import views as cas_views
from rest_framework import routers
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
# application. Such modules are expected to register models with the admin.
......@@ -26,25 +26,21 @@ admin.autodiscover()
urlpatterns = [
# Root API URL
path("api/", include(router.urls)),
# Authentication URLs
path('auth/login/', cas_views.login, name='cas_login'),
path('auth/logout/', cas_views.logout, {'next_page': '/'}, name='cas_logout'),
path("auth/login/", cas_views.login, name="cas_login"),
path("auth/logout/", cas_views.logout, name="cas_logout"),
# /admin - Administrator interface.
path('admin/', admin.site.urls, name='go_admin'),
path('auth/', include('rest_framework.urls'))
# # /view/<short> - View URL data. Cached for 15 minutes
# re_path(r'^view/(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$',
# cache_page(60 * 15)(go.views.view), name='view'),
# re_path(r'^view/(?P<short>[-\w]+)$',
# cache_page(60 * 15)(go.views.view), name='view'),
# # Redirection regex.
# re_path(r'^(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$',
# go.views.redirection, name='redirection'),
# re_path(r'^(?P<short>[-\w]+)$',
# go.views.redirection, name='redirection'),
path("admin/", admin.site.urls, name="go_admin"),
path("auth/", include("rest_framework.urls")),
path("auth/token/", views.CustomAuthToken.as_view())
# # /view/<short> - View URL data. Cached for 15 minutes
# re_path(r'^view/(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$',
# cache_page(60 * 15)(go.views.view), name='view'),
# re_path(r'^view/(?P<short>[-\w]+)$',
# cache_page(60 * 15)(go.views.view), name='view'),
# # Redirection regex.
# re_path(r'^(?P<short>([\U00010000-\U0010ffff][\U0000200D]?)+)$',
# go.views.redirection, name='redirection'),
# re_path(r'^(?P<short>[-\w]+)$',
# go.views.redirection, name='redirection'),
]
......@@ -4,53 +4,58 @@ go/views.py
The functions that handle a request to a given URL. Get some data, manipulate
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 permissions
from rest_framework.authentication import TokenAuthentication, SessionAuthentication
from .serializers import URLSerializer
from .models import URL
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.permissions import IsAuthenticated
class URLPermission(permissions.BasePermission):
"""Custom permission check on URL model operations."""
message = "You do not have the necessary approvals to perform that action."
def has_permission(self, request, view):
return request.user.registereduser.approved or request.user.is_staff
def has_object_permission(self, request, view, obj):
return obj.owner == request.user.registereduser or request.user.is_staff
class URLViewSet(viewsets.ModelViewSet):
"""
API endpoint that handles creation/read/update/deletion of URL objects.
"""
authentication_classes = (TokenAuthentication,)
serializer_class = URLSerializer
permission_classes = (URLPermission,)
lookup_field = 'short'
permission_classes = (URLPermission, IsAuthenticated)
lookup_field = "short"
def get_queryset(self):
if not self.request.user.is_staff:
return URL.objects.filter(owner=self.request.user.registereduser)
else:
return URL.objects.all()
return URL.objects.all()
def perform_create(self, serializer):
serializer.save(owner=self.request.user.registereduser)
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
class CustomAuthToken(ObtainAuthToken):
authentication_classes = (SessionAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request, *args, **kwargs):
token, created = Token.objects.get_or_create(user=request.user)
return Response({"token": token.key})
module.exports = {
module: {
rules: [{
test: /\.jsx$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}]
}
module: {
rules: [
{
test: /\.jsx$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
test: /\.css$/,
use: [
{
loader: "style-loader"
},
{
loader: "css-loader"
}
]
}
]
}
};
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment