Add loggedIn guard, add basic login component

This commit is contained in:
Christian Cueni 2019-10-02 16:11:15 +02:00
parent fb225b926d
commit 062269f030
11 changed files with 323 additions and 71 deletions

View File

@ -11,6 +11,7 @@
import SimpleLayout from '@/layouts/SimpleLayout'; import SimpleLayout from '@/layouts/SimpleLayout';
import BlankLayout from '@/layouts/BlankLayout'; import BlankLayout from '@/layouts/BlankLayout';
import FullScreenLayout from '@/layouts/FullScreenLayout'; import FullScreenLayout from '@/layouts/FullScreenLayout';
import PublicLayout from '@/layouts/PublicLayout';
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import MobileNavigation from '@/components/MobileNavigation'; import MobileNavigation from '@/components/MobileNavigation';
import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard'; import NewContentBlockWizard from '@/components/content-block-form/NewContentBlockWizard';
@ -36,6 +37,7 @@
SimpleLayout, SimpleLayout,
BlankLayout, BlankLayout,
FullScreenLayout, FullScreenLayout,
PublicLayout,
Modal, Modal,
MobileNavigation, MobileNavigation,
NewContentBlockWizard, NewContentBlockWizard,

View File

@ -4,71 +4,73 @@ import {ApolloClient} from 'apollo-client/index'
import {ApolloLink} from 'apollo-link' import {ApolloLink} from 'apollo-link'
import fetch from 'unfetch' import fetch from 'unfetch'
const httpLink = new HttpLink({ export default function (uri) {
// uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/', const httpLink = new HttpLink({
uri: '/api/graphql/', // uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/',
credentials: 'include', uri,
fetch: fetch, credentials: 'include',
headers: { fetch: fetch,
'X-CSRFToken': document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*=\s*([^;]*).*$)|^.*$/, '$1') 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}),
} }
} });
});
// TODO: Monkey-patching in a fix for an open issue suggesting that const consoleLink = new ApolloLink((operation, forward) => {
// `readQuery` should return null or undefined if the query is not yet in the // console.log(`starting request for ${operation.operationName}`);
// 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 forward(operation).map((data) => {
export default new ApolloClient({ // console.log(`ending request for ${operation.operationName}`);
link: composedLink,
// link: httpLink, return data
cache: cache, })
connectToDevTools: true });
})
// 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
})
}

View File

@ -0,0 +1,8 @@
mutation Login($input: LoginInput!) {
login(input: $input) {
success
errors {
field
}
}
}

View File

@ -0,0 +1,23 @@
<template>
<div class="container skillbox">
<logo></logo>
<router-view class="skillbox__content"></router-view>
<footer class="skillbox__footer">Footer</footer>
</div>
</template>
<script>
import Logo from '@/components/icons/Logo';
export default {
components: {Logo},
}
</script>
<style lang="scss" scoped>
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
@import "@/styles/_default-layout.scss";
</style>

View File

@ -3,7 +3,7 @@ import Vue from 'vue'
import axios from 'axios' import axios from 'axios'
import VueAxios from 'vue-axios' import VueAxios from 'vue-axios'
import VueVimeoPlayer from 'vue-vimeo-player' import VueVimeoPlayer from 'vue-vimeo-player'
import apolloClient from './graphql/client' import apolloClientFactory from './graphql/client'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
import App from './App' import App from './App'
import router from './router' import router from './router'
@ -63,8 +63,14 @@ if (process.env.GOOGLE_ANALYTICS_ID) {
Vue.directive('click-outside', clickOutside); Vue.directive('click-outside', clickOutside);
Vue.directive('auto-grow', autoGrow); Vue.directive('auto-grow', autoGrow);
const publicApolloClient = apolloClientFactory('/api/graphql-public/');
const privateApolloClient = apolloClientFactory('/api/graphql/');
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: apolloClient clients: {
publicClient: publicApolloClient
},
defaultClient: privateApolloClient
}); });
Validator.extend('required', required); Validator.extend('required', required);
@ -98,6 +104,28 @@ Vue.use(VeeValidate, {
Vue.filter('date', dateFilter); 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 */ /* eslint-disable no-new */
new Vue({ new Vue({
el: '#app', el: '#app',

158
client/src/pages/login.vue Normal file
View File

@ -0,0 +1,158 @@
<template>
<div class="login">
<h1 class="login__title">Login</h1>
<form class="login__form login-form" novalidate @submit.prevent="validateBeforeSubmit">
<div class="login-form__field sbform-input">
<label for="email" class="sbform-input__label">E-Mail</label>
<input
id="email"
name="email"
type="text"
v-model="email"
v-validate="'required'"
:class="{ 'sbform-input__input--error': errors.has('email') }"
class="change-form__email skillbox-input sbform-input__input"
autocomplete="off"
data-cy="email"
/>
<small
v-if="errors.has('email') && submitted"
class="sbform-input__error"
data-cy="email-local-errors"
>{{ errors.first('email') }}</small>
<small
v-for="error in emailErrors"
:key="error"
class="sbform-input__error"
data-cy="email-remote-errors"
>{{ error }}</small>
</div>
<div class="change-form__field sbform-input">
<label for="pw" class="sbform-input__label">Passwort</label>
<input
id="pw"
name="password"
type="password"
v-model="password"
v-validate="'required'"
:class="{ 'sbform-input__input--error': errors.has('password') }"
class="change-form__new skillbox-input sbform-input__input"
autocomplete="off"
data-cy="password"
/>
<small
v-if="errors.has('password') && submitted"
class="sbform-input__error"
data-cy="password-local-errors"
>{{ errors.first('password') }}</small>
<small
v-for="error in passwordErrors"
:key="error"
class="sbform-input__error"
data-cy="password-remote-errors"
>{{ error }}</small>
</div>
<div class="login-error">
<small class="sbform-input__error" v-if="loginError">{{loginError}}</small>
</div>
<button class="button button--primary change-form__submit" data-cy="change-password-button">Anmelden</button>
</form>
</div>
</template>
<script>
import LOGIN_MUTATION from '@/graphql/gql/mutations/login.gql';
export default {
components: {},
methods: {
validateBeforeSubmit() {
this.$validator.validate().then(result => {
this.submitted = true;
let that = this;
if (result) {
this.$apollo.mutate({
client: 'publicClient',
mutation: LOGIN_MUTATION,
variables: {
input: {
usernameInput: this.email,
passwordInput: this.password
}
},
update(
store,
{
data: {
login: { success }
}
}
) {
try {
console.log('success', success)
if (success) {
} else {
that.loginError = 'Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.';
}
} catch (e) {
that.loginError = 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie nochmals.';
}
}
});
}
});
},
resetForm() {
this.email = '';
this.password = '';
this.submitted = false;
this.$validator.reset();
}
},
data() {
return {
email: '',
password: '',
emailErrors: [],
passwordErrors: [],
loginError: ''
};
}
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_buttons.scss";
.sbform-input {
margin-bottom: 20px;
font-family: $sans-serif-font-family;
&__label {
margin-bottom: 10px;
display: inline-block;
}
&__input {
width: 100%;
&--error {
border-color: $color-error;
}
}
&__error {
margin-top: 10px;
color: $color-error;
display: inline-block;
}
&__hint {
margin-top: $small-spacing;
font-family: $sans-serif-font-family;
color: $color-silver-dark;
}
}
</style>

View File

@ -27,11 +27,23 @@ import newProject from '@/pages/newProject'
import surveyPage from '@/pages/survey' import surveyPage from '@/pages/survey'
import styleGuidePage from '@/pages/styleguide' import styleGuidePage from '@/pages/styleguide'
import moduleRoom from '@/pages/moduleRoom' import moduleRoom from '@/pages/moduleRoom'
import login from '@/pages/login'
import store from '@/store/index'; import store from '@/store/index';
const routes = [ 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', path: '/module/:slug',
component: moduleBase, component: moduleBase,
@ -118,6 +130,7 @@ const router = new Router({
return {x: 0, y: 0} return {x: 0, y: 0}
} }
}); });
router.afterEach((to, from) => { router.afterEach((to, from) => {
store.dispatch('showMobileNavigation', false); store.dispatch('showMobileNavigation', false);
}); });

View File

@ -75,3 +75,23 @@ class CommonRedirectMiddleware(MiddlewareMixin):
# or dummy image: return 'http://via.placeholder.com/{}'.format(m.group('dimensions')) # 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: if '.png' in path or '.jpg' in path or '.svg' in path or 'not-found' in path:
return 'https://picsum.photos/400/400' 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

View File

@ -116,6 +116,7 @@ MIDDLEWARE += [
'core.middleware.ThreadLocalMiddleware', 'core.middleware.ThreadLocalMiddleware',
'core.middleware.CommonRedirectMiddleware', 'core.middleware.CommonRedirectMiddleware',
'core.middleware.UserLoggedInCookieMiddleWare',
] ]
ROOT_URLCONF = 'core.urls' ROOT_URLCONF = 'core.urls'

View File

@ -1,6 +1,5 @@
import requests import requests
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView, \ from django.contrib.auth.views import PasswordResetView, PasswordResetDoneView, PasswordResetConfirmView, \
PasswordResetCompleteView PasswordResetCompleteView
@ -16,7 +15,6 @@ class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
pass pass
@login_required
@ensure_csrf_cookie @ensure_csrf_cookie
def home(request): def home(request):
if settings.DEBUG: if settings.DEBUG:

View File

@ -15,7 +15,6 @@ from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.views import PasswordResetView, PasswordResetConfirmView, INTERNAL_RESET_URL_TOKEN from django.contrib.auth.views import PasswordResetView, PasswordResetConfirmView, INTERNAL_RESET_URL_TOKEN
from graphene import relay from graphene import relay
from core import settings
from users.models import User from users.models import User