adding APIKey method
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 os
|
||||||
import json
|
import json
|
||||||
import ssl
|
import ssl
|
||||||
|
import secrets
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import pytz
|
import pytz
|
||||||
@@ -923,7 +923,18 @@ class CoachingGroup(models.Model):
|
|||||||
return Rower.objects.filter(mycoachgroup=self)
|
return Rower.objects.filter(mycoachgroup=self)
|
||||||
|
|
||||||
# Extension of User with rowing specific data
|
# 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):
|
class Rower(models.Model):
|
||||||
adaptivetypes = mytypes.adaptivetypes
|
adaptivetypes = mytypes.adaptivetypes
|
||||||
|
|||||||
@@ -116,6 +116,14 @@
|
|||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import json
|
|||||||
|
|
||||||
from rowers.ownapistuff import *
|
from rowers.ownapistuff import *
|
||||||
from rowers.views.apiviews import *
|
from rowers.views.apiviews import *
|
||||||
|
from rowers.models import APIKey
|
||||||
|
|
||||||
class OwnApi(TestCase):
|
class OwnApi(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -45,7 +46,7 @@ class OwnApi(TestCase):
|
|||||||
self.u.set_password(self.password)
|
self.u.set_password(self.password)
|
||||||
self.u.save()
|
self.u.save()
|
||||||
self.factory = APIRequestFactory()
|
self.factory = APIRequestFactory()
|
||||||
|
self.apikey = APIKey.objects.create(user=self.u)
|
||||||
|
|
||||||
def test_strokedataform(self):
|
def test_strokedataform(self):
|
||||||
login = self.c.login(username=self.u.username, password=self.password)
|
login = self.c.login(username=self.u.username, password=self.password)
|
||||||
@@ -245,6 +246,38 @@ class OwnApi(TestCase):
|
|||||||
self.assertEqual(response.status_code, 201)
|
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):
|
def test_strokedataform_empty(self):
|
||||||
login = self.c.login(username=self.u.username, password=self.password)
|
login = self.c.login(username=self.u.username, password=self.password)
|
||||||
|
|||||||
@@ -256,6 +256,8 @@ urlpatterns = [
|
|||||||
name='strokedata_tcx'),
|
name='strokedata_tcx'),
|
||||||
re_path(r'^api/rowingdata/workouts/$', views.strokedata_rowingdata,
|
re_path(r'^api/rowingdata/workouts/$', views.strokedata_rowingdata,
|
||||||
name='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/$', 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/(?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'),
|
re_path(r'^api/courses/kml/liked/$', views.get_crewnerd_liked, name='get_crewnerd_liked'),
|
||||||
@@ -735,6 +737,7 @@ urlpatterns = [
|
|||||||
re_path(r'^me/request/$', views.manager_requests_view,
|
re_path(r'^me/request/$', views.manager_requests_view,
|
||||||
name='manager_requests_view'),
|
name='manager_requests_view'),
|
||||||
re_path(r'^me/edit/$', views.rower_edit_view, name='rower_edit_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+)/$',
|
re_path(r'^me/edit/user/(?P<userid>\d+)/$',
|
||||||
views.rower_edit_view, name='rower_edit_view'),
|
views.rower_edit_view, name='rower_edit_view'),
|
||||||
re_path(r'^me/preferences/$', views.rower_prefs_view,
|
re_path(r'^me/preferences/$', views.rower_prefs_view,
|
||||||
|
|||||||
@@ -473,6 +473,58 @@ def strokedata_rowingdata(request):
|
|||||||
return response
|
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
|
@csrf_exempt
|
||||||
#@login_required()
|
#@login_required()
|
||||||
@api_view(["POST"])
|
@api_view(["POST"])
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ from rowers.models import (
|
|||||||
StandardCollection, CourseStandard,
|
StandardCollection, CourseStandard,
|
||||||
VirtualRaceFollower, TombStone, InstantPlan,
|
VirtualRaceFollower, TombStone, InstantPlan,
|
||||||
PlannedSessionStep,InStrokeAnalysis, ForceCurveAnalysis, SyncRecord,
|
PlannedSessionStep,InStrokeAnalysis, ForceCurveAnalysis, SyncRecord,
|
||||||
UserMessage,
|
UserMessage,APIKey,
|
||||||
)
|
)
|
||||||
from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerForm,
|
from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerForm,
|
||||||
RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
|
RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
|
||||||
@@ -307,6 +307,27 @@ import base64
|
|||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.contrib.auth import authenticate, login
|
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):
|
def view_or_basicauth(view, request, test_func, realm = "", *args, **kwargs):
|
||||||
@@ -348,6 +369,15 @@ def view_or_basicauth(view, request, test_func, realm = "", *args, **kwargs):
|
|||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
#
|
#
|
||||||
|
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 = ""):
|
def logged_in_or_basicauth(realm = ""):
|
||||||
"""
|
"""
|
||||||
A simple decorator that requires a user to be logged in. If they are not
|
A simple decorator that requires a user to be logged in. If they are not
|
||||||
|
|||||||
@@ -632,6 +632,12 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
|||||||
userform = UserForm(instance=r.user)
|
userform = UserForm(instance=r.user)
|
||||||
|
|
||||||
grants = AccessToken.objects.filter(user=request.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',
|
return render(request, 'rower_form.html',
|
||||||
{
|
{
|
||||||
'teams': get_my_teams(request.user),
|
'teams': get_my_teams(request.user),
|
||||||
@@ -640,8 +646,20 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""):
|
|||||||
'userform': userform,
|
'userform': userform,
|
||||||
'accountform': accountform,
|
'accountform': accountform,
|
||||||
'rower': r,
|
'rower': r,
|
||||||
|
'apikey': apikey.key,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required()
|
||||||
|
def rower_regenerate_apikey(request):
|
||||||
|
try:
|
||||||
|
apikey = APIKey.objects.get(user=request.user)
|
||||||
|
except APIKey.DoesNotExist:
|
||||||
|
apikey = APIKey.objects.create(user=request.user)
|
||||||
|
|
||||||
|
apikey.regenerate_key()
|
||||||
|
|
||||||
|
return HttpResponseRedirect(reverse('rower_edit_view'))
|
||||||
|
|
||||||
#simple initial settings page
|
#simple initial settings page
|
||||||
@login_required()
|
@login_required()
|
||||||
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
|
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
|
||||||
|
|||||||
@@ -485,6 +485,7 @@ REST_FRAMEWORK = {
|
|||||||
# 'rest_framework.authentication.SessionAuthentication',
|
# 'rest_framework.authentication.SessionAuthentication',
|
||||||
# 'rest_framework.authentication.TokenAuthentication',
|
# 'rest_framework.authentication.TokenAuthentication',
|
||||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||||
|
'rowers.authentication.APIKeyAuthentication',
|
||||||
),
|
),
|
||||||
'PAGE_SIZE': 20,
|
'PAGE_SIZE': 20,
|
||||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||||
|
|||||||
Reference in New Issue
Block a user