Merged in feature/move-login (pull request #37)

Feature/move login

Approved-by: Ramon Wenger <ramon.wenger@iterativ.ch>
This commit is contained in:
Christian Cueni 2019-10-23 06:58:46 +00:00
commit 38777cf914
45 changed files with 779 additions and 246 deletions

View File

@ -6,13 +6,17 @@ describe('Change Password Page', () => {
const validationErrorMsg = 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten'; const validationErrorMsg = 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten';
const validationOldWrongMsg = 'Die Eingabe ist falsch'; const validationOldWrongMsg = 'Die Eingabe ist falsch';
beforeEach(function () {
cy.clearCookies();
cy.visit('/me/profile');
cy.login('rahel.cueni', 'test');
});
after(function () { after(function () {
cy.exec("python ../server/manage.py reset_testuser_password rahel.cueni"); cy.exec("python ../server/manage.py reset_testuser_password rahel.cueni");
}); });
it('shows an empty form', () => { it('shows an empty form', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.get('[data-cy=password-change-success]').should('not.exist'); cy.get('[data-cy=password-change-success]').should('not.exist');
cy.get('[data-cy=old-password]').should('have.value', ''); cy.get('[data-cy=old-password]').should('have.value', '');
@ -20,73 +24,46 @@ describe('Change Password Page', () => {
}); });
it('shows errors if old password is not entered', () => { it('shows errors if old password is not entered', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword('', validNewPassword); cy.changePassword('', validNewPassword);
cy.get('[data-cy=old-password-local-errors]').should('contain', 'Dein aktuelles Passwort fehlt') cy.get('[data-cy=old-password-local-errors]').should('contain', 'Dein aktuelles Passwort fehlt')
}); });
it('shows errors if new password is not entered', () => { it('shows errors if new password is not entered', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword(validOldPassword, ''); cy.changePassword(validOldPassword, '');
cy.get('[data-cy=new-password-local-errors]').should('contain', 'Dein neues Passwort fehlt') cy.get('[data-cy=new-password-local-errors]').should('contain', 'Dein neues Passwort fehlt')
}); });
it('shows errors if new password is too short', () => { it('shows errors if new password is too short', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword(validOldPassword, 'Abc1!'); cy.changePassword(validOldPassword, 'Abc1!');
cy.get('[data-cy=new-password-local-errors]').should('contain', validationTooShort) cy.get('[data-cy=new-password-local-errors]').should('contain', validationTooShort)
}); });
it('shows errors if new password has no uppercase letter', () => { it('shows errors if new password has no uppercase letter', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword(validOldPassword, 'aabdddedddbc1!'); cy.changePassword(validOldPassword, 'aabdddedddbc1!');
cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg) cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg)
}); });
it('shows errors if new password has no lowercase letter', () => { it('shows errors if new password has no lowercase letter', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword(validOldPassword, 'ABCDDD334551!'); cy.changePassword(validOldPassword, 'ABCDDD334551!');
cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg) cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg)
}); });
it('shows errors if new password has no digit', () => { it('shows errors if new password has no digit', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword(validOldPassword, 'AbcdEEDE!'); cy.changePassword(validOldPassword, 'AbcdEEDE!');
cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg) cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg)
}); });
it('shows errors if new password has no special character', () => { it('shows errors if new password has no special character', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword(validOldPassword, 'AbcdEEDE09877'); cy.changePassword(validOldPassword, 'AbcdEEDE09877');
cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg) cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg)
}); });
it('shows errors if old password does not match', () => { it('shows errors if old password does not match', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword('test12345', validNewPassword); cy.changePassword('test12345', validNewPassword);
cy.get('[data-cy=old-password-remote-errors]').should('contain', validationOldWrongMsg) cy.get('[data-cy=old-password-remote-errors]').should('contain', validationOldWrongMsg)
}); });
it('shows success if change was successful', () => { it('shows success if change was successful', () => {
cy.login('rahel.cueni', 'test');
cy.visit('/me/profile');
cy.changePassword(validOldPassword, validNewPassword); cy.changePassword(validOldPassword, validNewPassword);
cy.get('[data-cy=password-change-success]').should('contain', 'Dein Password wurde erfolgreich geändert.'); cy.get('[data-cy=password-change-success]').should('contain', 'Dein Password wurde erfolgreich geändert.');
cy.get('[data-cy=old-password]').should('have.value', ''); cy.get('[data-cy=old-password]').should('have.value', '');

View File

@ -4,8 +4,8 @@ describe('Current Module', () => {
cy.startGraphQLCapture(); cy.startGraphQLCapture();
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.login('nico.zickgraf', 'test');
cy.visit('/module/lohn-und-budget'); cy.visit('/module/lohn-und-budget');
cy.login('nico.zickgraf', 'test');
cy.get('[data-cy=module-title]').should('contain', 'Lohn und Budget') cy.get('[data-cy=module-title]').should('contain', 'Lohn und Budget')

View File

@ -1,7 +1,7 @@
describe('The Logged In Home Page', () => { describe('The Logged In Home Page', () => {
it('successfully loads', () => { it('successfully loads', () => {
cy.login('test', 'test');
cy.visit('/'); cy.visit('/');
cy.login('test', 'test');
cy.get('.block-title__title').should('contain', 'Inhalte') cy.get('.block-title__title').should('contain', 'Inhalte')
}) })

View File

@ -1,13 +0,0 @@
describe('The Login CSRF Token', () => {
it('403 status without token', () => {
cy.loginByCsrf('some-token')
.its('status')
.should('eq', 403)
});
it('gets token from response body', () => {
cy.login('test', 'test')
})
});

View File

@ -1,15 +1,47 @@
describe('The Login Page', () => { describe('The Login Page', () => {
it('sets auth cookie when logging in via form submission', () => { it('login and redirect to main page', () => {
const username = 'test'; const username = 'test';
const password = 'test'; const password = 'test';
cy.visit('/'); cy.visit('/');
cy.login(username, password, true);
cy.get('body').contains('Neues Wissen erwerben');
});
cy.get('#id_username').type(username); it('user sees error message if username is omitted', () => {
cy.get('#id_password').type(`${password}{enter}`); const username = '';
const password = 'test';
cy.getCookie('sessionid').should('exist'); cy.visit('/');
cy.get('.start-page__header').should('exist') cy.login(username, password);
cy.get('[data-cy=email-local-errors]').contains('ist ein Pflichtfeld');
});
it('user sees error message if password is omitted', () => {
const username = 'test';
const password = '';
cy.visit('/');
cy.login(username, password);
cy.get('[data-cy=password-local-errors]').contains('ist ein Pflichtfeld');
});
it('user sees error message if credentials are invalid', () => {
const username = 'test';
const password = '12345';
cy.visit('/');
cy.login(username, password);
cy.get('[data-cy=login-error]').contains('Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.');
});
it('redirect after login', () => {
const username = 'test';
const password = 'test';
cy.visit('/book/topic/berufliche-grundbildung');
cy.login(username, password);
cy.get('body').contains('Berufliche Grundbildung');
}); });
// it('logs in programmatically without using the UI', () => { // it('logs in programmatically without using the UI', () => {
// cy.visit('/accounts/login/'); // have to get a csrf token by getting the base page first // cy.visit('/accounts/login/'); // have to get a csrf token by getting the base page first

View File

@ -1,9 +1,9 @@
describe('New project', () => { describe('New project', () => {
it('creates a new project and displays it', () => { it('creates a new project and displays it', () => {
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.visit('/portfolio');
cy.login('rahel.cueni', 'test'); cy.login('rahel.cueni', 'test');
cy.visit('/portfolio');
cy.get('[data-cy=add-project-button]').click(); cy.get('[data-cy=add-project-button]').click();
cy.get('[data-cy=page-form-input-titel]').type('Some random title'); cy.get('[data-cy=page-form-input-titel]').type('Some random title');
cy.get('[data-cy=page-form-input-beschreibung]').type('This description rocks'); cy.get('[data-cy=page-form-input-beschreibung]').type('This description rocks');

View File

@ -4,7 +4,8 @@ describe('Project Entry', () => {
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.startGraphQLCapture(); cy.startGraphQLCapture();
cy.login('rahel.cueni', 'test'); cy.login('rahel.cueni', 'test', true);
cy.get('body').contains('Neues Wissen erwerben');
}); });
it('should create a new project entry', () => { it('should create a new project entry', () => {

View File

@ -1,9 +1,9 @@
describe('The Room Page', () => { describe('The Room Page', () => {
it('displays new room entry with author name', () => { it('displays new room entry with author name', () => {
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.visit('/room/ein-historisches-festival');
cy.login('rahel.cueni', 'test'); cy.login('rahel.cueni', 'test');
cy.visit('/room/ein-historisches-festival');
cy.get('[data-cy=add-room-entry-button]').click(); cy.get('[data-cy=add-room-entry-button]').click();
cy.get('.add-content-element:first-of-type').click(); cy.get('.add-content-element:first-of-type').click();
cy.get('[data-cy=choose-text-widget]').click(); cy.get('[data-cy=choose-text-widget]').click();

View File

@ -1,15 +1,16 @@
describe('The Rooms Page', () => { describe('The Rooms Page', () => {
it('goes to the rooms page', () => { it('goes to the rooms page', () => {
cy.visit('/rooms');
cy.login('nico.zickgraf', 'test'); cy.login('nico.zickgraf', 'test');
cy.visit('/rooms');
cy.get('[data-cy=add-room]').should('exist'); cy.get('[data-cy=add-room]').should('exist');
}); });
it('add room should not exist for student', () => { it('add room should not exist for student', () => {
cy.visit('/rooms');
cy.login('rahel.cueni', 'test'); cy.login('rahel.cueni', 'test');
cy.visit('/rooms');
cy.get('[data-cy=add-room]').should('not.exist'); cy.get('[data-cy=add-room]').should('not.exist');
}); });

View File

@ -21,15 +21,15 @@ describe('Solutions', () => {
// cy.logout(); // cy.logout();
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.login('rahel.cueni', 'test');
cy.visit('/module/lohn-und-budget'); cy.visit('/module/lohn-und-budget');
cy.login('rahel.cueni', 'test');
cy.get('[data-cy=toggle-enable-solutions]') cy.get('[data-cy=toggle-enable-solutions]')
.should('not.exist'); .should('not.exist');
cy.get('[data-cy=solution]').should('not.exist'); cy.get('[data-cy=solution]').should('not.exist');
cy.logout(); cy.logout();
cy.login('nico.zickgraf', 'test');
cy.visit('/module/lohn-und-budget'); cy.visit('/module/lohn-und-budget');
cy.login('nico.zickgraf', 'test');
cy.get('[data-cy=toggle-enable-solutions]').click(); cy.get('[data-cy=toggle-enable-solutions]').click();
cy.waitFor('UpdateSolutionVisibility'); cy.waitFor('UpdateSolutionVisibility');
// cy.get('[data-cy=toggle-enable-solutions]').within(() => { // cy.get('[data-cy=toggle-enable-solutions]').within(() => {
@ -40,8 +40,8 @@ describe('Solutions', () => {
cy.logout(); cy.logout();
cy.login('rahel.cueni', 'test');
cy.visit('/module/lohn-und-budget'); cy.visit('/module/lohn-und-budget');
cy.login('rahel.cueni', 'test');
// cy.get('[data-cy=solution]').should('exist'); // cy.get('[data-cy=solution]').should('exist');
cy.get('[data-cy=solution]').first() cy.get('[data-cy=solution]').first()
.should('contain', 'anzeigen') .should('contain', 'anzeigen')

View File

@ -4,7 +4,8 @@ describe('Survey', () => {
cy.viewport('macbook-15'); cy.viewport('macbook-15');
cy.startGraphQLCapture(); cy.startGraphQLCapture();
cy.login('rahel.cueni', 'test'); cy.login('rahel.cueni', 'test', true);
cy.get('body').contains('Neues Wissen erwerben');
}); });
it('should display and fill out the survey', () => { it('should display and fill out the survey', () => {

View File

@ -24,26 +24,24 @@
// -- This is will overwrite an existing command -- // -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add("login", (username, password) => { Cypress.Commands.add("login", (username, password, visitLogin=false) => {
cy.request('/') if (visitLogin) {
.its('body') cy.visit('/login');
.then(body => { }
console.log(body);
const $html = Cypress.$(body);
const csrf = $html.find('input[name=csrfmiddlewaretoken]').val(); if (username != '') {
console.log(csrf); cy.get('[data-cy=email-input]').type(username);
cy.loginByCsrf(username, password, csrf) }
.then(resp => {
expect(resp.status).to.eq(200);
expect(resp.body).to.include('skillbox');
});
})
if (password != '') {
cy.get('[data-cy=password-input]').type(password);
}
cy.get('[data-cy=login-button]').click();
}); });
Cypress.Commands.add("logout", () => { Cypress.Commands.add("logout", () => {
cy.clearCookies(); cy.get('[data-cy=user-icon]').click();
cy.get('[data-cy=logout]').click();
}); });
Cypress.Commands.add('loginByCsrf', (username, password, csrftoken) => { Cypress.Commands.add('loginByCsrf', (username, password, csrftoken) => {

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

@ -1,5 +1,5 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" data-cy="user-icon">
<path <path
d="M50,0A50.12,50.12,0,0,0,.07,46.65S0,48.23,0,50s.07,3.25.07,3.36A50,50,0,1,0,50,0ZM24.14,86V79.84A15.18,15.18,0,0,1,39.31,64.67H60.69A15.18,15.18,0,0,1,75.86,79.84V86a44.27,44.27,0,0,1-51.72,0Zm57.47-5V79.84A20.94,20.94,0,0,0,60.69,58.93H39.31A20.94,20.94,0,0,0,18.39,79.84v1.31a44.4,44.4,0,1,1,63.22-.06Z"/> d="M50,0A50.12,50.12,0,0,0,.07,46.65S0,48.23,0,50s.07,3.25.07,3.36A50,50,0,1,0,50,0ZM24.14,86V79.84A15.18,15.18,0,0,1,39.31,64.67H60.69A15.18,15.18,0,0,1,75.86,79.84V86a44.27,44.27,0,0,1-51.72,0Zm57.47-5V79.84A20.94,20.94,0,0,0,60.69,58.93H39.31A20.94,20.94,0,0,0,18.39,79.84v1.31a44.4,44.4,0,1,1,63.22-.06Z"/>
<path <path

View File

@ -1,34 +1,34 @@
<template> <template>
<div class="pw-change"> <div class="pw-change">
<form class="pw-change__form change-form" novalidate @submit.prevent="validateBeforeSubmit"> <form class="pw-change__form change-form" novalidate @submit.prevent="validateBeforeSubmit">
<div class="change-form__field sbform-input"> <div class="change-form__field skillboxform-input">
<label for="old-pw" class="sbform-input__label">Aktuelles Passwort</label> <label for="old-pw" class="skillboxform-input__label">Aktuelles Passwort</label>
<input id="old-pw" <input id="old-pw"
name="oldPassword" name="oldPassword"
type="text" type="text"
v-model="oldPassword" v-model="oldPassword"
v-validate="'required'" v-validate="'required'"
:class="{ 'sbform-input__input--error': errors.has('oldPassword') }" :class="{ 'skillboxform-input__input--error': errors.has('oldPassword') }"
class="change-form__old skillbox-input sbform-input__input" class="change-form__old skillbox-input skillboxform-input__input"
autocomplete="off" autocomplete="off"
data-cy="old-password"> data-cy="old-password">
<small v-if="errors.has('oldPassword') && submitted" class="sbform-input__error" data-cy="old-password-local-errors">{{ errors.first('oldPassword') }}</small> <small v-if="errors.has('oldPassword') && submitted" class="skillboxform-input__error" data-cy="old-password-local-errors">{{ errors.first('oldPassword') }}</small>
<small v-for="error in oldPasswordErrors" :key="error" class=" sbform-input__error" data-cy="old-password-remote-errors">{{ error }}</small> <small v-for="error in oldPasswordErrors" :key="error" class=" skillboxform-input__error" data-cy="old-password-remote-errors">{{ error }}</small>
</div> </div>
<div class="change-form__field sbform-input"> <div class="change-form__field skillboxform-input">
<label for="new-pw" class="sbform-input__label">Neues Passwort</label> <label for="new-pw" class="skillboxform-input__label">Neues Passwort</label>
<input id="new-pw" <input id="new-pw"
name="newPassword" name="newPassword"
type="text" type="text"
v-model="newPassword" v-model="newPassword"
v-validate="'required|min:8|strongPassword'" v-validate="'required|min:8|strongPassword'"
:class="{ 'sbform-input__input--error': errors.has('newPassword') }" :class="{ 'skillboxform-input__input--error': errors.has('newPassword') }"
class="change-form__new skillbox-input sbform-input__input" class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off" autocomplete="off"
data-cy="new-password"> data-cy="new-password">
<small v-if="errors.has('newPassword') && submitted" class=" sbform-input__error" data-cy="new-password-local-errors">{{ errors.first('newPassword') }}</small> <small v-if="errors.has('newPassword') && submitted" class=" skillboxform-input__error" data-cy="new-password-local-errors">{{ errors.first('newPassword') }}</small>
<small v-for="error in newPasswordErrors" :key="error" class=" sbform-input__error" data-cy="new-password-remote-errors">{{ error }}</small> <small v-for="error in newPasswordErrors" :key="error" class=" skillboxform-input__error" data-cy="new-password-remote-errors">{{ error }}</small>
<p class="sbform-input__hint">Das Passwort muss mindestens 8 Zeichen lang sein und Grossbuchstaben, Zahlen und Sonderzeichen beinhalten.</p> <p class="skillboxform-input__hint">Das Passwort muss mindestens 8 Zeichen lang sein und Grossbuchstaben, Zahlen und Sonderzeichen beinhalten.</p>
</div> </div>
<button class="button button--primary change-form__submit" data-cy="change-password-button">Speichern</button> <button class="button button--primary change-form__submit" data-cy="change-password-button">Speichern</button>
</form> </form>
@ -82,35 +82,4 @@
} }
} }
.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> </style>

View File

@ -4,17 +4,18 @@ 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) {
const httpLink = new HttpLink({
// uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/', // uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/',
uri: '/api/graphql/', uri,
credentials: 'include', credentials: 'include',
fetch: fetch, fetch: fetch,
headers: { headers: {
'X-CSRFToken': document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*=\s*([^;]*).*$)|^.*$/, '$1') 'X-CSRFToken': document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*=\s*([^;]*).*$)|^.*$/, '$1')
} }
}); });
const consoleLink = new ApolloLink((operation, forward) => { const consoleLink = new ApolloLink((operation, forward) => {
// console.log(`starting request for ${operation.operationName}`); // console.log(`starting request for ${operation.operationName}`);
return forward(operation).map((data) => { return forward(operation).map((data) => {
@ -22,24 +23,24 @@ const consoleLink = new ApolloLink((operation, forward) => {
return data return data
}) })
}); });
// from https://github.com/apollographql/apollo-client/issues/1564#issuecomment-357492659 // from https://github.com/apollographql/apollo-client/issues/1564#issuecomment-357492659
const omitTypename = (key, value) => { const omitTypename = (key, value) => {
return key === '__typename' ? undefined : value return key === '__typename' ? undefined : value
}; };
const createOmitTypenameLink = new ApolloLink((operation, forward) => { const createOmitTypenameLink = new ApolloLink((operation, forward) => {
if (operation.variables) { if (operation.variables) {
operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename) operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename)
} }
return forward(operation) return forward(operation)
}); });
const composedLink = ApolloLink.from([createOmitTypenameLink, consoleLink, httpLink]); const composedLink = ApolloLink.from([createOmitTypenameLink, consoleLink, httpLink]);
const cache = new InMemoryCache({ const cache = new InMemoryCache({
cacheRedirects: { cacheRedirects: {
Query: { Query: {
contentBlock: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ContentBlockNode', id: args.id}), contentBlock: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ContentBlockNode', id: args.id}),
@ -51,24 +52,25 @@ const cache = new InMemoryCache({
projectEntry: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ProjectEntryNode', id: args.id}), projectEntry: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ProjectEntryNode', id: args.id}),
} }
} }
}); });
// TODO: Monkey-patching in a fix for an open issue suggesting that // 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 // `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: https://github.com/apollographql/apollo-feature-requests/issues/1
cache.originalReadQuery = cache.readQuery; cache.originalReadQuery = cache.readQuery;
cache.readQuery = (...args) => { cache.readQuery = (...args) => {
try { try {
return cache.originalReadQuery(...args); return cache.originalReadQuery(...args);
} catch (err) { } catch (err) {
return undefined; return undefined;
} }
}; };
// Create the apollo client // Create the apollo client
export default new ApolloClient({ return new ApolloClient({
link: composedLink, link: composedLink,
// link: httpLink, // link: httpLink,
cache: cache, cache: cache,
connectToDevTools: true connectToDevTools: true
}) })
}

View File

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

View File

@ -0,0 +1,33 @@
<template>
<div class="skillbox public">
<logo class="public__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";
.public {
max-width: 800px;
min-width: 320px;
&__logo {
position: relative;
left: -10px;
}
}
</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 */
function getCookieValue(cookieName) {
// https://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
let cookieValue = document.cookie.match('(^|[^;]+)\\s*' + cookieName + '\\s*=\\s*([^;]+)');
return cookieValue ? cookieValue.pop() : '';
}
function redirectIfLoginRequird(to) {
// public pages have the meta.public property set to true
return (!to.hasOwnProperty('meta') || !to.meta.hasOwnProperty('public') || !to.meta.public) && getCookieValue('loginStatus') !== 'true';
}
router.beforeEach((to, from, next) => {
if (redirectIfLoginRequird(to)) {
const redirectUrl = `/login?redirect=${to.path}`;
next(redirectUrl);
} else {
next();
}
});
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
el: '#app', el: '#app',

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

@ -0,0 +1,169 @@
<template>
<div class="login">
<h1 class="login__title">Melden Sie sich jetzt an</h1>
<form class="login__form login-form" novalidate @submit.prevent="validateBeforeSubmit">
<div class="login-form__field skillboxform-input">
<label for="email" class="skillboxform-input__label">E-Mail</label>
<input
id="email"
name="email"
type="text"
v-model="email"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('email') }"
class="change-form__email skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="email-input"
/>
<small
v-if="errors.has('email') && submitted"
class="skillboxform-input__error"
data-cy="email-local-errors"
>{{ errors.first('email') }}</small>
<small
v-for="error in emailErrors"
:key="error"
class="skillboxform-input__error"
data-cy="email-remote-errors"
>{{ error }}</small>
</div>
<div class="change-form__field skillboxform-input">
<label for="pw" class="skillboxform-input__label">Passwort</label>
<input
id="pw"
name="password"
type="password"
v-model="password"
v-validate="'required'"
:class="{ 'skillboxform-input__input--error': errors.has('password') }"
class="change-form__new skillbox-input skillboxform-input__input"
autocomplete="off"
data-cy="password-input"
/>
<small
v-if="errors.has('password') && submitted"
class="skillboxform-input__error"
data-cy="password-local-errors"
>{{ errors.first('password') }}</small>
<small
v-for="error in passwordErrors"
:key="error"
class="skillboxform-input__error"
data-cy="password-remote-errors"
>{{ error }}</small>
</div>
<div class="skillboxform-input">
<small class="skillboxform-input__error" data-cy="login-error" v-if="loginError">{{loginError}}</small>
</div>
<div class="actions">
<button class="button button--primary button--big actions__submit" data-cy="login-button">Anmelden</button>
<a class="actions__reset text-link" href="/accounts/password_reset/">Passwort vergessen?</a>
</div>
<!--div class="registration">
<p class="registration__text">Haben Sie noch kein Konto?</p>
<a class="registration__link text-link" href="/accounts/password_reset/">Jetzt registrieren</a>
</div-->
</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 {
if (success) {
const redirectUrl = that.$route.query.redirect ? that.$route.query.redirect : '/'
that.$router.push(redirectUrl);
} else {
that.loginError = 'Die E-Mail oder das Passwort ist falsch. Bitte versuchen Sie nochmals.';
}
} catch (e) {
console.warn(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: '',
submitted: false
};
}
};
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss";
.login {
&__title {
margin-top: 48px;
font-size: 2.75rem; // 44px
margin-bottom: 24px;
font-weight: 600;
}
}
.text-link {
font-family: $sans-serif-font-family;
color: $color-brand;
}
.actions {
&__reset {
display: inline-block;
margin-left: $large-spacing;
}
}
.registration {
margin-top: $large-spacing;
&__text {
font-family: $sans-serif-font-family;
margin-bottom: $small-spacing;
}
}
</style>

View File

@ -27,11 +27,26 @@ 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',
public: true
}
},
{ {
path: '/module/:slug', path: '/module/:slug',
component: moduleBase, component: moduleBase,
@ -118,6 +133,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

@ -23,6 +23,9 @@
border: 2px solid $color-silver-light; border: 2px solid $color-silver-light;
background-color: $color-silver-light; background-color: $color-silver-light;
} }
&--big {
padding: 15px;
}
} }
.icon-button { .icon-button {

View File

@ -0,0 +1,30 @@
.skillboxform-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;
}
}

View File

@ -18,3 +18,4 @@
@import "survey"; @import "survey";
@import "visibility"; @import "visibility";
@import "solutions"; @import "solutions";
@import "password_forms";

View File

@ -19,12 +19,13 @@ from surveys.schema import SurveysQuery
from surveys.mutations import SurveysMutations from surveys.mutations import SurveysMutations
from rooms.mutations import RoomMutations from rooms.mutations import RoomMutations
from rooms.schema import RoomsQuery, ModuleRoomsQuery from rooms.schema import RoomsQuery, ModuleRoomsQuery
from users.schema import UsersQuery from users.schema import AllUsersQuery, UsersQuery
from users.mutations import ProfileMutations from users.mutations import ProfileMutations
class Query(UsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery, class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery,
BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, SurveysQuery, graphene.ObjectType): StudentSubmissionQuery, BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, SurveysQuery,
graphene.ObjectType):
node = relay.Node.Field() node = relay.Node.Field()
if settings.DEBUG: if settings.DEBUG:

View File

@ -0,0 +1,14 @@
import graphene
from django.conf import settings
from graphene_django.debug import DjangoDebug
from users.mutations_public import UserMutations
class Mutation(UserMutations, graphene.ObjectType):
if settings.DEBUG:
debug = graphene.Field(DjangoDebug, name='__debug')
schema = graphene.Schema(mutation=Mutation)

View File

@ -1,13 +1,21 @@
from django.conf import settings from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
from api.schema_public import schema
from core.views import PrivateGraphQLView from core.views import PrivateGraphQLView
app_name = 'api' app_name = 'api'
urlpatterns = [ urlpatterns = [
url(r'^graphql-public', csrf_exempt(GraphQLView.as_view(schema=schema))),
url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view())), url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view())),
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += [url(r'^graphiql-public', csrf_exempt(GraphQLView.as_view(schema=schema, graphiql=True,
pretty=True)))]
urlpatterns += [url(r'^graphiql', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)))] urlpatterns += [url(r'^graphiql', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True, pretty=True)))]

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'
@ -172,8 +173,10 @@ else:
}, },
] ]
LOGOUT_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/login'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/login'
LOGIN_URL = LOGIN_REDIRECT_URL
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/ # https://docs.djangoproject.com/en/1.11/topics/i18n/

View File

@ -14,3 +14,4 @@ MIGRATION_MODULES = DisableMigrations()
# Email Settings # Email Settings
SENDGRID_API_KEY = "" SENDGRID_API_KEY = ""
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
LOGIN_REDIRECT_URL = '/accounts/login/'

View File

@ -1,5 +1,3 @@
@import "materialize/materialize";
$white: #fff; $white: #fff;
$grey-1: #F7F7F7; $grey-1: #F7F7F7;
@ -99,12 +97,69 @@ input[type=text], input[type=password], input[type=email], select {
} }
.logo { .logo {
color: $brand; width: 250px;
font-size: 36px; height: 48px;
font-weight: 800; position: relative;
font-family: Montserrat, Arial, sans-serif; left: -10px;
} }
.reset-heading { /* reset forms */
font-size: 2.4rem;
.reset__heading {
line-height: 4.5rem;
font-size: 44px;
margin-bottom: 24px;
margin-top: 52px;
font-weight: 600;
}
.reset__text {
margin-bottom: 52px;
}
.reset__form label {
font-weight: 600;
}
.reset__form input {
display: flex;
padding: 16px;
box-shadow: inset 0 1px 4px 0 rgba(0, 0, 0, 0.15);
border-radius: 4px;
box-sizing: border-box;
border: 1px solid #f0f0f0;
max-width: 100%;
background-color: #ffffff;
font-size: 16px;
font-family: "Montserrat", Arial, sans-serif;
font-weight: 400;
}
.reset__form button {
background: transparent;
border: 2px solid #17A887;
padding: 5px 15px;
border-radius: 3px;
font-family: "Montserrat", Arial, sans-serif;
font-weight: 400;
display: inline-flex;
cursor: pointer;
font-size: 100%;
}
.reset__form label {
font-weight: 600;
}
.reset__form div {
margin-bottom: 20px;
}
.container {
max-width: 800px;
min-width: 320px;
margin: 0 auto;
@media (max-width: 1023px) {
margin: 0 30px;
}
} }

View File

@ -28,6 +28,34 @@
<body class="{% block body_class %}{% endblock %}"> <body class="{% block body_class %}{% endblock %}">
<div class="container"> <div class="container">
<svg class="logo" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1350 250">
<path
d="M304.4,242.15a60,60,0,0,1-19.59-3.1,64.2,64.2,0,0,1-17.6-9.63l-2.94-2.22,21.17-34,3.58,3.21a21.91,21.91,0,0,0,6,4,15.21,15.21,0,0,0,5.81,1.09c4,0,6.51-1.44,8.08-4.68l1.15-2.19L263.73,85.72H313.8L334.27,143l17.38-57.3h48.8L353,208.39c-4.53,11.34-10.91,19.87-19,25.41h0C326,239.34,316,242.15,304.4,242.15Zm-29.33-17a53.63,53.63,0,0,0,12.38,6.3,51.94,51.94,0,0,0,17,2.66c10,0,18.42-2.33,25.12-6.94h0c6.71-4.62,12.1-11.92,16-21.71L388.67,93.79h-31l-22.74,75-26.79-75H275.94L319,195l-2.88,5.47c-2.87,5.92-8.18,9.11-15.29,9.11a23.28,23.28,0,0,1-8.88-1.69,24.83,24.83,0,0,1-4.58-2.53Z"
style="fill:#36c0a1"/>
<path
d="M458.66,113a12.63,12.63,0,0,0-6.43,1.39,4.55,4.55,0,0,0-2.36,4.18q0,3.22,4.4,5.25a93.59,93.59,0,0,0,14,4.61,178.08,178.08,0,0,1,21.33,7.29,40.28,40.28,0,0,1,14.79,11q6.32,7.39,6.33,19,0,17.8-14,28.19t-37.19,10.4A102.76,102.76,0,0,1,430,200.15a84.64,84.64,0,0,1-25.4-12.33l13.29-27.22a97.33,97.33,0,0,0,21.76,10.72A64.21,64.21,0,0,0,460.16,175a14.94,14.94,0,0,0,7.07-1.39,4.33,4.33,0,0,0,2.57-4q0-3.22-4.18-5.25a84.51,84.51,0,0,0-13.83-4.61A157.5,157.5,0,0,1,431,152.67a40,40,0,0,1-14.58-10.93q-6.22-7.29-6.22-18.86,0-18,13.72-28.51t36-10.5q26.79,0,51.23,14.15l-14.36,27.22Q473,113,458.66,113Z"
style="fill:#36c0a1"/>
<path d="M604.69,202.4l-21.22-40.51-8.79,9.22v31.3h-43.3V43.35h43.3v77.38l32.15-34.94h48.87l-42.66,45,42.87,71.6Z"
style="fill:#36c0a1"/>
<path
d="M712.25,36.49q6.22,6.22,6.22,16.08t-6.22,16.08q-6.22,6.22-16.08,6.22T680,68.64q-6.33-6.21-6.32-16.08T680,36.49q6.32-6.21,16.18-6.22T712.25,36.49Zm-37.51,49.3H718V202.4h-43.3Z"
style="fill:#36c0a1"/>
<path d="M748.47,43.35h43.3V202.4h-43.3Z" style="fill:#36c0a1"/>
<path d="M823.5,43.35h43.3V202.4H823.5Z" style="fill:#36c0a1"/>
<path
d="M1002.06,91.79A50.33,50.33,0,0,1,1021,113q6.75,13.72,6.75,31.73,0,17.8-6.54,31.19a48.35,48.35,0,0,1-18.54,20.69q-12,7.29-27.87,7.29A44,44,0,0,1,956.19,200a40.21,40.21,0,0,1-14.36-11.15v13.5h-43.3V43.35h43.3V99.29a38.85,38.85,0,0,1,13.93-11.15,41.53,41.53,0,0,1,18-3.86Q989.85,84.29,1002.06,91.79Zm-23.8,70.63q5.79-7.18,5.79-18.76t-5.79-18.76a18.82,18.82,0,0,0-15.43-7.18,18.59,18.59,0,0,0-15.22,7.18q-5.79,7.19-5.79,18.76t5.79,18.76a18.58,18.58,0,0,0,15.22,7.18A18.8,18.8,0,0,0,978.27,162.42Z"
style="fill:#36c0a1"/>
<path
d="M1142.8,91.69a54.24,54.24,0,0,1,22.62,20.9q8,13.5,8,31.51,0,17.8-8,31.4a54,54,0,0,1-22.62,21q-14.58,7.4-34.08,7.4t-34.19-7.4a53.86,53.86,0,0,1-22.73-21q-8-13.61-8-31.4,0-18,8-31.51a54.08,54.08,0,0,1,22.73-20.9q14.68-7.4,34.19-7.4T1142.8,91.69Zm-49.52,34.08q-5.79,7.18-5.79,18.76,0,11.79,5.79,18.86a18.92,18.92,0,0,0,15.43,7.07,18.7,18.7,0,0,0,15.22-7.07q5.79-7.07,5.79-18.86,0-11.58-5.79-18.76a18.6,18.6,0,0,0-15.22-7.18A18.81,18.81,0,0,0,1093.28,125.77Z"
style="fill:#36c0a1"/>
<path
d="M1176.45,85.79h49.73L1242.26,116l18-30.23h47.16L1271,142.6l39,59.81h-49.73l-18-33-20.58,33h-47.59l39-59.59Z"
style="fill:#36c0a1"/>
<path
d="M245,105.8A38.35,38.35,0,0,0,229.9,89.74h0a46.56,46.56,0,0,0-46.21,1.09A41.77,41.77,0,0,0,171.45,103a38.76,38.76,0,0,0-11.67-12,42.9,42.9,0,0,0-24.06-6.82,44.09,44.09,0,0,0-21.4,5.16,41.05,41.05,0,0,0-8.13,5.83v-9.4H58V201.83h48.19V144.37c0-5.32,1.23-9.42,3.77-12.55a11.7,11.7,0,0,1,9.27-4.46,9.48,9.48,0,0,1,7.75,3.35c2.09,2.45,3.11,5.75,3.11,10.09v61h48.19V144.37c0-5.26,1.24-9.49,3.69-12.59a11.44,11.44,0,0,1,9.15-4.43,9.48,9.48,0,0,1,7.75,3.35c2.09,2.45,3.11,5.75,3.11,10.09v61h48.19V129.28A51.17,51.17,0,0,0,245,105.8Zm-2.87,88h-32v-53c0-6.25-1.7-11.41-5-15.33a17.51,17.51,0,0,0-14-6.18h0a19.38,19.38,0,0,0-15.37,7.49c-3.61,4.55-5.44,10.48-5.44,17.6v49.39h-32v-53c0-6.25-1.7-11.41-5-15.33a17.53,17.53,0,0,0-14-6.18h0a19.66,19.66,0,0,0-15.44,7.45c-3.69,4.56-5.57,10.49-5.57,17.63v49.39h-32v-100h32v18.16h5.19l2.25-3.54a34.63,34.63,0,0,1,12.63-12,36.13,36.13,0,0,1,17.53-4.17,35,35,0,0,1,19.62,5.49,31.51,31.51,0,0,1,12.17,15.38l.46,1.18h6.52l.45-1a36.75,36.75,0,0,1,50.91-16.55,30,30,0,0,1,11.93,12.74,43.2,43.2,0,0,1,4.33,19.81Z"
style="fill:#36c0a1"/>
</svg>
{% block body %} {% block body %}
{% endblock %} {% endblock %}
</div> </div>

View File

@ -4,10 +4,9 @@
{% block title %}{% trans 'Passwort zurücksetzen abgeschlossen' %}{% endblock %} {% block title %}{% trans 'Passwort zurücksetzen abgeschlossen' %}{% endblock %}
{% block body %} {% block body %}
<h1 class="logo">myskillbox</h1> <div class="reset">
<h2 class="reset-heading">{% trans 'Passwort zurücksetzen abgeschlossen' %}</h2> <h2 class="reset__heading">{% trans 'Passwort zurücksetzen abgeschlossen' %}</h2>
<p>{% trans 'Ihr Passwort wurde zurückgesetzt. Sie können sich nun auf der Loginseite anmelden.' %}</p> <p class="reset__text">{% trans 'Ihr Passwort wurde zurückgesetzt. Sie können sich nun auf der Loginseite anmelden.' %}</p>
<p><a href="{% url "login" %}">{% trans 'Einloggen' %}</a></p> <p class="reset__text"><a href="/login">{% trans 'Einloggen' %}</a></p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,12 +5,13 @@
{% block title %}{% trans 'Setzen Sie Ihr Passwort' %}{% endblock %} {% block title %}{% trans 'Setzen Sie Ihr Passwort' %}{% endblock %}
{% block body %} {% block body %}
<h1 class="logo">myskillbox</h1> <div class="reset">
<h2 class="reset-heading">{% trans 'Setzen Sie Ihr neues Passwort' %}</h2> <h2 class="reset__heading">{% trans 'Setzen Sie Ihr neues Passwort' %}</h2>
<p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
<form method="post" class="mt-1"> <form method="post" class="mt-1 reset__form">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<button class="btn mt-1" type="submit" name="action">{% trans 'Passwort zurücksetzen' %}</button> <button class="btn mt-1" type="submit" name="action">{% trans 'Passwort zurücksetzen' %}</button>
</form> </form>
</div>
{% endblock %} {% endblock %}

View File

@ -5,8 +5,8 @@
{% block title %}{% trans 'Anweisungen versandt' %}{% endblock %} {% block title %}{% trans 'Anweisungen versandt' %}{% endblock %}
{% block body %} {% block body %}
<h1 class="logo">myskillbox</h1> <div class="reset">
<h2 class="reset-heading">{% trans 'Schauen Sie in Ihr Postfach' %}</h2> <h2 class="reset__heading">{% trans 'Schauen Sie in Ihr Postfach' %}</h2>
<p>{% trans 'Wir haben die Anweisungen, um Ihr Passwort zurückzusetzen, an Sie verschickt. Die E-Mail sollte in Kürze bei Ihnen ankommen.' %}</p> <p class="reset__text">{% trans 'Wir haben die Anweisungen, um Ihr Passwort zurückzusetzen, an Sie verschickt. Die E-Mail sollte in Kürze bei Ihnen ankommen.' %}</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,20 +5,17 @@
{% block title %}{% trans 'Passwort vergessen?' %}{% endblock %} {% block title %}{% trans 'Passwort vergessen?' %}{% endblock %}
{% block body %} {% block body %}
<h1 class="logo">myskillbox</h1> <div class="reset">
<h2 class="reset-heading">{% trans 'Passwort vergessen?' %}</h2> <h2 class="reset__heading">{% trans 'Passwort vergessen?' %}</h2>
<p>{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p> <p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
<form method="post" class="mt-1 reset__form">
<form method="post" class="mt-1">
{% csrf_token %} {% csrf_token %}
<div> <div>
{{ form.email.label_tag }} {{ form.email.label_tag }}
{{ form.email }} {{ form.email }}
</div> </div>
<button class="btn mt-1" type="submit" name="action">{% trans 'Passwort zurücksetzen' %}</button> <button type="submit" name="action">{% trans 'Passwort zurücksetzen' %}</button>
<input type="hidden" name="next" value="{{ next }}"/> <input type="hidden" name="next" value="{{ next }}"/>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -4,10 +4,9 @@
{% block title %}{% trans 'Sie haben es geschafft' %}{% endblock %} {% block title %}{% trans 'Sie haben es geschafft' %}{% endblock %}
{% block body %} {% block body %}
<h1 class="logo">myskillbox</h1> <div class="reset">
<h2 class="reset-heading">{% trans 'Sie haben es geschafft' %}</h2> <h2 class="reset__heading">{% trans 'Sie haben es geschafft' %}</h2>
<p>{% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p> <p class="reset__text">% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p>
<p><a href="{% url "login" %}">{% trans 'Jetzt anmelden' %}</a></p> <p class="reset__text"><a href="/login">{% trans 'Jetzt anmelden' %}</a></p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,13 +5,13 @@
{% block title %}{% trans 'Setzen Sie Ihr Passwort' %}{% endblock %} {% block title %}{% trans 'Setzen Sie Ihr Passwort' %}{% endblock %}
{% block body %} {% block body %}
<h1 class="logo">myskillbox</h1> <div class="reset">
<h2 class="reset-heading">{% trans 'Geben Sie ein persönliches Passwort ein:' %}</h2> <h2 class="reset__heading">{% trans 'Geben Sie ein persönliches Passwort ein:' %}</h2>
<p class="reset__text">{% trans 'Kein Problem! Geben Sie Ihre E-Mail-Adresse ein und erhalten Sie weitere Anweisungen.' %}</p>
<form method="post" class="mt-1"> <form method="post" class="mt-1 reset__form">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<button type="submit" name="action">{% trans 'Passwort speichern' %}</button>
<button class="btn mt-1" type="submit" name="action">{% trans 'Passwort speichern' %}</button>
</form> </form>
</div>
{% endblock %} {% endblock %}

View File

@ -5,9 +5,9 @@
{% block title %}{% trans 'Schauen Sie in Ihr Postfach' %}{% endblock %} {% block title %}{% trans 'Schauen Sie in Ihr Postfach' %}{% endblock %}
{% block body %} {% block body %}
<h1 class="logo">myskillbox</h1> <div class="reset">
<h2 class="reset-heading">{% trans 'Schauen Sie in Ihr Postfach' %}</h2> <h2 class="reset__heading">{% trans 'Schauen Sie in Ihr Postfach' %}</h2>
<p>{% trans 'Wir haben ein E-Mail mit allen weiteren Anweisungen an Sie verschickt. Die E-Mail sollte in Kürze bei Ihnen ankommen.' %}</p> <p class="reset__text">{% trans 'Wir haben ein E-Mail mit allen weiteren Anweisungen an Sie verschickt. Die E-Mail sollte in Kürze bei Ihnen ankommen.' %}</p>
<p>{% trans 'Hinweis: Ihre persönlichen Angaben für Ihr Benutzerkonto wurden zuvor in mySkillbox importiert. Sie können ausschliesslich die importierte E-Mail-Adresse verwenden. Wenn Sie nicht wissen, welche E-Mail-Adresse für Sie importiert wurde, können Sie Ihre Lehrperson fragen.' %}</p> <p class="reset__text">{% trans 'Hinweis: Ihre persönlichen Angaben für Ihr Benutzerkonto wurden zuvor in mySkillbox importiert. Sie können ausschliesslich die importierte E-Mail-Adresse verwenden. Wenn Sie nicht wissen, welche E-Mail-Adresse für Sie importiert wurde, können Sie Ihre Lehrperson fragen.' %}</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -5,13 +5,11 @@
{% block title %}{% trans 'Willkommen bei mySkillbox' %}{% endblock %} {% block title %}{% trans 'Willkommen bei mySkillbox' %}{% endblock %}
{% block body %} {% block body %}
<h1 class="logo">myskillbox</h1> <div class="reset">
<h2 class="reset-heading">{% trans 'Willkommen bei Myskillbox' %}</h2> <h2 class="reset__heading">{% trans 'Willkommen bei Myskillbox' %}</h2>
<p>{% trans 'Bevor Sie mySkillbox verwenden können, müssen Sie Ihre E-Mail-Adresse bestätigen und ein persönliches Passwort festlegen.' %}</p> <p class="reset__text">{% trans 'Bevor Sie mySkillbox verwenden können, müssen Sie Ihre E-Mail-Adresse bestätigen und ein persönliches Passwort festlegen.' %}</p>
<form method="post" class="mt-1 reset__form">
<form method="post" class="mt-1">
{% csrf_token %} {% csrf_token %}
<div> <div>
<label for="id_email">{% trans 'Geben Sie als erstes hier Ihre E-Mail-Adresse ein:' %}</label> <label for="id_email">{% trans 'Geben Sie als erstes hier Ihre E-Mail-Adresse ein:' %}</label>
{{ form.email }} {{ form.email }}
@ -19,6 +17,5 @@
<button class="btn mt-1" type="submit" name="action">{% trans 'E-Mail bestätigen' %}</button> <button class="btn mt-1" type="submit" name="action">{% trans 'E-Mail bestätigen' %}</button>
<input type="hidden" name="next" value="{{ next }}"/> <input type="hidden" name="next" value="{{ next }}"/>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,9 @@
import json import json
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings
from core import settings
from core.factories import UserFactory from core.factories import UserFactory
@ -17,7 +19,8 @@ class ApiAccessTestCase(TestCase):
def test_graphqlEndpoint_shouldNotBeAccessibleWithoutLogin(self): def test_graphqlEndpoint_shouldNotBeAccessibleWithoutLogin(self):
c = Client() c = Client()
response = c.post('/api/graphql/', data=self.query, content_type='application/json') response = c.post('/api/graphql/', data=self.query, content_type='application/json')
self.assertRedirects(response, '/accounts/login/?next=/api/graphql/') self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, '/login?next=/api/graphql/')
def test_graphqlEndpoint_shouldBeAccessibleWithLogin(self): def test_graphqlEndpoint_shouldBeAccessibleWithLogin(self):
user = UserFactory(username='admin') user = UserFactory(username='admin')
@ -27,3 +30,28 @@ class ApiAccessTestCase(TestCase):
response = c.post('/api/graphql/', data=self.query, content_type='application/json') response = c.post('/api/graphql/', data=self.query, content_type='application/json')
self.assertEqual(200, response.status_code) self.assertEqual(200, response.status_code)
def test_publicGraphqlEndpoint_shouldBeAccessibleWithoutLogin(self):
query= json.dumps({
'operationName': 'Login',
'query': '''
mutation Login($input: LoginInput!){
login(input: $input) {
success
errors {
field
}
}
}
''',
'variables': {
'input': {
'usernameInput': 'test',
'passwordInput': 'test'
}
},
})
c = Client()
response = c.post('/api/graphql-public/', data=query, content_type='application/json')
self.assertEqual(response.status_code, 200)

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

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-01
# @author: chrigu <christian.cueni@iterativ.ch>
import re
import graphene
from django.contrib.auth import authenticate, login
from graphene import relay
class FieldError(graphene.ObjectType):
code = graphene.String()
class MutationError(graphene.ObjectType):
field = graphene.String()
errors = graphene.List(FieldError)
class Login(relay.ClientIDMutation):
class Input:
username_input = graphene.String()
password_input = graphene.String()
success = graphene.Boolean()
errors = graphene.List(MutationError) # todo: change for consistency
@classmethod
def mutate_and_get_payload(cls, root, info, **kwargs):
user = authenticate(username=kwargs.get('username_input'), password=kwargs.get('password_input'))
if user is not None:
login(info.context, user)
return cls(success=True, errors=[])
else:
return cls(success=False, errors=['invalid_credentials'])
class UserMutations:
login = Login.Field()

View File

@ -3,7 +3,7 @@ from graphene import relay
from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField from graphene_django.filter import DjangoFilterConnectionField
from users.models import SchoolClass, User from users.models import User, SchoolClass
class SchoolClassNode(DjangoObjectType): class SchoolClassNode(DjangoObjectType):
@ -52,3 +52,16 @@ class UsersQuery(object):
return User.objects.none() return User.objects.none()
else: else:
return User.objects.all() return User.objects.all()
class AllUsersQuery(object):
me = graphene.Field(UserNode)
all_users = DjangoFilterConnectionField(UserNode)
def resolve_all_users(self, info, **kwargs):
if not info.context.user.is_superuser:
return User.objects.none()
else:
return User.objects.all()

View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
#
# ITerativ GmbH
# http://www.iterativ.ch/
#
# Copyright (c) 2019 ITerativ GmbH. All rights reserved.
#
# Created on 2019-10-02
# @author: chrigu <christian.cueni@iterativ.ch>
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase, RequestFactory
from graphene.test import Client
from api.schema_public import schema
from core.factories import UserFactory
class PasswordResetTests(TestCase):
def setUp(self):
self.user = UserFactory(username='aschi@iterativ.ch', email='aschi@iterativ.ch')
request = RequestFactory().post('/')
# adding session
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
self.client = Client(schema=schema, context_value=request)
def make_login_mutation(self, username, password):
mutation = '''
mutation Login($input: LoginInput!){
login(input: $input) {
success
errors {
field
}
}
}
'''
return self.client.execute(mutation, variables={
'input': {
'usernameInput': username,
'passwordInput': password
}
})
def test_user_can_login(self):
password = 'test123'
self.user.set_password(password)
self.user.save()
result = self.make_login_mutation(self.user.email, password)
self.assertTrue(result.get('data').get('login').get('success'))
self.assertTrue(self.user.is_authenticated)
def test_user_cannot_login_with_invalid_password(self):
password = 'test123'
self.user.set_password(password)
self.user.save()
result = self.make_login_mutation(self.user.email, 'test1234')
self.assertFalse(result.get('data').get('login').get('success'))