diff --git a/tailbone/api/__init__.py b/tailbone/api/__init__.py new file mode 100644 index 00000000..6c310fa7 --- /dev/null +++ b/tailbone/api/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2018 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Tailbone Web API +""" + +from __future__ import unicode_literals, absolute_import + +from .core import APIView, api + + +def includeme(config): + config.include('tailbone.api.auth') diff --git a/tailbone/api/auth.py b/tailbone/api/auth.py new file mode 100644 index 00000000..0664405a --- /dev/null +++ b/tailbone/api/auth.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2018 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Tailbone Web API - Auth Views +""" + +from __future__ import unicode_literals, absolute_import + +from rattail.db.auth import authenticate_user + +from tailbone.api import APIView, api +from tailbone.db import Session +from tailbone.auth import login_user, logout_user + + +class AuthenticationView(APIView): + + def user_info(self, user): + return { + 'ok': True, + 'user': { + 'uuid': user.uuid, + 'username': user.username, + 'display_name': user.display_name, + }, + } + + @api + def check_session(self): + """ + View to serve as "no-op" / ping action to check current user's session. + This will establish a server-side web session for the user if none + exists. Note that this also resets the user's session timer. + """ + if self.request.user: + return self.user_info(self.request.user) + return {} + + @api + def login(self): + """ + API login view. + """ + if self.request.method == 'OPTIONS': + return self.request.response + + username = self.request.json.get('username') + password = self.request.json.get('password') + if not (username and password): + return {'error': "Invalid username or password"} + + user = authenticate_user(Session(), username, password) + if not user: + return {'error': "Invalid username or password"} + + login_user(self.request, user) + return self.user_info(user) + + @api + def logout(self): + """ + API logout view. + """ + if self.request.method == 'OPTIONS': + return self.request.response + + logout_user(self.request) + return {'ok': True} + + @classmethod + def defaults(cls, config): + + # session + config.add_route('api.session', '/api/session', request_method='GET') + config.add_view(cls, attr='check_session', route_name='api.session', renderer='json') + + # login + config.add_route('api.login', '/api/login', request_method=('OPTIONS', 'POST')) + config.add_view(cls, attr='login', route_name='api.login', renderer='json') + + # logout + config.add_route('api.logout', '/api/logout', request_method=('OPTIONS', 'POST')) + config.add_view(cls, attr='logout', route_name='api.logout', renderer='json') + + +def includeme(config): + AuthenticationView.defaults(config) diff --git a/tailbone/api/core.py b/tailbone/api/core.py new file mode 100644 index 00000000..c8855161 --- /dev/null +++ b/tailbone/api/core.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2018 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Tailbone Web API - Core Views +""" + +from __future__ import unicode_literals, absolute_import + +from tailbone.views import View + + +def api(view_meth): + """ + Common decorator for all API views. Ideally this would not be needed..but + for now, alas, it is. + """ + def wrapped(view, *args, **kwargs): + + # TODO: why doesn't this work here...? (instead we have to repeat this + # code in lots of other places) + # if view.request.method == 'OPTIONS': + # return view.request.response + + # invoke the view logic first, since presumably it may involve a + # redirect in which case we don't really need to add the CSRF token. + # main known use case for this is the /logout endpoint - if that gets + # hit then the "current" (old) session will be destroyed, in which case + # we can't use the token from that, but instead must generate a new one. + result = view_meth(view, *args, **kwargs) + + # explicitly set CSRF token cookie, unless OPTIONS request + # TODO: why doesn't pyramid do this for us again? + if view.request.method != 'OPTIONS': + view.request.response.set_cookie(name='XSRF-TOKEN', + value=view.request.session.get_csrf_token()) + + return result + + return wrapped + + +class APIView(View): + """ + Base class for all API views. + """