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.routers import SimpleRouter
|
||||
|
||||
from c3nav.api.api import SessionViewSet
|
||||
from c3nav.editor.api import ChangeSetViewSet, EditorViewSet
|
||||
from c3nav.mapdata.api import (AccessRestrictionGroupViewSet, AccessRestrictionViewSet, AreaViewSet, BuildingViewSet,
|
||||
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'changesets', ChangeSetViewSet)
|
||||
router.register(r'session', SessionViewSet, base_name='session')
|
||||
|
||||
|
||||
class APIRoot(GenericAPIView):
|
||||
|
@ -77,6 +79,9 @@ class APIRoot(GenericAPIView):
|
|||
continue
|
||||
if name == 'editor-detail':
|
||||
name = 'editor-api'
|
||||
elif base == 'session':
|
||||
if name == 'session-list':
|
||||
name = 'session-info'
|
||||
if '-' in name:
|
||||
urls.setdefault(base, OrderedDict())[name.split('-', 1)[1]] = url
|
||||
else:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue