Commit 4a95d159 authored by David Haynes's avatar David Haynes 🙆

Outline of API in place

parent d490f3bc
......@@ -36,4 +36,3 @@ services:
- GO_EMAIL_HOST_PASSWORD=
- GO_EMAIL_FROM=
- GO_EMAIL_TO=
- SUPERUSER=dhaynes3
......@@ -8,7 +8,7 @@ export GO_SECRET_KEY
export GO_CREATE_SUPERUSER
GO_SECRET_KEY=$(dd if=/dev/urandom count=100 | tr -dc "A-Za-z0-9" | fold -w 60 | head -n1 2>/dev/null)
GO_CREATE_SUPERUSER="from django.contrib.auth import get_user_model; User = get_user_model(); me = User.objects.get(username='$SUPERUSER'); me.first_name = 'David'; me.last_name = 'Haynes'; me.save(); "
GO_CREATE_SUPERUSER="from django.contrib.auth import get_user_model; User = get_user_model(); me = User.objects.get(username='admin'); me.first_name = 'mr'; me.last_name = 'admin'; me.save(); "
python go/manage.py makemigrations
python go/manage.py makemigrations go
......@@ -16,3 +16,4 @@ python go/manage.py migrate
python go/manage.py createsuperuser --noinput --username="$SUPERUSER" --email="$SUPERUSER$GO_EMAIL_DOMAIN"
echo "$GO_CREATE_SUPERUSER" | python go/manage.py shell
python go/manage.py runserver 0.0.0.0:8000
\ No newline at end of file
......@@ -77,36 +77,29 @@ def create_user(tree: list):
Create a django user based off of the peoplefinder info we parsed earlier.
"""
print("Parsing CAS information.")
try:
username = tree[0][0].text
user, user_created = User.objects.get_or_create(username=username)
except Exception as ex:
print("CAS callback unsuccessful:", ex)
# error handling in pfinfo function
info_name = pfinfo(username)
# set and save the user's email
email_str = "%s%s" % (username, settings.EMAIL_DOMAIN)
# error handling in pfinfo function
info_name = pfinfo(username)
user, user_created = User.objects.get_or_create(
username=username,
email=email_str,
first_name=info_name[0],
last_name=info_name[1]
)
# Password is a required User object field, though doesn't matter for our
# purposes because all user auth is handled through CAS, not Django's login.
user.set_password('cas_used_instead')
user.save()
try:
if user_created:
print("Created user object %s." % username)
# set and save the user's email
email_str = "%s%s" % (username, settings.EMAIL_DOMAIN)
user.email = email_str
# Password is a required User object field, though doesn't matter for our
# purposes because all user auth is handled through CAS, not Django's login.
user.set_password('cas_used_instead')
user.save()
print("Added user's email, %s." % email_str)
user.first_name = info_name[0]
user.last_name = info_name[1]
user.save()
print("Added user's name, %s %s." % (info_name[0], info_name[1]))
print("User object creation process completed.")
else:
print("User object already exists.")
print("CAS callback successful.")
except Exception as ex:
print("Unhandled user creation error:", ex)
print("CAS callback unsuccessful:", ex)
......@@ -90,7 +90,7 @@ def handle_reguser_creation(sender, instance, created, **kwargs):
is called to create an associated RegisteredUser.
"""
if created:
RegisteredUser.objects.create(user=instance)
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):
......
......@@ -3,16 +3,27 @@ go/serializers.py
Define how data is translated from the database to json/API representation.
"""
# Django Imports
from django.contrib.auth.models import User, Group
# App Imports
from .models import URL, RegisteredUser
# Third Party Imports
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')
fields = ('url', 'username', 'email', 'first_name',
'last_name', 'is_staff')
class RegisteredUserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = RegisteredUser
fields = '__all__'
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class URLSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
fields = ('url', 'name')
model = URL
fields = '__all__'
......@@ -27,657 +27,684 @@ from .models import URL, RegisteredUser
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from .serializers import UserSerializer, GroupSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
from rest_framework import permissions
from .serializers import UserSerializer, URLSerializer, RegisteredUserSerializer
class CrudPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Group.objects.all()
serializer_class = GroupSerializer
return obj.owner == request.user.registereduser or request.user.is_staff
def index(request):
"""
If a user is logged in, this view displays all the information about all
of their URLs. Otherwise, it will show the public landing page.
"""
if not request.user.is_authenticated:
return render(request, 'landing.html')
if not request.user.registereduser.approved:
return render(request, 'not_registered.html')
# List of sort methods and their display name "Column" : "Name"
SORT_METHODS = {
"-date_created": "Most Recent",
"date_created": "Oldest",
"short": "Alphabetical (A-Z)",
"-short": "Alphabetical (Z-A)",
"-clicks": "Most Popular",
"clicks": "Least Popular",
"-expires": "Expiring Soon"
}
# Get the requested sort method, default to "-date_created" : "Most Recent"
sort_method = request.GET.get('sort', '-date_created')
# Get the current domain info
domain = "%ss://%s" % (request.scheme, request.META.get('HTTP_HOST')) + "/"
# Grab a list of all the URLs that are currently owned by the user
urls = URL.objects.filter(owner=request.user.registereduser)
# Check if provided sort method is valid, otherwise default
if sort_method in SORT_METHODS:
urls = urls.order_by(sort_method)
else:
urls = urls.order_by("-date_created")
# Render my_links passing the list of URLs, Domain, and Sort Methods to
# the template
return render(request, 'core/index.html', {
'urls': urls,
'domain': domain,
'sort_methods': SORT_METHODS
})
@login_required
def new_link(request):
class RegisteredUserViewSet(viewsets.ModelViewSet):
"""
This view handles the homepage that the user is presented with when
they request '/newLink'. If they're not logged in, they're redirected to
login. If they're logged in but not registered, they're given the
not_registered error page. If they are logged in AND registered, they
get the URL registration form.
API endpoint that allows RegisteredUsers to be viewed or edited.
"""
# If the user isn't approved, then display the you're not approved page.
if not request.user.registereduser.approved:
if request.user.registereduser.blocked:
return render(request, 'banned.html')
else:
return render(request, 'not_registered.html')
# Initialize a URL form
url_form = URLForm(host=request.META.get('HTTP_HOST')) # unbound form
# If a POST request is received, then the user has submitted a form and
# it's time to parse the form and create a new URL object
if request.method == 'POST':
# Now we initialize the form again but this time we have the POST
# request
url_form = URLForm(request.POST, host=request.META.get('HTTP_HOST'))
queryset = RegisteredUser.objects.all()
serializer_class = RegisteredUserSerializer
# Django will check the form to make sure it's valid
if url_form.is_valid():
# Call our post method to assemble our new URL object
res = post(request, url_form)
# 500 error
if res == 500:
return HttpResponseServerError(render(request, '500.html'))
# Redirect to the shiny new URL
return redirect('view', res.short)
# Else, there is an error, redisplay the form with the validation
# errors
else:
# Render index.html passing the form to the template
return render(request, 'core/new.html', {
'form': url_form,
})
# Render index.html passing the form to the template
return render(request, 'core/new.html', {
'form': url_form,
})
@login_required
def my_links(request):
"""
for compatibility, just in case
shows the same thing as /, but requires login to be consistent with
/newLink
"""
if not request.user.registereduser.approved:
if request.user.registereduser.blocked:
return render(request, 'banned.html')
else:
return render(request, 'not_registered.html')
return index(request)
# Rate limits are completely arbitrary
@ratelimit(key='user', rate='3/m', method='POST', block=True)
@ratelimit(key='user', rate='25/d', method='POST', block=True)
def post(request, url_form):
"""
Helper function that handles POST requests for the URL creation
"""
# We don't commit the url object yet because we need to add its
# owner, and parse its date field.
url = url_form.save(commit=False)
url.owner = request.user.registereduser
# If the user entered a short url, it's already been validated,
# so accept it. If they did not, however, then generate a
# random one and use that instead.
short = url_form.cleaned_data.get('short').strip()
# Check if a short URL was entered
if len(short) > 0:
url.short = short
else:
# If the user didn't enter a short url, generate a random
# one. However, if a random one can't be generated, return
# a 500 server error.
random_short = URL.generate_valid_short()
if random_short is None:
return 500
else:
url.short = random_short
# Grab the expiration field value. It's currently an unsable
# string value, so we need to parse it into a datetime object
# relative to right now.
expires = url_form.cleaned_data.get('expires')
# Determine what the expiration date is
if expires == URLForm.DAY:
url.expires = timezone.now() + timedelta(days=1)
elif expires == URLForm.WEEK:
url.expires = timezone.now() + timedelta(weeks=1)
elif expires == URLForm.MONTH:
url.expires = timezone.now() + timedelta(weeks=3)
elif expires == URLForm.CUSTOM:
url.expires = url_form.cleaned_data.get('expires_custom')
else:
pass # leave the field NULL
# Make sure that our new URL object is clean, then save it and
# let's redirect to view this baby.
url.full_clean()
url.save()
return url
def view(request, short):
class UserViewSet(viewsets.ModelViewSet):
"""
This view allows the user to "view details" about a URL. Note that they
do not need to be logged in to view this information.
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
# Get the current domain info
domain = "%ss://%s" % (request.scheme, request.META.get('HTTP_HOST')) + "/"
# Get the URL that is being requested
url = get_object_or_404(URL, short__iexact=short)
# Render view.html passing the specified URL and Domain to the template
return render(request, 'view.html', {
'url': url,
'domain': domain,
})
@login_required
def edit(request, short):
class URLViewSet(viewsets.ModelViewSet):
"""
This view allows a logged in user to edit the details of a Go link that they
own. They can modify any value that they wish. If `short` is modified then
we will need to create a new link and copy over stats from the previous.
API endpoint that handles creation/read/update/deletion of URL objects.
"""
# Do not allow unapproved users to edit links
if not request.user.registereduser.approved:
if request.user.registereduser.blocked:
return render(request, 'banned.html')
else:
return render(request, 'not_registered.html')
# Get the URL that is going to be edited
url = get_object_or_404(URL, short__iexact=short)
# If the RegisteredUser is the owner of the URL
if url.owner == request.user.registereduser:
# If a POST request is received, then the user has submitted a form and it's
# time to parse the form and edit that URL object
if request.method == 'POST':
# Now we initialize the form again but this time we have the POST
# request
url_form = EditForm(
request.POST, host=request.META.get('HTTP_HOST'))
# Make a copy of the old URL
copy = url
# Remove the old one
url.delete()
# Django will check the form to make sure it's valid
if url_form.is_valid():
# If the short changed then we need to create a new object and
# migrate some data over
if url_form.cleaned_data.get('short').strip() != copy.short:
# Parse the form and create a new URL object
res = post(request, url_form)
# If there is a 500 error returned, handle it
if res == 500:
return HttpResponseServerError(render(request, '500.html'))
# We can procede with the editing process
else:
# Migrate clicks data
res.clicks = copy.clicks
res.qrclicks = copy.qrclicks
res.socialclicks = copy.socialclicks
# Save the new URL
res.save()
# Redirect to the shiny new *edited URL
return redirect('view', res.short)
# The short was not edited and thus, we can directly edit the url
else:
if url_form.cleaned_data.get('destination').strip() != copy.destination:
copy.destination = url_form.cleaned_data.get(
'destination').strip()
copy.save()
# Grab the expiration field value. It's currently an unsable
# string value, so we need to parse it into a datetime object
# relative to right now.
expires = url_form.cleaned_data.get('expires')
# Determine what the expiration date is
if expires == URLForm.DAY:
edited_expires = timezone.now() + timedelta(days=1)
elif expires == URLForm.WEEK:
edited_expires = timezone.now() + timedelta(weeks=1)
elif expires == URLForm.MONTH:
edited_expires = timezone.now() + timedelta(weeks=3)
elif expires == URLForm.CUSTOM:
edited_expires = url_form.cleaned_data.get(
'expires_custom')
else:
pass # leave the field NULL
if edited_expires != copy.expires:
copy.expires = edited_expires
copy.save()
# Redirect to the shiny new *edited URL
return redirect('view', copy.short)
# Else, there is an error, redisplay the form with the validation errors
else:
# Render index.html passing the form to the template
return render(request, 'core/edit_link.html', {
'form': url_form
})
permission_classes = (CrudPermission,)
serializer_class = URLSerializer
def get_queryset(self):
"""
Return all objects that the current user owns unless they are admin in
which they get everything
"""
if not self.request.user.is_staff:
user = self.request.user.registereduser
return URL.objects.filter(owner=user)
else:
# Initial data set here
if url.expires != None:
# Initialize a URL form with an expire date
url_form = EditForm(host=request.META.get('HTTP_HOST'), initial={
'destination': url.destination,
'short': url.short,
'expires': 'Custom Date',
'expires_custom': url.expires
}) # unbound form
else:
# Initialize a URL form without an expire date
url_form = EditForm(host=request.META.get('HTTP_HOST'), initial={
'destination': url.destination,
'short': url.short,
'expires': 'Never',
}) # unbound form
# Render index.html passing the form to the template
return render(request, 'core/edit_link.html', {
'form': url_form
})
else:
# do not allow them to edit
raise PermissionDenied()
@login_required
def delete(request, short):
"""
This view deletes a URL if you have the permission to. User must be
logged in and registered, and must also be the owner of the URL.
"""
# Do not allow unapproved users to delete links
if not request.user.registereduser.approved:
return render(request, 'not_registered.html')
# Get the URL that is going to be deleted
url = get_object_or_404(URL, short__iexact=short)
# If the RegisteredUser is the owner of the URL
if url.owner == request.user.registereduser:
# remove the URL
url.delete()
# redirect to my_links
return redirect('my_links')
else:
# do not allow them to delete
raise PermissionDenied()
@login_required
def signup(request):
"""
This view presents the user with a registration form. You can register
yourself.
"""
# Do not display signup page to registered or approved users
if request.user.registereduser.blocked:
return render(request, 'banned.html')
elif request.user.registereduser.approved:
return redirect('/')
elif request.user.registereduser.registered:
return redirect('registered')
# Initialize our signup form
signup_form = SignupForm(
request,
initial={
'full_name': request.user.first_name + " " + request.user.last_name
}
)
# Set the full_name field to readonly since CAS will fill that in for them
signup_form.fields['full_name'].widget.attrs['readonly'] = 'readonly'
# If a POST request is received, then the user has submitted a form and it's
# time to parse the form and create a new RegisteredUser
if request.method == 'POST':
# Now we initialize the form again but this time we have the POST
# request
signup_form = SignupForm(
request, request.POST, instance=request.user.registereduser,
initial={
'full_name': request.user.first_name + " " + request.user.last_name
}
)
# set the readonly flag again for good measure
signup_form.fields['full_name'].widget.attrs['readonly'] = 'readonly'
# Django will check the form to make sure it's valid
if signup_form.is_valid():
# Grab data from the form and store into variables
description = signup_form.cleaned_data.get('description')
full_name = signup_form.cleaned_data.get('full_name')
organization = signup_form.cleaned_data.get('organization')
# Only send mail if we've defined the mailserver
if settings.EMAIL_HOST and settings.EMAIL_PORT:
user_mail = request.user.username + settings.EMAIL_DOMAIN
# Email sent to notify Admins
to_admin = EmailMessage(
'Signup from %s' % (request.user.registereduser.user),
######################
"""
%s signed up at %s\n\n
Username: %s\n
Organization: %s\n\n
Message: %s\n\n
You can contact the user directly by replying to this email or reply all to contact the user and notify the mailing list.\n
Please head to go.gmu.edu/manage to approve or deny this application.'
"""
% (
str(full_name), str(timezone.now()).strip(),
str(request.user.registereduser.user), str(organization),
str(description)
),
######################
settings.EMAIL_FROM,
[settings.EMAIL_TO],
reply_to=[user_mail]
)
to_admin.send()
# Confirmation email sent to Users
send_mail(
'We have received your Go application!',
######################
"""
Hey there %s,\n\n
The Go admins have received your application and are currently in the process of reviewing it.\n\n
You will receive another email when you have been approved.\n\n
- Go Admins
"""
% (str(full_name)),
######################
settings.EMAIL_FROM,
[user_mail]
)
# Make sure that our new RegisteredUser object is clean, then save
# it and let's redirect to tell the user they have registered.
signup_form.save()
return redirect('registered')
# render signup.html passing along the form and the current registered
# status
return render(request, 'core/signup.html', {
'form': signup_form,
'registered': False,
})
def redirection(request, short):
"""
This view redirects a user based on the short URL they requested.
"""
# Get the current domain info
domain = "%s://%s" % (request.scheme, request.META.get('HTTP_HOST')) + "/"
# Get the URL object that relates to the requested Go link
url = get_object_or_404(URL, short__iexact=short)
# Increment our clicks by one
url.clicks += 1
# Get the URL short link
doesExist = URL.objects.get(short__iexact=short)
# Checks to see if the link exists, if not we 404 the user.
if doesExist.destination is None:
return redirect('go/404.html')
# If the user is trying to make a Go link to itself, we 404 them
if url.destination == domain + short:
return redirect('404.html')
# If the user is coming from a QR request then increment qrclicks
if 'qr' in request.GET:
url.qrclicks += 1
# If the user is coming from a social media request then increment qrclicks
if 'social' in request.GET:
url.socialclicks += 1
# Save our data and redirect the user towards their destination
url.save()
return redirect(url.destination)
def staff_member_required(view_func, redirect_field_name=REDIRECT_FIELD_NAME, login_url='/'):
"""
Decorator function for views that checks that the user is logged in and is
a staff member, displaying the login page if necessary.
"""
return user_passes_test(
lambda u: u.is_active and u.is_staff,
login_url=login_url,
redirect_field_name=redirect_field_name
)(view_func)
@staff_member_required
def useradmin(request):
"""
This view is a simplified admin panel, so that staff don't need to log in
to approve links
"""
# If we receive a POST request
if request.POST:
# Get a list of the potential victims (users)
userlist = request.POST.getlist('username')
# If we're approving users
if '_approve' in request.POST:
for name in userlist:
to_approve = RegisteredUser.objects.get(
user__username__exact=name)
to_approve.approved = True
to_approve.save()
# Send an email letting them know they are approved
if settings.EMAIL_HOST and settings.EMAIL_PORT:
user_mail = to_approve.user.username + settings.EMAIL_DOMAIN
send_mail(
'Your Account has been Approved!',
######################
'Hey there %s,\n\n'
'The Go admins have reviewed your application and have '
'approved you to use Go!\n\n'
'Head over to go.gmu.edu to create your first address.\n\n'
'- Go Admins'
% (str(to_approve.full_name)),
######################
settings.EMAIL_FROM,
[user_mail]
)
# If we're denying users
elif '_deny' in request.POST:
for name in userlist:
to_deny = RegisteredUser.objects.get(
user__username__exact=name)
if settings.EMAIL_HOST and settings.EMAIL_PORT:
user_mail = to_deny.user.username + settings.EMAIL_DOMAIN
# Send an email letting them know they are denied
send_mail(
'Your Account has been Denied!',
######################
'Hey there %s,\n\n'
'The Go admins have reviewed your application and have '
'decided to not approve you to use Go.\n\n'
'Please reach out to srct@gmu.edu to appeal '
'this decision.\n\n'
'- Go Admins'
% (str(to_deny.full_name)),
######################
settings.EMAIL_FROM,
[user_mail]
)
# Delete their associated RegisteredUsers
to_deny.user.delete()
return HttpResponseRedirect('manage')
# If we're blocking users
elif '_block' in request.POST:
for name in userlist:
to_block = RegisteredUser.objects.get(
user__username__exact=name)
if settings.EMAIL_HOST and settings.EMAIL_PORT:
user_mail = to_block.user.username + settings.EMAIL_DOMAIN
send_mail(
'Your Account has been Blocked!',
######################
'Hey there %s,\n\n'
'The Go admins have reviewed your application and have '
'blocked you from using Go.\n\n'
'Please reach out to srct@gmu.edu to appeal '
'this decision.\n\n'
'- Go Admins'
% (str(to_block.full_name)),
######################
settings.EMAIL_FROM,
[user_mail]
)
to_block.blocked = True
to_block.approved = False
to_block.registered = False