diff --git a/client/src/App.vue b/client/src/App.vue index 1b99180e..479743b2 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -11,6 +11,7 @@ import SimpleLayout from '@/layouts/SimpleLayout'; import BlankLayout from '@/layouts/BlankLayout'; import FullScreenLayout from '@/layouts/FullScreenLayout'; + import PublicLayout from '@/layouts/PublicLayout'; import Modal from '@/components/Modal'; import MobileNavigation from '@/components/MobileNavigation'; import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard'; @@ -36,6 +37,7 @@ SimpleLayout, BlankLayout, FullScreenLayout, + PublicLayout, Modal, MobileNavigation, NewContentBlockWizard, diff --git a/client/src/graphql/client.js b/client/src/graphql/client.js index 84841780..6d0822b0 100644 --- a/client/src/graphql/client.js +++ b/client/src/graphql/client.js @@ -4,71 +4,73 @@ import {ApolloClient} from 'apollo-client/index' import {ApolloLink} from 'apollo-link' import fetch from 'unfetch' -const httpLink = new HttpLink({ - // uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/', - uri: '/api/graphql/', - credentials: 'include', - fetch: fetch, - headers: { - 'X-CSRFToken': document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*=\s*([^;]*).*$)|^.*$/, '$1') - } -}); - -const consoleLink = new ApolloLink((operation, forward) => { - // console.log(`starting request for ${operation.operationName}`); - - return forward(operation).map((data) => { - // console.log(`ending request for ${operation.operationName}`); - - return data - }) -}); - -// from https://github.com/apollographql/apollo-client/issues/1564#issuecomment-357492659 -const omitTypename = (key, value) => { - return key === '__typename' ? undefined : value -}; - -const createOmitTypenameLink = new ApolloLink((operation, forward) => { - if (operation.variables) { - operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename) - } - - return forward(operation) -}); - -const composedLink = ApolloLink.from([createOmitTypenameLink, consoleLink, httpLink]); - -const cache = new InMemoryCache({ - cacheRedirects: { - Query: { - contentBlock: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ContentBlockNode', id: args.id}), - chapter: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ChapterNode', id: args.id}), - assignment: (_, args, {getCacheKey}) => getCacheKey({__typename: 'AssignmentNode', id: args.id}), - objective: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveNode', id: args.id}), - objectiveGroup: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveGroupNode', id: args.id}), - module: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ModuleNode', id: args.id}), - projectEntry: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ProjectEntryNode', id: args.id}), +export default function (uri) { + const httpLink = new HttpLink({ + // uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/', + uri, + credentials: 'include', + fetch: fetch, + headers: { + 'X-CSRFToken': document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*=\s*([^;]*).*$)|^.*$/, '$1') } - } -}); + }); -// TODO: Monkey-patching in a fix for an open issue suggesting that -// `readQuery` should return null or undefined if the query is not yet in the -// cache: https://github.com/apollographql/apollo-feature-requests/issues/1 -cache.originalReadQuery = cache.readQuery; -cache.readQuery = (...args) => { - try { - return cache.originalReadQuery(...args); - } catch (err) { - return undefined; - } -}; + const consoleLink = new ApolloLink((operation, forward) => { + // console.log(`starting request for ${operation.operationName}`); -// Create the apollo client -export default new ApolloClient({ - link: composedLink, - // link: httpLink, - cache: cache, - connectToDevTools: true -}) + return forward(operation).map((data) => { + // console.log(`ending request for ${operation.operationName}`); + + return data + }) + }); + + // from https://github.com/apollographql/apollo-client/issues/1564#issuecomment-357492659 + const omitTypename = (key, value) => { + return key === '__typename' ? undefined : value + }; + + const createOmitTypenameLink = new ApolloLink((operation, forward) => { + if (operation.variables) { + operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename) + } + + return forward(operation) + }); + + const composedLink = ApolloLink.from([createOmitTypenameLink, consoleLink, httpLink]); + + const cache = new InMemoryCache({ + cacheRedirects: { + Query: { + contentBlock: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ContentBlockNode', id: args.id}), + chapter: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ChapterNode', id: args.id}), + assignment: (_, args, {getCacheKey}) => getCacheKey({__typename: 'AssignmentNode', id: args.id}), + objective: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveNode', id: args.id}), + objectiveGroup: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveGroupNode', id: args.id}), + module: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ModuleNode', id: args.id}), + projectEntry: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ProjectEntryNode', id: args.id}), + } + } + }); + + // TODO: Monkey-patching in a fix for an open issue suggesting that + // `readQuery` should return null or undefined if the query is not yet in the + // cache: https://github.com/apollographql/apollo-feature-requests/issues/1 + cache.originalReadQuery = cache.readQuery; + cache.readQuery = (...args) => { + try { + return cache.originalReadQuery(...args); + } catch (err) { + return undefined; + } + }; + + // Create the apollo client + return new ApolloClient({ + link: composedLink, + // link: httpLink, + cache: cache, + connectToDevTools: true + }) +} diff --git a/client/src/graphql/gql/mutations/login.gql b/client/src/graphql/gql/mutations/login.gql new file mode 100644 index 00000000..ff107cce --- /dev/null +++ b/client/src/graphql/gql/mutations/login.gql @@ -0,0 +1,8 @@ +mutation Login($input: LoginInput!) { + login(input: $input) { + success + errors { + field + } + } +} diff --git a/client/src/layouts/PublicLayout.vue b/client/src/layouts/PublicLayout.vue new file mode 100644 index 00000000..82ee90a3 --- /dev/null +++ b/client/src/layouts/PublicLayout.vue @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/client/src/main.js b/client/src/main.js index 4f314738..45559d88 100644 --- a/client/src/main.js +++ b/client/src/main.js @@ -3,7 +3,7 @@ import Vue from 'vue' import axios from 'axios' import VueAxios from 'vue-axios' import VueVimeoPlayer from 'vue-vimeo-player' -import apolloClient from './graphql/client' +import apolloClientFactory from './graphql/client' import VueApollo from 'vue-apollo' import App from './App' import router from './router' @@ -63,8 +63,14 @@ if (process.env.GOOGLE_ANALYTICS_ID) { Vue.directive('click-outside', clickOutside); Vue.directive('auto-grow', autoGrow); +const publicApolloClient = apolloClientFactory('/api/graphql-public/'); +const privateApolloClient = apolloClientFactory('/api/graphql/'); + const apolloProvider = new VueApollo({ - defaultClient: apolloClient + clients: { + publicClient: publicApolloClient + }, + defaultClient: privateApolloClient }); Validator.extend('required', required); @@ -98,6 +104,28 @@ Vue.use(VeeValidate, { Vue.filter('date', dateFilter); +/* logged in guard */ + +const publicPages = ['login'] + +function getCookieValue(a) { + var b = document.cookie.match('(^|[^;]+)\\s*' + a + '\\s*=\\s*([^;]+)'); + return b ? b.pop() : ''; +} + +function redirectIfLoginRequird(nameOfPage) { + return publicPages.indexOf(nameOfPage) === -1 && getCookieValue('loginStatus') !== 'True'; +} + +router.beforeEach((to, from, next) => { + if (redirectIfLoginRequird(to.name)) { + next('/login'); + } else { + next(); + } + // todo handle public pages for user +}); + /* eslint-disable no-new */ new Vue({ el: '#app', diff --git a/client/src/pages/login.vue b/client/src/pages/login.vue new file mode 100644 index 00000000..bc33bce7 --- /dev/null +++ b/client/src/pages/login.vue @@ -0,0 +1,158 @@ + + + Login + + + E-Mail + + {{ errors.first('email') }} + {{ error }} + + + Passwort + + {{ errors.first('password') }} + {{ error }} + + + {{loginError}} + + Anmelden + + + + + + + diff --git a/client/src/router/index.js b/client/src/router/index.js index fac2196a..3a9b7533 100644 --- a/client/src/router/index.js +++ b/client/src/router/index.js @@ -27,11 +27,23 @@ import newProject from '@/pages/newProject' import surveyPage from '@/pages/survey' import styleGuidePage from '@/pages/styleguide' import moduleRoom from '@/pages/moduleRoom' +import login from '@/pages/login' import store from '@/store/index'; const routes = [ - {path: '/', component: start, meta: {layout: 'blank'}}, + { + path: '/', + name: 'home', + component: start, + meta: {layout: 'blank'} + }, + { + path: '/login', + name: 'login', + component: login, + meta: {layout: 'public'} + }, { path: '/module/:slug', component: moduleBase, @@ -118,6 +130,7 @@ const router = new Router({ return {x: 0, y: 0} } }); + router.afterEach((to, from) => { store.dispatch('showMobileNavigation', false); }); diff --git a/server/core/middleware.py b/server/core/middleware.py index e6508116..c0b362c5 100644 --- a/server/core/middleware.py +++ b/server/core/middleware.py @@ -75,3 +75,23 @@ class CommonRedirectMiddleware(MiddlewareMixin): # or dummy image: return 'http://via.placeholder.com/{}'.format(m.group('dimensions')) if '.png' in path or '.jpg' in path or '.svg' in path or 'not-found' in path: return 'https://picsum.photos/400/400' + + +# https://stackoverflow.com/questions/4898408/how-to-set-a-login-cookie-in-django +class UserLoggedInCookieMiddleWare(MiddlewareMixin): + """ + Middleware to set user cookie + If user is authenticated and there is no cookie, set the cookie, + If the user is not authenticated and the cookie remains, delete it + """ + + cookie_name = 'loginStatus' + + def process_response(self, request, response): + #if user and no cookie, set cookie + if request.user.is_authenticated and not request.COOKIES.get(self.cookie_name): + response.set_cookie(self.cookie_name, 'true') + elif not request.user.is_authenticated and request.COOKIES.get(self.cookie_name): + #else if if no user and cookie remove user cookie, logout + response.delete_cookie(self.cookie_name) + return response diff --git a/server/core/settings.py b/server/core/settings.py index 8437b685..54738b93 100644 --- a/server/core/settings.py +++ b/server/core/settings.py @@ -116,6 +116,7 @@ MIDDLEWARE += [ 'core.middleware.ThreadLocalMiddleware', 'core.middleware.CommonRedirectMiddleware', + 'core.middleware.UserLoggedInCookieMiddleWare', ] ROOT_URLCONF = 'core.urls' diff --git a/server/core/views.py b/server/core/views.py index 3a103a86..1cece2c1 100644 --- a/server/core/views.py +++ b/server/core/views.py @@ -1,6 +1,5 @@ import requests from django.conf import settings -from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.views import PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView, \ PasswordResetCompleteView @@ -16,7 +15,6 @@ class PrivateGraphQLView(LoginRequiredMixin, GraphQLView): pass -@login_required @ensure_csrf_cookie def home(request): if settings.DEBUG: diff --git a/server/users/mutations_public.py b/server/users/mutations_public.py index d02b6bc8..3ace25c6 100644 --- a/server/users/mutations_public.py +++ b/server/users/mutations_public.py @@ -15,7 +15,6 @@ from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.views import PasswordResetView, PasswordResetConfirmView, INTERNAL_RESET_URL_TOKEN from graphene import relay -from core import settings from users.models import User