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 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', '');

View File

@ -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')

View File

@ -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')
})

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', () => {
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

View File

@ -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');

View File

@ -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', () => {

View File

@ -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();

View File

@ -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');
});

View File

@ -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')

View File

@ -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', () => {

View File

@ -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) => {

View File

@ -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,

View File

@ -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

View File

@ -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>

View File

@ -4,71 +4,73 @@ import {ApolloClient} from 'apollo-client/index'
import {ApolloLink} from 'apollo-link'
import fetch from 'unfetch'
const httpLink = new HttpLink({
// uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/',
uri: '/api/graphql/',
credentials: 'include',
fetch: fetch,
headers: {
'X-CSRFToken': document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*=\s*([^;]*).*$)|^.*$/, '$1')
}
});
const consoleLink = new ApolloLink((operation, forward) => {
// console.log(`starting request for ${operation.operationName}`);
return forward(operation).map((data) => {
// console.log(`ending request for ${operation.operationName}`);
return data
})
});
// from https://github.com/apollographql/apollo-client/issues/1564#issuecomment-357492659
const omitTypename = (key, value) => {
return key === '__typename' ? undefined : value
};
const createOmitTypenameLink = new ApolloLink((operation, forward) => {
if (operation.variables) {
operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename)
}
return forward(operation)
});
const composedLink = ApolloLink.from([createOmitTypenameLink, consoleLink, httpLink]);
const cache = new InMemoryCache({
cacheRedirects: {
Query: {
contentBlock: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ContentBlockNode', id: args.id}),
chapter: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ChapterNode', id: args.id}),
assignment: (_, args, {getCacheKey}) => getCacheKey({__typename: 'AssignmentNode', id: args.id}),
objective: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveNode', id: args.id}),
objectiveGroup: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveGroupNode', id: args.id}),
module: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ModuleNode', id: args.id}),
projectEntry: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ProjectEntryNode', id: args.id}),
export default function (uri) {
const httpLink = new HttpLink({
// uri: process.env.NODE_ENV !== 'production' ? 'http://localhost:8000/api/graphql/' : '/api/graphql/',
uri,
credentials: 'include',
fetch: fetch,
headers: {
'X-CSRFToken': document.cookie.replace(/(?:(?:^|.*;\s*)csrftoken\s*=\s*([^;]*).*$)|^.*$/, '$1')
}
}
});
});
// TODO: Monkey-patching in a fix for an open issue suggesting that
// `readQuery` should return null or undefined if the query is not yet in the
// cache: https://github.com/apollographql/apollo-feature-requests/issues/1
cache.originalReadQuery = cache.readQuery;
cache.readQuery = (...args) => {
try {
return cache.originalReadQuery(...args);
} catch (err) {
return undefined;
}
};
const consoleLink = new ApolloLink((operation, forward) => {
// console.log(`starting request for ${operation.operationName}`);
// Create the apollo client
export default new ApolloClient({
link: composedLink,
// link: httpLink,
cache: cache,
connectToDevTools: true
})
return forward(operation).map((data) => {
// console.log(`ending request for ${operation.operationName}`);
return data
})
});
// from https://github.com/apollographql/apollo-client/issues/1564#issuecomment-357492659
const omitTypename = (key, value) => {
return key === '__typename' ? undefined : value
};
const createOmitTypenameLink = new ApolloLink((operation, forward) => {
if (operation.variables) {
operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename)
}
return forward(operation)
});
const composedLink = ApolloLink.from([createOmitTypenameLink, consoleLink, httpLink]);
const cache = new InMemoryCache({
cacheRedirects: {
Query: {
contentBlock: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ContentBlockNode', id: args.id}),
chapter: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ChapterNode', id: args.id}),
assignment: (_, args, {getCacheKey}) => getCacheKey({__typename: 'AssignmentNode', id: args.id}),
objective: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveNode', id: args.id}),
objectiveGroup: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ObjectiveGroupNode', id: args.id}),
module: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ModuleNode', id: args.id}),
projectEntry: (_, args, {getCacheKey}) => getCacheKey({__typename: 'ProjectEntryNode', id: args.id}),
}
}
});
// TODO: Monkey-patching in a fix for an open issue suggesting that
// `readQuery` should return null or undefined if the query is not yet in the
// cache: https://github.com/apollographql/apollo-feature-requests/issues/1
cache.originalReadQuery = cache.readQuery;
cache.readQuery = (...args) => {
try {
return cache.originalReadQuery(...args);
} catch (err) {
return undefined;
}
};
// Create the apollo client
return new ApolloClient({
link: composedLink,
// link: httpLink,
cache: cache,
connectToDevTools: true
})
}

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 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',

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 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);
});

View File

@ -23,6 +23,9 @@
border: 2px solid $color-silver-light;
background-color: $color-silver-light;
}
&--big {
padding: 15px;
}
}
.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 "visibility";
@import "solutions";
@import "password_forms";

View File

@ -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:

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.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)))]

View File

@ -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

View File

@ -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/

View File

@ -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/'

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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)

View File

@ -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:

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.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()

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'))