Merged in feature/move-login (pull request #37)
Feature/move login Approved-by: Ramon Wenger <ramon.wenger@iterativ.ch>
This commit is contained in:
commit
38777cf914
|
|
@ -6,13 +6,17 @@ describe('Change Password Page', () => {
|
|||
const validationErrorMsg = 'Das Passwort muss Grossbuchstaben, Zahlen und Sonderzeichen beinhalten';
|
||||
const validationOldWrongMsg = 'Die Eingabe ist falsch';
|
||||
|
||||
beforeEach(function () {
|
||||
cy.clearCookies();
|
||||
cy.visit('/me/profile');
|
||||
cy.login('rahel.cueni', 'test');
|
||||
});
|
||||
|
||||
after(function () {
|
||||
cy.exec("python ../server/manage.py reset_testuser_password rahel.cueni");
|
||||
});
|
||||
|
||||
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=old-password]').should('have.value', '');
|
||||
|
|
@ -20,73 +24,46 @@ describe('Change Password Page', () => {
|
|||
});
|
||||
|
||||
it('shows errors if old password is not entered', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword('', validNewPassword);
|
||||
cy.get('[data-cy=old-password-local-errors]').should('contain', 'Dein aktuelles Passwort fehlt')
|
||||
});
|
||||
|
||||
it('shows errors if new password is not entered', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword(validOldPassword, '');
|
||||
cy.get('[data-cy=new-password-local-errors]').should('contain', 'Dein neues Passwort fehlt')
|
||||
});
|
||||
|
||||
it('shows errors if new password is too short', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword(validOldPassword, 'Abc1!');
|
||||
cy.get('[data-cy=new-password-local-errors]').should('contain', validationTooShort)
|
||||
});
|
||||
|
||||
it('shows errors if new password has no uppercase letter', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword(validOldPassword, 'aabdddedddbc1!');
|
||||
cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg)
|
||||
});
|
||||
|
||||
it('shows errors if new password has no lowercase letter', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword(validOldPassword, 'ABCDDD334551!');
|
||||
cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg)
|
||||
});
|
||||
|
||||
it('shows errors if new password has no digit', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword(validOldPassword, 'AbcdEEDE!');
|
||||
cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg)
|
||||
});
|
||||
|
||||
it('shows errors if new password has no special character', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword(validOldPassword, 'AbcdEEDE09877');
|
||||
cy.get('[data-cy=new-password-local-errors]').should('contain', validationErrorMsg)
|
||||
});
|
||||
|
||||
it('shows errors if old password does not match', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword('test12345', validNewPassword);
|
||||
cy.get('[data-cy=old-password-remote-errors]').should('contain', validationOldWrongMsg)
|
||||
});
|
||||
|
||||
it('shows success if change was successful', () => {
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/me/profile');
|
||||
|
||||
cy.changePassword(validOldPassword, validNewPassword);
|
||||
cy.get('[data-cy=password-change-success]').should('contain', 'Dein Password wurde erfolgreich geändert.');
|
||||
cy.get('[data-cy=old-password]').should('have.value', '');
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ describe('Current Module', () => {
|
|||
cy.startGraphQLCapture();
|
||||
cy.viewport('macbook-15');
|
||||
|
||||
cy.login('nico.zickgraf', 'test');
|
||||
cy.visit('/module/lohn-und-budget');
|
||||
cy.login('nico.zickgraf', 'test');
|
||||
|
||||
cy.get('[data-cy=module-title]').should('contain', 'Lohn und Budget')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
describe('The Logged In Home Page', () => {
|
||||
it('successfully loads', () => {
|
||||
cy.login('test', 'test');
|
||||
cy.visit('/');
|
||||
cy.login('test', 'test');
|
||||
|
||||
cy.get('.block-title__title').should('contain', 'Inhalte')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
||||
});
|
||||
|
|
@ -1,15 +1,47 @@
|
|||
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 password = 'test';
|
||||
|
||||
cy.visit('/');
|
||||
cy.login(username, password, true);
|
||||
cy.get('body').contains('Neues Wissen erwerben');
|
||||
});
|
||||
|
||||
cy.get('#id_username').type(username);
|
||||
cy.get('#id_password').type(`${password}{enter}`);
|
||||
it('user sees error message if username is omitted', () => {
|
||||
const username = '';
|
||||
const password = 'test';
|
||||
|
||||
cy.getCookie('sessionid').should('exist');
|
||||
cy.get('.start-page__header').should('exist')
|
||||
cy.visit('/');
|
||||
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', () => {
|
||||
// cy.visit('/accounts/login/'); // have to get a csrf token by getting the base page first
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
describe('New project', () => {
|
||||
it('creates a new project and displays it', () => {
|
||||
cy.viewport('macbook-15');
|
||||
cy.visit('/portfolio');
|
||||
cy.login('rahel.cueni', 'test');
|
||||
|
||||
cy.visit('/portfolio');
|
||||
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-beschreibung]').type('This description rocks');
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ describe('Project Entry', () => {
|
|||
|
||||
cy.viewport('macbook-15');
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
describe('The Room Page', () => {
|
||||
it('displays new room entry with author name', () => {
|
||||
cy.viewport('macbook-15');
|
||||
cy.visit('/room/ein-historisches-festival');
|
||||
cy.login('rahel.cueni', 'test');
|
||||
|
||||
cy.visit('/room/ein-historisches-festival');
|
||||
cy.get('[data-cy=add-room-entry-button]').click();
|
||||
cy.get('.add-content-element:first-of-type').click();
|
||||
cy.get('[data-cy=choose-text-widget]').click();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
describe('The Rooms Page', () => {
|
||||
it('goes to the rooms page', () => {
|
||||
cy.visit('/rooms');
|
||||
cy.login('nico.zickgraf', 'test');
|
||||
|
||||
cy.visit('/rooms');
|
||||
|
||||
cy.get('[data-cy=add-room]').should('exist');
|
||||
});
|
||||
|
||||
it('add room should not exist for student', () => {
|
||||
cy.visit('/rooms');
|
||||
cy.login('rahel.cueni', 'test');
|
||||
|
||||
cy.visit('/rooms');
|
||||
|
||||
cy.get('[data-cy=add-room]').should('not.exist');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ describe('Solutions', () => {
|
|||
// cy.logout();
|
||||
cy.viewport('macbook-15');
|
||||
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/module/lohn-und-budget');
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.get('[data-cy=toggle-enable-solutions]')
|
||||
.should('not.exist');
|
||||
cy.get('[data-cy=solution]').should('not.exist');
|
||||
cy.logout();
|
||||
|
||||
cy.login('nico.zickgraf', 'test');
|
||||
cy.visit('/module/lohn-und-budget');
|
||||
cy.login('nico.zickgraf', 'test');
|
||||
cy.get('[data-cy=toggle-enable-solutions]').click();
|
||||
cy.waitFor('UpdateSolutionVisibility');
|
||||
// cy.get('[data-cy=toggle-enable-solutions]').within(() => {
|
||||
|
|
@ -40,8 +40,8 @@ describe('Solutions', () => {
|
|||
|
||||
cy.logout();
|
||||
|
||||
cy.login('rahel.cueni', 'test');
|
||||
cy.visit('/module/lohn-und-budget');
|
||||
cy.login('rahel.cueni', 'test');
|
||||
// cy.get('[data-cy=solution]').should('exist');
|
||||
cy.get('[data-cy=solution]').first()
|
||||
.should('contain', 'anzeigen')
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ describe('Survey', () => {
|
|||
|
||||
cy.viewport('macbook-15');
|
||||
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', () => {
|
||||
|
|
|
|||
|
|
@ -24,26 +24,24 @@
|
|||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add("login", (username, password) => {
|
||||
cy.request('/')
|
||||
.its('body')
|
||||
.then(body => {
|
||||
console.log(body);
|
||||
const $html = Cypress.$(body);
|
||||
Cypress.Commands.add("login", (username, password, visitLogin=false) => {
|
||||
if (visitLogin) {
|
||||
cy.visit('/login');
|
||||
}
|
||||
|
||||
const csrf = $html.find('input[name=csrfmiddlewaretoken]').val();
|
||||
console.log(csrf);
|
||||
cy.loginByCsrf(username, password, csrf)
|
||||
.then(resp => {
|
||||
expect(resp.status).to.eq(200);
|
||||
expect(resp.body).to.include('skillbox');
|
||||
});
|
||||
})
|
||||
if (username != '') {
|
||||
cy.get('[data-cy=email-input]').type(username);
|
||||
}
|
||||
|
||||
if (password != '') {
|
||||
cy.get('[data-cy=password-input]').type(password);
|
||||
}
|
||||
cy.get('[data-cy=login-button]').click();
|
||||
});
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,34 +1,34 @@
|
|||
<template>
|
||||
<div class="pw-change">
|
||||
<form class="pw-change__form change-form" novalidate @submit.prevent="validateBeforeSubmit">
|
||||
<div class="change-form__field sbform-input">
|
||||
<label for="old-pw" class="sbform-input__label">Aktuelles Passwort</label>
|
||||
<div class="change-form__field skillboxform-input">
|
||||
<label for="old-pw" class="skillboxform-input__label">Aktuelles Passwort</label>
|
||||
<input id="old-pw"
|
||||
name="oldPassword"
|
||||
type="text"
|
||||
v-model="oldPassword"
|
||||
v-validate="'required'"
|
||||
:class="{ 'sbform-input__input--error': errors.has('oldPassword') }"
|
||||
class="change-form__old skillbox-input sbform-input__input"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('oldPassword') }"
|
||||
class="change-form__old skillbox-input skillboxform-input__input"
|
||||
autocomplete="off"
|
||||
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-for="error in oldPasswordErrors" :key="error" class=" sbform-input__error" data-cy="old-password-remote-errors">{{ error }}</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=" skillboxform-input__error" data-cy="old-password-remote-errors">{{ error }}</small>
|
||||
</div>
|
||||
<div class="change-form__field sbform-input">
|
||||
<label for="new-pw" class="sbform-input__label">Neues Passwort</label>
|
||||
<div class="change-form__field skillboxform-input">
|
||||
<label for="new-pw" class="skillboxform-input__label">Neues Passwort</label>
|
||||
<input id="new-pw"
|
||||
name="newPassword"
|
||||
type="text"
|
||||
v-model="newPassword"
|
||||
v-validate="'required|min:8|strongPassword'"
|
||||
:class="{ 'sbform-input__input--error': errors.has('newPassword') }"
|
||||
class="change-form__new skillbox-input sbform-input__input"
|
||||
:class="{ 'skillboxform-input__input--error': errors.has('newPassword') }"
|
||||
class="change-form__new skillbox-input skillboxform-input__input"
|
||||
autocomplete="off"
|
||||
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-for="error in newPasswordErrors" :key="error" class=" sbform-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>
|
||||
<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=" skillboxform-input__error" data-cy="new-password-remote-errors">{{ error }}</small>
|
||||
<p class="skillboxform-input__hint">Das Passwort muss mindestens 8 Zeichen lang sein und Grossbuchstaben, Zahlen und Sonderzeichen beinhalten.</p>
|
||||
</div>
|
||||
<button class="button button--primary change-form__submit" data-cy="change-password-button">Speichern</button>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ import {ApolloClient} from 'apollo-client/index'
|
|||
import {ApolloLink} from 'apollo-link'
|
||||
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: '/api/graphql/',
|
||||
uri,
|
||||
credentials: 'include',
|
||||
fetch: fetch,
|
||||
headers: {
|
||||
'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}`);
|
||||
|
||||
return forward(operation).map((data) => {
|
||||
|
|
@ -22,24 +23,24 @@ const consoleLink = new ApolloLink((operation, forward) => {
|
|||
|
||||
return data
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// from https://github.com/apollographql/apollo-client/issues/1564#issuecomment-357492659
|
||||
const omitTypename = (key, value) => {
|
||||
// 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) => {
|
||||
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 composedLink = ApolloLink.from([createOmitTypenameLink, consoleLink, httpLink]);
|
||||
|
||||
const cache = new InMemoryCache({
|
||||
const cache = new InMemoryCache({
|
||||
cacheRedirects: {
|
||||
Query: {
|
||||
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}),
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
// 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
|
||||
export default new ApolloClient({
|
||||
// Create the apollo client
|
||||
return new ApolloClient({
|
||||
link: composedLink,
|
||||
// link: httpLink,
|
||||
cache: cache,
|
||||
connectToDevTools: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
mutation Login($input: LoginInput!) {
|
||||
login(input: $input) {
|
||||
success
|
||||
errors {
|
||||
field
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 */
|
||||
|
||||
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 */
|
||||
new Vue({
|
||||
el: '#app',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -27,11 +27,26 @@ 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',
|
||||
public: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/module/:slug',
|
||||
component: moduleBase,
|
||||
|
|
@ -118,6 +133,7 @@ const router = new Router({
|
|||
return {x: 0, y: 0}
|
||||
}
|
||||
});
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
store.dispatch('showMobileNavigation', false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@
|
|||
border: 2px solid $color-silver-light;
|
||||
background-color: $color-silver-light;
|
||||
}
|
||||
&--big {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,3 +18,4 @@
|
|||
@import "survey";
|
||||
@import "visibility";
|
||||
@import "solutions";
|
||||
@import "password_forms";
|
||||
|
|
|
|||
|
|
@ -19,12 +19,13 @@ from surveys.schema import SurveysQuery
|
|||
from surveys.mutations import SurveysMutations
|
||||
from rooms.mutations import RoomMutations
|
||||
from rooms.schema import RoomsQuery, ModuleRoomsQuery
|
||||
from users.schema import UsersQuery
|
||||
from users.schema import AllUsersQuery, UsersQuery
|
||||
from users.mutations import ProfileMutations
|
||||
|
||||
|
||||
class Query(UsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery, StudentSubmissionQuery,
|
||||
BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, SurveysQuery, graphene.ObjectType):
|
||||
class Query(UsersQuery, AllUsersQuery, ModuleRoomsQuery, RoomsQuery, ObjectivesQuery, BookQuery, AssignmentsQuery,
|
||||
StudentSubmissionQuery, BasicKnowledgeQuery, PortfolioQuery, MyActivityQuery, SurveysQuery,
|
||||
graphene.ObjectType):
|
||||
node = relay.Node.Field()
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -1,13 +1,21 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
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
|
||||
|
||||
app_name = 'api'
|
||||
urlpatterns = [
|
||||
url(r'^graphql-public', csrf_exempt(GraphQLView.as_view(schema=schema))),
|
||||
url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view())),
|
||||
]
|
||||
|
||||
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)))]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ MIDDLEWARE += [
|
|||
|
||||
'core.middleware.ThreadLocalMiddleware',
|
||||
'core.middleware.CommonRedirectMiddleware',
|
||||
'core.middleware.UserLoggedInCookieMiddleWare',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'core.urls'
|
||||
|
|
@ -172,8 +173,10 @@ else:
|
|||
},
|
||||
]
|
||||
|
||||
LOGOUT_REDIRECT_URL = '/'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGOUT_REDIRECT_URL = '/login'
|
||||
LOGIN_REDIRECT_URL = '/login'
|
||||
|
||||
LOGIN_URL = LOGIN_REDIRECT_URL
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
|
|
|||
|
|
@ -14,3 +14,4 @@ MIGRATION_MODULES = DisableMigrations()
|
|||
# Email Settings
|
||||
SENDGRID_API_KEY = ""
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
|
||||
LOGIN_REDIRECT_URL = '/accounts/login/'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
@import "materialize/materialize";
|
||||
|
||||
$white: #fff;
|
||||
|
||||
$grey-1: #F7F7F7;
|
||||
|
|
@ -99,12 +97,69 @@ input[type=text], input[type=password], input[type=email], select {
|
|||
}
|
||||
|
||||
.logo {
|
||||
color: $brand;
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
font-family: Montserrat, Arial, sans-serif;
|
||||
width: 250px;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
.reset-heading {
|
||||
font-size: 2.4rem;
|
||||
/* reset forms */
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,34 @@
|
|||
|
||||
<body class="{% block body_class %}{% endblock %}">
|
||||
<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 %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@
|
|||
{% block title %}{% trans 'Passwort zurücksetzen abgeschlossen' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="logo">myskillbox</h1>
|
||||
<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><a href="{% url "login" %}">{% trans 'Einloggen' %}</a></p>
|
||||
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Passwort zurücksetzen abgeschlossen' %}</h2>
|
||||
<p class="reset__text">{% trans 'Ihr Passwort wurde zurückgesetzt. Sie können sich nun auf der Loginseite anmelden.' %}</p>
|
||||
<p class="reset__text"><a href="/login">{% trans 'Einloggen' %}</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@
|
|||
{% block title %}{% trans 'Setzen Sie Ihr Passwort' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="logo">myskillbox</h1>
|
||||
<h2 class="reset-heading">{% trans 'Setzen Sie Ihr neues Passwort' %}</h2>
|
||||
|
||||
<form method="post" class="mt-1">
|
||||
<div class="reset">
|
||||
<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 reset__form">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button class="btn mt-1" type="submit" name="action">{% trans 'Passwort zurücksetzen' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
{% block title %}{% trans 'Anweisungen versandt' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="logo">myskillbox</h1>
|
||||
<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>
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Schauen Sie in Ihr Postfach' %}</h2>
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,17 @@
|
|||
{% block title %}{% trans 'Passwort vergessen?' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="logo">myskillbox</h1>
|
||||
<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>
|
||||
|
||||
<form method="post" class="mt-1">
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Passwort vergessen?' %}</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 reset__form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
{{ form.email.label_tag }}
|
||||
{{ form.email }}
|
||||
</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 }}"/>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@
|
|||
{% block title %}{% trans 'Sie haben es geschafft' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="logo">myskillbox</h1>
|
||||
<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><a href="{% url "login" %}">{% trans 'Jetzt anmelden' %}</a></p>
|
||||
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Sie haben es geschafft' %}</h2>
|
||||
<p class="reset__text">% trans 'Ihr Passwort wurde erfolgreich gespeichert. Sie können sich nun anmelden.' %}</p>
|
||||
<p class="reset__text"><a href="/login">{% trans 'Jetzt anmelden' %}</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
{% block title %}{% trans 'Setzen Sie Ihr Passwort' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="logo">myskillbox</h1>
|
||||
<h2 class="reset-heading">{% trans 'Geben Sie ein persönliches Passwort ein:' %}</h2>
|
||||
|
||||
<form method="post" class="mt-1">
|
||||
<div class="reset">
|
||||
<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 reset__form">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
|
||||
<button class="btn mt-1" type="submit" name="action">{% trans 'Passwort speichern' %}</button>
|
||||
<button type="submit" name="action">{% trans 'Passwort speichern' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
{% block title %}{% trans 'Schauen Sie in Ihr Postfach' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="logo">myskillbox</h1>
|
||||
<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>{% 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 class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Schauen Sie in Ihr Postfach' %}</h2>
|
||||
<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 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>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,11 @@
|
|||
{% block title %}{% trans 'Willkommen bei mySkillbox' %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1 class="logo">myskillbox</h1>
|
||||
<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>
|
||||
|
||||
<form method="post" class="mt-1">
|
||||
<div class="reset">
|
||||
<h2 class="reset__heading">{% trans 'Willkommen bei Myskillbox' %}</h2>
|
||||
<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">
|
||||
{% csrf_token %}
|
||||
|
||||
<div>
|
||||
<label for="id_email">{% trans 'Geben Sie als erstes hier Ihre E-Mail-Adresse ein:' %}</label>
|
||||
{{ form.email }}
|
||||
|
|
@ -19,6 +17,5 @@
|
|||
<button class="btn mt-1" type="submit" name="action">{% trans 'E-Mail bestätigen' %}</button>
|
||||
<input type="hidden" name="next" value="{{ next }}"/>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import json
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from core import settings
|
||||
from core.factories import UserFactory
|
||||
|
||||
|
||||
|
|
@ -17,7 +19,8 @@ class ApiAccessTestCase(TestCase):
|
|||
def test_graphqlEndpoint_shouldNotBeAccessibleWithoutLogin(self):
|
||||
c = Client()
|
||||
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):
|
||||
user = UserFactory(username='admin')
|
||||
|
|
@ -27,3 +30,28 @@ class ApiAccessTestCase(TestCase):
|
|||
response = c.post('/api/graphql/', data=self.query, content_type='application/json')
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ from graphene import relay
|
|||
from graphene_django import DjangoObjectType
|
||||
from graphene_django.filter import DjangoFilterConnectionField
|
||||
|
||||
from users.models import SchoolClass, User
|
||||
from users.models import User, SchoolClass
|
||||
|
||||
|
||||
class SchoolClassNode(DjangoObjectType):
|
||||
|
|
@ -52,3 +52,16 @@ class UsersQuery(object):
|
|||
return User.objects.none()
|
||||
else:
|
||||
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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
Loading…
Reference in New Issue