Merge branch 'feature/basicauth' into develop
This commit is contained in:
15
rowers/authentication.py
Normal file
15
rowers/authentication.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from rest_framework.authentication import BaseAuthentication
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rowers.models import APIKey
|
||||
|
||||
class APIKeyAuthentication(BaseAuthentication):
|
||||
def authenticate(self, request):
|
||||
api_key = request.META.get('HTTP_AUTHORIZATION')
|
||||
if not api_key:
|
||||
return None
|
||||
try:
|
||||
api_key = APIKey.objects.get(key=api_key, is_active=True)
|
||||
except APIKey.DoesNotExist:
|
||||
raise AuthenticationFailed('Invalid API key')
|
||||
|
||||
return (api_key.user, None)
|
||||
@@ -27,7 +27,7 @@ from django.contrib.admin.widgets import AdminDateWidget, AdminTimeWidget, Admin
|
||||
import os
|
||||
import json
|
||||
import ssl
|
||||
|
||||
import secrets
|
||||
|
||||
import re
|
||||
import pytz
|
||||
@@ -923,7 +923,18 @@ class CoachingGroup(models.Model):
|
||||
return Rower.objects.filter(mycoachgroup=self)
|
||||
|
||||
# Extension of User with rowing specific data
|
||||
class APIKey(models.Model):
|
||||
key = models.CharField(max_length=50, unique=True, default=secrets.token_urlsafe)
|
||||
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username}:{self.key}"
|
||||
|
||||
def regenerate_key(self):
|
||||
self.key = secrets.token_urlsafe()
|
||||
self.save()
|
||||
|
||||
class Rower(models.Model):
|
||||
adaptivetypes = mytypes.adaptivetypes
|
||||
|
||||
@@ -7,37 +7,38 @@
|
||||
{% block main %}
|
||||
<h1>Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||
|
||||
<p>You are currently connected to:
|
||||
{% if rower.c2token is not None and rower.c2token != '' %}
|
||||
Concept2 Logbook,
|
||||
{% endif %}
|
||||
{% if rower.nktoken is not None and rower.nktoken != '' %}
|
||||
NK Logbook,
|
||||
{% endif %}
|
||||
{% if rower.sporttrackstoken is not None and rower.sporttrackstoken != '' %}
|
||||
SportTracks,
|
||||
{% endif %}
|
||||
{% if rower.tptoken is not None and rower.tptoken != '' %}
|
||||
TrainingPeaks,
|
||||
{% endif %}
|
||||
{% if rower.polartoken is not None and rower.polartoken != '' %}
|
||||
Polar,
|
||||
{% endif %}
|
||||
{% if rower.garmintoken is not None and rower.garmintoken != '' %}
|
||||
Garmin Connect,
|
||||
{% endif %}
|
||||
{% if rower.stravatoken is not None and rower.stravatoken != '' %}
|
||||
Strava,
|
||||
{% endif %}
|
||||
{% if rower.rp3token is not None and rower.rp3token != '' %}
|
||||
RP3
|
||||
{% endif %}
|
||||
{% if rower.rojabo_token is not None and rower.rojabo_token != '' %}
|
||||
Rojabo
|
||||
{% endif %}
|
||||
|
||||
</p>
|
||||
|
||||
<ul class="main-content">
|
||||
<li class="grid_2">
|
||||
<p>You are currently connected to:
|
||||
{% if rower.c2token is not None and rower.c2token != '' %}
|
||||
Concept2 Logbook,
|
||||
{% endif %}
|
||||
{% if rower.nktoken is not None and rower.nktoken != '' %}
|
||||
NK Logbook,
|
||||
{% endif %}
|
||||
{% if rower.sporttrackstoken is not None and rower.sporttrackstoken != '' %}
|
||||
SportTracks,
|
||||
{% endif %}
|
||||
{% if rower.tptoken is not None and rower.tptoken != '' %}
|
||||
TrainingPeaks,
|
||||
{% endif %}
|
||||
{% if rower.polartoken is not None and rower.polartoken != '' %}
|
||||
Polar,
|
||||
{% endif %}
|
||||
{% if rower.garmintoken is not None and rower.garmintoken != '' %}
|
||||
Garmin Connect,
|
||||
{% endif %}
|
||||
{% if rower.stravatoken is not None and rower.stravatoken != '' %}
|
||||
Strava,
|
||||
{% endif %}
|
||||
{% if rower.rp3token is not None and rower.rp3token != '' %}
|
||||
RP3
|
||||
{% endif %}
|
||||
{% if rower.rojabo_token is not None and rower.rojabo_token != '' %}
|
||||
Rojabo
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if form.errors %}
|
||||
<p style="color: red;">
|
||||
Please correct the error{{ form.errors|pluralize }} below.
|
||||
@@ -71,24 +72,57 @@
|
||||
a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete
|
||||
check box.
|
||||
</p>
|
||||
<p>Click on one of the icons below to connect to the service of your
|
||||
choice or to renew the authorization.</p>
|
||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
|
||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
{% if grants %}
|
||||
<h2>Applications</h2>
|
||||
<table width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Application</th>
|
||||
<th>Scope</th>
|
||||
<th>Revoke</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for grant in grants %}
|
||||
<tr>
|
||||
<td>{{ grant.application }}</td>
|
||||
<td>{{ grant.scope }}</td>
|
||||
<td>
|
||||
<a href="/rowers/me/revokeapp/{{ grant.application.id }}/">Revoke</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<h2>API Key</h2>
|
||||
<p>{{ apikey }}</p>
|
||||
<p>
|
||||
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
|
||||
</p>
|
||||
This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<p>Click on one of the icons below to connect to the service of your
|
||||
choice or to renew the authorization.</p>
|
||||
<p><a href="/rowers/me/stravaauthorize/"><img src="/static/img/ConnectWithStrava.png" alt="connect with strava" width="120"></a></p>
|
||||
<p><a href="/rowers/me/c2authorize/"><img src="/static/img/blueC2logo.png" alt="connect with Concept2" width="120"></a></p>
|
||||
<p><a href="/rowers/me/nkauthorize/"><img src="/static/img/NKLiNKLogbook.png" alt="connect with NK Logbook" width="120"></a></p>
|
||||
<p><a href="/rowers/me/sporttracksauthorize/"><img src="/static/img/sporttracks-button.png" alt="connect with SportTracks" width="120"></a></p>
|
||||
<p><a href="/rowers/me/polarauthorize/"><img src="/static/img/Polar_connectwith_btn_white.png"
|
||||
alt="connect with Polar" width="130"></a></p>
|
||||
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
|
||||
alt="connect with Polar" width="130"></a></p>
|
||||
|
||||
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
|
||||
alt="connect with Garmin" width="130"></a></p>
|
||||
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
|
||||
alt="connect with RP3" width="130"></a></p>
|
||||
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
|
||||
alt="connect with Rojabo" width="130"></a></p>
|
||||
|
||||
<p><a href="/rowers/me/tpauthorize/"><img src="/static/img/TP_logo_horz_2_color.png"
|
||||
alt="connect with Polar" width="130"></a></p>
|
||||
|
||||
<p><a href="/rowers/me/garminauthorize"><img src="/static/img/garmin_badge_130.png"
|
||||
alt="connect with Garmin" width="130"></a></p>
|
||||
<p><a href="/rowers/me/rp3authorize"><img src="/static/img/logo-rp3-full-black.png"
|
||||
alt="connect with RP3" width="130"></a></p>
|
||||
<p><a href="/rowers/me/rojaboauthorize"><img src="/static/img/rojabo.png"
|
||||
alt="connect with Rojabo" width="130"></a></p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -116,6 +116,14 @@
|
||||
</table>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="grid_2">
|
||||
<h2>API Key</h2>
|
||||
<p>{{ apikey }}</p>
|
||||
<p>
|
||||
<a href="/rowers/me/regenerateapikey/">Regenerate</a>
|
||||
</p>
|
||||
This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import json
|
||||
|
||||
from rowers.ownapistuff import *
|
||||
from rowers.views.apiviews import *
|
||||
from rowers.models import APIKey
|
||||
|
||||
class OwnApi(TestCase):
|
||||
def setUp(self):
|
||||
@@ -44,9 +45,8 @@ class OwnApi(TestCase):
|
||||
self.password = faker.word()
|
||||
self.u.set_password(self.password)
|
||||
self.u.save()
|
||||
|
||||
self.factory = APIRequestFactory()
|
||||
|
||||
self.apikey = APIKey.objects.create(user=self.u)
|
||||
|
||||
def test_strokedataform(self):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
@@ -210,6 +210,75 @@ class OwnApi(TestCase):
|
||||
response = strokedata_tcx(request)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
def test_strokedataform_rowingdata(self):
|
||||
|
||||
url = reverse('strokedata_rowingdata')
|
||||
|
||||
filename = 'rowers/tests/testdata/testdata.csv'
|
||||
f = open(filename, 'rb')
|
||||
|
||||
# Use Basic Auth header (alternative if using a custom view)
|
||||
credentials = f"{self.u.username}:{self.password}"
|
||||
base64_credentials = base64.b64encode(credentials.encode()).decode()
|
||||
|
||||
# Use Basic Auth header
|
||||
headers = {
|
||||
"HTTP_AUTHORIZATION": f"Basic {base64_credentials}"
|
||||
}
|
||||
|
||||
form_data = {
|
||||
"workouttype": "rower",
|
||||
"boattype": "1x",
|
||||
"notes": "A test file upload",
|
||||
}
|
||||
|
||||
# Send POST request
|
||||
response = self.client.post(
|
||||
url,
|
||||
{"file": f, **form_data},
|
||||
format="multipart", # Ensure multipart/form-data is used
|
||||
**headers, # Optional if login doesn't suffice
|
||||
)
|
||||
|
||||
f.close()
|
||||
|
||||
# Assertions
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
|
||||
def test_strokedataform_rowingdata_apikey(self):
|
||||
|
||||
url = reverse('strokedata_rowingdata_apikey')
|
||||
|
||||
filename = 'rowers/tests/testdata/testdata.csv'
|
||||
f = open(filename, 'rb')
|
||||
|
||||
# Use API Key header
|
||||
headers = {
|
||||
"HTTP_AUTHORIZATION": self.apikey.key,
|
||||
}
|
||||
|
||||
form_data = {
|
||||
"workouttype": "rower",
|
||||
"boattype": "1x",
|
||||
"notes": "A test file upload",
|
||||
}
|
||||
|
||||
# Send POST request
|
||||
response = self.client.post(
|
||||
url,
|
||||
{"file": f, **form_data},
|
||||
format="multipart", # Ensure multipart/form-data is used
|
||||
**headers, # Optional if login doesn't suffice
|
||||
)
|
||||
|
||||
f.close()
|
||||
|
||||
# Assertions
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
|
||||
|
||||
def test_strokedataform_empty(self):
|
||||
login = self.c.login(username=self.u.username, password=self.password)
|
||||
self.assertTrue(login)
|
||||
|
||||
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
BIN
rowers/tests/testdata/testdata.tcx.gz
vendored
Binary file not shown.
@@ -254,6 +254,10 @@ urlpatterns = [
|
||||
name='strokedatajson_v3'),
|
||||
re_path(r'^api/TCX/workouts/$', views.strokedata_tcx,
|
||||
name='strokedata_tcx'),
|
||||
re_path(r'^api/rowingdata/workouts/$', views.strokedata_rowingdata,
|
||||
name='strokedata_rowingdata'),
|
||||
re_path(r'^api/rowingdata/$', views.strokedata_rowingdata_apikey,
|
||||
name='strokedata_rowingdata_apikey'),
|
||||
re_path(r'^api/courses/$', views.course_list, name='course_list'),
|
||||
re_path(r'^api/courses/(?P<id>\d+)/$', views.get_crewnerd_kml, name='get_crewnerd_kml'),
|
||||
re_path(r'^api/courses/kml/liked/$', views.get_crewnerd_liked, name='get_crewnerd_liked'),
|
||||
@@ -733,6 +737,7 @@ urlpatterns = [
|
||||
re_path(r'^me/request/$', views.manager_requests_view,
|
||||
name='manager_requests_view'),
|
||||
re_path(r'^me/edit/$', views.rower_edit_view, name='rower_edit_view'),
|
||||
re_path(r'^me/regenerateapikey/$', views.rower_regenerate_apikey, name='rower_regenerate_apikey'),
|
||||
re_path(r'^me/edit/user/(?P<userid>\d+)/$',
|
||||
views.rower_edit_view, name='rower_edit_view'),
|
||||
re_path(r'^me/preferences/$', views.rower_prefs_view,
|
||||
|
||||
@@ -22,6 +22,8 @@ import rowingdata.tcxtools as tcxtools
|
||||
from rowingdata import TCXParser, rowingdata
|
||||
import arrow
|
||||
|
||||
import base64
|
||||
|
||||
class XMLParser(BaseParser):
|
||||
media_type = "application/xml"
|
||||
|
||||
@@ -425,6 +427,110 @@ def get_crewnerd_liked(request):
|
||||
|
||||
# Stroke data views
|
||||
|
||||
@csrf_exempt
|
||||
@logged_in_or_basicauth()
|
||||
#@api_view(["POST"])
|
||||
#@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True)
|
||||
#@permission_classes([IsAuthenticated])
|
||||
def strokedata_rowingdata(request):
|
||||
"""
|
||||
Upload a .csv file (rowingdata standard) through API, using Basic Auth
|
||||
"""
|
||||
r = getrower(request.user)
|
||||
if r.rowerplan == 'freecoach':
|
||||
return HttpResponseNotAllowed("This endpoint is for users, not for free coach accounts")
|
||||
|
||||
if request.method != 'POST':
|
||||
return HttpResponseNotAllowed("Method not supported") # pragma: no cover
|
||||
|
||||
form = DocumentsForm(request.POST, request.FILES)
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest(json.dumps(form.errors))
|
||||
|
||||
f = form.cleaned_data['file']
|
||||
if f is None:
|
||||
return HttpResponseBadRequest("Missing file")
|
||||
|
||||
filename, completefilename = handle_uploaded_file(f)
|
||||
|
||||
uploadoptions = {
|
||||
'secret': settings.UPLOAD_SERVICE_SECRET,
|
||||
'user': r.user.id,
|
||||
'file': completefilename,
|
||||
'workouttype': form.cleaned_data['workouttype'],
|
||||
'boattype': form.cleaned_data['boattype'],
|
||||
'title': form.cleaned_data['title'],
|
||||
'rpe': form.cleaned_data['rpe'],
|
||||
'notes': form.cleaned_data['notes']
|
||||
}
|
||||
|
||||
url = settings.UPLOAD_SERVICE_URL
|
||||
|
||||
_ = myqueue(queuehigh,
|
||||
handle_request_post,
|
||||
url,
|
||||
uploadoptions)
|
||||
response = JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
}
|
||||
)
|
||||
response.status_code = 201
|
||||
return response
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@api_view(["POST"])
|
||||
@permission_required('rower.is_not_freecoach', fn=get_user_by_userid, raise_exception=True)
|
||||
@permission_classes([IsAuthenticated])
|
||||
def strokedata_rowingdata_apikey(request):
|
||||
"""
|
||||
Upload a .csv file (rowingdata standard) through API, using
|
||||
"""
|
||||
|
||||
r = getrower(request.user)
|
||||
if r.rowerplan == 'freecoach':
|
||||
return HttpResponseNotAllowed("This endpoint is for users, not for free coach accounts")
|
||||
|
||||
if request.method != 'POST':
|
||||
return HttpResponseNotAllowed("Method not supported")
|
||||
|
||||
form = DocumentsForm(request.POST, request.FILES)
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest(json.dumps(form.errors))
|
||||
|
||||
f = form.cleaned_data['file']
|
||||
if f is None:
|
||||
return HttpResponseBadRequest("Missing file")
|
||||
|
||||
filename, completefilename = handle_uploaded_file(f)
|
||||
|
||||
uploadoptions = {
|
||||
'secret': settings.UPLOAD_SERVICE_SECRET,
|
||||
'user': r.user.id,
|
||||
'file': completefilename,
|
||||
'workouttype': form.cleaned_data['workouttype'],
|
||||
'boattype': form.cleaned_data['boattype'],
|
||||
'title': form.cleaned_data['title'],
|
||||
'rpe': form.cleaned_data['rpe'],
|
||||
'notes': form.cleaned_data['notes']
|
||||
}
|
||||
|
||||
url = settings.UPLOAD_SERVICE_URL
|
||||
|
||||
_ = myqueue(queuehigh,
|
||||
handle_request_post,
|
||||
url,
|
||||
uploadoptions)
|
||||
response = JsonResponse(
|
||||
{
|
||||
"status": "success",
|
||||
}
|
||||
)
|
||||
response.status_code = 201
|
||||
return response
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
#@login_required()
|
||||
@api_view(["POST"])
|
||||
|
||||
@@ -93,7 +93,8 @@ from django.http import (
|
||||
HttpResponse, HttpResponseRedirect,
|
||||
JsonResponse,
|
||||
HttpResponseForbidden, HttpResponseNotAllowed,
|
||||
HttpResponseNotFound, Http404
|
||||
HttpResponseNotFound, Http404,
|
||||
HttpResponseBadRequest,
|
||||
)
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from rowers.forms import (
|
||||
@@ -163,7 +164,7 @@ from rowers.models import (
|
||||
StandardCollection, CourseStandard,
|
||||
VirtualRaceFollower, TombStone, InstantPlan,
|
||||
PlannedSessionStep,InStrokeAnalysis, ForceCurveAnalysis, SyncRecord,
|
||||
UserMessage,
|
||||
UserMessage,APIKey,
|
||||
)
|
||||
from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerForm,
|
||||
RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
|
||||
@@ -302,6 +303,142 @@ from rowers.weather import get_wind_data, get_airport_code, get_metar_data
|
||||
|
||||
from oauth2_provider.models import Application, Grant, AccessToken
|
||||
|
||||
import base64
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.auth import authenticate, login
|
||||
|
||||
def view_or_apikey(view, request, test_func, realm = "", *args, **kwargs):
|
||||
if test_func(request.user):
|
||||
return view(request, *args, **kwargs)
|
||||
|
||||
if 'Authorization' in request.META:
|
||||
api_key = request.META.get('Authorization')
|
||||
if api_key:
|
||||
try:
|
||||
api_key = APIKey.objects.get(key=api_key, is_active=True)
|
||||
except APIKey.DoesNotExist:
|
||||
raise AuthenticationFailed('Invalid API key')
|
||||
|
||||
login(request, api_key.user, backend='django.contrib.auth.backends.ModelBackend')
|
||||
request.user = api_key.user
|
||||
return view(request, *args, **kwargs)
|
||||
|
||||
response = HttpResponse()
|
||||
response.status_code = 401
|
||||
response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
|
||||
return response
|
||||
|
||||
#############################################################################
|
||||
#
|
||||
def view_or_basicauth(view, request, test_func, realm = "", *args, **kwargs):
|
||||
"""
|
||||
This is a helper function used by both 'logged_in_or_basicauth' and
|
||||
'has_perm_or_basicauth' that does the nitty of determining if they
|
||||
are already logged in or if they have provided proper http-authorization
|
||||
and returning the view if all goes well, otherwise responding with a 401.
|
||||
"""
|
||||
if test_func(request.user):
|
||||
# Already logged in, just return the view.
|
||||
#
|
||||
return view(request, *args, **kwargs)
|
||||
|
||||
# They are not logged in. See if they provided login credentials
|
||||
#
|
||||
if 'HTTP_AUTHORIZATION' in request.META:
|
||||
auth = request.META['HTTP_AUTHORIZATION'].split()
|
||||
if len(auth) == 2:
|
||||
# NOTE: We are only support basic authentication for now.
|
||||
#
|
||||
if auth[0].lower() == "basic":
|
||||
uname, passwd = base64.b64decode(auth[1]).decode("utf-8").split(':')
|
||||
user = authenticate(username=uname, password=passwd)
|
||||
if user is not None:
|
||||
if user.is_active:
|
||||
login(request, user)
|
||||
request.user = user
|
||||
return view(request, *args, **kwargs)
|
||||
|
||||
# Either they did not provide an authorization header or
|
||||
# something in the authorization attempt failed. Send a 401
|
||||
# back to them to ask them to authenticate.
|
||||
#
|
||||
response = HttpResponse()
|
||||
response.status_code = 401
|
||||
response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
|
||||
return response
|
||||
|
||||
#############################################################################
|
||||
#
|
||||
def logged_in_or_apikey(realm = ""):
|
||||
def view_decorator(func):
|
||||
def wrapper(request, *args, **kwargs):
|
||||
return view_or_apikey(func, request,
|
||||
lambda u: u.is_authenticated,
|
||||
realm, *args, **kwargs)
|
||||
return wrapper
|
||||
return view_decorator
|
||||
|
||||
def logged_in_or_basicauth(realm = ""):
|
||||
"""
|
||||
A simple decorator that requires a user to be logged in. If they are not
|
||||
logged in the request is examined for a 'authorization' header.
|
||||
|
||||
If the header is present it is tested for basic authentication and
|
||||
the user is logged in with the provided credentials.
|
||||
|
||||
If the header is not present a http 401 is sent back to the
|
||||
requestor to provide credentials.
|
||||
|
||||
The purpose of this is that in several django projects I have needed
|
||||
several specific views that need to support basic authentication, yet the
|
||||
web site as a whole used django's provided authentication.
|
||||
|
||||
The uses for this are for urls that are access programmatically such as
|
||||
by rss feed readers, yet the view requires a user to be logged in. Many rss
|
||||
readers support supplying the authentication credentials via http basic
|
||||
auth (and they do NOT support a redirect to a form where they post a
|
||||
username/password.)
|
||||
|
||||
Use is simple:
|
||||
|
||||
@logged_in_or_basicauth
|
||||
def your_view:
|
||||
...
|
||||
|
||||
You can provide the name of the realm to ask for authentication within.
|
||||
"""
|
||||
def view_decorator(func):
|
||||
def wrapper(request, *args, **kwargs):
|
||||
return view_or_basicauth(func, request,
|
||||
lambda u: u.is_authenticated,
|
||||
realm, *args, **kwargs)
|
||||
return wrapper
|
||||
return view_decorator
|
||||
|
||||
#############################################################################
|
||||
#
|
||||
def has_perm_or_basicauth(perm, realm = ""):
|
||||
"""
|
||||
This is similar to the above decorator 'logged_in_or_basicauth'
|
||||
except that it requires the logged in user to have a specific
|
||||
permission.
|
||||
|
||||
Use:
|
||||
|
||||
@logged_in_or_basicauth('asforums.view_forumcollection')
|
||||
def your_view:
|
||||
...
|
||||
|
||||
"""
|
||||
def view_decorator(func):
|
||||
def wrapper(request, *args, **kwargs):
|
||||
return view_or_basicauth(func, request,
|
||||
lambda u: u.has_perm(perm),
|
||||
realm, *args, **kwargs)
|
||||
return wrapper
|
||||
return view_decorator
|
||||
|
||||
|
||||
import django_rq
|
||||
queue = django_rq.get_queue('default')
|
||||
queuelow = django_rq.get_queue('low')
|
||||
|
||||
@@ -520,10 +520,19 @@ def rower_exportsettings_view(request, userid=0):
|
||||
}
|
||||
]
|
||||
|
||||
grants = AccessToken.objects.filter(user=request.user)
|
||||
try:
|
||||
apikey = APIKey.objects.get(user=request.user)
|
||||
except APIKey.DoesNotExist:
|
||||
apikey = APIKey.objects.create(user=request.user)
|
||||
|
||||
|
||||
return render(request, 'rower_exportsettings.html',
|
||||
{'form': form,
|
||||
'rower': r,
|
||||
'breadcrumbs': breadcrumbs,
|
||||
'grants': grants,
|
||||
'apikey': apikey.key,
|
||||
})
|
||||
|
||||
|
||||
@@ -632,6 +641,12 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
||||
userform = UserForm(instance=r.user)
|
||||
|
||||
grants = AccessToken.objects.filter(user=request.user)
|
||||
try:
|
||||
apikey = APIKey.objects.get(user=request.user)
|
||||
except APIKey.DoesNotExist:
|
||||
apikey = APIKey.objects.create(user=request.user)
|
||||
|
||||
|
||||
return render(request, 'rower_form.html',
|
||||
{
|
||||
'teams': get_my_teams(request.user),
|
||||
@@ -640,8 +655,24 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
||||
'userform': userform,
|
||||
'accountform': accountform,
|
||||
'rower': r,
|
||||
'apikey': apikey.key,
|
||||
})
|
||||
|
||||
@login_required()
|
||||
def rower_regenerate_apikey(request):
|
||||
try:
|
||||
referer = request.META['HTTP_REFERER']
|
||||
except KeyError:
|
||||
referer = '/rowers/me/edit/'
|
||||
try:
|
||||
apikey = APIKey.objects.get(user=request.user)
|
||||
except APIKey.DoesNotExist:
|
||||
apikey = APIKey.objects.create(user=request.user)
|
||||
|
||||
apikey.regenerate_key()
|
||||
|
||||
return HttpResponseRedirect(referer)
|
||||
|
||||
#simple initial settings page
|
||||
@login_required()
|
||||
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
|
||||
|
||||
@@ -4931,7 +4931,6 @@ def workout_upload_api(request):
|
||||
post_data = {k: q.getlist(k) if len(
|
||||
q.getlist(k)) > 1 else v for k, v in q.items()}
|
||||
|
||||
|
||||
# only allow local host
|
||||
hostt = request.get_host().split(':')
|
||||
if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']:
|
||||
|
||||
@@ -481,10 +481,11 @@ REST_FRAMEWORK = {
|
||||
# 'rest_framework.permissions.DjangoModelPermissions'
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
#'rest_framework.authentication.BasicAuthentication',
|
||||
# 'rest_framework.authentication.SessionAuthentication',
|
||||
# 'rest_framework.authentication.TokenAuthentication',
|
||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||
'rowers.authentication.APIKeyAuthentication',
|
||||
),
|
||||
'PAGE_SIZE': 20,
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
|
||||
Reference in New Issue
Block a user