Session API
This commit is contained in:
parent
1491fc5de3
commit
cdb14a1e2c
5 changed files with 188 additions and 0 deletions
96
src/c3nav/api/api.py
Normal file
96
src/c3nav/api/api.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
from django.contrib.auth import login, logout
|
||||||
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
|
from django.middleware import csrf
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from rest_framework.authentication import SessionAuthentication
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from c3nav.api.models import Token
|
||||||
|
|
||||||
|
|
||||||
|
class SessionViewSet(ViewSet):
|
||||||
|
"""
|
||||||
|
Session for Login, Logout, etc…
|
||||||
|
Don't forget to set X-Csrftoken for POST requests!
|
||||||
|
|
||||||
|
/login – POST with fields token or username and password to log in
|
||||||
|
/get_token – POST with fields username and password to get a login token
|
||||||
|
/logout - POST to log out
|
||||||
|
"""
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
return Response({
|
||||||
|
'is_authenticated': request.user.is_authenticated,
|
||||||
|
'csrf_token': csrf.get_token(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'])
|
||||||
|
def login(self, request, *args, **kwargs):
|
||||||
|
# django-rest-framework doesn't do this for logged out requests
|
||||||
|
SessionAuthentication().enforce_csrf(request)
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return ParseError(_('Log out first.'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = request.json_body
|
||||||
|
except AttributeError:
|
||||||
|
data = request.POST
|
||||||
|
|
||||||
|
if 'token' in data:
|
||||||
|
try:
|
||||||
|
token = Token.get_by_token(data['token'])
|
||||||
|
except Token.DoesNotExist:
|
||||||
|
raise PermissionDenied(_('This token does not exist or is no longer valid.'))
|
||||||
|
user = token.user
|
||||||
|
elif 'username' in data:
|
||||||
|
form = AuthenticationForm(request, data=data)
|
||||||
|
if not form.is_valid():
|
||||||
|
raise ParseError(form.errors)
|
||||||
|
user = form.user_cache
|
||||||
|
else:
|
||||||
|
raise ParseError(_('You need to send a token or username and password.'))
|
||||||
|
|
||||||
|
login(request, user)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'detail': _('Login successful.'),
|
||||||
|
'csrf_token': csrf.get_token(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'])
|
||||||
|
def get_token(self, request, *args, **kwargs):
|
||||||
|
# django-rest-framework doesn't do this for logged out requests
|
||||||
|
SessionAuthentication().enforce_csrf(request)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = request.json_body
|
||||||
|
except AttributeError:
|
||||||
|
data = request.POST
|
||||||
|
|
||||||
|
form = AuthenticationForm(request, data=data)
|
||||||
|
if not form.is_valid():
|
||||||
|
raise ParseError(form.errors)
|
||||||
|
|
||||||
|
token = form.user_cache.login_tokens.create()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'token': token.get_token(),
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['post'])
|
||||||
|
def logout(self, request, *args, **kwargs):
|
||||||
|
# django-rest-framework doesn't do this for logged out requests
|
||||||
|
SessionAuthentication().enforce_csrf(request)
|
||||||
|
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return ParseError(_('Not logged in.'))
|
||||||
|
|
||||||
|
logout(request)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'detail': _('Logout successful.'),
|
||||||
|
'csrf_token': csrf.get_token(request),
|
||||||
|
})
|
31
src/c3nav/api/migrations/0001_initial.py
Normal file
31
src/c3nav/api/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 2.1.1 on 2018-11-23 21:19
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Token',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('secret', models.CharField(max_length=64, verbose_name='secret')),
|
||||||
|
('session_auth_hash', models.CharField(max_length=128, verbose_name='session auth hash')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_tokens', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'login tokens',
|
||||||
|
'verbose_name_plural': 'login tokens',
|
||||||
|
'default_related_name': 'login_tokens',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
src/c3nav/api/migrations/__init__.py
Normal file
0
src/c3nav/api/migrations/__init__.py
Normal file
56
src/c3nav/api/models.py
Normal file
56
src/c3nav/api/models.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import string
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.crypto import constant_time_compare, get_random_string
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class Token(models.Model):
|
||||||
|
"""
|
||||||
|
Token for log in via API
|
||||||
|
"""
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
||||||
|
secret = models.CharField(max_length=64, verbose_name=_('secret'))
|
||||||
|
session_auth_hash = models.CharField(_('session auth hash'), max_length=128)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('login tokens')
|
||||||
|
verbose_name_plural = _('login tokens')
|
||||||
|
default_related_name = 'login_tokens'
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.secret:
|
||||||
|
self.secret = get_random_string(64, string.ascii_letters + string.digits)
|
||||||
|
if not self.session_auth_hash:
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.session_auth_hash = self.user.get_session_auth_hash()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
return '%d:%s' % (self.pk, self.secret)
|
||||||
|
|
||||||
|
def verify(self):
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
return constant_time_compare(
|
||||||
|
self.session_auth_hash,
|
||||||
|
self.user.get_session_auth_hash()
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_token(cls, token: str):
|
||||||
|
try:
|
||||||
|
pk, secret = token.split(':', 1)
|
||||||
|
except ValueError:
|
||||||
|
raise cls.DoesNotExist
|
||||||
|
|
||||||
|
if not pk.isdigit() or not secret:
|
||||||
|
raise cls.DoesNotExist
|
||||||
|
|
||||||
|
obj = cls.objects.select_related('user').get(pk=pk, secret=secret)
|
||||||
|
|
||||||
|
if not obj.verify():
|
||||||
|
obj.delete()
|
||||||
|
raise cls.DoesNotExist
|
||||||
|
|
||||||
|
return obj
|
|
@ -8,6 +8,7 @@ from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import SimpleRouter
|
from rest_framework.routers import SimpleRouter
|
||||||
|
|
||||||
|
from c3nav.api.api import SessionViewSet
|
||||||
from c3nav.editor.api import ChangeSetViewSet, EditorViewSet
|
from c3nav.editor.api import ChangeSetViewSet, EditorViewSet
|
||||||
from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionViewSet, AreaViewSet, BuildingViewSet,
|
from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionViewSet, AreaViewSet, BuildingViewSet,
|
||||||
ColumnViewSet, CrossDescriptionViewSet, DoorViewSet, HoleViewSet,
|
ColumnViewSet, CrossDescriptionViewSet, DoorViewSet, HoleViewSet,
|
||||||
|
@ -49,6 +50,7 @@ router.register(r'routing', RoutingViewSet, base_name='routing')
|
||||||
|
|
||||||
router.register(r'editor', EditorViewSet, base_name='editor')
|
router.register(r'editor', EditorViewSet, base_name='editor')
|
||||||
router.register(r'changesets', ChangeSetViewSet)
|
router.register(r'changesets', ChangeSetViewSet)
|
||||||
|
router.register(r'session', SessionViewSet, base_name='session')
|
||||||
|
|
||||||
|
|
||||||
class APIRoot(GenericAPIView):
|
class APIRoot(GenericAPIView):
|
||||||
|
@ -77,6 +79,9 @@ class APIRoot(GenericAPIView):
|
||||||
continue
|
continue
|
||||||
if name == 'editor-detail':
|
if name == 'editor-detail':
|
||||||
name = 'editor-api'
|
name = 'editor-api'
|
||||||
|
elif base == 'session':
|
||||||
|
if name == 'session-list':
|
||||||
|
name = 'session-info'
|
||||||
if '-' in name:
|
if '-' in name:
|
||||||
urls.setdefault(base, OrderedDict())[name.split('-', 1)[1]] = url
|
urls.setdefault(base, OrderedDict())[name.split('-', 1)[1]] = url
|
||||||
else:
|
else:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue