diff --git a/.gitignore b/.gitignore index 7f4f91f..41d99c0 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,5 @@ fabric.properties /src/staticfiles/ node_modules /bend/static/ +/fend/src/server/data/schema.json +screenshots diff --git a/bend/deps/testing.txt b/bend/deps/testing.txt index 253d8b3..10f9ea7 100644 --- a/bend/deps/testing.txt +++ b/bend/deps/testing.txt @@ -1,2 +1,3 @@ prospector==0.12.4 -coverage==4.2 \ No newline at end of file +coverage==4.2 +selenium==3.0.2 diff --git a/bend/src/ango/settings/base.py b/bend/src/ango/settings/base.py index 07cabbb..0711957 100644 --- a/bend/src/ango/settings/base.py +++ b/bend/src/ango/settings/base.py @@ -3,13 +3,16 @@ """ from os.path import dirname, join +import datetime # Build paths inside the project like this: join(BASE_DIR, "directory") BASE_DIR = dirname(dirname(dirname(dirname(__file__)))) # Define STATIC_ROOT for the collectstatic command STATIC_ROOT = join(BASE_DIR, 'static') - +STATIC_URL = '/static/' +MEDIA_ROOT = join(BASE_DIR, 'media') +MEDIA_URL = "/media/" # Application definition INSTALLED_APPS = [ @@ -87,19 +90,17 @@ USE_TZ = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ - -STATIC_URL = '/static/' GRAPHENE = { 'SCHEMA': 'ango.schema.schema', # Where your Graphene schema lives - 'SCHEMA_OUTPUT': 'fend/server/data/schema.json' # defaults to schema.json + 'SCHEMA_OUTPUT': 'fend/src/server/data/schema.json' # defaults to schema.json } WEBPACK_LOADER = { 'DEFAULT': { 'BUNDLE_DIR_NAME': 'bundles/', - 'STATS_FILE': join('static', 'webpack-stats.json'), + 'STATS_FILE': join(BASE_DIR, 'static', 'webpack-stats.json'), } -} \ No newline at end of file +} + +JWT_EXPIRATION_DELTA = datetime.timedelta(days=7) diff --git a/bend/src/ango/settings/prod.py b/bend/src/ango/settings/prod.py index d44acfc..0edf69f 100644 --- a/bend/src/ango/settings/prod.py +++ b/bend/src/ango/settings/prod.py @@ -26,10 +26,6 @@ } -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.9/howto/static-files/ -STATIC_ROOT = join(BASE_DIR, 'staticfiles') -STATIC_URL = '/static/' # Simplified static file serving. # https://warehouse.python.org/project/whitenoise/ diff --git a/bend/src/ango/tests.py b/bend/src/ango/tests.py new file mode 100644 index 0000000..e278919 --- /dev/null +++ b/bend/src/ango/tests.py @@ -0,0 +1,25 @@ +from django.test import LiveServerTestCase +from selenium import webdriver +import os + + +if not os.path.exists('./screenshots'): + os.makedirs('./screenshots') + + +class HomePageTest(LiveServerTestCase): + def setUp(self): + self.selenium = webdriver.Firefox() + super(HomePageTest, self).setUp() + + def tearDown(self): + self.selenium.quit() + super(HomePageTest, self).tearDown() + + def test_home_page(self): + selenium = self.selenium + selenium.implicitly_wait(10) # seconds + # Opening the link we want to test + selenium.get(self.live_server_url) + selenium.save_screenshot('./screenshots/ff_landing.png') + assert 'Relay Fullstack' in selenium.page_source diff --git a/bend/src/ango/urls.py b/bend/src/ango/urls.py index ab15735..861907b 100644 --- a/bend/src/ango/urls.py +++ b/bend/src/ango/urls.py @@ -17,11 +17,12 @@ from django.contrib import admin from django.views.generic import TemplateView from graphene_django.views import GraphQLView +from django.views.decorators.csrf import csrf_exempt urlpatterns = [ url(r'^$', TemplateView.as_view(template_name='index.html'), name='home'), url(r'^admin/', admin.site.urls), - url(r'^graphql', GraphQLView.as_view(graphiql=True)), - url(r'^.*/$', TemplateView.as_view(template_name='index.html')), + url(r'^graphql', csrf_exempt(GraphQLView.as_view(graphiql=True))), + url(r'^.*$', TemplateView.as_view(template_name='index.html')), ] diff --git a/bend/src/users/jwt_util.py b/bend/src/users/jwt_util.py index 5d55ef4..baa5e3f 100644 --- a/bend/src/users/jwt_util.py +++ b/bend/src/users/jwt_util.py @@ -1,4 +1,16 @@ from jwt_auth.forms import JSONWebTokenForm +from jwt_auth.mixins import JSONWebTokenAuthMixin + +jwtMixin = JSONWebTokenAuthMixin.authenticate_header + +import jwt + +from jwt_auth import settings, exceptions +from jwt_auth.utils import get_authorization_header +from jwt_auth.compat import json, smart_text, User + +jwt_decode_handler = settings.JWT_DECODE_HANDLER +jwt_get_user_id_from_payload = settings.JWT_PAYLOAD_GET_USER_ID_HANDLER def loginUser(username, password): @@ -8,8 +20,53 @@ def loginUser(username, password): if not form.is_valid(): return print("JWT form not valid") - context_dict = { - 'token': form.object['token'] - } + return form.object['token'] + + +def authenticate(request): + auth = get_authorization_header(request).split() + auth_header_prefix = settings.JWT_AUTH_HEADER_PREFIX.lower() + if not auth or smart_text(auth[0].lower()) != auth_header_prefix: + raise exceptions.AuthenticationFailed() + + if len(auth) == 1: + msg = 'Invalid Authorization header. No credentials provided.' + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = ('Invalid Authorization header. Credentials string ' + 'should not contain spaces.') + raise exceptions.AuthenticationFailed(msg) + + + try: + payload = jwt_decode_handler(auth[1]) + except jwt.ExpiredSignature: + msg = 'Signature has expired.' + print(msg, auth[1]) + raise exceptions.AuthenticationFailed(msg) + except jwt.DecodeError: + msg = 'Error decoding signature.' + raise exceptions.AuthenticationFailed(msg) + + user = authenticate_credentials(payload) + + return (user, auth[1]) + + +def authenticate_credentials(payload): + """ + Returns an active user that matches the payload's user id and email. + """ + try: + user_id = jwt_get_user_id_from_payload(payload) + + if user_id: + user = User.objects.get(pk=user_id, is_active=True) + else: + msg = 'Invalid payload' + raise exceptions.AuthenticationFailed(msg) + except User.DoesNotExist: + msg = 'Invalid signature' + raise exceptions.AuthenticationFailed(msg) - return context_dict + return user diff --git a/bend/src/users/readme.md b/bend/src/users/readme.md index 064871d..7a41743 100644 --- a/bend/src/users/readme.md +++ b/bend/src/users/readme.md @@ -74,16 +74,67 @@ query { ``` mutation { - loginUser(input: { - username: "ncrmro", - password: "testpassword" - }) { - user { - username, - email, + loginUser(input: {username: "ncrmro", password: "testpassword"}) { + viewer { + username + email + todomodel(first: 2) { + edges { + node { + text + } + } + } + } + jwtToken + } +} + +query { + login(username:"ncrmro", password: "testpassword") { + viewer{ + username, + dateJoined }, - token + jwtToken } } + +query {viewer(jwtToken: ""){ + viewer { + username, + isAuthenticated, + isAnonymous + } +}} + + ``` +``` +{"input_0": {"username": "ncrmro", "password": "testpassword"}} + +mutation LoginUserMutation($input_0:LogInUserInput!) { + loginUser(input:$input_0) { + clientMutationId, + ...F0 + } +} +fragment F0 on LogInUserPayload { + viewer { + username, + id + } +} + +{ + "data": { + "loginUser": { + "clientMutationId": null, + "viewer": { + "id": "VXNlck5vZGU6MQ==" + } + } + } +} +``` \ No newline at end of file diff --git a/bend/src/users/schema.py b/bend/src/users/schema.py index 105706f..9d485a0 100644 --- a/bend/src/users/schema.py +++ b/bend/src/users/schema.py @@ -1,43 +1,51 @@ -from graphene import AbstractType, Node, Field, String, relay, ObjectType, Int, Boolean, ID +from graphene import AbstractType, Node, Field, String, relay, ObjectType, Int, Boolean, ID, InputObjectType from django.contrib.auth.models import AnonymousUser, User from graphene import AbstractType, Node, relay from graphene_django.filter import DjangoFilterConnectionField from graphene_django.types import DjangoObjectType -from .jwt_util import loginUser +from .jwt_util import loginUser, authenticate +from jwt_auth import settings, exceptions +import jwt + +jwt_decode_handler = settings.JWT_DECODE_HANDLER class UserNode(DjangoObjectType): class Meta: model = User + only_fields = ( + 'id', + 'last_login', + 'is_superuser', + 'username', + 'first_name', + 'last_name', + 'email', + 'is_staff', + 'is_active', + 'date_joined', + 'todomodel' + ) interfaces = (Node,) -class CurrentUser(ObjectType): - id = ID() - username = String() - password = String() - is_anonymous = Boolean() - is_authenticated = Boolean() - is_staff = Boolean() - is_active = Boolean() - - class UserQueries(AbstractType): - user = Node.Field(UserNode) + user = Field(UserNode) all_users = DjangoFilterConnectionField(UserNode) + viewer = Field(UserNode) - current_user = Field(CurrentUser) - - def resolve_current_user(self, args, context, info): - anon = AnonymousUser - return CurrentUser( - id=anon.id, - username=anon.username, - is_anonymous=anon.is_anonymous, - is_authenticated=anon.is_authenticated, - is_staff=anon.is_staff, - is_active=anon.is_active, - ) + def resolve_viewer(self, args, context, info): + try: + check_token = authenticate(context) + print('Found Token in Auth Header', check_token) + token_user = check_token[0] + user = User.objects.get(id=token_user.id, username=token_user.username) + return user + except: + return User( + id=0, + username="" + ) class LogInUser(relay.ClientIDMutation): @@ -45,16 +53,37 @@ class Input: username = String(required=True) password = String(required=True) - user = Field(UserNode) - token = String() + viewer = Field(UserNode) + jwt_token = String() + + @classmethod + def mutate_and_get_payload(cls, input, context, info): + print("Logging user in", input, context, info) + username = input.get('username') + password = input.get('password') + jwt_token = loginUser(username, password) + viewer = User.objects.get(username=username) + return LogInUser(viewer, jwt_token) + + +class CreateUser(relay.ClientIDMutation): + class Input: + username = String(required=True) + password = String(required=True) + + viewer = Field(UserNode) + jwt_token = String() @classmethod def mutate_and_get_payload(cls, input, context, info): + print("Logging user in", input, context, info) username = input.get('username') password = input.get('password') - token = loginUser(username, password) - return LogInUser(token=token) + viewer = User.objects.create_user(username=username, password=password) + jwt_token = loginUser(username, password) + return CreateUser(viewer, jwt_token) class UserMutations(AbstractType): login_user = LogInUser.Field() + create_user = CreateUser.Field() diff --git a/circle.yml b/circle.yml index b0e6a38..a123ba2 100644 --- a/circle.yml +++ b/circle.yml @@ -9,8 +9,11 @@ machine: general: artifacts: - ".coverage" + - "screenshots" dependencies: pre: + - wget -qO- https://github.com/mozilla/geckodriver/releases/download/v0.13.0/geckodriver-v0.13.0-linux64.tar.gz | tar -xvz -C /home/ubuntu/bin + - chmod +x /home/ubuntu/bin/geckodriver - | if [[ ! -e ~/.yarn/bin/yarn || $(yarn --version) != "${YARN_VERSION}" ]]; then echo "Download and install Yarn." @@ -26,6 +29,8 @@ dependencies: - ~/.cache/yarn post: - pip install -r ./bend/deps/testing.txt + - ./bend/manage.py collectstatic --no-input + test: override: diff --git a/fend/src/client/components/App/AppComponent.js b/fend/src/client/components/App/AppComponent.js index 1be0e7f..f7c96f7 100644 --- a/fend/src/client/components/App/AppComponent.js +++ b/fend/src/client/components/App/AppComponent.js @@ -8,14 +8,20 @@ import "react-mdl/extra/css/material.cyan-red.min.css"; export default class App extends React.Component { static propTypes = { + router: React.PropTypes.object.isRequired, children: React.PropTypes.object.isRequired, viewer: React.PropTypes.object.isRequired }; + render() { + const viewer = this.props.viewer; + const userLoggedIn = viewer.username ? true : false; + return (
Always a pleasure scaffolding your apps
diff --git a/fend/src/client/components/App/AppContainer.js b/fend/src/client/components/App/AppContainer.js index 6d44699..75a2f8f 100644 --- a/fend/src/client/components/App/AppContainer.js +++ b/fend/src/client/components/App/AppContainer.js @@ -5,7 +5,11 @@ import Footer from "../Footer/FooterContainer"; export default Relay.createContainer(App, { fragments: { viewer: () => Relay.QL` - fragment on User { + fragment on UserNode { + id, + username, + email, + dateJoined, ${Footer.getFragment('viewer')} }` } diff --git a/fend/src/client/components/Dashboard/DashboardComponent.js b/fend/src/client/components/Dashboard/DashboardComponent.js index a859132..e1f1ec7 100644 --- a/fend/src/client/components/Dashboard/DashboardComponent.js +++ b/fend/src/client/components/Dashboard/DashboardComponent.js @@ -2,7 +2,7 @@ import React from "react"; -export default class Feature extends React.Component { +export default class Dashboard extends React.Component { static propTypes = { viewer: React.PropTypes.object.isRequired }; @@ -10,6 +10,9 @@ export default class Feature extends React.Component { render() { return (This is the dashboard
This is the landing page
+